diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index 2a31213560c92d3..945ac8d99e61c95 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -360,7 +360,7 @@ functions. start_new_session=False, pass_fds=(), *, group=None, \ extra_groups=None, user=None, umask=-1, \ encoding=None, errors=None, text=None, pipesize=-1, \ - process_group=None) + process_group=None, force_hide=False) Execute a child program in a new process. On POSIX, the class uses :meth:`os.execvpe`-like behavior to execute the child program. On Windows, @@ -457,6 +457,10 @@ functions. into the shell (e.g. :command:`dir` or :command:`copy`). You do not need ``shell=True`` to run a batch file or console-based executable. + On Windows, ``force_hide=True`` attempts to start the application without creating + or showing any windows. Some applications may ignore this request, and applications + that are hidden often cannot be used or exited by users. + .. note:: Read the `Security Considerations`_ section before using ``shell=True``. diff --git a/Lib/subprocess.py b/Lib/subprocess.py index d38cc756ec479f2..acd7bd2445a3630 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -964,6 +964,8 @@ class Popen: startupinfo and creationflags (Windows only) + force_hide (Windows only) + restore_signals (POSIX only) start_new_session (POSIX only) @@ -996,7 +998,8 @@ def __init__(self, args, bufsize=-1, executable=None, restore_signals=True, start_new_session=False, pass_fds=(), *, user=None, group=None, extra_groups=None, encoding=None, errors=None, text=None, umask=-1, pipesize=-1, - process_group=None): + process_group=None, + force_hide=False): """Create new Popen instance.""" if not _can_fork_exec: raise OSError( @@ -1041,6 +1044,9 @@ def __init__(self, args, bufsize=-1, executable=None, if creationflags != 0: raise ValueError("creationflags is only supported on Windows " "platforms") + if force_hide: + raise ValueError("force_hide is only supported on Windows " + "platforms") self.args = args self.stdin = None @@ -1218,7 +1224,9 @@ def __init__(self, args, bufsize=-1, executable=None, errread, errwrite, restore_signals, gid, gids, uid, umask, - start_new_session, process_group) + start_new_session, + process_group, + force_hide) except: # Cleanup if the child failed starting. for f in filter(None, (self.stdin, self.stdout, self.stderr)): @@ -1623,7 +1631,8 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, unused_restore_signals, unused_gid, unused_gids, unused_uid, unused_umask, - unused_start_new_session, unused_process_group): + unused_start_new_session, unused_process_group, + force_hide): """Execute program (MS Windows version)""" assert not pass_fds, "pass_fds not supported on Windows." @@ -1687,10 +1696,23 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, # the ones in the handle_list close_fds = False + if force_hide or (shell and not (startupinfo.dwFlags & _winapi.STARTF_USESHOWWINDOW)): + # We pass SW_HIDE to the process so that it will not display any + # window even if it normally would. + startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _winapi.SW_HIDE + + if force_hide and not (creationflags & _winapi.DETACHED_PROCESS): + # If the child process is flagged as SUBSYSTEM_CONSOLE, then + # it either inherits the console of its parent, if the parent + # has one, or allocates a new console. An inherited console may + # be visible even if SW_HIDE is set. By setting the creation + # flag CREATE_NEW_CONSOLE, the child is forced to allocate a + # new console that will have a hidden window. + # Note: CREATE_NEW_CONSOLE cannot be used with DETACHED_PROCESS. + creationflags |= _winapi.CREATE_NEW_CONSOLE + if shell: - if not startupinfo.dwFlags & _winapi.STARTF_USESHOWWINDOW: - startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = _winapi.SW_HIDE if not executable: # gh-101283: without a fully-qualified path, before Windows # checks the system directories, it first looks in the @@ -1998,7 +2020,8 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, errread, errwrite, restore_signals, gid, gids, uid, umask, - start_new_session, process_group): + start_new_session, process_group, + unused_force_hide): """Execute program (POSIX version)""" if isinstance(args, (str, bytes)): diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index fc94b9a972828c1..de33ed19d35cfe3 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -59,6 +59,11 @@ raise unittest.SkipTest("test module requires subprocess") mswindows = (sys.platform == "win32") +if mswindows: + try: + import ctypes + except ImportError: + ctypes = None # # Depends on the following external programs: Python @@ -2593,6 +2598,10 @@ def test_invalid_args(self): [sys.executable, "-c", "import sys; sys.exit(47)"], creationflags=47) + self.assertRaises(ValueError, subprocess.call, + [sys.executable, "-c", + "import sys; sys.exit(47)"], + force_hide=47) def test_shell_sequence(self): # Run command through the shell (sequence) @@ -3818,6 +3827,19 @@ def test_creationflags(self): support.skip_on_low_desktop_heap_memory_subprocess(rc) self.assertEqual(rc, 0) + def test_force_hide(self): + if ctypes: + script = textwrap.dedent(r''' + import sys, ctypes + GetConsoleWindow = ctypes.WinDLL('kernel32').GetConsoleWindow + IsWindowVisible = ctypes.WinDLL('user32').IsWindowVisible + sys.exit(IsWindowVisible(GetConsoleWindow())) + ''') + else: + script = 'import sys; sys.exit(0)' + rc = subprocess.call([sys.executable, '-c', script], force_hide=True) + self.assertEqual(rc, 0) + def test_invalid_args(self): # invalid arguments should raise ValueError self.assertRaises(ValueError, subprocess.call, diff --git a/Misc/NEWS.d/next/Windows/2021-10-29-15-02-46.bpo-30082.VhSkKB.rst b/Misc/NEWS.d/next/Windows/2021-10-29-15-02-46.bpo-30082.VhSkKB.rst new file mode 100644 index 000000000000000..fde87c48500cdfa --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2021-10-29-15-02-46.bpo-30082.VhSkKB.rst @@ -0,0 +1,3 @@ +Adds new ``force_hide`` argument to :mod:`subprocess` functions. +This passes ``SW_HIDE`` to the new process, which most applications +will use to not display any window even if they normally would.