Skip to content

Commit b32fdee

Browse files
committed
Fix #189: don't fall back to fork() on macOS; redirect worker stderr
Two changes for the macOS Tahoe segfault reported in issue #189: 1. process_worker_client.py: drop the spawn -> fork fallback when running on darwin. macOS Tahoe (26+) widened the set of system libraries that abort if initialized after fork() — CoreFoundation, Security.framework, libsystem_trace, libsystem_info — so any library the parent has loaded (psycopg, getaddrinfo, setproctitle, NSCharacterSet) can crash the forked child. Letting spawn fail and surfacing it to the caller lets the in-process fallback in query_execution take over instead. Cost: query cancellation isn't available when spawn fails. Gain: no hard crashes ("Worker connection closed.") on macOS. 2. process_worker.py: redirect the worker's stdout/stderr to a log file and enable faulthandler at entry. The worker can SIGPIPE on libpq notice writes when its inherited FDs point to a Textual-managed pipe, and any future C-level fault on the worker side is otherwise invisible to the parent (the pipe just dies). The log path honors $SQLIT_WORKER_LOG and defaults to <tmpdir>/sqlit-worker.log. Refs the celery (Tahoe + setproctitle, celery#9894) and gunicorn (Tahoe + NSCharacterSet, gunicorn#3526) reports for the broader fork- safety regression on Tahoe.
1 parent f898235 commit b32fdee

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

sqlit/domains/process_worker/app/process_worker.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,39 @@
22

33
from __future__ import annotations
44

5+
import faulthandler
6+
import os
57
import pickle
8+
import sys
9+
import tempfile
610
import threading
711
import time
812
from collections import deque
913
from dataclasses import dataclass, field
1014
from multiprocessing.connection import Connection
15+
from pathlib import Path
1116
from typing import Any
1217

18+
19+
def _open_worker_log() -> Any | None:
20+
"""Open the worker log file, honoring SQLIT_WORKER_LOG when set.
21+
22+
The parent process may attach this worker to a Textual-managed pipe;
23+
libpq notice writes or unexpected stderr output would then SIGPIPE the
24+
child. Redirecting both streams to a real file avoids that and gives a
25+
place to land C-level fault tracebacks for future diagnosis.
26+
"""
27+
override = os.environ.get("SQLIT_WORKER_LOG")
28+
if override:
29+
path = Path(override).expanduser()
30+
else:
31+
path = Path(tempfile.gettempdir()) / "sqlit-worker.log"
32+
try:
33+
path.parent.mkdir(parents=True, exist_ok=True)
34+
return path.open("a", buffering=1)
35+
except OSError:
36+
return None
37+
1338
from sqlit.domains.connections.domain.config import ConnectionConfig
1439
from sqlit.domains.connections.providers.catalog import get_provider
1540
from sqlit.domains.connections.providers.config_service import normalize_connection_config
@@ -493,6 +518,14 @@ def _get_provider(self, db_type: str) -> Any | None:
493518

494519
def run_process_worker(conn: Connection) -> None:
495520
"""Process entrypoint for query execution."""
521+
log_file = _open_worker_log()
522+
if log_file is not None:
523+
sys.stdout = log_file
524+
sys.stderr = log_file
525+
try:
526+
faulthandler.enable(file=log_file)
527+
except (RuntimeError, ValueError):
528+
pass
496529
state = _WorkerState(conn=conn)
497530
try:
498531
while True:

sqlit/domains/process_worker/app/process_worker_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ def _maybe_fallback_start(self, error: Exception) -> None:
6363
raise error
6464
if os.name != "posix" or sys.platform.startswith("win"):
6565
raise error
66+
# macOS (especially Tahoe 26+) aborts in the child whenever a forked
67+
# process touches CoreFoundation / Security.framework / os_log —
68+
# which happens inside psycopg, getaddrinfo, and many other stdlib
69+
# paths. Forking here causes hard segfaults (issue #189), so we
70+
# surface the spawn failure and let the caller fall back to
71+
# in-process execution.
72+
if sys.platform == "darwin":
73+
raise error
6674
try:
6775
self._start_with_context(get_context("fork"))
6876
except Exception:

0 commit comments

Comments
 (0)