_dumbwin32proc.py 13 KB

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