diff --git a/synodic_client/application/bootstrap.py b/synodic_client/application/bootstrap.py index 1a73e47..0eed0ff 100644 --- a/synodic_client/application/bootstrap.py +++ b/synodic_client/application/bootstrap.py @@ -17,6 +17,7 @@ from synodic_client.config import set_dev_mode from synodic_client.logging import configure_logging +from synodic_client.subprocess_patch import apply as _apply_subprocess_patch from synodic_client.protocol import extract_uri_from_args from synodic_client.updater import initialize_velopack @@ -24,6 +25,7 @@ _dev_mode = '--dev' in sys.argv[1:] _debug = '--debug' in sys.argv[1:] set_dev_mode(_dev_mode) +_apply_subprocess_patch() configure_logging(debug=_debug) initialize_velopack() diff --git a/synodic_client/application/qt.py b/synodic_client/application/qt.py index af5528d..54f9106 100644 --- a/synodic_client/application/qt.py +++ b/synodic_client/application/qt.py @@ -31,6 +31,7 @@ resolve_config, resolve_update_config, ) +from synodic_client.subprocess_patch import apply as _apply_subprocess_patch from synodic_client.updater import initialize_velopack @@ -72,6 +73,23 @@ def _process_uri(uri: str, handler: Callable[[str], None]) -> None: handler(manifests[0]) +def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None: + """Cancel every pending asyncio task on *loop*. + + Called synchronously from the ``aboutToQuit`` handler. Each task + receives a cancellation request; when the event loop processes its + remaining iterations the ``CancelledError`` propagates and the + tasks finish cleanly. + """ + _logger = logging.getLogger(__name__) + pending = [t for t in asyncio.all_tasks(loop) if not t.done()] + if not pending: + return + _logger.info('Cancelling %d pending async task(s)', len(pending)) + for task in pending: + task.cancel() + + def _install_exception_hook(logger: logging.Logger) -> None: """Redirect unhandled exceptions to the log file. @@ -163,6 +181,7 @@ def application(*, uri: str | None = None, dev_mode: bool = False, debug: bool = """ # Activate dev-mode namespacing before anything reads config paths. set_dev_mode(dev_mode) + _apply_subprocess_patch() # Configure logging before Velopack so install/uninstall hooks and # first-run diagnostics are captured in the log file. @@ -221,6 +240,18 @@ def _handle_install_uri(manifest_url: str) -> None: if uri: _process_uri(uri, _handle_install_uri) + # --- Graceful shutdown --- + # aboutToQuit fires synchronously when app.quit() is called but + # before the event loop stops, giving us a window to cancel + # in-flight async tasks and stop timers. + + def _on_about_to_quit() -> None: + logger.info('Application shutting down — cancelling async tasks') + _tray.shutdown() + _cancel_all_tasks(loop) + + app.aboutToQuit.connect(_on_about_to_quit) + # qasync integrates the asyncio event loop with Qt's event loop, # enabling async/await usage in the GUI layer without dedicated threads. with loop: diff --git a/synodic_client/application/screen/projects.py b/synodic_client/application/screen/projects.py index b366cbc..ebf6e12 100644 --- a/synodic_client/application/screen/projects.py +++ b/synodic_client/application/screen/projects.py @@ -22,7 +22,7 @@ from synodic_client.application.screen.install import SetupPreviewWidget from synodic_client.application.screen.schema import PreviewPhase from synodic_client.application.screen.sidebar import ManifestSidebar -from synodic_client.application.screen.spinner import SpinnerWidget +from synodic_client.application.screen.spinner import LoadingIndicator from synodic_client.application.theme import COMPACT_MARGINS from synodic_client.resolution import ResolvedConfig @@ -91,9 +91,10 @@ def _init_ui(self) -> None: self._empty_placeholder.setStyleSheet('color: grey; font-size: 13px;') self._stack.addWidget(self._empty_placeholder) - outer.addLayout(right, stretch=1) + self._loading_indicator = LoadingIndicator('Loading projects\u2026') + self._stack.addWidget(self._loading_indicator) - self._loading_spinner = SpinnerWidget('Loading projects\u2026', parent=self) + outer.addLayout(right, stretch=1) # --- Public API --- @@ -106,7 +107,8 @@ def refresh(self) -> None: async def _async_refresh(self) -> None: """Refresh the sidebar and stacked widgets from the porringer cache.""" self._refresh_in_progress = True - self._loading_spinner.start() + self._loading_indicator.start() + self._stack.setCurrentWidget(self._loading_indicator) self._sidebar.set_enabled(False) try: @@ -167,7 +169,7 @@ async def _async_refresh(self) -> None: except Exception: logger.exception('Failed to refresh projects') finally: - self._loading_spinner.stop() + self._loading_indicator.stop() self._sidebar.set_enabled(True) self._refresh_in_progress = False diff --git a/synodic_client/application/screen/screen.py b/synodic_client/application/screen/screen.py index a41aa80..943885d 100644 --- a/synodic_client/application/screen/screen.py +++ b/synodic_client/application/screen/screen.py @@ -50,7 +50,7 @@ ProjectInstance, RefreshData, ) -from synodic_client.application.screen.spinner import SpinnerWidget +from synodic_client.application.screen.spinner import LoadingIndicator from synodic_client.application.screen.update_banner import UpdateBanner from synodic_client.application.theme import ( COMPACT_MARGINS, @@ -197,7 +197,8 @@ def _init_ui(self) -> None: self._scroll.setWidget(self._container) outer.addWidget(self._scroll) - self._loading_spinner = SpinnerWidget('Loading tools\u2026', parent=self) + self._loading_indicator = LoadingIndicator('Loading tools\u2026') + outer.addWidget(self._loading_indicator) # Periodic timer to refresh relative timestamps (every 60s) self._timestamp_timer = QTimer(self) @@ -224,10 +225,10 @@ def _build_toolbar(self) -> QHBoxLayout: toolbar.addWidget(check_btn) self._check_btn = check_btn - update_all_btn = QPushButton('Update All') - update_all_btn.setToolTip('Upgrade all auto-update-enabled plugins now') - update_all_btn.clicked.connect(self.update_all_requested.emit) - toolbar.addWidget(update_all_btn) + self._update_all_btn = QPushButton('Update All') + self._update_all_btn.setToolTip('Upgrade all auto-update-enabled plugins now') + self._update_all_btn.clicked.connect(self.update_all_requested.emit) + toolbar.addWidget(self._update_all_btn) return toolbar @@ -248,7 +249,10 @@ async def _async_refresh(self) -> None: background task so the widget tree renders immediately. """ self._refresh_in_progress = True - self._loading_spinner.start() + self._scroll.hide() + self._loading_indicator.start() + self._check_btn.setEnabled(False) + self._update_all_btn.setEnabled(False) need_deferred_check = False try: @@ -259,7 +263,10 @@ async def _async_refresh(self) -> None: logger.exception('Failed to refresh tools') need_deferred_check = False finally: - self._loading_spinner.stop() + self._loading_indicator.stop() + self._scroll.show() + self._check_btn.setEnabled(True) + self._update_all_btn.setEnabled(True) self._refresh_in_progress = False # Fire-and-forget: detect updates in the background, then patch diff --git a/synodic_client/application/screen/spinner.py b/synodic_client/application/screen/spinner.py index 40260d0..29adade 100644 --- a/synodic_client/application/screen/spinner.py +++ b/synodic_client/application/screen/spinner.py @@ -2,20 +2,23 @@ Provides :class:`SpinnerCanvas` — a lightweight, palette-aware spinning arc that can be sized and styled for any context — and -:class:`SpinnerWidget` — a self-positioning overlay variant with an -optional text label. +:class:`LoadingIndicator` — a centred spinner-plus-label widget suited +for embedding in layouts as a loading placeholder. :class:`SpinnerCanvas` is used directly in plugin rows and action cards -where only a small inline indicator is needed. :class:`SpinnerWidget` -wraps a canvas and centres itself over its parent for modal-style use. +where only a small inline indicator is needed. :class:`LoadingIndicator` +wraps a canvas with an optional label and is designed to be placed into +a ``QStackedWidget`` page or swapped with content by the consumer. """ from __future__ import annotations -from PySide6.QtCore import QEvent, QRect, Qt, QTimer +from PySide6.QtCore import QRect, Qt, QTimer from PySide6.QtGui import QPainter, QPen from PySide6.QtWidgets import QHBoxLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget +from synodic_client.application.theme import LOADING_LABEL_STYLE + _DEFAULT_SIZE = 24 _DEFAULT_PEN = 3 _INTERVAL = 50 @@ -83,23 +86,36 @@ def tick(self) -> None: self.update() -class SpinnerWidget(QWidget): - """Animated spinner circle with optional text label. +class LoadingIndicator(QWidget): + """Centred spinner arc with an optional text label. + + Designed to be placed into a layout — for example as a page in a + ``QStackedWidget`` or shown/hidden alongside content. The widget + expands to fill available space and centres its contents. + + The consumer is responsible for swapping visibility or stack pages; + this component manages only its own animation and display state. + + Typical usage:: - When a *parent* is provided the widget configures itself as a - floating overlay that fills the parent's geometry automatically. - No ``resizeEvent`` override, ``setSizePolicy``, ``raise_()``, or - ``lower()`` call is needed by the consumer — just ``start()`` and - ``stop()``. + indicator = LoadingIndicator('Loading…') + stack.addWidget(indicator) + + # begin loading + indicator.start() + stack.setCurrentWidget(indicator) + + # finish loading + indicator.stop() + stack.setCurrentWidget(content) """ def __init__(self, text: str = '', parent: QWidget | None = None) -> None: - """Initialize the spinner. + """Create a loading indicator. Args: text: Optional label shown beside the spinner arc. - parent: Optional parent widget. When set, the spinner - becomes a floating overlay that tracks the parent size. + parent: Optional parent widget. """ super().__init__(parent) self.hide() @@ -109,6 +125,8 @@ def __init__(self, text: str = '', parent: QWidget | None = None) -> None: self._timer.setInterval(_INTERVAL) self._timer.timeout.connect(self._canvas.tick) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.addStretch() @@ -118,6 +136,7 @@ def __init__(self, text: str = '', parent: QWidget | None = None) -> None: row.addStretch() row.addWidget(self._canvas) self._label = QLabel(text) + self._label.setStyleSheet(LOADING_LABEL_STYLE) if text: row.addWidget(self._label) row.addStretch() @@ -125,34 +144,25 @@ def __init__(self, text: str = '', parent: QWidget | None = None) -> None: outer.addLayout(row) outer.addStretch() - # Auto-overlay: track parent geometry via event filter - if parent is not None: - self.setAutoFillBackground(True) - self.setStyleSheet('background: palette(window);') - self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - parent.installEventFilter(self) - self.setGeometry(parent.rect()) - - # -- Event filter (overlay geometry tracking) -------------------------- + # -- Public API -------------------------------------------------------- - def eventFilter(self, obj: object, event: QEvent) -> bool: - """Resize to match the parent whenever it resizes.""" - parent = self.parent() - if event.type() == QEvent.Type.Resize and obj is parent and isinstance(parent, QWidget): - self.setGeometry(parent.rect()) - return False + @property + def running(self) -> bool: + """Return ``True`` if the animation is currently active.""" + return self._timer.isActive() - # -- Public API -------------------------------------------------------- + def set_text(self, text: str) -> None: + """Update the label text.""" + self._label.setText(text) + self._label.setVisible(bool(text)) def start(self) -> None: - """Show the overlay and start the animation.""" - self.raise_() - self.show() + """Reset the arc angle, start the animation, and show the widget.""" self._canvas._angle = 0 self._timer.start() + self.show() def stop(self) -> None: - """Stop the animation, hide, and move below siblings.""" + """Stop the animation and hide the widget.""" self._timer.stop() self.hide() - self.lower() diff --git a/synodic_client/application/screen/tool_update_controller.py b/synodic_client/application/screen/tool_update_controller.py index bce34b0..2f7c17e 100644 --- a/synodic_client/application/screen/tool_update_controller.py +++ b/synodic_client/application/screen/tool_update_controller.py @@ -74,6 +74,16 @@ def __init__( self._tool_task: asyncio.Task[None] | None = None self._tool_update_timer: QTimer | None = None + def shutdown(self) -> None: + """Stop timers and cancel in-flight tasks for a clean exit.""" + if self._tool_update_timer is not None: + self._tool_update_timer.stop() + self._tool_update_timer = None + if self._tool_task is not None and not self._tool_task.done(): + self._tool_task.cancel() + self._tool_task = None + logger.info('ToolUpdateOrchestrator shut down') + # -- Timer management -- @staticmethod @@ -175,6 +185,9 @@ async def _do_tool_update(self, porringer: API) -> None: if coordinator is not None: coordinator.invalidate() self._on_tool_update_finished(result) + except asyncio.CancelledError: + logger.debug('Tool update cancelled (shutdown)') + raise except Exception as exc: logger.exception('Tool update failed') self._on_tool_update_error(str(exc)) @@ -238,6 +251,9 @@ async def _async_runtime_plugin_update( if coordinator is not None: coordinator.invalidate() self._on_tool_update_finished(result, updating_plugin=signal_key, manual=True) + except asyncio.CancelledError: + logger.debug('Runtime plugin update cancelled (shutdown)') + raise except Exception as exc: logger.exception('Runtime tool update failed') tools_view = self._window.tools_view @@ -270,6 +286,9 @@ async def _async_single_plugin_update(self, porringer: API, plugin_name: str) -> if coordinator is not None: coordinator.invalidate() self._on_tool_update_finished(result, updating_plugin=plugin_name, manual=True) + except asyncio.CancelledError: + logger.debug('Single plugin update cancelled (shutdown)') + raise except Exception as exc: logger.exception('Tool update failed') tools_view = self._window.tools_view @@ -326,6 +345,9 @@ async def _async_single_package_update( updating_package=(plugin_name, package_name), manual=True, ) + except asyncio.CancelledError: + logger.debug('Runtime package update cancelled (shutdown)') + raise except Exception as exc: logger.exception('Runtime package update failed') tools_view = self._window.tools_view @@ -348,6 +370,9 @@ async def _async_single_package_update( updating_package=(plugin_name, package_name), manual=True, ) + except asyncio.CancelledError: + logger.debug('Package update cancelled (shutdown)') + raise except Exception as exc: logger.exception('Package update failed') tools_view = self._window.tools_view @@ -470,6 +495,9 @@ async def _async_single_package_remove( if coordinator is not None: coordinator.invalidate() self._on_package_remove_finished(result, plugin_name, package_name) + except asyncio.CancelledError: + logger.debug('Package removal cancelled (shutdown)') + raise except Exception as exc: logger.exception('Package removal failed') tools_view = self._window.tools_view diff --git a/synodic_client/application/screen/tray.py b/synodic_client/application/screen/tray.py index 22b1986..e616758 100644 --- a/synodic_client/application/screen/tray.py +++ b/synodic_client/application/screen/tray.py @@ -141,6 +141,12 @@ def _is_user_active() -> bool: """ return any(w.isVisible() for w in QApplication.topLevelWidgets() if isinstance(w, QMainWindow)) + def shutdown(self) -> None: + """Stop all timers and cancel in-flight tasks for a clean exit.""" + self._update_controller.shutdown() + self._tool_orchestrator.shutdown() + logger.info('TrayScreen shut down') + def _on_settings_changed(self, config: ResolvedConfig) -> None: """React to a change made in the settings window.""" self._config = config diff --git a/synodic_client/application/theme.py b/synodic_client/application/theme.py index 473dc0c..e6e4e73 100644 --- a/synodic_client/application/theme.py +++ b/synodic_client/application/theme.py @@ -47,6 +47,7 @@ # --------------------------------------------------------------------------- HEADER_STYLE = 'font-size: 14px; font-weight: bold;' MUTED_STYLE = 'color: grey;' +LOADING_LABEL_STYLE = 'color: grey; font-size: 13px;' COMMAND_HEADER_STYLE = 'color: grey; margin-top: 6px;' # --------------------------------------------------------------------------- diff --git a/synodic_client/application/update_controller.py b/synodic_client/application/update_controller.py index 45759ce..b1c809e 100644 --- a/synodic_client/application/update_controller.py +++ b/synodic_client/application/update_controller.py @@ -111,6 +111,16 @@ def set_user_active_predicate(self, predicate: Callable[[], bool]) -> None: """ self._is_user_active = predicate + def shutdown(self) -> None: + """Stop timers and cancel in-flight tasks for a clean exit.""" + if self._auto_update_timer is not None: + self._auto_update_timer.stop() + self._auto_update_timer = None + if self._update_task is not None and not self._update_task.done(): + self._update_task.cancel() + self._update_task = None + logger.info('UpdateController shut down') + # ------------------------------------------------------------------ # Config helpers # ------------------------------------------------------------------ @@ -250,6 +260,9 @@ async def _async_check(self, *, silent: bool) -> None: result = await check_for_update(self._client) self._on_check_finished(result, silent=silent) logger.info('[DIAG] Self-update check completed (silent=%s)', silent) + except asyncio.CancelledError: + logger.debug('Update check cancelled (shutdown)') + raise except Exception as exc: logger.exception('Update check failed') self._on_check_error(str(exc), silent=silent) @@ -311,6 +324,9 @@ async def _async_download(self, version: str) -> None: on_progress=self._on_download_progress, ) self._on_download_finished(success, version) + except asyncio.CancelledError: + logger.debug('Update download cancelled (shutdown)') + raise except Exception as exc: logger.exception('Update download failed') self._on_download_error(str(exc)) diff --git a/synodic_client/application/workers.py b/synodic_client/application/workers.py index 6295c50..6e2fe89 100644 --- a/synodic_client/application/workers.py +++ b/synodic_client/application/workers.py @@ -34,7 +34,11 @@ async def check_for_update(client: Client) -> UpdateInfo | None: An ``UpdateInfo`` result, or ``None`` when no updater is initialised. """ loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, client.check_for_update) + try: + return await loop.run_in_executor(None, client.check_for_update) + except asyncio.CancelledError: + logger.debug('check_for_update cancelled') + raise async def download_update( @@ -61,7 +65,11 @@ def progress_callback(percentage: int) -> None: return client.download_update(progress_callback) - return await loop.run_in_executor(None, _run) + try: + return await loop.run_in_executor(None, _run) + except asyncio.CancelledError: + logger.debug('download_update cancelled') + raise async def run_tool_updates( @@ -107,24 +115,28 @@ async def run_tool_updates( plugins=plugins, include_packages=include_packages, ) - async for event in porringer.sync.execute_stream( - params, - plugins=discovered_plugins, - ): - if event.kind == ProgressEventKind.ACTION_COMPLETED and event.result is not None: - action_result = event.result - if action_result.skipped: - if action_result.skip_reason in { - SkipReason.ALREADY_LATEST, - SkipReason.ALREADY_INSTALLED, - }: - result.already_latest += 1 - elif action_result.success: - result.updated += 1 - if action_result.action.package: - result.updated_packages.add(str(action_result.action.package.name)) - else: - result.failed += 1 + try: + async for event in porringer.sync.execute_stream( + params, + plugins=discovered_plugins, + ): + if event.kind == ProgressEventKind.ACTION_COMPLETED and event.result is not None: + action_result = event.result + if action_result.skipped: + if action_result.skip_reason in { + SkipReason.ALREADY_LATEST, + SkipReason.ALREADY_INSTALLED, + }: + result.already_latest += 1 + elif action_result.success: + result.updated += 1 + if action_result.action.package: + result.updated_packages.add(str(action_result.action.package.name)) + else: + result.failed += 1 + except asyncio.CancelledError: + logger.debug('run_tool_updates cancelled during manifest processing') + raise result.manifests_processed += 1 return result diff --git a/synodic_client/subprocess_patch.py b/synodic_client/subprocess_patch.py new file mode 100644 index 0000000..80f786c --- /dev/null +++ b/synodic_client/subprocess_patch.py @@ -0,0 +1,82 @@ +"""Suppress console-window flashes for child processes on Windows. + +When the application runs as a windowed executable (``console=False``), +every subprocess that launches a console program (pip, pipx, uv, winget, +etc.) would briefly flash a visible console window. This module patches +``subprocess.Popen.__init__`` to inject two complementary flags: + +* ``CREATE_NO_WINDOW`` in *creationflags* — prevents Windows from + allocating a new console for the child process. +* ``STARTUPINFO`` with ``STARTF_USESHOWWINDOW`` and + ``wShowWindow=SW_HIDE`` — tells Windows to pass ``SW_HIDE`` as the + initial ``nCmdShow`` to the child, suppressing the brief window flash + that some GUI-subsystem tools (e.g. ``winget.exe``) produce even + without a console. + +Since ``asyncio.create_subprocess_exec`` and all other high-level +subprocess APIs ultimately call ``subprocess.Popen``, patching +``Popen.__init__`` is sufficient. + +The PyInstaller runtime hook (``rthook_no_console.py``) applies the same +patch for frozen builds. This module covers the dev-mode entry point +where the rthook does not run. + +Call :func:`apply` once at process startup — it is idempotent. +""" + +from __future__ import annotations + +import subprocess +import sys +from typing import Any + +_applied = False + + +def apply() -> None: + """Activate the subprocess-suppression patch (idempotent, Windows-only).""" + global _applied # noqa: PLW0603 + if _applied or sys.platform != "win32": + return + _applied = True + + _patch_popen() + + +# ------------------------------------------------------------------ +# subprocess.Popen patch +# ------------------------------------------------------------------ + +_CREATE_NO_WINDOW: int = 0 +_STARTF_USESHOWWINDOW: int = 0 +_SW_HIDE: int = 0 + +if sys.platform == "win32": + _CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW # 0x0800_0000 + _STARTF_USESHOWWINDOW = subprocess.STARTF_USESHOWWINDOW + _SW_HIDE = 0 + + +def _inject_hidden_flags(kwargs: dict[str, Any]) -> None: + """Mutate *kwargs* so the child process has no visible window. + + Flags are OR-ed (not replaced) so caller-supplied values are + preserved. An existing ``startupinfo`` object is augmented + rather than overwritten. + """ + kwargs["creationflags"] = kwargs.get("creationflags", 0) | _CREATE_NO_WINDOW + + startupinfo = kwargs.get("startupinfo") or subprocess.STARTUPINFO() + startupinfo.dwFlags |= _STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _SW_HIDE + kwargs["startupinfo"] = startupinfo + + +def _patch_popen() -> None: + _original_init = subprocess.Popen.__init__ + + def _patched_init(self: subprocess.Popen, *args: Any, **kwargs: Any) -> None: # type: ignore[type-arg] + _inject_hidden_flags(kwargs) + _original_init(self, *args, **kwargs) + + subprocess.Popen.__init__ = _patched_init # type: ignore[method-assign] diff --git a/tool/pyinstaller/rthook_no_console.py b/tool/pyinstaller/rthook_no_console.py index 577c64e..a36f044 100644 --- a/tool/pyinstaller/rthook_no_console.py +++ b/tool/pyinstaller/rthook_no_console.py @@ -3,7 +3,7 @@ When the application is built as a windowed executable (``console=False``), every ``subprocess.Popen`` call that launches a console program (pip, pipx, uv, winget, etc.) would briefly flash a visible console window. This hook -patches every ``subprocess.Popen`` call with two complementary mitigations: +patches ``subprocess.Popen.__init__`` with two complementary mitigations: * ``CREATE_NO_WINDOW`` in *creationflags* — prevents Windows from allocating a new console for the child process. @@ -12,6 +12,11 @@ child, suppressing the brief window flash that some GUI-subsystem tools (e.g. ``winget.exe``) produce even without a console. +Since ``asyncio.create_subprocess_exec`` and all other high-level subprocess +APIs ultimately call ``subprocess.Popen``, patching ``Popen.__init__`` is +sufficient. Flags are OR-ed (not replaced) so any caller-supplied values +are preserved. + Placed as a runtime hook so the patch is active before any application or library code spawns subprocesses. """ @@ -27,9 +32,6 @@ _STARTF_USESHOWWINDOW = _sp.STARTF_USESHOWWINDOW _CREATE_NO_WINDOW = _sp.CREATE_NO_WINDOW - # [DIAG] Toggle to log every subprocess spawn to stderr. - _SUBPROCESS_LOGGING = True - _original_init = subprocess.Popen.__init__ def _patched_init(self: subprocess.Popen, *args: Any, **kwargs: Any) -> None: @@ -40,10 +42,6 @@ def _patched_init(self: subprocess.Popen, *args: Any, **kwargs: Any) -> None: startupinfo.wShowWindow = _SW_HIDE kwargs['startupinfo'] = startupinfo - if _SUBPROCESS_LOGGING: - cmd = args[0] if args else kwargs.get('args', '') - print(f'[DIAG] subprocess.Popen: {cmd}', file=sys.stderr, flush=True) - _original_init(self, *args, **kwargs) subprocess.Popen.__init__ = _patched_init