Skip to content

Refactor InstallUpdate.read_output() and consolidate dialog windows#69

Merged
ericbsd merged 1 commit intomasterfrom
refactor-frontend-dialog
Feb 7, 2026
Merged

Refactor InstallUpdate.read_output() and consolidate dialog windows#69
ericbsd merged 1 commit intomasterfrom
refactor-frontend-dialog

Conversation

@ericbsd
Copy link
Member

@ericbsd ericbsd commented Feb 7, 2026

This commit simplifies the package update installation process and standardizes window close behavior across the application.

Frontend refactoring (frontend.py):

  • Moved subprocess creation from frontend to backend via command_output()
  • Extracted read_output() into focused helper methods:
    • process_output() - runs commands and reads stdout line-by-line
    • log_failure() - writes error details to update.failed file
    • needs_reboot() - checks if installed packages require reboot
    • is_pkg_only_update() - detects pkg-only updates
    • install_packages() - handles install with retry logic for temp file failures
    • fetch_packages() - downloads package updates
    • bootstrap_major_upgrade() - bootstraps pkg for major version upgrades
    • prepare_backup() - creates ZFS boot environment backups
  • Improved returncode==3 retry logic: delete failed packages, collect for reinstall after upgrade completes (max 5 retries)
  • Fixed race condition: replaced if proc.poll() is not None with if not line: break for EOF detection, then proc.wait()
  • Moved update_progress() from common.py (only used here)
  • Fixed bug where clicking "Install update" quit the app in check-now mode by changing from "destroy" to "delete-event" signal

Dialog consolidation (dialog.py):

  • Created BaseDialog class with common initialization and close handling
  • All 9 dialog classes now inherit from BaseDialog: FailedUpdate, RestartSystem, UpdateCompleted, NoUpdateAvailable, UpdateStationOpen, MirrorSyncing, ServerUnreachable, SomethingIsWrong, NotRoot
  • Standardized close behavior using Data.close_session flag
  • Return True from on_close() to prevent GTK double-destroy
  • Removed common.py (no longer needed)

Backend additions (backend.py):

  • Added command_output() - creates Popen process for real-time output reading (follows software-station pattern)

Notification fixes (notification.py):

  • Fixed MajorUpgradeWindow to use "delete-event" instead of "destroy" to prevent unexpected app quit when buttons clicked

All windows now use "delete-event" consistently and won't unexpectedly quit when destroyed programmatically. Code is cleaner, more maintainable, and follows consistent patterns throughout.

Summary by Sourcery

Refactor the update installation flow and dialog handling to centralize subprocess management, improve robustness of package upgrades, and standardize window close behavior across the application.

New Features:

  • Add a command_output helper in the backend to expose long-running commands with live output.
  • Introduce a reusable BaseDialog class that encapsulates common dialog window setup and close handling.
  • Add a custom clean_build setup.py command to remove build artifacts and metadata directories.

Bug Fixes:

  • Prevent the main application from quitting unexpectedly by consistently using the GTK delete-event for window close handling.
  • Fix race conditions and EOF handling when reading subprocess output during package upgrades.
  • Ensure the update check flow correctly distinguishes between no updates, errors, and already-running updates.

Enhancements:

  • Break up UpdateWindow.read_output into smaller helpers for fetching, installing, bootstrapping, backup, and reboot detection to simplify the update logic and make it more maintainable.
  • Improve retry behavior for transient pkg failures by deleting problematic packages, tracking them, and reinstalling them after a successful upgrade.
  • Unify dialog close behavior based on Data.close_session and remove the now-redundant common module.
  • Tighten various conditionals and method signatures for clarity (e.g., boolean checks, unused parameters, classmethod helpers).

Build:

  • Extend setup.py cmdclass with a clean_build command alongside existing i18n-related build and clean commands.

This commit simplifies the package update installation process and standardizes window close behavior across the application.

