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