Skip to content

Commit 42b3ddf

Browse files
authored
fix: resolve multiprocessing compatibility issues with cx_Freeze (#714)
Follow-up #584 - Add multiprocessing.freeze_support() to main entry point for frozen executable support - Create dedicated multiprocessing_worker module to ensure proper module imports in child processes - Update cx_Freeze configuration to include multiprocessing modules and worker module - Refactor process creation to use centralized worker system instead of direct Process instantiation - Update OCR handler and conversation tab to use new multiprocessing architecture - Fix type annotations for process attributes to avoid import dependencies Fixes multiprocessing functionality when application is compiled with cx_Freeze while maintaining compatibility with source code execution.
1 parent c36a3c1 commit 42b3ddf

6 files changed

Lines changed: 125 additions & 37 deletions

File tree

basilisk/__main__.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import argparse
10+
import multiprocessing
1011
import os
1112
import sys
1213

@@ -48,32 +49,30 @@ def parse_args():
4849
--log_level, -L (str | None): Sets the logging level. Valid levels are DEBUG, INFO, WARNING, ERROR, CRITICAL. Defaults to None.
4950
--no-env-account, -N (bool): Disables loading accounts from environment variables. Defaults to False.
5051
--minimize, -m (bool): Starts the application in a minimized window state. Defaults to False.
51-
-n (bool): Shows a message window if another application instance is already running. Defaults to False.
52-
bskc_file (str | None): Path to a Basilisk conversation file to open. Defaults to None.
52+
--show_already_running_msg, -n (bool): Shows a message if the application is already running. Defaults to False.
5353
5454
Returns:
55-
argparse.Namespace: Parsed command-line arguments with their respective values.
56-
57-
Example:
58-
python -m basilisk --language en --log_level DEBUG --minimize conversation.bskc
55+
argparse.Namespace: Parsed command-line arguments with their values.
5956
"""
60-
parser = argparse.ArgumentParser(
61-
prog=APP_NAME,
62-
description="Runs the application with customized configurations.",
63-
)
57+
parser = argparse.ArgumentParser(description=f"Run {APP_NAME}")
6458
parser.add_argument(
65-
"--language", "-l", help="Set the application language", default=None
59+
"--language",
60+
"-l",
61+
type=str,
62+
default=None,
63+
help="Set the application language",
6664
)
6765
parser.add_argument(
6866
"--log_level",
6967
"-L",
70-
help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
68+
type=str,
7169
default=None,
70+
help="Set the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
7271
)
7372
parser.add_argument(
7473
"--no-env-account",
7574
"-N",
76-
help="Disable loading accounts from environment variables",
75+
help="Do not load accounts from environment variables",
7776
action="store_true",
7877
)
7978
parser.add_argument(
@@ -98,6 +97,9 @@ def parse_args():
9897

9998

10099
if __name__ == '__main__':
100+
# Enable multiprocessing support for frozen executables
101+
multiprocessing.freeze_support()
102+
101103
os.makedirs(TMP_DIR, exist_ok=True)
102104
global_vars.args = parse_args()
103105
singleton_instance = SingletonInstance(FILE_LOCK_PATH)

basilisk/gui/conversation_tab.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import re
2121
import threading
2222
import time
23-
from multiprocessing import Process
2423
from typing import TYPE_CHECKING, Any, Optional
2524

2625
import wx
@@ -168,7 +167,7 @@ def __init__(
168167
self.last_time = 0
169168
self.recording_thread: Optional[RecordingThread] = None
170169
self.task = None
171-
self.process: Optional[Process] = None
170+
self.process: Optional[Any] = None # multiprocessing.Process
172171
self._stop_completion = False
173172
self.ocr_handler = OCRHandler(self)
174173
self.init_ui()

basilisk/gui/ocr_handler.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
import logging
11-
from multiprocessing import Process, Queue, Value
11+
from multiprocessing import Queue, Value
1212
from typing import TYPE_CHECKING, Any, Optional
1313

1414
import wx
@@ -46,7 +46,7 @@ def __init__(self, parent: ConversationTab):
4646
"""
4747
self.conf = config.conf()
4848
self.parent = parent
49-
self.process: Optional[Process] = None
49+
self.process: Optional[Any] = None # multiprocessing.Process
5050
self.ocr_button: Optional[wx.Button] = None
5151

5252
def create_ocr_widget(self, parent: wx.Window) -> wx.Button:
@@ -312,21 +312,17 @@ def on_ocr(self, event: wx.CommandEvent):
312312
global_vars.args.log_level or self.conf.general.log_level.name
313313
)
314314
kwargs = {
315-
"result_queue": result_queue,
316-
"cancel_flag": cancel_flag,
317315
"api_key": current_account.api_key.get_secret_value(),
318316
"base_url": current_account.custom_base_url
319317
or current_account.provider.base_url,
320318
"attachments": attachment_files,
321319
"log_level": log_level,
322320
}
323321

