##// END OF EJS Templates
Garbage collect finished processes
Takafumi Arakaki -
Show More
@@ -1,283 +1,288 b''
1 1 """Magic functions for running cells in various scripts."""
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (c) 2012 The IPython Development Team.
4 4 #
5 5 # Distributed under the terms of the Modified BSD License.
6 6 #
7 7 # The full license is in the file COPYING.txt, distributed with this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 # Stdlib
15 15 import os
16 16 import re
17 17 import sys
18 18 import signal
19 19 import time
20 20 from subprocess import Popen, PIPE
21 21 import atexit
22 22
23 23 # Our own packages
24 24 from IPython.config.configurable import Configurable
25 25 from IPython.core import magic_arguments
26 26 from IPython.core.error import UsageError
27 27 from IPython.core.magic import (
28 28 Magics, magics_class, line_magic, cell_magic
29 29 )
30 30 from IPython.lib.backgroundjobs import BackgroundJobManager
31 31 from IPython.testing.skipdoctest import skip_doctest
32 32 from IPython.utils import py3compat
33 33 from IPython.utils.process import find_cmd, FindCmdError, arg_split
34 34 from IPython.utils.traitlets import List, Dict
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Magic implementation classes
38 38 #-----------------------------------------------------------------------------
39 39
40 40 def script_args(f):
41 41 """single decorator for adding script args"""
42 42 args = [
43 43 magic_arguments.argument(
44 44 '--out', type=str,
45 45 help="""The variable in which to store stdout from the script.
46 46 If the script is backgrounded, this will be the stdout *pipe*,
47 47 instead of the stderr text itself.
48 48 """
49 49 ),
50 50 magic_arguments.argument(
51 51 '--err', type=str,
52 52 help="""The variable in which to store stderr from the script.
53 53 If the script is backgrounded, this will be the stderr *pipe*,
54 54 instead of the stderr text itself.
55 55 """
56 56 ),
57 57 magic_arguments.argument(
58 58 '--bg', action="store_true",
59 59 help="""Whether to run the script in the background.
60 60 If given, the only way to see the output of the command is
61 61 with --out/err.
62 62 """
63 63 ),
64 64 magic_arguments.argument(
65 65 '--proc', type=str,
66 66 help="""The variable in which to store Popen instance.
67 67 This is used only when --bg option is given.
68 68 """
69 69 ),
70 70 ]
71 71 for arg in args:
72 72 f = arg(f)
73 73 return f
74 74
75 75 @magics_class
76 76 class ScriptMagics(Magics, Configurable):
77 77 """Magics for talking to scripts
78 78
79 79 This defines a base `%%script` cell magic for running a cell
80 80 with a program in a subprocess, and registers a few top-level
81 81 magics that call %%script with common interpreters.
82 82 """
83 83 script_magics = List(config=True,
84 84 help="""Extra script cell magics to define
85 85
86 86 This generates simple wrappers of `%%script foo` as `%%foo`.
87 87
88 88 If you want to add script magics that aren't on your path,
89 89 specify them in script_paths
90 90 """,
91 91 )
92 92 def _script_magics_default(self):
93 93 """default to a common list of programs if we find them"""
94 94
95 95 defaults = []
96 96 to_try = []
97 97 if os.name == 'nt':
98 98 defaults.append('cmd')
99 99 to_try.append('powershell')
100 100 to_try.extend([
101 101 'sh',
102 102 'bash',
103 103 'perl',
104 104 'ruby',
105 105 'python3',
106 106 'pypy',
107 107 ])
108 108
109 109 for cmd in to_try:
110 110 if cmd in self.script_paths:
111 111 defaults.append(cmd)
112 112 else:
113 113 try:
114 114 find_cmd(cmd)
115 115 except FindCmdError:
116 116 # command not found, ignore it
117 117 pass
118 118 except ImportError:
119 119 # Windows without pywin32, find_cmd doesn't work
120 120 pass
121 121 else:
122 122 defaults.append(cmd)
123 123 return defaults
124 124
125 125 script_paths = Dict(config=True,
126 126 help="""Dict mapping short 'ruby' names to full paths, such as '/opt/secret/bin/ruby'
127 127
128 128 Only necessary for items in script_magics where the default path will not
129 129 find the right interpreter.
130 130 """
131 131 )
132 132
133 133 def __init__(self, shell=None):
134 134 Configurable.__init__(self, config=shell.config)
135 135 self._generate_script_magics()
136 136 Magics.__init__(self, shell=shell)
137 137 self.job_manager = BackgroundJobManager()
138 138 self.bg_processes = []
139 139 atexit.register(self.kill_bg_processes)
140 140
141 141 def __del__(self):
142 142 self.kill_bg_processes()
143 143
144 144 def _generate_script_magics(self):
145 145 cell_magics = self.magics['cell']
146 146 for name in self.script_magics:
147 147 cell_magics[name] = self._make_script_magic(name)
148 148
149 149 def _make_script_magic(self, name):
150 150 """make a named magic, that calls %%script with a particular program"""
151 151 # expand to explicit path if necessary:
152 152 script = self.script_paths.get(name, name)
153 153
154 154 @magic_arguments.magic_arguments()
155 155 @script_args
156 156 def named_script_magic(line, cell):
157 157 # if line, add it as cl-flags
158 158 if line:
159 159 line = "%s %s" % (script, line)
160 160 else:
161 161 line = script
162 162 return self.shebang(line, cell)
163 163
164 164 # write a basic docstring:
165 165 named_script_magic.__doc__ = \
166 166 """%%{name} script magic
167 167
168 168 Run cells with {script} in a subprocess.
169 169
170 170 This is a shortcut for `%%script {script}`
171 171 """.format(**locals())
172 172
173 173 return named_script_magic
174 174
175 175 @magic_arguments.magic_arguments()
176 176 @script_args
177 177 @cell_magic("script")
178 178 def shebang(self, line, cell):
179 179 """Run a cell via a shell command
180 180
181 181 The `%%script` line is like the #! line of script,
182 182 specifying a program (bash, perl, ruby, etc.) with which to run.
183 183
184 184 The rest of the cell is run by that program.
185 185
186 186 Examples
187 187 --------
188 188 ::
189 189
190 190 In [1]: %%script bash
191 191 ...: for i in 1 2 3; do
192 192 ...: echo $i
193 193 ...: done
194 194 1
195 195 2
196 196 3
197 197 """
198 198 argv = arg_split(line, posix = not sys.platform.startswith('win'))
199 199 args, cmd = self.shebang.parser.parse_known_args(argv)
200 200
201 201 p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE)
202 202
203 203 cell = cell.encode('utf8', 'replace')
204 204 if args.bg:
205 205 self.bg_processes.append(p)
206 self._gc_bg_processes()
206 207 if args.out:
207 208 self.shell.user_ns[args.out] = p.stdout
208 209 if args.err:
209 210 self.shell.user_ns[args.err] = p.stderr
210 211 self.job_manager.new(self._run_script, p, cell, daemon=True)
211 212 if args.proc:
212 213 self.shell.user_ns[args.proc] = p
213 214 return
214 215
215 216 try:
216 217 out, err = p.communicate(cell)
217 218 except KeyboardInterrupt:
218 219 try:
219 220 p.send_signal(signal.SIGINT)
220 221 time.sleep(0.1)
221 222 if p.poll() is not None:
222 223 print "Process is interrupted."
223 224 return
224 225 p.terminate()
225 226 time.sleep(0.1)
226 227 if p.poll() is not None:
227 228 print "Process is terminated."
228 229 return
229 230 p.kill()
230 231 print "Process is killed."
231 232 except OSError:
232 233 pass
233 234 except Exception as e:
234 235 print "Error while terminating subprocess (pid=%i): %s" \
235 236 % (p.pid, e)
236 237 return
237 238 out = py3compat.bytes_to_str(out)
238 239 err = py3compat.bytes_to_str(err)
239 240 if args.out:
240 241 self.shell.user_ns[args.out] = out
241 242 else:
242 243 sys.stdout.write(out)
243 244 sys.stdout.flush()
244 245 if args.err:
245 246 self.shell.user_ns[args.err] = err
246 247 else:
247 248 sys.stderr.write(err)
248 249 sys.stderr.flush()
249 250
250 251 def _run_script(self, p, cell):
251 252 """callback for running the script in the background"""
252 253 p.stdin.write(cell)
253 254 p.stdin.close()
254 255 p.wait()
255 256
256 257 @line_magic("killbgscripts")
257 258 def killbgscripts(self, _nouse_=''):
258 259 """Kill all BG processes started by %%script and its family."""
259 260 self.kill_bg_processes()
260 261 print "All background processes were killed."
261 262
262 263 def kill_bg_processes(self):
263 264 """Kill all BG processes which are still running."""
264 265 for p in self.bg_processes:
265 266 if p.poll() is None:
266 267 try:
267 268 p.send_signal(signal.SIGINT)
268 269 except:
269 270 pass
270 271 time.sleep(0.1)
271 272 for p in self.bg_processes:
272 273 if p.poll() is None:
273 274 try:
274 275 p.terminate()
275 276 except:
276 277 pass
277 278 time.sleep(0.1)
278 279 for p in self.bg_processes:
279 280 if p.poll() is None:
280 281 try:
281 282 p.kill()
282 283 except:
283 284 pass
285 self._gc_bg_processes()
286
287 def _gc_bg_processes(self):
288 self.bg_processes = [p for p in self.bg_processes if p.poll() is None]
General Comments 0
You need to be logged in to leave comments. Login now