| 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 |