324-
self.process = Process(
325-
target=run_task, args=(engine.handle_ocr,), kwargs=kwargs
322+
self.process = run_task(
323+
engine.handle_ocr, result_queue, cancel_flag, **kwargs
326324
)
327325

328-
self.process.daemon = True # Ensure process terminates when parent does
329-
self.process.start()
330326
log.debug("OCR process started: %s", self.process.pid)
331327

332328
wx.CallLater(

basilisk/multiprocessing_worker.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Multiprocessing worker functions for cx_Freeze compatibility.
2+
3+
This module contains worker functions that are called by separate processes.
4+
Having them in a separate module ensures they can be properly imported
5+
by child processes when the application is frozen with cx_Freeze.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
import multiprocessing
12+
from typing import TYPE_CHECKING, Any, Callable
13+
14+
if TYPE_CHECKING:
15+
from multiprocessing import Queue
16+
17+
18+
def setup_worker_logging(log_level: str) -> None:
19+
"""Set up logging for worker processes.
20+
21+
Args:
22+
log_level: The logging level to set
23+
"""
24+
# Configure logging for the worker process
25+
level = getattr(logging, log_level.upper(), logging.INFO)
26+
logging.basicConfig(
27+
level=level,
28+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29+
)
30+
31+
32+
def run_task_worker(
33+
task: Callable, result_queue: Queue, cancel_flag, *args: Any, **kwargs: Any
34+
) -> None:
35+
"""Run a task in a separate process.
36+
37+
Args:
38+
task: The task to run
39+
result_queue: The queue to store the task result
40+
cancel_flag: The flag to indicate if the task should be cancelled
41+
*args: The task arguments
42+
**kwargs: The task keyword arguments
43+
"""
44+
try:
45+
# Set up logging if log_level is provided
46+
if "log_level" in kwargs:
47+
setup_worker_logging(kwargs["log_level"])
48+
49+
# Add required parameters to kwargs
50+
kwargs["result_queue"] = result_queue
51+
kwargs["cancel_flag"] = cancel_flag
52+
53+
# Execute the task
54+
result = task(*args, **kwargs)
55+
56+
# Return result if not cancelled
57+
if not cancel_flag.value:
58+
result_queue.put(("result", result))
59+
except Exception as e:
60+
import traceback
61+
62+
error_trace = traceback.format_exc()
63+
result_queue.put(("error", f"{str(e)}\n{error_trace}"))
64+
65+
66+
def start_worker_process(
67+
task: Callable, result_queue: Queue, cancel_flag, *args: Any, **kwargs: Any
68+
) -> multiprocessing.Process:
69+
"""Start a worker process to run a task.
70+
71+
Args:
72+
task: The task to run
73+
result_queue: The queue to store the task result
74+
cancel_flag: The flag to indicate if the task should be cancelled
75+
*args: The task arguments
76+
**kwargs: The task keyword arguments
77+
78+
Returns:
79+
The started process
80+
"""
81+
process = multiprocessing.Process(
82+
target=run_task_worker,
83+
args=(task, result_queue, cancel_flag, *args),
84+
kwargs=kwargs,
85+
)
86+
process.daemon = True
87+
process.start()
88+
return process
89+
90+
91+
# Required for cx_Freeze multiprocessing support
92+
if __name__ == "__main__":
93+
multiprocessing.freeze_support()

basilisk/process_helper.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
from __future__ import annotations
44

5+
import multiprocessing
56
from typing import TYPE_CHECKING, Any, Callable
67

8+
from .multiprocessing_worker import start_worker_process
9+
710
if TYPE_CHECKING:
811
from multiprocessing import Queue
912

1013

1114
def run_task(
1215
task: Callable, result_queue: Queue, cancel_flag, *args: Any, **kwargs: Any
13-
):
16+
) -> multiprocessing.Process:
1417
"""Run a task in a separate process.
1518
1619
Args:
@@ -19,15 +22,10 @@ def run_task(
1922
cancel_flag: The flag to indicate if the task should be cancelled
2023
*args: The task arguments
2124
**kwargs: The task keyword arguments
25+
26+
Returns:
27+
The started process
2228
"""
23-
try:
24-
kwargs["result_queue"] = result_queue
25-
kwargs["cancel_flag"] = cancel_flag
26-
result = task(*args, **kwargs)
27-
if not cancel_flag.value:
28-
result_queue.put(("result", result))
29-
except Exception as e:
30-
import traceback
31-
32-
error_trace = traceback.format_exc()
33-
result_queue.put(("error", f"{str(e)}\n{error_trace}"))
29+
return start_worker_process(
30+
task, result_queue, cancel_flag, *args, **kwargs
31+
)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ excludes = [
124124
"wint32gui", "win32ui", "win32uiold", "winreg",
125125
]
126126
include_files = ["basilisk/res"]
127-
includes = ["numpy", "win32timezone"]
127+
includes = ["numpy", "win32timezone", "multiprocessing", "multiprocessing.spawn", "multiprocessing.util"]
128128
include_msvcr = true
129-
packages = ["numpy", "basilisk.provider_engine", "keyring", "fsspec.implementations", "upath.implementations"]
129+
packages = ["numpy", "basilisk.provider_engine", "basilisk.multiprocessing_worker", "keyring", "fsspec.implementations", "upath.implementations", "multiprocessing"]
130130
zip_include_packages = [
131131
"anyio", "annotated_types", "anthropic", "asyncio",
132132
"backports", "cachetools", "certifi", "cffi", "charset_normalizer", "concurrent", "collections", "colorama", "ctypes", "curses",

0 commit comments

Comments
 (0)