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