Frontend refactoring (frontend.py):
  - Moved subprocess creation from frontend to backend via command_output()
  - Extracted read_output() into focused helper methods:
    * process_output() - runs commands and reads stdout line-by-line
    * log_failure() - writes error details to update.failed file
    * needs_reboot() - checks if installed packages require reboot
    * is_pkg_only_update() - detects pkg-only updates
    * install_packages() - handles install with retry logic for temp file failures
    * fetch_packages() - downloads package updates
    * bootstrap_major_upgrade() - bootstraps pkg for major version upgrades
    * prepare_backup() - creates ZFS boot environment backups
  - Improved returncode==3 retry logic: delete failed packages, collect for
    reinstall after upgrade completes (max 5 retries)
  - Fixed race condition: replaced `if proc.poll() is not None` with
    `if not line: break` for EOF detection, then proc.wait()
  - Moved update_progress() from common.py (only used here)
  - Fixed bug where clicking "Install update" quit the app in check-now mode
    by changing from "destroy" to "delete-event" signal

Dialog consolidation (dialog.py):
  - Created BaseDialog class with common initialization and close handling
  - All 9 dialog classes now inherit from BaseDialog:
    FailedUpdate, RestartSystem, UpdateCompleted, NoUpdateAvailable,
    UpdateStationOpen, MirrorSyncing, ServerUnreachable, SomethingIsWrong,
    NotRoot
  - Standardized close behavior using Data.close_session flag
  - Return True from on_close() to prevent GTK double-destroy
  - Removed common.py (no longer needed)

Backend additions (backend.py):
  - Added command_output() - creates Popen process for real-time output
    reading (follows software-station pattern)

Notification fixes (notification.py):
  - Fixed MajorUpgradeWindow to use "delete-event" instead of "destroy"
    to prevent unexpected app quit when buttons clicked

All windows now use "delete-event" consistently and won't unexpectedly
quit when destroyed programmatically. Code is cleaner, more maintainable,
and follows consistent patterns throughout.
@ericbsd ericbsd requested review from a team as code owners February 7, 2026 16:54
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 7, 2026

Reviewer's Guide

Refactors the update installation flow to use backend-managed subprocesses and smaller helper methods, introduces robust retry logic and reboot detection, and consolidates dialog window behavior on a shared base class with consistent GTK delete-event handling, while adding a backend command_output helper and a custom setup.py clean command.

Sequence diagram for updated package install and dialog flow

