Skip to content

fix(process): forcefully disable multiprocessing if the user's machine is bad - #273

Merged
NSPC911 merged 5 commits into
masterfrom
multimonster-a60
May 1, 2026
Merged

fix(process): forcefully disable multiprocessing if the user's machine is bad#273
NSPC911 merged 5 commits into
masterfrom
multimonster-a60

Conversation

@NSPC911

@NSPC911 NSPC911 commented Apr 30, 2026

Copy link
Copy Markdown
Owner

keep retrying the process until a method works, or you just disable multiprocessing (which isnt the best outcome, but whatever tbh i hate how elusive this bug is)

resolves #266 again

Summary by Sourcery

Adjust multiprocessing usage to dynamically fall back to synchronous processing or disable it on problematic systems, improving robustness of drive watching and preview generation.

Bug Fixes:

  • Handle multiprocessing ValueError related to fds_to_keep by switching start methods or disabling multiprocessing to avoid crashes during drive watching and previews.
  • Ensure drive refresh and automatic drive watching fall back to synchronous drive discovery when multiprocessing fails or is disabled.
  • Avoid failing SVG, image, and PDF previews when multiprocessing-based resampling or loading raises specific errors by retrying synchronously.

Enhancements:

  • Introduce an application-level flag to control whether multiprocessing-based processes are allowed at runtime.
  • Add a shared utility to manage multiprocessing start method selection and update UI notifications accordingly.
  • Document potential ValueError conditions in preview methods to clarify error behavior for SVG and image previews.

Chores:

  • Remove global configuration of the multiprocessing start method at application startup in favor of on-demand adjustment when errors occur.

@sourcery-ai

sourcery-ai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a runtime safeguard around all multiprocessing-based operations, automatically switching start methods or falling back to synchronous processing and a global flag when the environment cannot safely use multiprocessing, and updates drive watching and preview pipelines to respect this flag.

Sequence diagram for multiprocessing failure and sync fallback in image preview

sequenceDiagram
    participant User
    participant PreviewContainer
    participant Application
    participant PreviewUtils
    participant Utils as multiprocessing_process_error_checker

    User->>PreviewContainer: show_image_preview()
    PreviewContainer->>Application: read MULTIPROCESSING_PROCESS_ALLOWED
    alt multiprocessing allowed
        PreviewContainer->>PreviewUtils: resample_file(file_path)
        PreviewUtils-->>PreviewContainer: ValueError fds_to_keep
        PreviewContainer->>Utils: multiprocessing_process_error_checker(app, exc)
        alt checker returns true
            Utils-->>PreviewContainer: true
            alt start method adjusted
                PreviewContainer->>PreviewUtils: resample_file(file_path)
                PreviewUtils-->>PreviewContainer: pil_object
            else multiprocessing disabled
                PreviewContainer->>Application: set MULTIPROCESSING_PROCESS_ALLOWED = False
                PreviewContainer->>PreviewUtils: resample_file_sync(file_path)
                PreviewUtils-->>PreviewContainer: pil_object
            end
        else checker returns false
            Utils-->>PreviewContainer: false
            PreviewContainer-->>User: propagate ValueError
        end
    else multiprocessing not allowed
        PreviewContainer->>PreviewUtils: resample_file_sync(file_path)
        PreviewUtils-->>PreviewContainer: pil_object
    end
    PreviewContainer-->>User: display image preview
Loading

Updated class diagram for multiprocessing safeguards

classDiagram
    class Application {
        bool MULTIPROCESSING_PROCESS_ALLOWED
        watch_for_changes_and_update()
    }

    class PreviewContainer {
        show_resvg_preview()
        show_image_preview()
        load_pdf_pages(first_page int, last_page int) list~PILImage~
    }

    class PinnedSidebar {
        list~str~ DRIVES
        refresh_drives(os_type str) void
    }

    class PreviewUtils {
        +load_svg(file_path str) bytes
        +load_svg_sync(file_path str) bytes
        +resample_file_sync(file_path str) Image
        +resample_sync(image Image) Image
        +resample_batch_sync(images list~PILImage~) list~PILImage~
    }

    class Utils {
        +multiprocessing_process_error_checker(app App, exc Exception) bool
    }

    class DriveUtils {
        +get_mounted_drives(os_type str) list~str~
        +get_mounted_drives_worker(result_queue multiprocessing.Queue, os_type str) void
    }

    class DriveWorkers {
        +get_mounted_drives(os_type str) list~str~
        +get_mounted_drives_worker(result_queue multiprocessing.Queue, os_type str) void
    }

    Application "1" o-- "1" PinnedSidebar : owns
    Application "1" o-- "many" PreviewContainer : uses

    PreviewContainer --> Application : app
    PinnedSidebar --> Application : app

    PreviewContainer ..> PreviewUtils : uses
    PreviewContainer ..> Utils : uses

    PinnedSidebar ..> DriveUtils : uses
    PinnedSidebar ..> Utils : uses

    Application ..> DriveWorkers : uses
    Application ..> Utils : uses
Loading

Flow diagram for multiprocessing_process_error_checker behavior

flowchart TD
    A[Exception in multiprocessing operation] --> B{exc is ValueError and message contains fds_to_keep}
    B -->|no| Z[Return false]
    B -->|yes| C[Get current multiprocessing start method]
    C --> D{start method}

    D -->|None| E[Try set_start_method forkserver]
    E --> F{ValueError cannot find context}
    F -->|no error| G[Notify app multiprocessing is now using forkserver]
    F -->|error with cannot find context| H[set_start_method spawn]
    H --> I[Notify app multiprocessing is now using spawn]

    D -->|fork| J[set_start_method forkserver]
    J --> G

    D -->|forkserver| H

    D -->|spawn| K[Set app.MULTIPROCESSING_PROCESS_ALLOWED to False]

    G --> Y[Return true]
    I --> Y
    K --> Y
