Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

159 lines
5.3 KiB
Python

"""Background (daemon) lifecycle for the OpenSRE gateway process.
The CLI and the interactive shell both drive the daemon through these helpers:
a detached ``python -m gateway.manager`` child whose output is captured in
``~/.opensre/gateway/gateway.log`` and whose PID is tracked in ``gateway.pid``.
The running process reports per-component state (web app, Telegram chat, task
scheduler) through ``components.json``.
"""
from __future__ import annotations
import contextlib
import json
import os
import signal
import subprocess
import sys
import time
from collections import deque
from collections.abc import Sequence
from pathlib import Path
from config.constants import OPENSRE_HOME_DIR
GATEWAY_LOG_FILE: Path = OPENSRE_HOME_DIR / "gateway" / "gateway.log"
GATEWAY_PID_FILE: Path = OPENSRE_HOME_DIR / "gateway" / "gateway.pid"
GATEWAY_COMPONENTS_FILE: Path = OPENSRE_HOME_DIR / "gateway" / "components.json"
def gateway_daemon_pid() -> int | None:
"""Return the live daemon PID, clearing a stale pidfile on the way."""
try:
pid = int(GATEWAY_PID_FILE.read_text().strip())
except (OSError, ValueError):
return None
if _alive(pid):
return pid
GATEWAY_PID_FILE.unlink(missing_ok=True)
return None
def start_gateway_daemon(
*, startup_wait: float = 2.0, argv: Sequence[str] | None = None
) -> tuple[bool, str]:
"""Spawn the gateway as a detached background process.
Returns ``(ok, message)``. Starting an already-running gateway is a no-op
success; a child that dies during ``startup_wait`` is a failure and the
message carries the log tail.
"""
if (pid := gateway_daemon_pid()) is not None:
return True, f"OpenSRE gateway already running (pid {pid})."
GATEWAY_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with GATEWAY_LOG_FILE.open("ab") as log:
process = subprocess.Popen(
argv or (sys.executable, "-m", "gateway.manager"),
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
GATEWAY_PID_FILE.write_text(f"{process.pid}\n")
deadline = time.monotonic() + startup_wait
while time.monotonic() < deadline and process.poll() is None:
time.sleep(0.1)
if process.poll() is not None:
GATEWAY_PID_FILE.unlink(missing_ok=True)
tail = read_gateway_log_tail(10) or "(log empty)"
return False, f"OpenSRE gateway exited during startup:\n{tail}"
return True, f"OpenSRE gateway started (pid {process.pid})."
def stop_gateway_daemon(*, timeout: float = 10.0) -> tuple[bool, str]:
"""SIGTERM the daemon, escalating to SIGKILL when it exceeds *timeout*."""
pid = gateway_daemon_pid()
if pid is None:
return True, "OpenSRE gateway is not running."
os.kill(pid, signal.SIGTERM)
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if not _alive(pid):
_clear_runtime_files()
return True, f"OpenSRE gateway stopped (pid {pid})."
time.sleep(0.2)
# A long-poll or in-flight turn can outlive the graceful window — force it.
os.kill(pid, signal.SIGKILL)
time.sleep(0.5)
if _alive(pid):
return False, f"OpenSRE gateway (pid {pid}) survived SIGKILL."
_clear_runtime_files()
return True, f"OpenSRE gateway force-killed after {timeout:g}s (pid {pid})."
def _clear_runtime_files() -> None:
GATEWAY_PID_FILE.unlink(missing_ok=True)
clear_component_status()
def write_component_status(components: dict[str, str]) -> None:
"""Record the running process's per-component state (called by the manager)."""
GATEWAY_COMPONENTS_FILE.parent.mkdir(parents=True, exist_ok=True)
payload = {"pid": os.getpid(), "started_at": time.time(), "components": components}
GATEWAY_COMPONENTS_FILE.write_text(json.dumps(payload, indent=2))
def read_component_status() -> dict[str, str]:
"""Return the live process's component states ({} when it is not running)."""
try:
payload = json.loads(GATEWAY_COMPONENTS_FILE.read_text())
pid = int(payload["pid"])
except (OSError, ValueError, KeyError, TypeError):
return {}
if pid <= 0 or not _alive(pid):
return {}
return {str(name): str(detail) for name, detail in payload.get("components", {}).items()}
def clear_component_status() -> None:
GATEWAY_COMPONENTS_FILE.unlink(missing_ok=True)
def read_gateway_log_tail(lines: int = 50) -> str:
"""Return the last *lines* of the gateway log ('' when there is none)."""
try:
with GATEWAY_LOG_FILE.open("r", errors="replace") as log:
return "".join(deque(log, maxlen=lines)).rstrip("\n")
except OSError:
return ""
def _alive(pid: int) -> bool:
with contextlib.suppress(ChildProcessError):
os.waitpid(pid, os.WNOHANG) # reap first if the daemon is our own child
try:
os.kill(pid, 0)
except ProcessLookupError:
return False
except PermissionError:
pass # exists, owned by another user
return True
__all__ = [
"GATEWAY_COMPONENTS_FILE",
"GATEWAY_LOG_FILE",
"GATEWAY_PID_FILE",
"clear_component_status",
"gateway_daemon_pid",
"read_component_status",
"read_gateway_log_tail",
"start_gateway_daemon",
"stop_gateway_daemon",
"write_component_status",
]