sequenceDiagram
    actor User
    participant UpdateWindow
    participant Backend as backend_command_output
    participant Pkg as pkg_static
    participant Dialog as Dialogs

    User->>UpdateWindow: Click Install update
    UpdateWindow->>UpdateWindow: read_output(progress)
    Note over UpdateWindow: Initialize env, reboot flag, pkg-only flag, option, fraction

    alt Backup enabled
        UpdateWindow->>UpdateWindow: prepare_backup(progress, fraction)
        UpdateWindow->>bectl: get_be_list / destroy_be / create_be
    end

    alt Major upgrade
        UpdateWindow->>UpdateWindow: bootstrap_major_upgrade(env, progress, fraction)
        UpdateWindow->>Backend: command_output("env ... pkg bootstrap -f")
        Backend-->>UpdateWindow: Popen process
        loop Read bootstrap output
            UpdateWindow->>Pkg: Read stdout line
            UpdateWindow->>UpdateWindow: process_output updates progress
        end
        alt Bootstrap failed
            UpdateWindow->>UpdateWindow: fail = True
            UpdateWindow->>UpdateWindow: stop_tread(True, update_pkg, reboot)
            UpdateWindow->>Dialogs: FailedUpdate()
            UpdateWindow->>UpdateWindow: win.destroy()
            UpdateWindow->>UpdateWindow: return
        end
    end

    UpdateWindow->>UpdateWindow: fetch_packages(env, option, packages, progress, fraction)
    UpdateWindow->>Backend: command_output("pkg-static upgrade -Fy...")
    Backend-->>UpdateWindow: Popen process
    loop Read fetch output
        UpdateWindow->>Pkg: Read stdout line
        UpdateWindow->>UpdateWindow: process_output updates progress
    end
    alt Fetch failed
        UpdateWindow->>UpdateWindow: log_failure(stdout + stderr)
        UpdateWindow->>UpdateWindow: fail = True
    else Fetch succeeded
        UpdateWindow->>UpdateWindow: install_packages(env, option, packages, progress, fraction)
        loop Up to 5 retries
            UpdateWindow->>Backend: command_output("pkg-static upgrade -y...")
            Backend-->>UpdateWindow: Popen process
            loop Read install output
                UpdateWindow->>Pkg: Read stdout line
                UpdateWindow->>UpdateWindow: process_output updates progress
            end
            alt returncode == 3 and temp file error
                UpdateWindow->>Backend: command_output("pkg-static rquery ...")
                UpdateWindow->>Backend: command_output("pkg-static delete ...")
                alt Delete failed
                    UpdateWindow->>UpdateWindow: log_failure(delete_stdout + stderr)
                    UpdateWindow->>UpdateWindow: fail = True
                    UpdateWindow->>UpdateWindow: break
                else Delete succeeded
                    UpdateWindow->>UpdateWindow: Record package for reinstall
                    UpdateWindow->>UpdateWindow: Update progress "Removed ... will reinstall"
                end
            else returncode != 0
                UpdateWindow->>UpdateWindow: log_failure(install_stdout + stderr)
                UpdateWindow->>UpdateWindow: fail = True
                UpdateWindow->>UpdateWindow: break
            else Success
                UpdateWindow->>UpdateWindow: Update progress "Software packages upgrade completed"
                UpdateWindow->>UpdateWindow: break
            end
        end
        alt Packages recorded for reinstall
            loop For each package
                UpdateWindow->>Backend: command_output("pkg-static install -y package")
                Backend-->>UpdateWindow: Popen process
                alt Reinstall failed
                    UpdateWindow->>UpdateWindow: log_failure(reinstall_stdout + stderr)
                    UpdateWindow->>UpdateWindow: fail = True
                    UpdateWindow->>UpdateWindow: break
                end
            end
        end
    end

    UpdateWindow->>UpdateWindow: win.destroy()
    UpdateWindow->>UpdateWindow: stop_tread(fail, update_pkg, reboot)

    alt fail == True
        UpdateWindow->>Dialogs: FailedUpdate()
    else fail == False and reboot == True
        UpdateWindow->>Dialogs: RestartSystem()
    else fail == False and reboot == False
        UpdateWindow->>Dialogs: UpdateCompleted()
    end
Loading

Class diagram for dialog hierarchy and update window helpers

