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