Loading

File-Level Changes

Change Details Files
Introduce a global application flag and helper to detect bad multiprocessing environments and progressively downgrade strategy (forkserver → spawn → no Process).
  • Add Application.MULTIPROCESSING_PROCESS_ALLOWED flag defaulting to True.
  • Implement multiprocessing_process_error_checker to inspect ValueError/fds_to_keep errors, adjust multiprocessing start_method, and eventually disable Process usage by flipping the app flag.
  • Stop forcing the multiprocessing start method in main so it can be determined and adjusted lazily at runtime.
src/rovr/app.py
src/rovr/functions/utils.py
src/rovr/__main__.py
Make drive watching and pinned sidebar drive refresh resilient to multiprocessing failures with synchronous fallbacks and retry logic.
  • Change the drive watcher loop counter to start at -1 so the first check can be retried immediately after a multiprocessing failure.
  • Wrap drive watcher multiprocessing usage with the new flag and error checker, falling back to direct drive_workers.get_mounted_drives when multiprocessing is disabled or deemed bad.
  • Update PinnedSidebar.refresh_drives to respect the flag, fall back to synchronous drive_utils.get_mounted_drives on error, and only early-return when the error checker indicates multiprocessing should remain enabled.
src/rovr/app.py
src/rovr/core/pinned_sidebar.py
Harden preview generation (SVG, images, PDFs) against multiprocessing issues and provide synchronous equivalents for all preview operations.
  • Extend preview methods to document raised ValueError when failures are not related to fds_to_keep.
  • Conditionally call multiprocessing-based preview_utils functions only when MULTIPROCESSING_PROCESS_ALLOWED is True, with try/except around ValueError to consult the error checker and retry synchronously if needed.
  • Add synchronous helper functions for SVG loading and image resampling (single and batch) and wire them in as fallbacks when multiprocessing is disabled or deemed broken.
src/rovr/core/preview_container.py
src/rovr/functions/preview_utils.py

Assessment against linked issues

Issue Objective Addressed Explanation
#266 Eliminate the continuous ValueError: bad value(s) in fds_to_keep warnings during normal TUI usage on Unix/NixOS by correctly handling problematic multiprocessing usage.
#266 Prevent errors when viewing images/SVG/PDF previews that are caused by the same multiprocessing fds_to_keep issue, while keeping previews functional.

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In watch_for_changes_and_update, new_drives can be referenced before assignment (e.g., when the multiprocessing branch times out and doesn't populate the queue), so consider guarding the comparison or ensuring new_drives is always set in all control paths where it's later used.
  • In multiprocessing_process_error_checker, multiprocessing.set_start_method can raise RuntimeError if the start method is already set, and you're not passing force=True; it would be safer to handle this case explicitly or avoid repeated changes to the start method once initialized.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `watch_for_changes_and_update`, `new_drives` can be referenced before assignment (e.g., when the multiprocessing branch times out and doesn't populate the queue), so consider guarding the comparison or ensuring `new_drives` is always set in all control paths where it's later used.
- In `multiprocessing_process_error_checker`, `multiprocessing.set_start_method` can raise `RuntimeError` if the start method is already set, and you're not passing `force=True`; it would be safer to handle this case explicitly or avoid repeated changes to the start method once initialized.

## Individual Comments

### Comment 1
<location path="src/rovr/app.py" line_range="662-671" />
<code_context>
+                    if self.MULTIPROCESSING_PROCESS_ALLOWED:
</code_context>
<issue_to_address>
**issue (bug_risk):** Possible use of `new_drives` before assignment when the subprocess finishes but produces no output.

Because `new_drives` is only set inside `elif not result_queue.empty()`, if the subprocess exits within the timeout but the queue is empty, execution reaches `if new_drives != drives():` and raises `UnboundLocalError`. It also makes an empty-but-valid result indistinguishable from an error.

You can avoid this by initializing `new_drives` and checking it explicitly, e.g.:

```python
new_drives: list[str] | None = None
...
if self.MULTIPROCESSING_PROCESS_ALLOWED:
    ...
    elif not result_queue.empty():
        new_drives = result_queue.get_nowait()
else:
    new_drives = drive_workers.get_mounted_drives(os_type)

if new_drives is not None and new_drives != drives():
    self.query_one(PinnedSidebar).reload_pins()
```
</issue_to_address>

### Comment 2
<location path="src/rovr/functions/utils.py" line_range="211-220" />
<code_context>
+def multiprocessing_process_error_checker(app: App, exc: Exception) -> bool:
</code_context>
<issue_to_address>
**issue (bug_risk):** `multiprocessing.set_start_method` calls can raise `RuntimeError` when the start method is already set, which isn’t handled here.

The helper currently only anticipates `ValueError`, but `set_start_method` can also raise `RuntimeError` once the multiprocessing context is initialized (e.g., if this runs more than once per process), turning the intended recovery path into an uncaught exception.

To make this more robust, either:
- Use `force=True` when setting the start method (if acceptable), and/or
- Catch `RuntimeError` and handle it as a non-recoverable configuration (e.g., log and disable `MULTIPROCESSING_PROCESS_ALLOWED`).

For instance:
```python
try:
    multiprocessing.set_start_method("forkserver", force=True)
except (ValueError, RuntimeError):
    ...
```
</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.

Comment thread src/rovr/app.py
Comment thread src/rovr/functions/utils.py
@NSPC911
NSPC911 merged commit b0dcba9 into master May 1, 2026
19 checks passed
@NSPC911
NSPC911 deleted the multimonster-a60 branch May 1, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Unix] ValueError: bad value(s) in fds_to_keep

1 participant