_dumbwin32proc.py 13 KB

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