Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import io 

2import os 

3import six 

4import tornado.web 

5import tornado.gen 

6from threading import RLock 

7from .basehandler import BaseHandler 

8from gramex.config import app_log 

9from gramex.cache import Subprocess 

10 

11 

12class ProcessHandler(BaseHandler): 

13 ''' 

14 Runs sub-processes with transformations. It accepts these parameters: 

15 

16 :arg list/string args: The first value is the command. The rest are optional 

17 string arguments. This is the same as in `Popen`_. 

18 :arg boolean shell: ``True`` passes the ``args`` through the shell, allowing 

19 wildcards like ``*``. If ``shell=True`` then use a single string for 

20 ``args`` that includes the arguments. 

21 :arg string cwd: Current working directory from where the command will run. 

22 Defaults to the same directory Gramex ran from. 

23 :arg string stdout: The process output can be sent to: 

24 

25 - ``pipe``: Display the (transformed) output. This is the default 

26 - ``false``: Ignore the output 

27 - ``filename.txt``: Save output to a ``filename.txt`` 

28 

29 :arg string stderr: The process error stream has the same options as stdout. 

30 :arg string stdin: (**TODO**) 

31 :arg int/string buffer: 'line' will write lines as they are generated. 

32 Numbers indicate the number of bytes to buffer. Defaults to 

33 ``io.DEFAULT_BUFFER_SIZE``. 

34 :arg dict headers: HTTP headers to set on the response. 

35 :arg dict transform: (**TODO**) 

36 Transformations that should be applied to the files. The key matches a 

37 `glob pattern`_ (e.g. ``'*.md'`` or ``'data/*'``.) The value is a dict 

38 with the same structure as :class:`FunctionHandler`, and accepts these 

39 keys: 

40 

41 ``encoding`` 

42 The encoding to load the file as. If you don't specify an encoding, 

43 file contents are passed to ``function`` as a binary string. 

44 

45 ``function`` 

46 A string that resolves into any Python function or method (e.g. 

47 ``markdown.markdown``). By default, it is called with the file 

48 contents as ``function(content)`` and the result is rendered as-is 

49 (hence must be a string.) 

50 

51 ``args`` 

52 optional positional arguments to be passed to the function. By 

53 default, this is just ``['content']`` where ``content`` is the file 

54 contents. You can also pass the handler via ``['handler']``, or both 

55 of them in any order. 

56 

57 ``kwargs``: 

58 an optional list of keyword arguments to be passed to the function. 

59 A value with of ``handler`` and ``content`` is replaced with the 

60 RequestHandler and file contents respectively. 

61 

62 ``headers``: 

63 HTTP headers to set on the response. 

64 

65 .. _Popen: https://docs.python.org/3/library/subprocess.html#subprocess.Popen 

66 

67 ''' 

68 @classmethod 

69 def setup(cls, args, shell=False, cwd=None, buffer=0, headers={}, **kwargs): 

70 super(ProcessHandler, cls).setup(**kwargs) 

71 cls.cmdargs = args 

72 cls.shell = shell 

73 cls._write_lock = RLock() 

74 cls.buffer_size = buffer 

75 # Normalize current directory for path, if provided 

76 cls.cwd = cwd if cwd is None else os.path.abspath(cwd) 

77 # File handles for stdout/stderr are cached in cls.handles 

78 cls.handles = {} 

79 

80 cls.headers = headers 

81 cls.post = cls.get 

82 

83 def stream_callbacks(self, targets, name): 

84 # stdout/stderr are can be specified as a scalar or a list. 

85 # Convert it into a list of callback fn(data) 

86 

87 # if no target is specified, stream to RequestHandler 

88 if targets is None: 

89 targets = ['pipe'] 

90 # if a string is specified, treat it as the sole file output 

91 elif not isinstance(targets, list): 

92 targets = [targets] 

93 

94 callbacks = [] 

95 for target in targets: 

96 # pipe write to the RequestHandler 

97 if target == 'pipe': 

98 callbacks.append(self._write) 

99 # false-y values are ignored. (False, 0, etc) 

100 elif not target: 

101 pass 

102 # strings are treated as files 

103 elif isinstance(target, six.string_types): 103 ↛ 111line 103 didn't jump to line 111, because the condition on line 103 was never false

104 # cache file handles for re-use between stdout, stderr 

105 if target not in self.handles: 

106 self.handles[target] = io.open(target, mode='wb') 

107 handle = self.handles[target] 

108 callbacks.append(handle.write) 

109 # warn on unknown parameters (e.g. numbers, True, etc) 

110 else: 

111 app_log.warning('ProcessHandler: %s: %s is not implemented' % (name, target)) 

112 return callbacks 

113 

114 def initialize(self, stdout=None, stderr=None, stdin=None, **kwargs): 

115 super(ProcessHandler, self).initialize(**kwargs) 

116 self.stream_stdout = self.stream_callbacks(stdout, name='stdout') 

117 self.stream_stderr = self.stream_callbacks(stderr, name='stderr') 

118 

119 @tornado.gen.coroutine 

120 def get(self, *path_args): 

121 if self.redirects: 121 ↛ 122line 121 didn't jump to line 122, because the condition on line 121 was never true

122 self.save_redirect_page() 

123 for header_name, header_value in self.headers.items(): 

124 self.set_header(header_name, header_value) 

125 

126 proc = Subprocess( 

127 self.cmdargs, 

128 shell=self.shell, 

129 cwd=self.cwd, 

130 stream_stdout=self.stream_stdout, 

131 stream_stderr=self.stream_stderr, 

132 buffer_size=self.buffer_size, 

133 ) 

134 yield proc.wait_for_exit() 

135 # Wait for process to finish 

136 proc.proc.wait() 

137 if self.redirects: 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true

138 self.redirect_next() 

139 

140 def _write(self, data): 

141 with self._write_lock: 

142 self.write(data) 

143 # Flush every time. This disables Etag, but processes have 

144 # side-effects, so we should not be caching these requests anyway. 

145 self.flush() 

146 

147 def on_finish(self): 

148 '''Close all open handles after the request has finished''' 

149 for target, handle in self.handles.items(): 

150 handle.close() 

151 super(ProcessHandler, self).on_finish()