##// END OF EJS Templates
jobctrl: use shell if the command line has shell control chars (|><), so that "bzr log | less" etc. work
Ville M. Vainio -
Show More
@@ -1,240 +1,240 b''
1 """ Preliminary "job control" extensions for IPython
1 """ Preliminary "job control" extensions for IPython
2
2
3 requires python 2.4 (or separate 'subprocess' module
3 requires python 2.4 (or separate 'subprocess' module
4
4
5 This provides 2 features, launching background jobs and killing foreground jobs from another IPython instance.
5 This provides 2 features, launching background jobs and killing foreground jobs from another IPython instance.
6
6
7 Launching background jobs:
7 Launching background jobs:
8
8
9 Usage:
9 Usage:
10
10
11 [ipython]|2> import jobctrl
11 [ipython]|2> import jobctrl
12 [ipython]|3> &ls
12 [ipython]|3> &ls
13 <3> <jobctrl.IpyPopen object at 0x00D87FD0>
13 <3> <jobctrl.IpyPopen object at 0x00D87FD0>
14 [ipython]|4> _3.go
14 [ipython]|4> _3.go
15 -----------> _3.go()
15 -----------> _3.go()
16 ChangeLog
16 ChangeLog
17 IPython
17 IPython
18 MANIFEST.in
18 MANIFEST.in
19 README
19 README
20 README_Windows.txt
20 README_Windows.txt
21
21
22 ...
22 ...
23
23
24 Killing foreground tasks:
24 Killing foreground tasks:
25
25
26 Launch IPython instance, run a blocking command:
26 Launch IPython instance, run a blocking command:
27
27
28 [Q:/ipython]|1> import jobctrl
28 [Q:/ipython]|1> import jobctrl
29 [Q:/ipython]|2> cat
29 [Q:/ipython]|2> cat
30
30
31 Now launch a new IPython prompt and kill the process:
31 Now launch a new IPython prompt and kill the process:
32
32
33 IPython 0.8.3.svn.r2919 [on Py 2.5]
33 IPython 0.8.3.svn.r2919 [on Py 2.5]
34 [Q:/ipython]|1> import jobctrl
34 [Q:/ipython]|1> import jobctrl
35 [Q:/ipython]|2> %tasks
35 [Q:/ipython]|2> %tasks
36 6020: 'cat ' (Q:\ipython)
36 6020: 'cat ' (Q:\ipython)
37 [Q:/ipython]|3> %kill
37 [Q:/ipython]|3> %kill
38 SUCCESS: The process with PID 6020 has been terminated.
38 SUCCESS: The process with PID 6020 has been terminated.
39 [Q:/ipython]|4>
39 [Q:/ipython]|4>
40
40
41 (you don't need to specify PID for %kill if only one task is running)
41 (you don't need to specify PID for %kill if only one task is running)
42 """
42 """
43
43
44 from subprocess import *
44 from subprocess import *
45 import os,shlex,sys,time
45 import os,shlex,sys,time
46 import threading,Queue
46 import threading,Queue
47
47
48 from IPython import genutils
48 from IPython import genutils
49
49
50 import IPython.ipapi
50 import IPython.ipapi
51
51
52 if os.name == 'nt':
52 if os.name == 'nt':
53 def kill_process(pid):
53 def kill_process(pid):
54 os.system('taskkill /F /PID %d' % pid)
54 os.system('taskkill /F /PID %d' % pid)
55 else:
55 else:
56 def kill_process(pid):
56 def kill_process(pid):
57 os.system('kill -9 %d' % pid)
57 os.system('kill -9 %d' % pid)
58
58
59
59
60
60
61 class IpyPopen(Popen):
61 class IpyPopen(Popen):
62 def go(self):
62 def go(self):
63 print self.communicate()[0]
63 print self.communicate()[0]
64 def __repr__(self):
64 def __repr__(self):
65 return '<IPython job "%s" PID=%d>' % (self.line, self.pid)
65 return '<IPython job "%s" PID=%d>' % (self.line, self.pid)
66
66
67 def kill(self):
67 def kill(self):
68 kill_process(self.pid)
68 kill_process(self.pid)
69
69
70 def startjob(job):
70 def startjob(job):
71 p = IpyPopen(shlex.split(job), stdout=PIPE, shell = False)
71 p = IpyPopen(shlex.split(job), stdout=PIPE, shell = False)
72 p.line = job
72 p.line = job
73 return p
73 return p
74
74
75 class AsyncJobQ(threading.Thread):
75 class AsyncJobQ(threading.Thread):
76 def __init__(self):
76 def __init__(self):
77 threading.Thread.__init__(self)
77 threading.Thread.__init__(self)
78 self.q = Queue.Queue()
78 self.q = Queue.Queue()
79 self.output = []
79 self.output = []
80 self.stop = False
80 self.stop = False
81 def run(self):
81 def run(self):
82 while 1:
82 while 1:
83 cmd,cwd = self.q.get()
83 cmd,cwd = self.q.get()
84 if self.stop:
84 if self.stop:
85 self.output.append("** Discarding: '%s' - %s" % (cmd,cwd))
85 self.output.append("** Discarding: '%s' - %s" % (cmd,cwd))
86 continue
86 continue
87 self.output.append("** Task started: '%s' - %s" % (cmd,cwd))
87 self.output.append("** Task started: '%s' - %s" % (cmd,cwd))
88
88
89 p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd = cwd)
89 p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT, cwd = cwd)
90 out = p.stdout.read()
90 out = p.stdout.read()
91 self.output.append("** Task complete: '%s'\n" % cmd)
91 self.output.append("** Task complete: '%s'\n" % cmd)
92 self.output.append(out)
92 self.output.append(out)
93
93
94 def add(self,cmd):
94 def add(self,cmd):
95 self.q.put_nowait((cmd, os.getcwd()))
95 self.q.put_nowait((cmd, os.getcwd()))
96
96
97 def dumpoutput(self):
97 def dumpoutput(self):
98 while self.output:
98 while self.output:
99 item = self.output.pop(0)
99 item = self.output.pop(0)
100 print item
100 print item
101
101
102 _jobq = None
102 _jobq = None
103
103
104 def jobqueue_f(self, line):
104 def jobqueue_f(self, line):
105
105
106 global _jobq
106 global _jobq
107 if not _jobq:
107 if not _jobq:
108 print "Starting jobqueue - do '&some_long_lasting_system_command' to enqueue"
108 print "Starting jobqueue - do '&some_long_lasting_system_command' to enqueue"
109 _jobq = AsyncJobQ()
109 _jobq = AsyncJobQ()
110 _jobq.setDaemon(True)
110 _jobq.setDaemon(True)
111 _jobq.start()
111 _jobq.start()
112 ip.jobq = _jobq.add
112 ip.jobq = _jobq.add
113 return
113 return
114 if line.strip() == 'stop':
114 if line.strip() == 'stop':
115 print "Stopping and clearing jobqueue, %jobqueue start to start again"
115 print "Stopping and clearing jobqueue, %jobqueue start to start again"
116 _jobq.stop = True
116 _jobq.stop = True
117 return
117 return
118 if line.strip() == 'start':
118 if line.strip() == 'start':
119 _jobq.stop = False
119 _jobq.stop = False
120 return
120 return
121
121
122 def jobctrl_prefilter_f(self,line):
122 def jobctrl_prefilter_f(self,line):
123 if line.startswith('&'):
123 if line.startswith('&'):
124 pre,fn,rest = self.split_user_input(line[1:])
124 pre,fn,rest = self.split_user_input(line[1:])
125
125
126 line = ip.IP.expand_aliases(fn,rest)
126 line = ip.IP.expand_aliases(fn,rest)
127 if not _jobq:
127 if not _jobq:
128 return '_ip.startjob(%s)' % genutils.make_quoted_expr(line)
128 return '_ip.startjob(%s)' % genutils.make_quoted_expr(line)
129 return '_ip.jobq(%s)' % genutils.make_quoted_expr(line)
129 return '_ip.jobq(%s)' % genutils.make_quoted_expr(line)
130
130
131 raise IPython.ipapi.TryNext
131 raise IPython.ipapi.TryNext
132
132
133 def jobq_output_hook(self):
133 def jobq_output_hook(self):
134 if not _jobq:
134 if not _jobq:
135 return
135 return
136 _jobq.dumpoutput()
136 _jobq.dumpoutput()
137
137
138
138
139
139
140 def job_list(ip):
140 def job_list(ip):
141 keys = ip.db.keys('tasks/*')
141 keys = ip.db.keys('tasks/*')
142 ents = [ip.db[k] for k in keys]
142 ents = [ip.db[k] for k in keys]
143 return ents
143 return ents
144
144
145 def magic_tasks(self,line):
145 def magic_tasks(self,line):
146 """ Show a list of tasks.
146 """ Show a list of tasks.
147
147
148 A 'task' is a process that has been started in IPython when 'jobctrl' extension is enabled.
148 A 'task' is a process that has been started in IPython when 'jobctrl' extension is enabled.
149 Tasks can be killed with %kill.
149 Tasks can be killed with %kill.
150
150
151 '%tasks clear' clears the task list (from stale tasks)
151 '%tasks clear' clears the task list (from stale tasks)
152 """
152 """
153 ip = self.getapi()
153 ip = self.getapi()
154 if line.strip() == 'clear':
154 if line.strip() == 'clear':
155 for k in ip.db.keys('tasks/*'):
155 for k in ip.db.keys('tasks/*'):
156 print "Clearing",ip.db[k]
156 print "Clearing",ip.db[k]
157 del ip.db[k]
157 del ip.db[k]
158 return
158 return
159
159
160 ents = job_list(ip)
160 ents = job_list(ip)
161 if not ents:
161 if not ents:
162 print "No tasks running"
162 print "No tasks running"
163 for pid,cmd,cwd,t in ents:
163 for pid,cmd,cwd,t in ents:
164 dur = int(time.time()-t)
164 dur = int(time.time()-t)
165 print "%d: '%s' (%s) %d:%02d" % (pid,cmd,cwd, dur / 60,dur%60)
165 print "%d: '%s' (%s) %d:%02d" % (pid,cmd,cwd, dur / 60,dur%60)
166
166
167 def magic_kill(self,line):
167 def magic_kill(self,line):
168 """ Kill a task
168 """ Kill a task
169
169
170 Without args, either kill one task (if only one running) or show list (if many)
170 Without args, either kill one task (if only one running) or show list (if many)
171 With arg, assume it's the process id.
171 With arg, assume it's the process id.
172
172
173 %kill is typically (much) more powerful than trying to terminate a process with ctrl+C.
173 %kill is typically (much) more powerful than trying to terminate a process with ctrl+C.
174 """
174 """
175 ip = self.getapi()
175 ip = self.getapi()
176 jobs = job_list(ip)
176 jobs = job_list(ip)
177
177
178 if not line.strip():
178 if not line.strip():
179 if len(jobs) == 1:
179 if len(jobs) == 1:
180 kill_process(jobs[0][0])
180 kill_process(jobs[0][0])
181 else:
181 else:
182 magic_tasks(self,line)
182 magic_tasks(self,line)
183 return
183 return
184
184
185 try:
185 try:
186 pid = int(line)
186 pid = int(line)
187 kill_process(pid)
187 kill_process(pid)
188 except ValueError:
188 except ValueError:
189 magic_tasks(self,line)
189 magic_tasks(self,line)
190
190
191 if sys.platform == 'win32':
191 if sys.platform == 'win32':
192 shell_internal_commands = 'break chcp cls copy ctty date del erase dir md mkdir path prompt rd rmdir time type ver vol'.split()
192 shell_internal_commands = 'break chcp cls copy ctty date del erase dir md mkdir path prompt rd rmdir time type ver vol'.split()
193 else:
193 else:
194 # todo linux commands
194 # todo linux commands
195 shell_internal_commands = []
195 shell_internal_commands = []
196
196
197
197
198 def jobctrl_shellcmd(ip,cmd):
198 def jobctrl_shellcmd(ip,cmd):
199 """ os.system replacement that stores process info to db['tasks/t1234'] """
199 """ os.system replacement that stores process info to db['tasks/t1234'] """
200 cmd = cmd.strip()
200 cmd = cmd.strip()
201 cmdname = cmd.split(None,1)[0]
201 cmdname = cmd.split(None,1)[0]
202 if cmdname in shell_internal_commands:
202 if cmdname in shell_internal_commands or '|' in cmd or '>' in cmd or '<' in cmd:
203 use_shell = True
203 use_shell = True
204 else:
204 else:
205 use_shell = False
205 use_shell = False
206
206
207 jobentry = None
207 jobentry = None
208 try:
208 try:
209 try:
209 try:
210 p = Popen(cmd,shell = use_shell)
210 p = Popen(cmd,shell = use_shell)
211 except WindowsError:
211 except WindowsError:
212 if use_shell:
212 if use_shell:
213 # try with os.system
213 # try with os.system
214 os.system(cmd)
214 os.system(cmd)
215 return
215 return
216 else:
216 else:
217 # have to go via shell, sucks
217 # have to go via shell, sucks
218 p = Popen(cmd,shell = True)
218 p = Popen(cmd,shell = True)
219
219
220 jobentry = 'tasks/t' + str(p.pid)
220 jobentry = 'tasks/t' + str(p.pid)
221 ip.db[jobentry] = (p.pid,cmd,os.getcwd(),time.time())
221 ip.db[jobentry] = (p.pid,cmd,os.getcwd(),time.time())
222 p.communicate()
222 p.communicate()
223
223
224 finally:
224 finally:
225 if jobentry:
225 if jobentry:
226 del ip.db[jobentry]
226 del ip.db[jobentry]
227
227
228
228
229 def install():
229 def install():
230 global ip
230 global ip
231 ip = IPython.ipapi.get()
231 ip = IPython.ipapi.get()
232 # needed to make startjob visible as _ip.startjob('blah')
232 # needed to make startjob visible as _ip.startjob('blah')
233 ip.startjob = startjob
233 ip.startjob = startjob
234 ip.set_hook('input_prefilter', jobctrl_prefilter_f)
234 ip.set_hook('input_prefilter', jobctrl_prefilter_f)
235 ip.set_hook('shell_hook', jobctrl_shellcmd)
235 ip.set_hook('shell_hook', jobctrl_shellcmd)
236 ip.expose_magic('kill',magic_kill)
236 ip.expose_magic('kill',magic_kill)
237 ip.expose_magic('tasks',magic_tasks)
237 ip.expose_magic('tasks',magic_tasks)
238 ip.expose_magic('jobqueue',jobqueue_f)
238 ip.expose_magic('jobqueue',jobqueue_f)
239 ip.set_hook('pre_prompt_hook', jobq_output_hook)
239 ip.set_hook('pre_prompt_hook', jobq_output_hook)
240 install()
240 install()
General Comments 0
You need to be logged in to leave comments. Login now