classDiagram
    class Data {
    }

    class BaseDialog {
      +Gtk.Window window
      +BaseDialog(title: str)
      +on_close(_widget: Gtk.Widget, _event)
    }

    class FailedUpdate {
      +FailedUpdate()
    }

    class RestartSystem {
      +RestartSystem()
    }

    class UpdateCompleted {
      +UpdateCompleted()
    }

    class NoUpdateAvailable {
      +NoUpdateAvailable()
    }

    class UpdateStationOpen {
      +UpdateStationOpen()
    }

    class MirrorSyncing {
      +MirrorSyncing()
    }

    class ServerUnreachable {
      +ServerUnreachable()
    }

    class SomethingIsWrong {
      +SomethingIsWrong()
    }

    class NotRoot {
      +NotRoot()
    }

    class MajorUpgradeWindow {
      +MajorUpgradeWindow()
      +on_clicked(widget: Gtk.Widget)
      +on_close(_widget: Gtk.Widget, _event)
    }

    class UpdateWindow {
      +Gtk.Window window
      +delete_event(_widget: Gtk.Widget, _event)
      +read_output(progress: Gtk.ProgressBar)
      +install_packages(env: str, option: str, packages: str, progress: Gtk.ProgressBar, fraction: float) bool
      +fetch_packages(env: str, option: str, packages: str, progress: Gtk.ProgressBar, fraction: float) bool
      +bootstrap_major_upgrade(env: str, progress: Gtk.ProgressBar, fraction: float) bool
      +prepare_backup(progress: Gtk.ProgressBar, fraction: float)
      +stop_tread(fail: bool, update_pkg: bool, reboot: bool)
      +should_destroy_be(be_line: str, today_str: str) bool
      +log_failure(text: str)
      +process_output(command: str, progress: Gtk.ProgressBar, fraction: float) tuple
      +needs_reboot() bool
      +is_pkg_only_update() tuple
    }

    class NotificationManager {
      +notify()
      +on_activated(notification, _action_name)
    }

    class MajorUpgradeStatusIcon {
      +left_click(status_icon: Gtk.StatusIcon)
    }

    class Backend {
      +run_command(command: str, check: bool) CompletedProcess
      +command_output(command: str) Popen
      +check_for_update() bool
      +get_default_repo_url() str
      +get_abi_upgrade() str
      +get_current_abi() str
      +get_pkg_upgrade(option: str) str
    }

    class Utils {
      +update_progress(progress: Gtk.ProgressBar, fraction: float, text: str)
    }

    BaseDialog <|-- FailedUpdate
    BaseDialog <|-- RestartSystem
    BaseDialog <|-- UpdateCompleted
    BaseDialog <|-- NoUpdateAvailable
    BaseDialog <|-- UpdateStationOpen
    BaseDialog <|-- MirrorSyncing
    BaseDialog <|-- ServerUnreachable
    BaseDialog <|-- SomethingIsWrong
    BaseDialog <|-- NotRoot

    UpdateWindow ..> Backend : uses
    UpdateWindow ..> Utils : uses
    FailedUpdate ..> Backend : uses get_detail
    RestartSystem ..> Backend : uses on_reboot
    NotificationManager ..> MajorUpgradeWindow : creates
    NotificationManager ..> UpdateWindow : creates via StartCheckUpdate
    MajorUpgradeWindow ..> Data : reads flags
    BaseDialog ..> Data : reads close_session
    UpdateWindow ..> Data : reads and writes update state

    Backend ..> Data : reads and writes update metadata
Loading

Flow diagram for install_packages retry and reinstall logic

flowchart TD
    A_Start["install_packages(env, option, packages, progress, fraction)"] --> B_Init["Show 'Package updates downloaded' and 'Installing package updates'"]
    B_Init --> C_Setup["packages_to_reinstall = []\nmax_retries = 5"]
    C_Setup --> D_LoopStart{Retry < max_retries}

    D_LoopStart -->|Yes| E_RunInstall["process_output('pkg-static upgrade -y' + option + packages)"]
    E_RunInstall --> F_CheckRC{return_code == 3
    & temp file error}

    F_CheckRC -->|Yes| G_ParseFailed["Extract failed_package from install_text"]
    G_ParseFailed --> H_QueryName["command_output('pkg-static rquery ...')"]
    H_QueryName --> I_Delete["process_output('pkg-static delete -y package_name')"]
    I_Delete --> J_DeleteOK{return_code == 0}

    J_DeleteOK -->|No| K_LogDeleteFailure["log_failure(delete_text + stderr_text)"]
    K_LogDeleteFailure --> L_Fail["fail = True\nreturn False"]

    J_DeleteOK -->|Yes| M_RecordReinstall["packages_to_reinstall.append(package_name)"]
    M_RecordReinstall --> N_UpdateMsg["Update progress: 'Removed failed_package, will reinstall after upgrade'"]
    N_UpdateMsg --> O_NextRetry["retry += 1"]
    O_NextRetry --> D_LoopStart

    F_CheckRC -->|No| P_CheckNonZero{return_code != 0}
    P_CheckNonZero -->|Yes| Q_LogInstallFailure["log_failure(install_text + stderr_text)"]
    Q_LogInstallFailure --> L_Fail

    P_CheckNonZero -->|No_Success| R_SuccessMsg["Update progress: 'Software packages upgrade completed'"]
    R_SuccessMsg --> S_Break["Break retry loop"]

    D_LoopStart -->|No_max_retries| T_MaxRetries["log_failure(install_text + stderr_text)"]
    T_MaxRetries --> L_Fail

    S_Break --> U_ReinstallLoopStart{packages_to_reinstall not empty}
    U_ReinstallLoopStart -->|Yes| V_ForEach["For each package_name in packages_to_reinstall"]
    V_ForEach --> W_ReinstallMsg["Update progress: 'Reinstalling package_name'"]
    W_ReinstallMsg --> X_RunReinstall["process_output('pkg-static install -y package_name')"]
    X_RunReinstall --> Y_ReinstallOK{return_code == 0}

    Y_ReinstallOK -->|No| Z_LogReinstallFailure["log_failure(reinstall_text + stderr_text)"]
    Z_LogReinstallFailure --> L_Fail

    Y_ReinstallOK -->|Yes| AA_NextPackage["Next package"]
    AA_NextPackage --> U_ReinstallLoopStart

    U_ReinstallLoopStart -->|No| AB_Return["return True if last return_code == 0 else False"]

    L_Fail --> AC_End["Return False"]
    AB_Return --> AD_End["End install_packages"]
