Skip to content

Commit b0dcba9

Browse files
authored
fix(process): forcefully disable multiprocessing if the user's machine is bad (#273)
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
1 parent bbd38af commit b0dcba9

6 files changed

Lines changed: 170 additions & 63 deletions

File tree

src/rovr/__main__.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -348,19 +348,12 @@ def _get_version() -> list[str]:
348348
if args.force_first_launch:
349349
return
350350

351-
import multiprocessing # noqa: I001
352-
353-
import rovr.monkey_patches._classes # noqa: F401
351+
import rovr.monkey_patches._classes # noqa: F401, I001
354352
import rovr.monkey_patches._platform # noqa: F401
355353

356354
from rovr.functions.config import set_nested_value
357355
from rovr.variables.constants import config
358356

359-
try:
360-
multiprocessing.set_start_method("forkserver", force=True)
361-
except ValueError:
362-
multiprocessing.set_start_method("spawn", force=True)
363-
364357
for feature_path in args.with_features:
365358
set_nested_value(cast(dict, config), feature_path, True)
366359

src/rovr/app.py

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
PreviewContainer,
5555
)
5656
from rovr.footer import Clipboard, MetadataContainer, ProcessContainer
57-
from rovr.functions.drive_workers import get_mounted_drives_worker
57+
from rovr.functions import drive_workers
5858
from rovr.functions.path import (
5959
dump_exc,
6060
ensure_existing_directory,
@@ -63,6 +63,7 @@
6363
normalise,
6464
)
6565
from rovr.functions.themes import get_custom_themes
66+
from rovr.functions.utils import multiprocessing_process_error_checker
6667
from rovr.header import HeaderArea
6768
from rovr.navigation_widgets import (
6869
BackButton,
@@ -126,6 +127,8 @@ class Application(App, inherit_bindings=False):
126127
)
127128
CLICK_CHAIN_TIME_THRESHOLD = config["interface"]["double_click_delay"]
128129

