Files
openworker/coworker/server/run.py
T
Rohit C PrasadandDevika 2b45018ffa OpenWorker: initial import
Imported from andrewyng/aisuite@1b4bbf303e
(contents of its platform/ directory, hoisted to the repo root).
Development history prior to this commit lives in that repository.

Co-authored-by: Devika <devikaverma11@gmail.com>
2026-07-21 11:09:41 -07:00

157 lines
5.7 KiB
Python

"""Launch the server with uvicorn. Used by the desktop GUI sidecar and `coworker-server`."""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
from ..config import load_config
from ..permissions import Mode
from ..secrets import state_dir
from .app import create_app
from .manager import SessionManager
def _exit_when_orphaned() -> None:
"""When launched as a desktop sidecar (`COWORKER_EXIT_WITH_PARENT=1`), exit if the parent
process dies — even on an abrupt kill (e.g. the Tauri dev watcher restarting the app, or a
crash) that skips the shell's graceful child-kill. Standalone `coworker-server` runs are
unaffected.
The GUI passes its own PID in `COWORKER_PARENT_PID`. Watching that explicit PID (not
getppid) is what makes this work under PyInstaller onefile, where this process is a
*grandchild* of the GUI — the bootloader sits in between, so getppid() points at the
bootloader and a re-parenting check never fires when the GUI dies (the bug that leaked
a server pair on every app quit).
POSIX: poll the PID with kill(pid, 0). Windows: no re-parenting semantics at all, so
block on a process handle and exit the moment it signals (i.e. the parent exited).
"""
if os.environ.get("COWORKER_EXIT_WITH_PARENT") != "1":
return
import threading
try:
parent = int(os.environ.get("COWORKER_PARENT_PID") or 0)
except ValueError:
parent = 0
parent = parent or os.getppid() # standalone fallback: our direct spawner
if sys.platform == "win32":
_watch_parent_windows(parent)
return
import time
original_ppid = os.getppid()
def watch() -> None:
while True:
time.sleep(1.5)
try:
os.kill(parent, 0) # liveness probe only; signal 0 delivers nothing
except ProcessLookupError:
os._exit(0)
except PermissionError:
pass # alive, but owned by someone else (shouldn't happen) — keep waiting
# Secondary signal: our direct parent died (covers PID-reuse edge cases).
if os.getppid() != original_ppid:
os._exit(0)
threading.Thread(target=watch, daemon=True).start()
def _watch_parent_windows(parent: int) -> None:
"""Block on a handle to the parent process; exit only when it actually terminates.
Best-effort — any failure leaves the parent's RunEvent::ExitRequested kill as the primary
cleanup path. Two correctness points that bit us before:
- `OpenProcess` returns a 64-bit HANDLE; ctypes defaults the return type to a 32-bit int,
which truncates the handle to garbage. Declare restype/argtypes so the handle is valid.
- Only `os._exit` on WAIT_OBJECT_0 (the parent genuinely died). A bad handle yields
WAIT_FAILED immediately — treating that as "parent died" would kill a perfectly healthy
server seconds after startup (exactly the freeze we saw)."""
import ctypes
import threading
from ctypes import wintypes
SYNCHRONIZE = 0x0010_0000
INFINITE = 0xFFFF_FFFF
WAIT_OBJECT_0 = 0x0000_0000
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.OpenProcess.restype = wintypes.HANDLE
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
kernel32.WaitForSingleObject.restype = wintypes.DWORD
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
handle = kernel32.OpenProcess(SYNCHRONIZE, False, parent)
if not handle:
return
def watch() -> None:
if kernel32.WaitForSingleObject(handle, INFINITE) == WAIT_OBJECT_0:
os._exit(0)
threading.Thread(target=watch, daemon=True).start()
def build_app(workspace: str | None, model: str, mode: str):
manager = SessionManager(
workspace=Path(workspace).expanduser().resolve() if workspace else None,
data_dir=state_dir(),
model=model,
mode=Mode(mode),
)
return create_app(manager)
def _ensure_ca_bundle() -> None:
"""Point SSL at certifi's CA bundle if the interpreter has none configured. macOS framework
Python ships without a usable system trust store for `aiohttp` (it builds an `ssl` context with
no CAs), so the Slack Socket-Mode client fails with CERTIFICATE_VERIFY_FAILED. `httpx`/`requests`
bundle certifi already; aiohttp honours the SSL_CERT_FILE env var, so set it once at startup.
"""
if os.environ.get("SSL_CERT_FILE"):
return
try:
import certifi
os.environ["SSL_CERT_FILE"] = certifi.where()
except Exception:
pass
def main(argv=None) -> None:
_ensure_ca_bundle()
cfg = load_config() # global config supplies defaults
parser = argparse.ArgumentParser(prog="coworker-server")
parser.add_argument("--cwd", default=None, help="optional seed/default workspace")
parser.add_argument("--model", default=cfg.model)
parser.add_argument(
"--mode",
default=cfg.mode,
choices=["discuss", "plan", "interactive", "auto"],
)
parser.add_argument("--host", default=cfg.host)
parser.add_argument("--port", type=int, default=cfg.port)
args = parser.parse_args(argv)
# Publish the ACTUAL bound port so loopback URLs (the managed-OAuth callback)
# target this process, not config.port. The desktop shell runs the sidecar on
# a random free port (to coexist with a hand-run server on 8765), so the
# managed-connect redirect must follow the real port, not the 8765 default.
os.environ["COWORKER_PORT"] = str(args.port)
import uvicorn
_exit_when_orphaned()
app = build_app(args.cwd, args.model, args.mode)
uvicorn.run(app, host=args.host, port=args.port)
if __name__ == "__main__":
main()