Coverage for gramex\handlers\processhandler.py: 93%

# Coverage for gramex\handlers\processhandler.py : 93%

## 61 statements 58 run 3 missing 0 excluded 3 partial

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

1 import io

2 import os

3 import six

4 import tornado.web

5 import tornado.gen

6 from threading import RLock

7 from .basehandler import BaseHandler

8 from gramex.config import app_log

9 from gramex.cache import Subprocess

10

11

12 class 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()