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