root / trunk / twisted / internet / _dumbwin32proc.py

Revision 24392, 10.7 kB (checked in by exarkun, 1 year ago)

Merge process-exited-hook-1291-3

Author: exarkun
Reviewer: radix
Fixes: #1291

Add processExited to IProcessProtocol, a callback which is invoked when
a child process exits. This is distinct from IProcessProtocol.processEnded
which is only called after the child process exits and all of the file
descriptors associated with it have been closed.

Line 
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 from twisted.internet._baseprocess import BaseProcess
36
37 def debug(msg):
38     import sys
39     print msg
40     sys.stdout.flush()
41
42 class _Reaper(_pollingfile._PollableResource):
43
44     def __init__(self, proc):
45         self.proc = proc
46
47     def checkWork(self):
48         if win32event.WaitForSingleObject(self.proc.hProcess, 0) != win32event.WAIT_OBJECT_0:
49             return 0
50         exitCode = win32process.GetExitCodeProcess(self.proc.hProcess)
51         self.deactivate()
52         self.proc.processEnded(exitCode)
53         return 0
54
55
56 def _findShebang(filename):
57     """
58     Look for a #! line, and return the value following the #! if one exists, or
59     None if this file is not a script.
60
61     I don't know if there are any conventions for quoting in Windows shebang
62     lines, so this doesn't support any; therefore, you may not pass any
63     arguments to scripts invoked as filters.  That's probably wrong, so if
64     somebody knows more about the cultural expectations on Windows, please feel
65     free to fix.
66
67     This shebang line support was added in support of the CGI tests;
68     appropriately enough, I determined that shebang lines are culturally
69     accepted in the Windows world through this page::
70
71         http://www.cgi101.com/learn/connect/winxp.html
72
73     @param filename: str representing a filename
74
75     @return: a str representing another filename.
76     """
77     f = file(filename, 'ru')
78     if f.read(2) == '#!':
79         exe = f.readline(1024).strip('\n')
80         return exe
81
82 def _invalidWin32App(pywinerr):
83     """
84     Determine if a pywintypes.error is telling us that the given process is
85     'not a valid win32 application', i.e. not a PE format executable.
86
87     @param pywinerr: a pywintypes.error instance raised by CreateProcess
88
89     @return: a boolean
90     """
91
92     # Let's do this better in the future, but I have no idea what this error
93     # is; MSDN doesn't mention it, and there is no symbolic constant in
94     # win32process module that represents 193.
95
96     return pywinerr.args[0] == 193
97
98 class Process(_pollingfile._PollingTimer, BaseProcess):
99     """A process that integrates with the Twisted event loop.
100
101     If your subprocess is a python program, you need to:
102
103      - Run python.exe with the '-u' command line option - this turns on
104        unbuffered I/O. Buffering stdout/err/in can cause problems, see e.g.
105        http://support.microsoft.com/default.aspx?scid=kb;EN-US;q1903
106
107      - If you don't want Windows messing with data passed over
108        stdin/out/err, set the pipes to be in binary mode::
109
110         import os, sys, mscvrt
111         msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
112         msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
113         msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
114
115     """
116     implements(IProcessTransport, IConsumer, IProducer)
117
118     closedNotifies = 0
119
120     def __init__(self, reactor, protocol, command, args, environment, path):
121         _pollingfile._PollingTimer.__init__(self, reactor)
122         BaseProcess.__init__(self, 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.CreateProcess(
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         # set up everything
216         self.stdout = _pollingfile._PollableReadPipe(
217             self.hStdoutR,
218             lambda data: self.proto.childDataReceived(1, data),
219             self.outConnectionLost)
220
221         self.stderr = _pollingfile._PollableReadPipe(
222                 self.hStderrR,
223                 lambda data: self.proto.childDataReceived(2, data),
224                 self.errConnectionLost)
225
226         self.stdin = _pollingfile._PollableWritePipe(
227             self.hStdinW, self.inConnectionLost)
228
229         for pipewatcher in self.stdout, self.stderr, self.stdin:
230             self._addPollableResource(pipewatcher)
231
232
233         # notify protocol
234         self.proto.makeConnection(self)
235
236         self._addPollableResource(_Reaper(self))
237
238
239     def signalProcess(self, signalID):
240         if self.pid is None:
241             raise error.ProcessExitedAlready()
242         if signalID in ("INT", "TERM", "KILL"):
243             win32process.TerminateProcess(self.hProcess, 1)
244
245
246     def _getReason(self, status):
247         if status == 0:
248             return error.ProcessDone(status)
249         return error.ProcessTerminated(status)
250
251
252     def write(self, data):
253         """Write data to the process' stdin."""
254         self.stdin.write(data)
255
256     def writeSequence(self, seq):
257         """Write data to the process' stdin."""
258         self.stdin.writeSequence(seq)
259
260     def closeChildFD(self, fd):
261         if fd == 0:
262             self.closeStdin()
263         elif fd == 1:
264             self.closeStdout()
265         elif fd == 2:
266             self.closeStderr()
267         else:
268             raise NotImplementedError("Only standard-IO file descriptors available on win32")
269
270     def closeStdin(self):
271         """Close the process' stdin.
272         """
273         self.stdin.close()
274
275     def closeStderr(self):
276         self.stderr.close()
277
278     def closeStdout(self):
279         self.stdout.close()
280
281     def loseConnection(self):
282         """Close the process' stdout, in and err."""
283         self.closeStdin()
284         self.closeStdout()
285         self.closeStderr()
286
287
288     def outConnectionLost(self):
289         self.proto.childConnectionLost(1)
290         self.connectionLostNotify()
291
292
293     def errConnectionLost(self):
294         self.proto.childConnectionLost(2)
295         self.connectionLostNotify()
296
297
298     def inConnectionLost(self):
299         self.proto.childConnectionLost(0)
300         self.connectionLostNotify()
301
302
303     def connectionLostNotify(self):
304         """
305         Will be called 3 times, by stdout/err threads and process handle.
306         """
307         self.closedNotifies += 1
308         self.maybeCallProcessEnded()
309
310
311     def maybeCallProcessEnded(self):
312         if self.closedNotifies == 3 and self.lostProcess:
313             BaseProcess.maybeCallProcessEnded(self)
314
315
316     # IConsumer
317     def registerProducer(self, producer, streaming):
318         self.stdin.registerProducer(producer, streaming)
319
320     def unregisterProducer(self):
321         self.stdin.unregisterProducer()
322
323     # IProducer
324     def pauseProducing(self):
325         self._pause()
326
327     def resumeProducing(self):
328         self._unpause()
329
330     def stopProducing(self):
331         self.loseConnection()
332
333
334     def __repr__(self):
335         """
336         Return a string representation of the process.
337         """
338         return "<%s pid=%s>" % (self.__class__.__name__, self.pid)
Note: See TracBrowser for help on using the browser.