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