diff --git a/tests/test_process.py b/tests/test_process.py index c7be41e..81057ca 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -38,6 +38,30 @@ def __exit__(self, exc_type, exc_val, exc_tb): class _TestProcess: + @unittest.skipUnless(sys.platform == "win32", "Windows startupinfo") + def test_process_startupinfo(self): + async def test(): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + proc = await asyncio.create_subprocess_exec( + sys.executable, + "-c", + "import sys; sys.stdout.write(sys.stdin.read())", + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo, + ) + + out, err = await proc.communicate(b"startupinfo") + self.assertEqual(out, b"startupinfo") + self.assertEqual(err, b"") + self.assertEqual(proc.returncode, 0) + + self.loop.run_until_complete(test()) + def get_num_fds(self): # Winloop comment: use num_handles() as a meaningful # substitute for num_fds() on Windows. @@ -940,6 +964,29 @@ async def test(): class Test_UV_Process(_TestProcess, tb.UVTestCase): + @unittest.skipUnless(sys.platform == "win32", "Windows startupinfo") + def test_process_startupinfo_unsupported_flags(self): + async def test(): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESTDHANDLES + + with self.assertRaisesRegex(ValueError, "startupinfo.dwFlags"): + await asyncio.create_subprocess_exec( + sys.executable, "-c", "pass", startupinfo=startupinfo, + ) + + self.loop.run_until_complete(test()) + + @unittest.skipIf(sys.platform == "win32", "POSIX startupinfo validation") + def test_process_startupinfo_posix(self): + async def test(): + with self.assertRaisesRegex(ValueError, "startupinfo"): + await asyncio.create_subprocess_exec( + sys.executable, "-c", "pass", startupinfo=object(), + ) + + self.loop.run_until_complete(test()) + @unittest.skipIf(sys.platform != "win32", "problem is windows related only") def test_windows_issue_153(self): """Winloop issue 153 is related to bringing back the shlex parser diff --git a/winloop/handles/process.pxd b/winloop/handles/process.pxd index 970abcf..3e57c9e 100644 --- a/winloop/handles/process.pxd +++ b/winloop/handles/process.pxd @@ -25,7 +25,7 @@ cdef class UVProcess(UVHandle): cdef _init(self, Loop loop, list args, dict env, cwd, start_new_session, _stdin, _stdout, _stderr, pass_fds, - debug_flags, preexec_fn, restore_signals) + debug_flags, preexec_fn, restore_signals, startupinfo) cdef _after_fork(self) @@ -33,8 +33,10 @@ cdef class UVProcess(UVHandle): cdef _init_args(self, list args) cdef _init_env(self, dict env) cdef _init_files(self, _stdin, _stdout, _stderr) + cdef _init_startupinfo(self, startupinfo) cdef _init_options(self, list args, dict env, cwd, start_new_session, - _stdin, _stdout, _stderr, bint force_fork) + _stdin, _stdout, _stderr, bint force_fork, + startupinfo) cdef _close_after_spawn(self, int fd) @@ -77,4 +79,4 @@ cdef class UVProcessTransport(UVProcess): _stdin, _stdout, _stderr, pass_fds, waiter, debug_flags, - preexec_fn, restore_signals) + preexec_fn, restore_signals, startupinfo) diff --git a/winloop/handles/process.pyx b/winloop/handles/process.pyx index b82d186..0c70940 100644 --- a/winloop/handles/process.pyx +++ b/winloop/handles/process.pyx @@ -25,7 +25,8 @@ cdef class UVProcess(UVHandle): cdef _init(self, Loop loop, list args, dict env, cwd, start_new_session, _stdin, _stdout, _stderr, # std* can be defined as macros in C - pass_fds, debug_flags, preexec_fn, restore_signals): + pass_fds, debug_flags, preexec_fn, restore_signals, + startupinfo): global __forking @@ -58,7 +59,8 @@ cdef class UVProcess(UVHandle): try: self._init_options(args, env, cwd, start_new_session, - _stdin, _stdout, _stderr, force_fork) + _stdin, _stdout, _stderr, force_fork, + startupinfo) restore_inheritable = set() if pass_fds: @@ -254,10 +256,12 @@ cdef class UVProcess(UVHandle): return ret cdef _init_options(self, list args, dict env, cwd, start_new_session, - _stdin, _stdout, _stderr, bint force_fork): + _stdin, _stdout, _stderr, bint force_fork, + startupinfo): memset(&self.options, 0, sizeof(uv.uv_process_options_t)) + self._init_startupinfo(startupinfo) self._init_env(env) self.options.env = self.uv_opt_env @@ -307,6 +311,30 @@ cdef class UVProcess(UVHandle): self.options.exit_cb = &__uvprocess_on_exit_callback self._init_files(_stdin, _stdout, _stderr) +# Winloop needs startupinfo in order to work with playwright +# SEE: https://github.com/Vizonex/Winloop/issues/34 + cdef _init_startupinfo(self, startupinfo): + if startupinfo is None: + return + if not system.PLATFORM_IS_WINDOWS: + raise ValueError('startupinfo is not supported') + if startupinfo.dwFlags & ~subprocess_STARTF_USESHOWWINDOW: + raise ValueError( + 'startupinfo.dwFlags only supports STARTF_USESHOWWINDOW') + attribute_list = startupinfo.lpAttributeList + if attribute_list and ( + attribute_list.get('handle_list') + or set(attribute_list) != {'handle_list'} + ): + raise ValueError( + 'non-empty startupinfo.lpAttributeList is not supported') + if startupinfo.dwFlags & subprocess_STARTF_USESHOWWINDOW: + if startupinfo.wShowWindow != subprocess_SW_HIDE: + raise ValueError( + 'startupinfo.wShowWindow only supports SW_HIDE') + # Match STARTF_USESHOWWINDOW with SW_HIDE, without changing + # whether the child has a console. + self.options.flags |= uv.UV_PROCESS_WINDOWS_HIDE_GUI cdef _init_args(self, list args): cdef: @@ -644,7 +672,8 @@ cdef class UVProcessTransport(UVProcess): waiter, debug_flags, preexec_fn, - restore_signals): + restore_signals, + startupinfo): cdef UVProcessTransport handle handle = UVProcessTransport.__new__(UVProcessTransport) @@ -656,7 +685,8 @@ cdef class UVProcessTransport(UVProcess): pass_fds, debug_flags, preexec_fn, - restore_signals) + restore_signals, + startupinfo) if handle._init_futs: handle._stdio_ready = 0 diff --git a/winloop/includes/stdlib.pxi b/winloop/includes/stdlib.pxi index 0f38931..eef6211 100644 --- a/winloop/includes/stdlib.pxi +++ b/winloop/includes/stdlib.pxi @@ -165,6 +165,8 @@ cdef threading_main_thread = threading.main_thread cdef int subprocess_PIPE = subprocess.PIPE cdef int subprocess_STDOUT = subprocess.STDOUT cdef int subprocess_DEVNULL = subprocess.DEVNULL +cdef int subprocess_STARTF_USESHOWWINDOW = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) +cdef int subprocess_SW_HIDE = getattr(subprocess, "SW_HIDE", 0) cdef subprocess_SubprocessError = subprocess.SubprocessError cdef int signal_SIGABRT = signal.SIGABRT diff --git a/winloop/includes/uv.pxd b/winloop/includes/uv.pxd index 8e824ce..919f243 100644 --- a/winloop/includes/uv.pxd +++ b/winloop/includes/uv.pxd @@ -477,7 +477,8 @@ cdef extern from "uv.h" nogil: UV_PROCESS_SETGID = 2, UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS = 4, UV_PROCESS_DETACHED = 8, - UV_PROCESS_WINDOWS_HIDE = 16 + UV_PROCESS_WINDOWS_HIDE = 16, + UV_PROCESS_WINDOWS_HIDE_GUI = 64 ctypedef enum uv_stdio_flags: UV_IGNORE = 0x00, diff --git a/winloop/loop.pyx b/winloop/loop.pyx index 47c1791..351fe71 100644 --- a/winloop/loop.pyx +++ b/winloop/loop.pyx @@ -2825,8 +2825,6 @@ cdef class Loop: raise ValueError("universal_newlines must be False") if bufsize != 0: raise ValueError("bufsize must be 0") - if startupinfo is not None: - raise ValueError('startupinfo is not supported') if creationflags != 0: raise ValueError('creationflags is not supported') @@ -2848,7 +2846,8 @@ cdef class Loop: waiter, debug_flags, preexec_fn, - restore_signals) + restore_signals, + startupinfo) try: await waiter