OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.test.test_process -*- | |
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 """ | |
6 http://isometric.sixsided.org/_/gates_in_the_head/ | |
7 """ | |
8 | |
9 import os | |
10 | |
11 # Win32 imports | |
12 import win32api | |
13 import win32con | |
14 import win32event | |
15 import win32file | |
16 import win32pipe | |
17 import win32process | |
18 import win32security | |
19 | |
20 import pywintypes | |
21 | |
22 # security attributes for pipes | |
23 PIPE_ATTRS_INHERITABLE = win32security.SECURITY_ATTRIBUTES() | |
24 PIPE_ATTRS_INHERITABLE.bInheritHandle = 1 | |
25 | |
26 from zope.interface import implements | |
27 from twisted.internet.interfaces import IProcessTransport, IConsumer, IProducer | |
28 | |
29 from twisted.python.win32 import quoteArguments | |
30 | |
31 from twisted.internet import error | |
32 from twisted.python import failure | |
33 | |
34 from twisted.internet import _pollingfile | |
35 | |
36 def debug(msg): | |
37 import sys | |
38 print msg | |
39 sys.stdout.flush() | |
40 | |
41 class _Reaper(_pollingfile._PollableResource): | |
42 | |
43 def __init__(self, proc): | |
44 self.proc = proc | |
45 | |
46 def checkWork(self): | |
47 if win32event.WaitForSingleObject(self.proc.hProcess, 0) != win32event.W
AIT_OBJECT_0: | |
48 return 0 | |
49 exitCode = win32process.GetExitCodeProcess(self.proc.hProcess) | |
50 self.deactivate() | |
51 self.proc.processEnded(exitCode) | |
52 return 0 | |
53 | |
54 | |
55 def _findShebang(filename): | |
56 """ | |
57 Look for a #! line, and return the value following the #! if one exists, or | |
58 None if this file is not a script. | |
59 | |
60 I don't know if there are any conventions for quoting in Windows shebang | |
61 lines, so this doesn't support any; therefore, you may not pass any | |
62 arguments to scripts invoked as filters. That's probably wrong, so if | |
63 somebody knows more about the cultural expectations on Windows, please feel | |
64 free to fix. | |
65 | |
66 This shebang line support was added in support of the CGI tests; | |
67 appropriately enough, I determined that shebang lines are culturally | |
68 accepted in the Windows world through this page: | |
69 | |
70 http://www.cgi101.com/learn/connect/winxp.html | |
71 | |
72 @param filename: str representing a filename | |
73 | |
74 @return: a str representing another filename. | |
75 """ | |
76 f = file(filename, 'ru') | |
77 if f.read(2) == '#!': | |
78 exe = f.readline(1024).strip('\n') | |
79 return exe | |
80 | |
81 def _invalidWin32App(pywinerr): | |
82 """ | |
83 Determine if a pywintypes.error is telling us that the given process is | |
84 'not a valid win32 application', i.e. not a PE format executable. | |
85 | |
86 @param pywinerr: a pywintypes.error instance raised by CreateProcess | |
87 | |
88 @return: a boolean | |
89 """ | |
90 | |
91 # Let's do this better in the future, but I have no idea what this error | |
92 # is; MSDN doesn't mention it, and there is no symbolic constant in | |
93 # win32process module that represents 193. | |
94 | |
95 return pywinerr.args[0] == 193 | |
96 | |
97 class Process(_pollingfile._PollingTimer): | |
98 """A process that integrates with the Twisted event loop. | |
99 | |
100 If your subprocess is a python program, you need to: | |
101 | |
102 - Run python.exe with the '-u' command line option - this turns on | |
103 unbuffered I/O. Buffering stdout/err/in can cause problems, see e.g. | |
104 http://support.microsoft.com/default.aspx?scid=kb;EN-US;q1903 | |
105 | |
106 - If you don't want Windows messing with data passed over | |
107 stdin/out/err, set the pipes to be in binary mode:: | |
108 | |
109 import os, sys, mscvrt | |
110 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) | |
111 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) | |
112 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) | |
113 | |
114 """ | |
115 implements(IProcessTransport, IConsumer, IProducer) | |
116 | |
117 buffer = '' | |
118 pid = None | |
119 | |
120 def __init__(self, reactor, protocol, command, args, environment, path): | |
121 _pollingfile._PollingTimer.__init__(self, reactor) | |
122 self.protocol = protocol | |
123 | |
124 # security attributes for pipes | |
125 sAttrs = win32security.SECURITY_ATTRIBUTES() | |
126 sAttrs.bInheritHandle = 1 | |
127 | |
128 # create the pipes which will connect to the secondary process | |
129 self.hStdoutR, hStdoutW = win32pipe.CreatePipe(sAttrs, 0) | |
130 self.hStderrR, hStderrW = win32pipe.CreatePipe(sAttrs, 0) | |
131 hStdinR, self.hStdinW = win32pipe.CreatePipe(sAttrs, 0) | |
132 | |
133 win32pipe.SetNamedPipeHandleState(self.hStdinW, | |
134 win32pipe.PIPE_NOWAIT, | |
135 None, | |
136 None) | |
137 | |
138 # set the info structure for the new process. | |
139 StartupInfo = win32process.STARTUPINFO() | |
140 StartupInfo.hStdOutput = hStdoutW | |
141 StartupInfo.hStdError = hStderrW | |
142 StartupInfo.hStdInput = hStdinR | |
143 StartupInfo.dwFlags = win32process.STARTF_USESTDHANDLES | |
144 | |
145 # Create new handles whose inheritance property is false | |
146 currentPid = win32api.GetCurrentProcess() | |
147 | |
148 tmp = win32api.DuplicateHandle(currentPid, self.hStdoutR, currentPid, 0,
0, | |
149 win32con.DUPLICATE_SAME_ACCESS) | |
150 win32file.CloseHandle(self.hStdoutR) | |
151 self.hStdoutR = tmp | |
152 | |
153 tmp = win32api.DuplicateHandle(currentPid, self.hStderrR, currentPid, 0,
0, | |
154 win32con.DUPLICATE_SAME_ACCESS) | |
155 win32file.CloseHandle(self.hStderrR) | |
156 self.hStderrR = tmp | |
157 | |
158 tmp = win32api.DuplicateHandle(currentPid, self.hStdinW, currentPid, 0,
0, | |
159 win32con.DUPLICATE_SAME_ACCESS) | |
160 win32file.CloseHandle(self.hStdinW) | |
161 self.hStdinW = tmp | |
162 | |
163 # Add the specified environment to the current environment - this is | |
164 # necessary because certain operations are only supported on Windows | |
165 # if certain environment variables are present. | |
166 | |
167 env = os.environ.copy() | |
168 env.update(environment or {}) | |
169 | |
170 cmdline = quoteArguments(args) | |
171 # TODO: error detection here. | |
172 def doCreate(): | |
173 self.hProcess, self.hThread, self.pid, dwTid = win32process.CreatePr
ocess( | |
174 command, cmdline, None, None, 1, 0, env, path, StartupInfo) | |
175 try: | |
176 doCreate() | |
177 except pywintypes.error, pwte: | |
178 if not _invalidWin32App(pwte): | |
179 # This behavior isn't _really_ documented, but let's make it | |
180 # consistent with the behavior that is documented. | |
181 raise OSError(pwte) | |
182 else: | |
183 # look for a shebang line. Insert the original 'command' | |
184 # (actually a script) into the new arguments list. | |
185 sheb = _findShebang(command) | |
186 if sheb is None: | |
187 raise OSError( | |
188 "%r is neither a Windows executable, " | |
189 "nor a script with a shebang line" % command) | |
190 else: | |
191 args = list(args) | |
192 args.insert(0, command) | |
193 cmdline = quoteArguments(args) | |
194 origcmd = command | |
195 command = sheb | |
196 try: | |
197 # Let's try again. | |
198 doCreate() | |
199 except pywintypes.error, pwte2: | |
200 # d'oh, failed again! | |
201 if _invalidWin32App(pwte2): | |
202 raise OSError( | |
203 "%r has an invalid shebang line: " | |
204 "%r is not a valid executable" % ( | |
205 origcmd, sheb)) | |
206 raise OSError(pwte2) | |
207 | |
208 win32file.CloseHandle(self.hThread) | |
209 | |
210 # close handles which only the child will use | |
211 win32file.CloseHandle(hStderrW) | |
212 win32file.CloseHandle(hStdoutW) | |
213 win32file.CloseHandle(hStdinR) | |
214 | |
215 self.closed = 0 | |
216 self.closedNotifies = 0 | |
217 | |
218 # set up everything | |
219 self.stdout = _pollingfile._PollableReadPipe( | |
220 self.hStdoutR, | |
221 lambda data: self.protocol.childDataReceived(1, data), | |
222 self.outConnectionLost) | |
223 | |
224 self.stderr = _pollingfile._PollableReadPipe( | |
225 self.hStderrR, | |
226 lambda data: self.protocol.childDataReceived(2, data), | |
227 self.errConnectionLost) | |
228 | |
229 self.stdin = _pollingfile._PollableWritePipe( | |
230 self.hStdinW, self.inConnectionLost) | |
231 | |
232 for pipewatcher in self.stdout, self.stderr, self.stdin: | |
233 self._addPollableResource(pipewatcher) | |
234 | |
235 | |
236 # notify protocol | |
237 self.protocol.makeConnection(self) | |
238 | |
239 # (maybe?) a good idea in win32er, otherwise not | |
240 # self.reactor.addEvent(self.hProcess, self, 'inConnectionLost') | |
241 | |
242 | |
243 def signalProcess(self, signalID): | |
244 if self.pid is None: | |
245 raise error.ProcessExitedAlready() | |
246 if signalID in ("INT", "TERM", "KILL"): | |
247 os.popen('taskkill /T /F /PID %s' % self.pid) | |
248 | |
249 def processEnded(self, status): | |
250 """ | |
251 This is called when the child terminates. | |
252 """ | |
253 self.pid = None | |
254 if status == 0: | |
255 err = error.ProcessDone(status) | |
256 else: | |
257 err = error.ProcessTerminated(status) | |
258 self.protocol.processEnded(failure.Failure(err)) | |
259 | |
260 | |
261 def write(self, data): | |
262 """Write data to the process' stdin.""" | |
263 self.stdin.write(data) | |
264 | |
265 def writeSequence(self, seq): | |
266 """Write data to the process' stdin.""" | |
267 self.stdin.writeSequence(seq) | |
268 | |
269 def closeChildFD(self, fd): | |
270 if fd == 0: | |
271 self.closeStdin() | |
272 elif fd == 1: | |
273 self.closeStdout() | |
274 elif fd == 2: | |
275 self.closeStderr() | |
276 else: | |
277 raise NotImplementedError("Only standard-IO file descriptors availab
le on win32") | |
278 | |
279 def closeStdin(self): | |
280 """Close the process' stdin. | |
281 """ | |
282 self.stdin.close() | |
283 | |
284 def closeStderr(self): | |
285 self.stderr.close() | |
286 | |
287 def closeStdout(self): | |
288 self.stdout.close() | |
289 | |
290 def loseConnection(self): | |
291 """Close the process' stdout, in and err.""" | |
292 self.closeStdin() | |
293 self.closeStdout() | |
294 self.closeStderr() | |
295 | |
296 def outConnectionLost(self): | |
297 self.protocol.childConnectionLost(1) | |
298 self.connectionLostNotify() | |
299 | |
300 def errConnectionLost(self): | |
301 self.protocol.childConnectionLost(2) | |
302 self.connectionLostNotify() | |
303 | |
304 def inConnectionLost(self): | |
305 self.protocol.childConnectionLost(0) | |
306 self.connectionLostNotify() | |
307 | |
308 def connectionLostNotify(self): | |
309 """Will be called 3 times, by stdout/err threads and process handle.""" | |
310 self.closedNotifies = self.closedNotifies + 1 | |
311 if self.closedNotifies == 3: | |
312 self.closed = 1 | |
313 self._addPollableResource(_Reaper(self)) | |
314 | |
315 # IConsumer | |
316 def registerProducer(self, producer, streaming): | |
317 self.stdin.registerProducer(producer, streaming) | |
318 | |
319 def unregisterProducer(self): | |
320 self.stdin.unregisterProducer() | |
321 | |
322 # IProducer | |
323 def pauseProducing(self): | |
324 self._pause() | |
325 | |
326 def resumeProducing(self): | |
327 self._unpause() | |
328 | |
329 def stopProducing(self): | |
330 self.loseConnection() | |
331 | |
332 | |
333 def __repr__(self): | |
334 """ | |
335 Return a string representation of the process. | |
336 """ | |
337 return "<%s pid=%s>" % (self.__class__.__name__, self.pid) | |
OLD | NEW |