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