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