1#!/usr/bin/env python3
2# This file is part of Xpra.
3# This code is released under the terms of the CC-BY-SA license:
4# https://creativecommons.org/licenses/by-sa/2.0/legalcode
5# This subprocess.Popen code was found here:
6# https://stackoverflow.com/questions/29566330/
7
8#@PydevCodeAnalysisIgnore
9#pylint: disable=unused-argument
10
11import os
12import subprocess
13import ctypes
14from ctypes import wintypes
15
16kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
17advapi32 = ctypes.WinDLL('advapi32', use_last_error=True)
18
19ERROR_INVALID_HANDLE = 0x0006
20INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
21INVALID_DWORD_VALUE = wintypes.DWORD(-1).value
22
23DEBUG_PROCESS                    = 0x00000001
24DEBUG_ONLY_THIS_PROCESS          = 0x00000002
25CREATE_SUSPENDED                 = 0x00000004
26DETACHED_PROCESS                 = 0x00000008
27CREATE_NEW_CONSOLE               = 0x00000010
28CREATE_NEW_PROCESS_GROUP         = 0x00000200
29CREATE_UNICODE_ENVIRONMENT       = 0x00000400
30CREATE_SEPARATE_WOW_VDM          = 0x00000800
31CREATE_SHARED_WOW_VDM            = 0x00001000
32INHERIT_PARENT_AFFINITY          = 0x00010000
33CREATE_PROTECTED_PROCESS         = 0x00040000
34EXTENDED_STARTUPINFO_PRESENT     = 0x00080000
35CREATE_BREAKAWAY_FROM_JOB        = 0x01000000
36CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000
37CREATE_DEFAULT_ERROR_MODE        = 0x04000000
38CREATE_NO_WINDOW                 = 0x08000000
39
40STARTF_USESHOWWINDOW    = 0x00000001
41STARTF_USESIZE          = 0x00000002
42STARTF_USEPOSITION      = 0x00000004
43STARTF_USECOUNTCHARS    = 0x00000008
44STARTF_USEFILLATTRIBUTE = 0x00000010
45STARTF_RUNFULLSCREEN    = 0x00000020
46STARTF_FORCEONFEEDBACK  = 0x00000040
47STARTF_FORCEOFFFEEDBACK = 0x00000080
48STARTF_USESTDHANDLES    = 0x00000100
49STARTF_USEHOTKEY        = 0x00000200
50STARTF_TITLEISLINKNAME  = 0x00000800
51STARTF_TITLEISAPPID     = 0x00001000
52STARTF_PREVENTPINNING   = 0x00002000
53
54SW_HIDE            = 0
55SW_SHOWNORMAL      = 1
56SW_SHOWMINIMIZED   = 2
57SW_SHOWMAXIMIZED   = 3
58SW_SHOWNOACTIVATE  = 4
59SW_SHOW            = 5
60SW_MINIMIZE        = 6
61SW_SHOWMINNOACTIVE = 7
62SW_SHOWNA          = 8
63SW_RESTORE         = 9
64SW_SHOWDEFAULT     = 10 # ~STARTUPINFO
65SW_FORCEMINIMIZE   = 11
66
67LOGON_WITH_PROFILE        = 0x00000001
68LOGON_NETCREDENTIALS_ONLY = 0x00000002
69
70STD_INPUT_HANDLE  = wintypes.DWORD(-10).value
71STD_OUTPUT_HANDLE = wintypes.DWORD(-11).value
72STD_ERROR_HANDLE  = wintypes.DWORD(-12).value
73
74class HANDLE(wintypes.HANDLE):
75    __slots__ = 'closed',
76
77    def __int__(self):
78        return self.value or 0
79
80    def Detach(self):
81        if not getattr(self, 'closed', False):
82            self.closed = True
83            value = int(self)
84            self.value = None
85            return value
86        raise ValueError("already closed")
87
88    def Close(self, CloseHandle=kernel32.CloseHandle):
89        if self and not getattr(self, 'closed', False):
90            CloseHandle(self.Detach())
91
92    __del__ = Close
93
94    def __repr__(self):
95        return "%s(%d)" % (self.__class__.__name__, int(self))
96
97class PROCESS_INFORMATION(ctypes.Structure):
98    """https://msdn.microsoft.com/en-us/library/ms684873"""
99    __slots__ = '_cached_hProcess', '_cached_hThread'
100
101    _fields_ = (('_hProcess',   HANDLE),
102                ('_hThread',    HANDLE),
103                ('dwProcessId', wintypes.DWORD),
104                ('dwThreadId',  wintypes.DWORD))
105
106    @property
107    def hProcess(self):
108        if not hasattr(self, '_cached_hProcess'):
109            self._cached_hProcess = self._hProcess
110        return self._cached_hProcess
111
112    @property
113    def hThread(self):
114        if not hasattr(self, '_cached_hThread'):
115            self._cached_hThread = self._hThread
116        return self._cached_hThread
117
118    def __del__(self):
119        try:
120            self.hProcess.Close()
121        finally:
122            self.hThread.Close()
123
124LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION)
125
126LPBYTE = ctypes.POINTER(wintypes.BYTE)
127
128class STARTUPINFO(ctypes.Structure):
129    """https://msdn.microsoft.com/en-us/library/ms686331"""
130    _fields_ = (('cb',              wintypes.DWORD),
131                ('lpReserved',      wintypes.LPWSTR),
132                ('lpDesktop',       wintypes.LPWSTR),
133                ('lpTitle',         wintypes.LPWSTR),
134                ('dwX',             wintypes.DWORD),
135                ('dwY',             wintypes.DWORD),
136                ('dwXSize',         wintypes.DWORD),
137                ('dwYSize',         wintypes.DWORD),
138                ('dwXCountChars',   wintypes.DWORD),
139                ('dwYCountChars',   wintypes.DWORD),
140                ('dwFillAttribute', wintypes.DWORD),
141                ('dwFlags',         wintypes.DWORD),
142                ('wShowWindow',     wintypes.WORD),
143                ('cbReserved2',     wintypes.WORD),
144                ('lpReserved2',     LPBYTE),
145                ('hStdInput',       wintypes.HANDLE),
146                ('hStdOutput',      wintypes.HANDLE),
147                ('hStdError',       wintypes.HANDLE))
148
149    def __init__(self, **kwds):
150        self.cb = ctypes.sizeof(self)
151        super().__init__(**kwds)
152
153class PROC_THREAD_ATTRIBUTE_LIST(ctypes.Structure):
154    pass
155
156PPROC_THREAD_ATTRIBUTE_LIST = ctypes.POINTER(PROC_THREAD_ATTRIBUTE_LIST)
157
158class STARTUPINFOEX(STARTUPINFO):
159    _fields_ = (('lpAttributeList', PPROC_THREAD_ATTRIBUTE_LIST),)
160
161LPSTARTUPINFO = ctypes.POINTER(STARTUPINFO)
162LPSTARTUPINFOEX = ctypes.POINTER(STARTUPINFOEX)
163
164class SECURITY_ATTRIBUTES(ctypes.Structure):
165    _fields_ = (('nLength',              wintypes.DWORD),
166                ('lpSecurityDescriptor', wintypes.LPVOID),
167                ('bInheritHandle',       wintypes.BOOL))
168    def __init__(self, **kwds):
169        self.nLength = ctypes.sizeof(self)
170        super().__init__(**kwds)
171
172LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)
173
174class HANDLE_IHV(HANDLE):
175    pass
176
177class DWORD_IDV(wintypes.DWORD):
178    pass
179
180def _check_ihv(result, func, args):
181    if result.value == INVALID_HANDLE_VALUE:
182        raise ctypes.WinError(ctypes.get_last_error())
183    return result.value
184
185def _check_idv(result, func, args):
186    if result.value == INVALID_DWORD_VALUE:
187        raise ctypes.WinError(ctypes.get_last_error())
188    return result.value
189
190def _check_bool(result, func, args):
191    if not result:
192        raise ctypes.WinError(ctypes.get_last_error())
193    return args
194
195def WIN(func, restype, *argtypes):
196    func.restype = restype
197    func.argtypes = argtypes
198    if issubclass(restype, HANDLE_IHV):
199        func.errcheck = _check_ihv
200    elif issubclass(restype, DWORD_IDV):
201        func.errcheck = _check_idv
202    else:
203        func.errcheck = _check_bool
204
205# https://msdn.microsoft.com/en-us/library/ms724211
206WIN(kernel32.CloseHandle, wintypes.BOOL,
207    wintypes.HANDLE,) # _In_ HANDLE hObject
208
209# https://msdn.microsoft.com/en-us/library/ms685086
210WIN(kernel32.ResumeThread, DWORD_IDV,
211    wintypes.HANDLE,) # _In_ hThread
212
213# https://msdn.microsoft.com/en-us/library/ms682425
214WIN(kernel32.CreateProcessW, wintypes.BOOL,
215    wintypes.LPCWSTR,       # _In_opt_    lpApplicationName
216    wintypes.LPWSTR,        # _Inout_opt_ lpCommandLine
217    LPSECURITY_ATTRIBUTES,  # _In_opt_    lpProcessAttributes
218    LPSECURITY_ATTRIBUTES,  # _In_opt_    lpThreadAttributes
219    wintypes.BOOL,          # _In_        bInheritHandles
220    wintypes.DWORD,         # _In_        dwCreationFlags
221    wintypes.LPCWSTR,       # _In_opt_    lpEnvironment
222    wintypes.LPCWSTR,       # _In_opt_    lpCurrentDirectory
223    LPSTARTUPINFO,          # _In_        lpStartupInfo
224    LPPROCESS_INFORMATION)  # _Out_       lpProcessInformation
225
226# https://msdn.microsoft.com/en-us/library/ms682429
227WIN(advapi32.CreateProcessAsUserW, wintypes.BOOL,
228    wintypes.HANDLE,        # _In_opt_    hToken
229    wintypes.LPCWSTR,       # _In_opt_    lpApplicationName
230    wintypes.LPWSTR,        # _Inout_opt_ lpCommandLine
231    LPSECURITY_ATTRIBUTES,  # _In_opt_    lpProcessAttributes
232    LPSECURITY_ATTRIBUTES,  # _In_opt_    lpThreadAttributes
233    wintypes.BOOL,          # _In_        bInheritHandles
234    wintypes.DWORD,         # _In_        dwCreationFlags
235    wintypes.LPCWSTR,       # _In_opt_    lpEnvironment
236    wintypes.LPCWSTR,       # _In_opt_    lpCurrentDirectory
237    LPSTARTUPINFO,          # _In_        lpStartupInfo
238    LPPROCESS_INFORMATION)  # _Out_       lpProcessInformation
239
240# https://msdn.microsoft.com/en-us/library/ms682434
241WIN(advapi32.CreateProcessWithTokenW, wintypes.BOOL,
242    wintypes.HANDLE,        # _In_        hToken
243    wintypes.DWORD,         # _In_        dwLogonFlags
244    wintypes.LPCWSTR,       # _In_opt_    lpApplicationName
245    wintypes.LPWSTR,        # _Inout_opt_ lpCommandLine
246    wintypes.DWORD,         # _In_        dwCreationFlags
247    wintypes.LPCWSTR,       # _In_opt_    lpEnvironment
248    wintypes.LPCWSTR,       # _In_opt_    lpCurrentDirectory
249    LPSTARTUPINFO,          # _In_        lpStartupInfo
250    LPPROCESS_INFORMATION)  # _Out_       lpProcessInformation
251
252# https://msdn.microsoft.com/en-us/library/ms682431
253WIN(advapi32.CreateProcessWithLogonW, wintypes.BOOL,
254    wintypes.LPCWSTR,       # _In_        lpUsername
255    wintypes.LPCWSTR,       # _In_opt_    lpDomain
256    wintypes.LPCWSTR,       # _In_        lpPassword
257    wintypes.DWORD,         # _In_        dwLogonFlags
258    wintypes.LPCWSTR,       # _In_opt_    lpApplicationName
259    wintypes.LPWSTR,        # _Inout_opt_ lpCommandLine
260    wintypes.DWORD,         # _In_        dwCreationFlags
261    wintypes.LPCWSTR,       # _In_opt_    lpEnvironment
262    wintypes.LPCWSTR,       # _In_opt_    lpCurrentDirectory
263    LPSTARTUPINFO,          # _In_        lpStartupInfo
264    LPPROCESS_INFORMATION)  # _Out_       lpProcessInformation
265
266
267CREATION_TYPE_NORMAL = 0
268CREATION_TYPE_LOGON  = 1
269CREATION_TYPE_TOKEN  = 2
270CREATION_TYPE_USER   = 3
271
272class CREATIONINFO:
273    __slots__ = ('dwCreationType',
274        'lpApplicationName', 'lpCommandLine', 'bUseShell',
275        'lpProcessAttributes', 'lpThreadAttributes', 'bInheritHandles',
276        'dwCreationFlags', 'lpEnvironment', 'lpCurrentDirectory',
277        'hToken', 'lpUsername', 'lpDomain', 'lpPassword', 'dwLogonFlags')
278
279    def __init__(self, dwCreationType=CREATION_TYPE_NORMAL,
280                 lpApplicationName=None, lpCommandLine=None, bUseShell=False,
281                 lpProcessAttributes=None, lpThreadAttributes=None,
282                 bInheritHandles=False, dwCreationFlags=0, lpEnvironment=None,
283                 lpCurrentDirectory=None, hToken=None, dwLogonFlags=0,
284                 lpUsername=None, lpDomain=None, lpPassword=None):
285        self.dwCreationType = dwCreationType
286        self.lpApplicationName = lpApplicationName
287        self.lpCommandLine = lpCommandLine
288        self.bUseShell = bUseShell
289        self.lpProcessAttributes = lpProcessAttributes
290        self.lpThreadAttributes = lpThreadAttributes
291        self.bInheritHandles = bInheritHandles
292        self.dwCreationFlags = dwCreationFlags
293        self.lpEnvironment = lpEnvironment
294        self.lpCurrentDirectory = lpCurrentDirectory
295        self.hToken = hToken
296        self.lpUsername = lpUsername
297        self.lpDomain = lpDomain
298        self.lpPassword = lpPassword
299        self.dwLogonFlags = dwLogonFlags
300
301def create_environment(environ):
302    if environ is not None:
303        items = ['%s=%s' % (k, environ[k]) for k in sorted(environ)]
304        buf = '\x00'.join(items)
305        length = len(buf) + 2 if buf else 1
306        return ctypes.create_unicode_buffer(buf, length)
307
308def create_process(commandline=None, creationinfo=None, startupinfo=None):
309    if creationinfo is None:
310        creationinfo = CREATIONINFO()
311
312    if startupinfo is None:
313        startupinfo = STARTUPINFO()
314    elif isinstance(startupinfo, subprocess.STARTUPINFO):
315        startupinfo = STARTUPINFO(dwFlags=startupinfo.dwFlags,
316                        hStdInput=startupinfo.hStdInput,
317                        hStdOutput=startupinfo.hStdOutput,
318                        hStdError=startupinfo.hStdError,
319                        wShowWindow=startupinfo.wShowWindow)
320
321    si, ci, pi = startupinfo, creationinfo, PROCESS_INFORMATION()
322
323    if commandline is None:
324        commandline = ci.lpCommandLine
325
326    if commandline is not None:
327        if ci.bUseShell:
328            si.dwFlags |= STARTF_USESHOWWINDOW
329            si.wShowWindow = SW_HIDE
330            comspec = os.environ.get("ComSpec", os.path.join(
331                        os.environ["SystemRoot"], "System32", "cmd.exe"))
332            commandline = '"{}" /c "{}"'.format(comspec, commandline)
333        commandline = ctypes.create_unicode_buffer(commandline)
334
335    dwCreationFlags = ci.dwCreationFlags | CREATE_UNICODE_ENVIRONMENT
336    lpEnvironment = create_environment(ci.lpEnvironment)
337
338    if (dwCreationFlags & DETACHED_PROCESS and
339       ((dwCreationFlags & CREATE_NEW_CONSOLE) or
340        (ci.dwCreationType == CREATION_TYPE_LOGON) or
341        (ci.dwCreationType == CREATION_TYPE_TOKEN))):
342        raise RuntimeError('DETACHED_PROCESS is incompatible with '
343                           'CREATE_NEW_CONSOLE, which is implied for '
344                           'the logon and token creation types')
345
346    if ci.dwCreationType == CREATION_TYPE_NORMAL:
347
348        kernel32.CreateProcessW(
349            ci.lpApplicationName, commandline,
350            ci.lpProcessAttributes, ci.lpThreadAttributes, ci.bInheritHandles,
351            dwCreationFlags, lpEnvironment, ci.lpCurrentDirectory,
352            ctypes.byref(si), ctypes.byref(pi))
353
354    elif ci.dwCreationType == CREATION_TYPE_LOGON:
355
356        advapi32.CreateProcessWithLogonW(
357            ci.lpUsername, ci.lpDomain, ci.lpPassword, ci.dwLogonFlags,
358            ci.lpApplicationName, commandline,
359            dwCreationFlags, lpEnvironment, ci.lpCurrentDirectory,
360            ctypes.byref(si), ctypes.byref(pi))
361
362    elif ci.dwCreationType == CREATION_TYPE_TOKEN:
363
364        advapi32.CreateProcessWithTokenW(
365            ci.hToken, ci.dwLogonFlags,
366            ci.lpApplicationName, commandline,
367            dwCreationFlags, lpEnvironment, ci.lpCurrentDirectory,
368            ctypes.byref(si), ctypes.byref(pi))
369
370    elif ci.dwCreationType == CREATION_TYPE_USER:
371
372        advapi32.CreateProcessAsUserW(
373            ci.hToken,
374            ci.lpApplicationName, commandline,
375            ci.lpProcessAttributes, ci.lpThreadAttributes, ci.bInheritHandles,
376            dwCreationFlags, lpEnvironment, ci.lpCurrentDirectory,
377            ctypes.byref(si), ctypes.byref(pi))
378
379    else:
380        raise ValueError('invalid process creation type')
381
382    return pi
383
384
385class Popen(subprocess.Popen):
386    def __init__(self, *args, **kwds):
387        ci = self._creationinfo = kwds.pop('creationinfo', CREATIONINFO())
388        if kwds.pop('suspended', False):
389            ci.dwCreationFlags |= CREATE_SUSPENDED
390        self._child_started = False
391        super().__init__(*args, **kwds)
392
393    def _execute_child(self, args, executable, preexec_fn, close_fds,
394                       pass_fds, cwd, env, startupinfo, creationflags,
395                       shell, p2cread, p2cwrite, c2pread, c2pwrite, errread,
396                       errwrite, restore_signals, start_new_session):
397        """Execute program (MS Windows version)"""
398        assert not pass_fds, "pass_fds not supported on Windows."
399        commandline = (args if isinstance(args, str) else
400                       subprocess.list2cmdline(args))
401        self._common_execute_child(executable, commandline, shell,
402                close_fds, creationflags, env, cwd,
403                startupinfo, p2cread, c2pwrite, errwrite)
404
405    def _common_execute_child(self, executable, commandline, shell,
406                              close_fds, creationflags, env, cwd,
407                              startupinfo, p2cread, c2pwrite, errwrite,
408                              to_close=()):
409
410        ci = self._creationinfo
411        if executable is not None:
412            ci.lpApplicationName = executable
413        if commandline:
414            ci.lpCommandLine = commandline
415        if shell:
416            ci.bUseShell = shell
417        if not close_fds:
418            ci.bInheritHandles = int(not close_fds)
419        if creationflags:
420            ci.dwCreationFlags |= creationflags
421        if env is not None:
422            ci.lpEnvironment = env
423        if cwd is not None:
424            ci.lpCurrentDirectory = cwd
425
426        if startupinfo is None:
427            startupinfo = STARTUPINFO()
428        si = self._startupinfo = startupinfo
429
430        default = -1
431        if default not in (p2cread, c2pwrite, errwrite):
432            si.dwFlags |= STARTF_USESTDHANDLES
433            si.hStdInput  = int( p2cread)
434            si.hStdOutput = int(c2pwrite)
435            si.hStdError  = int(errwrite)
436
437        try:
438            pi = create_process(creationinfo=ci, startupinfo=si)
439        finally:
440            if p2cread != -1:
441                p2cread.Close()
442            if c2pwrite != -1:
443                c2pwrite.Close()
444            if errwrite != -1:
445                errwrite.Close()
446            if hasattr(self, '_devnull'):
447                os.close(self._devnull)  #pylint: disable=no-member
448
449        if not ci.dwCreationFlags & CREATE_SUSPENDED:
450            self._child_started = True
451
452        # Retain the process handle, but close the thread handle
453        # if it's no longer needed.
454        self._processinfo = pi
455        self._handle = pi.hProcess.Detach()
456        self.pid = pi.dwProcessId
457        if self._child_started:
458            pi.hThread.Close()
459
460    def start(self):
461        if self._child_started:
462            raise RuntimeError("processes can only be started once")
463        hThread = self._processinfo.hThread
464        prev_count = kernel32.ResumeThread(hThread)
465        if prev_count > 1:
466            for _ in range(1, prev_count):
467                if kernel32.ResumeThread(hThread) <= 1:
468                    break
469            else:
470                raise RuntimeError('cannot start the main thread')
471        # The thread's previous suspend count was 0 or 1,
472        # so it should be running now.
473        self._child_started = True
474        hThread.Close()
475
476    def __del__(self):
477        if not self._child_started:
478            try:
479                if hasattr(self, '_processinfo'):
480                    self._processinfo.hThread.Close()
481            finally:
482                if hasattr(self, '_handle'):
483                    self.terminate()
484        super().__del__()        #pylint: disable=no-member
485