130+
MULTIPROCESSING_PROCESS_ALLOWED: bool = True
131+
129132
def __init__(
130133
self,
131134
startup_path: str = "",
@@ -588,9 +591,10 @@ def watch_for_changes_and_update(self) -> None:
588591
state_mtime = path.getmtime(state_path)
589592
drives = lambda: self.app.query_one(PinnedSidebar).DRIVES # noqa: E731
590593
drive_update_every = int(config["interface"]["drive_watcher_frequency"])
591-
count: int = 0
594+
count: int = -2
592595
style_available: bool = self.CUSTOM_STYLE_AVAILABLE
593596
custom_style_path = path.join(RovrVars.ROVRCONFIG, "style.tcss")
597+
new_drives: list[str] | None = None
594598

595599
i_should_shut_down = lambda: ( # noqa: E731
596600
self._shutdown_event.is_set() or self.return_code is not None
@@ -656,37 +660,44 @@ def watch_for_changes_and_update(self) -> None:
656660
# check drives
657661
if count == 0 and not reload_called:
658662
try:
659-
# Run drive check in a separate process using multiprocessing.Process
660-
# Using Queue to get the result back from the process
661-
result_queue: multiprocessing.Queue[list[str]] = (
662-
multiprocessing.Queue()
663-
)
663+
if self.MULTIPROCESSING_PROCESS_ALLOWED:
664+
# Run drive check in a separate process using multiprocessing.Process
665+
# Using Queue to get the result back from the process
666+
result_queue: multiprocessing.Queue[list[str]] = (
667+
multiprocessing.Queue()
668+
)
664669

665-
process = multiprocessing.Process(
666-
target=get_mounted_drives_worker, args=(result_queue, os_type)
667-
)
668-
process.start()
669-
process.join(timeout=2.0)
670+
process = multiprocessing.Process(
671+
target=drive_workers.get_mounted_drives_worker,
672+
args=(result_queue, os_type),
673+
)
674+
process.start()
675+
process.join(timeout=2.0)
670676

671-
if process.is_alive():
672-
# Timeout - terminate the process
673-
process.terminate()
674-
process.join(timeout=0.5)
675677
if process.is_alive():
676-
process.kill()
677-
elif not result_queue.empty():
678-
# Process completed successfully
679-
new_drives = result_queue.get_nowait()
680-
if new_drives != drives():
681-
self.query_one(PinnedSidebar).reload_pins()
678+
# Timeout - terminate the process
679+
process.terminate()
680+
process.join(timeout=0.5)
681+
if process.is_alive():
682+
process.kill()
683+
elif not result_queue.empty():
684+
# Process completed successfully
685+
new_drives = result_queue.get_nowait()
686+
else:
687+
new_drives = drive_workers.get_mounted_drives(os_type)
688+
if new_drives is not None and new_drives != drives():
689+
self.query_one(PinnedSidebar).reload_pins()
682690
except Exception as exc:
683-
self.notify(
684-
f"{type(exc).__name__}: {exc}",
685-
title="Drives Watcher",
686-
severity="warning",
687-
markup=False,
688-
)
689-
dump_exc(self, exc)
691+
if multiprocessing_process_error_checker(self, exc):
692+
count = -1 # try again immediately on next loop
693+
else:
694+
self.notify(
695+
f"{type(exc).__name__}: {exc}",
696+
title="Drives Watcher",
697+
severity="warning",
698+
markup=False,
699+
)
700+
dump_exc(self, exc)
690701
if i_should_shut_down():
691702
return
692703

src/rovr/core/pinned_sidebar.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from rovr.functions import icons as icon_utils
1313
from rovr.functions import path as path_utils
1414
from rovr.functions import pins as pin_utils
15+
from rovr.functions.utils import multiprocessing_process_error_checker
1516
from rovr.variables.constants import bindings, config, os_type
1617
from rovr.widgets import Input, OptionList
1718

@@ -142,28 +143,33 @@ def refresh_drives(
142143
) -> None:
143144
# force refresh
144145
try:
145-
result_queue: multiprocessing.Queue[list[str]] = multiprocessing.Queue()
146-
process = multiprocessing.Process(
147-
target=drive_utils.get_mounted_drives_worker,
148-
args=(result_queue, os_type),
149-
)
150-
process.start()
151-
process.join(timeout=2.0)
146+
if self.app.MULTIPROCESSING_PROCESS_ALLOWED:
147+
result_queue: multiprocessing.Queue[list[str]] = multiprocessing.Queue()
148+
process = multiprocessing.Process(
149+
target=drive_utils.get_mounted_drives_worker,
150+
args=(result_queue, os_type),
151+
)
152+
process.start()
153+
process.join(timeout=2.0)
152154

153-
if process.is_alive():
154-
process.terminate()
155-
process.join(timeout=0.5)
156155
if process.is_alive():
157-
process.kill()
158-
return
156+
process.terminate()
157+
process.join(timeout=0.5)
158+
if process.is_alive():
159+
process.kill()
160+
return
159161

160-
if result_queue.empty():
161-
return
162+
if result_queue.empty():
163+
return
162164

163-
drives = result_queue.get_nowait()
164-
self.DRIVES = drives
165-
except Exception:
166-
return
165+
drives = result_queue.get_nowait()
166+
else:
167+
drives = drive_utils.get_mounted_drives(os_type)
168+
except Exception as exc:
169+
if not multiprocessing_process_error_checker(self.app, exc):
170+
return
171+
drives = drive_utils.get_mounted_drives(os_type)
172+
self.DRIVES = drives
167173
for drive in drives:
168174
if access(drive, R_OK):
169175
new_id = f"{path_utils.compress(drive)}-drives"

src/rovr/core/preview_container.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
from rovr.functions import path as path_utils
3434
from rovr.functions import preview_utils
3535
from rovr.functions.pdf import get_pdf_images, get_pdf_info
36-
from rovr.functions.utils import should_cancel
36+
from rovr.functions.preview_utils import (
37+
load_svg_sync,
38+
resample_batch_sync,
39+
resample_file_sync,
40+
resample_sync,
41+
)
42+
from rovr.functions.utils import multiprocessing_process_error_checker, should_cancel
3743
from rovr.variables.constants import PreviewContainerTitles, config, file_one
3844
from rovr.widgets import Static
3945

@@ -313,15 +319,28 @@ def show_font_preview(self) -> None:
313319
return
314320

315321
def show_resvg_preview(self) -> None:
316-
"""Show svg preview using resvg"""
322+
"""Show svg preview using resvg.
323+
324+
Raises:
325+
ValueError: If SVG loading fails for non-fds_to_keep reasons.
326+
"""
317327
if should_cancel() or self._current_file_path is None:
318328
return
319329
self.app.call_from_thread(setattr, self, "border_title", titles.svg)
320330

321331
# load svg as bytes
322332
try:
323333
self.call_next(self.LOADER_WIDGET.update, "loading svg...")
324-
png_bytes = preview_utils.load_svg(self._current_file_path)
334+
if self.app.MULTIPROCESSING_PROCESS_ALLOWED:
335+
try:
336+
png_bytes = preview_utils.load_svg(self._current_file_path)
337+
except ValueError as exc:
338+
if multiprocessing_process_error_checker(self.app, exc):
339+
png_bytes = load_svg_sync(self._current_file_path)
340+
else:
341+
raise
342+
else:
343+
png_bytes = load_svg_sync(self._current_file_path)
325344
if png_bytes is None:
326345
self.notify(
327346
"Failed to load SVG. The file may be corrupted or not an SVG file.",
@@ -339,7 +358,16 @@ def show_resvg_preview(self) -> None:
339358

340359
self.call_next(self.LOADER_WIDGET.update, "resampling svg...")
341360

342-
pil_object = preview_utils.resample(Image.open(BytesIO(png_bytes)))
361+
if self.app.MULTIPROCESSING_PROCESS_ALLOWED:
362+
try:
363+
pil_object = preview_utils.resample(Image.open(BytesIO(png_bytes)))
364+
except ValueError as exc:
365+
if multiprocessing_process_error_checker(self.app, exc):
366+
pil_object = resample_sync(Image.open(BytesIO(png_bytes)))
367+
else:
368+
raise
369+
else:
370+
pil_object = resample_sync(Image.open(BytesIO(png_bytes)))
343371

344372
if should_cancel():
345373
return
@@ -381,13 +409,26 @@ def show_resvg_preview(self) -> None:
381409
return
382410

383411
def show_image_preview(self) -> None:
384-
"""Show image preview. Runs in a thread."""
412+
"""Show image preview. Runs in a thread.
413+
414+
Raises:
415+
ValueError: If image loading fails for non-fds_to_keep reasons.
416+
"""
385417
if should_cancel() or self._current_file_path is None:
386418
return
387419
self.app.call_from_thread(setattr, self, "border_title", titles.image)
388420

389421
try:
390-
pil_object = preview_utils.resample_file(self._current_file_path)
422+
if self.app.MULTIPROCESSING_PROCESS_ALLOWED:
423+
try:
424+
pil_object = preview_utils.resample_file(self._current_file_path)
425+
except ValueError as exc:
426+
if multiprocessing_process_error_checker(self.app, exc):
427+
pil_object = resample_file_sync(self._current_file_path)
428+
else:
429+
raise
430+
else:
431+
pil_object = resample_file_sync(self._current_file_path)
391432
if pil_object is None:
392433
return
393434
except UnidentifiedImageError:
@@ -480,7 +521,14 @@ def load_pdf_pages(self, first_page: int, last_page: int) -> list[PILImage]:
480521
"Obtained 0 pages from Poppler. Something may have gone wrong..."
481522
)
482523
# Resample images once when loaded for better performance
483-
return preview_utils.resample_batch(result)
524+
if self.app.MULTIPROCESSING_PROCESS_ALLOWED:
525+
try:
526+
return preview_utils.resample_batch(result)
527+
except ValueError as exc:
528+
if multiprocessing_process_error_checker(self.app, exc):
529+
return resample_batch_sync(result)
530+
raise
531+
return resample_batch_sync(result)
484532

485533
def show_pdf_preview(self) -> None:
486534
"""

src/rovr/functions/preview_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,26 @@ def load_svg(file_path: str) -> bytes | None:
223223
raise result
224224
return result
225225
return None
226+
227+
228+
def load_svg_sync(file_path: str) -> bytes | None:
229+
from resvg_py import svg_to_bytes
230+
231+
return svg_to_bytes(svg_path=file_path)
232+
233+
234+
def resample_file_sync(file_path: str) -> Image.Image | None:
235+
image = Image.open(file_path)
236+
image = _depalette(image)
237+
return image.resize(MAX_IMAGE_SIZE, RESAMPLING_METHOD)
238+
239+
240+
def resample_sync(image: Image.Image) -> Image.Image:
241+
image = _depalette(image)
242+
return image.resize(MAX_IMAGE_SIZE, RESAMPLING_METHOD)
243+
244+
245+
def resample_batch_sync(images: list[PILImage]) -> list[PILImage]:
246+
return [
247+
_depalette(image).resize(MAX_IMAGE_SIZE, RESAMPLING_METHOD) for image in images
248+
]

src/rovr/functions/utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import multiprocessing
12
import shlex
23
import subprocess
34
from contextlib import suppress
@@ -6,7 +7,7 @@
67

78
from humanize import naturalsize
89
from textual import events
9-
from textual.app import ScreenStackError
10+
from textual.app import App, ScreenStackError
1011
from textual.dom import DOMNode
1112
from textual.message import Message
1213
from textual.screen import Screen, ScreenResultType
@@ -205,3 +206,28 @@ def dismiss(
205206
if screen in screen.app.screen_stack:
206207
with suppress(ScreenStackError):
207208
screen.dismiss(result)
209+
210+
211+
def multiprocessing_process_error_checker(app: App, exc: Exception) -> bool:
212+
if isinstance(exc, ValueError) and "fds_to_keep" in str(exc):
213+
match multiprocessing.get_start_method(allow_none=True):
214+
case None:
215+
# try forkserver
216+
try:
217+
multiprocessing.set_start_method("forkserver", force=True)
218+
app.notify("multiprocessing is now using forkserver")
219+
except ValueError as val_exc:
220+
if "cannot find context" in str(val_exc):
221+
multiprocessing.set_start_method("spawn", force=True)
222+
app.notify("multiprocessing is now using spawn")
223+
case "fork": # theoretically this shouldn't happen
224+
multiprocessing.set_start_method("forkserver", force=True)
225+
app.notify("multiprocessing is now using forkserver")
226+
case "forkserver":
227+
multiprocessing.set_start_method("spawn", force=True)
228+
app.notify("multiprocessing is now using spawn")
229+
case "spawn":
230+
# nothing else we can do, except forcefully stop using Process
231+
app.MULTIPROCESSING_PROCESS_ALLOWED = False
232+
return True
233+
return False

0 commit comments

Comments
 (0)