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

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
12class ProcessHandler(BaseHandler):
13 '''
14 Runs sub-processes with transformations. It accepts these parameters:
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:
25 - ``pipe``: Display the (transformed) output. This is the default
26 - ``false``: Ignore the output
27 - ``filename.txt``: Save output to a ``filename.txt``
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:
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.
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.)
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.
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.
62 ``headers``:
63 HTTP headers to set on the response.
65 .. _Popen: https://docs.python.org/3/library/subprocess.html#subprocess.Popen
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 = {}
80 cls.headers = headers
81 cls.post = cls.get
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)
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]
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
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')
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)
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()
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()
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()