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