Loading

File-Level Changes

Change Details Files
Refactor InstallUpdate.read_output into smaller helpers and move subprocess creation to backend for streaming command output.
  • Add backend.command_output helper that returns a Popen process configured for line-by-line stdout reading
  • Extract read_output responsibilities into process_output, log_failure, needs_reboot, is_pkg_only_update, install_packages, fetch_packages, bootstrap_major_upgrade, and prepare_backup methods
  • Change progress updates to use a standalone update_progress function and GLib.idle_add with new helper methods
  • Replace proc.poll-based EOF detection with checking for empty lines then calling proc.wait to avoid race conditions
update_station/frontend.py
update_station/backend.py
Improve pkg upgrade/install behavior including temp-file failure retries, reboot detection, and pkg-only update handling.
  • Add needs_reboot helper reading need_reboot.json and intersecting with upgrade package set
  • Refine pkg-only update detection and second_update flag handling via is_pkg_only_update
  • Implement install_packages with bounded retry loop (max 5) for returncode 3 temp file failures, deleting and later reinstalling affected packages
  • Split fetch_packages and bootstrap_major_upgrade to handle major upgrade bootstrapping and fetch failures with shared error logging
update_station/frontend.py
Standardize dialog window creation and close behavior on a shared BaseDialog class using Data.close_session and delete-event.
  • Introduce BaseDialog that creates a centered Gtk.Window with delete-event bound to a shared on_close handler that either Gtk.main_quit or destroys the window based on Data.close_session
  • Update all dialog classes (FailedUpdate, RestartSystem, UpdateCompleted, NoUpdateAvailable, UpdateStationOpen, MirrorSyncing, ServerUnreachable, SomethingIsWrong, NotRoot) to subclass BaseDialog and use self.window instead of local window variables
  • Wire all dialog close buttons to the instance on_close method instead of a global on_close function
  • Remove now-redundant common.py module and its on_close implementation
update_station/dialog.py
update_station/common.py
update_station/data.py
Align main window, notification, and major-upgrade flows on delete-event semantics and boolean checks for flags.
  • Change UpdateWindow to connect delete-event instead of destroy and update its delete_event signature to accept (widget, event) while consulting Data.close_session and updating state
  • In StartCheckUpdate, simplify boolean checks, structure update_available branching, and adjust stop_tread ordering (destroy window after invoking start_window)
  • Update notification paths to use truthy checks for Data.major_upgrade/Data.kernel_upgrade and adjust handler parameter naming
  • Change MajorUpgradeWindow to use delete-event with an on_close handler that mirrors BaseDialog behavior to prevent unintended quitting
update_station/frontend.py
update_station/notification.py
Enhance setup tooling with a custom clean command for build artifacts and small backend docstring/style tweaks.
  • Introduce CleanCommand in setup.py to remove build, dist, and egg-info directories, and register it as clean_build in cmdclass
  • Make minor backend helpers more consistent by adding docstring return descriptions and whitespace adjustments
setup.py
update_station/backend.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 1 other issue, and left some high level feedback:

Security issues:

  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • The new install_packages() retry flow makes it hard to reason about failure cases: if the delete of a failed package returns a non‑zero code you break the retry loop but still proceed into the reinstall loop and finally return return_code == 0 (where return_code refers to the last pkg-static upgrade call, not the delete); consider explicitly treating delete failures as a hard error (set fail/return False) and skipping the reinstall loop in that case.
  • process_output() always appends stderr to stdout and returns both, but the callers sometimes only log the concatenation without preserving which phase failed (bootstrap vs fetch vs install); if you intend to keep a single update.failed file, consider prefixing sections or including the command that failed so that downstream troubleshooting can distinguish which step produced which output.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `install_packages()` retry flow makes it hard to reason about failure cases: if the delete of a failed package returns a non‑zero code you `break` the retry loop but still proceed into the reinstall loop and finally return `return_code == 0` (where `return_code` refers to the last `pkg-static upgrade` call, not the delete); consider explicitly treating delete failures as a hard error (set `fail`/return `False`) and skipping the reinstall loop in that case.
- `process_output()` always appends stderr to stdout and returns both, but the callers sometimes only log the concatenation without preserving which phase failed (bootstrap vs fetch vs install); if you intend to keep a single `update.failed` file, consider prefixing sections or including the command that failed so that downstream troubleshooting can distinguish which step produced which output.

## Individual Comments

### Comment 1
<location> `update_station/backend.py:77-81` </location>
<code_context>
+    )
+
+
 def check_for_update() -> bool:
     """
     Check if there is an update.
+    
     :return: True if there is an update else False.
     """
     kernel_version_change()
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The check_for_update return type/docstring suggest a strict bool, but callers rely on a third "None" state.

In `frontend.StartCheckUpdate.check_for_update`, the code treats `check_for_update()` as returning `True`, `False`, or `None` (`elif update_available is not None:` + `else`). This conflicts with the `-> bool` signature and docstring that promise only a boolean. Please either (a) change the return type/docstring to something like `Optional[bool]` with `None` meaning error/unknown, or (b) adjust the implementation/callers so the function truly always returns a `bool` and handles errors separately.

Suggested implementation:

```python
def check_for_update() -> "Optional[bool]":
    """
    Check if there is an update.

    :return: True if there is an update, False if there is no update, or None if
        the update status could not be determined (e.g. due to an error).
    """
    kernel_version_change()

```

1. Ensure `Optional` is imported from `typing` in this module, e.g. `from typing import Optional`. If this file already uses `from __future__ import annotations`, you can keep the forward reference `"Optional[bool]"`; otherwise, you may replace it with `Optional[bool]` after adding the import.
2. If there are any type hints or stubs (e.g. in `.pyi` files or type-checking-only blocks) that declare `check_for_update` as returning `bool`, update them to `Optional[bool]` as well.
3. If this function is re-exported or documented elsewhere with a hard `bool` return type, those references should be updated to describe the tri-state (`True`/`False`/`None`) behavior.
</issue_to_address>

### Comment 2
<location> `update_station/backend.py:67-74` </location>
<code_context>
    return Popen(
        command,
        shell=True,
        stdout=PIPE,
        stderr=PIPE,
        close_fds=True,
        universal_newlines=True
    )
</code_context>

<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@ericbsd ericbsd merged commit 7e65ac1 into master Feb 7, 2026
1 of 2 checks passed
@ericbsd ericbsd deleted the refactor-frontend-dialog branch February 7, 2026 16:58
@github-project-automation github-project-automation bot moved this from In Review to Done in Development Tracker Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant