mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 20:16:29 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
"""Worker commands — start, stop, and status for the optional local worker.
|
||||
|
||||
The worker is an optional background process that maintains a warm
|
||||
backend adapter, reducing cold-start costs for repeated analysis operations.
|
||||
When the worker is not running, all commands function identically in
|
||||
one-shot mode (direct backend adapter initialization).
|
||||
|
||||
Worker start is idempotent: if already running, it reports "already running".
|
||||
Worker stop is idempotent: if not running, it reports "not running".
|
||||
Worker status reports running/stopped state with PID and uptime_seconds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from binary_analysis.domain.errors import BinaryAnalysisError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path to the binary CLI entrypoint (for starting worker subprocess)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
_BINARY_CLI = os.path.join(_SCRIPTS_DIR, "binary")
|
||||
|
||||
|
||||
def add_subparser(subparsers: Any) -> argparse.ArgumentParser:
|
||||
"""Register the worker subcommand."""
|
||||
parser: argparse.ArgumentParser = subparsers.add_parser(
|
||||
"worker",
|
||||
help="Manage the optional local worker daemon.",
|
||||
description=(
|
||||
"Manage the optional local worker daemon. The worker is an "
|
||||
"optional background process that maintains a warm backend "
|
||||
"adapter for faster repeated analysis. All CLI commands "
|
||||
"function correctly without the worker via one-shot mode."
|
||||
),
|
||||
)
|
||||
worker_sub = parser.add_subparsers(dest="worker_command", help="Worker subcommands")
|
||||
|
||||
# worker start
|
||||
start_parser = worker_sub.add_parser(
|
||||
"start",
|
||||
help="Start the optional local worker daemon (idempotent).",
|
||||
description="Start the local worker daemon. If already running, reports 'already running'.",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--daemon",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help=argparse.SUPPRESS, # Hidden; daemon mode is default
|
||||
)
|
||||
|
||||
# worker stop
|
||||
_stop_parser = worker_sub.add_parser(
|
||||
"stop",
|
||||
help="Stop the local worker daemon (idempotent).",
|
||||
description="Stop the local worker daemon. If not running, reports 'not running'.",
|
||||
)
|
||||
|
||||
# worker status
|
||||
_status_parser = worker_sub.add_parser(
|
||||
"status",
|
||||
help="Report worker daemon state.",
|
||||
description="Report whether the worker is running or stopped, with PID and uptime.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def execute(args: argparse.Namespace) -> dict[str, Any]:
|
||||
"""Dispatch to the appropriate worker subcommand."""
|
||||
worker_cmd = getattr(args, "worker_command", None)
|
||||
|
||||
if not worker_cmd:
|
||||
raise BinaryAnalysisError("No worker subcommand specified. Available: start, stop, status.")
|
||||
|
||||
if worker_cmd == "start":
|
||||
return execute_start(args)
|
||||
elif worker_cmd == "stop":
|
||||
return execute_stop(args)
|
||||
elif worker_cmd == "status":
|
||||
return execute_status(args)
|
||||
else:
|
||||
raise BinaryAnalysisError(f"Unknown worker subcommand: {worker_cmd}")
|
||||
|
||||
|
||||
def execute_start(args: argparse.Namespace) -> dict[str, Any]:
|
||||
"""Start the worker daemon.
|
||||
|
||||
Idempotent: if the worker is already running, reports success with
|
||||
a message indicating "already running".
|
||||
"""
|
||||
from binary_analysis.worker.client import get_worker_status
|
||||
|
||||
status = get_worker_status()
|
||||
|
||||
if status["state"] == "running":
|
||||
return {
|
||||
"success": True,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "INFO",
|
||||
"category": "worker",
|
||||
"message": f"Worker already running (PID {status['pid']}).",
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"status": "already_running",
|
||||
"pid": status["pid"],
|
||||
"uptime_seconds": status["uptime_seconds"],
|
||||
},
|
||||
}
|
||||
|
||||
# Start the worker in the background
|
||||
# The worker runs the server module directly
|
||||
import subprocess as _sp
|
||||
|
||||
try:
|
||||
proc = _sp.Popen(
|
||||
[sys.executable, "-m", "binary_analysis.worker.server"],
|
||||
stdout=_sp.DEVNULL,
|
||||
stderr=_sp.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# Wait briefly for the worker to start
|
||||
deadline = time.monotonic() + 10.0
|
||||
started = False
|
||||
while time.monotonic() < deadline:
|
||||
status = get_worker_status()
|
||||
if status["state"] == "running":
|
||||
started = True
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
if not started and proc.poll() is not None:
|
||||
# Worker didn't start in time; check if process is still alive
|
||||
return {
|
||||
"success": False,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "worker",
|
||||
"message": f"Worker process exited with code {proc.returncode}.",
|
||||
}
|
||||
],
|
||||
"data": {"status": "failed"},
|
||||
}
|
||||
|
||||
status = get_worker_status()
|
||||
return {
|
||||
"success": True,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "INFO",
|
||||
"category": "worker",
|
||||
"message": f"Worker started (PID {status['pid']}).",
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"status": "started",
|
||||
"pid": status["pid"],
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "worker",
|
||||
"message": f"Failed to start worker: {e}",
|
||||
}
|
||||
],
|
||||
"data": {"status": "error"},
|
||||
}
|
||||
|
||||
|
||||
def execute_stop(args: argparse.Namespace) -> dict[str, Any]:
|
||||
"""Stop the worker daemon.
|
||||
|
||||
Idempotent: if the worker is not running, reports success with
|
||||
a message indicating "not running".
|
||||
"""
|
||||
from binary_analysis.worker.client import WorkerClient, get_worker_status, read_pid
|
||||
|
||||
status = get_worker_status()
|
||||
|
||||
if status["state"] == "stopped":
|
||||
return {
|
||||
"success": True,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "INFO",
|
||||
"category": "worker",
|
||||
"message": "Worker not running.",
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"status": "not_running",
|
||||
},
|
||||
}
|
||||
|
||||
# Try graceful shutdown via the socket
|
||||
with contextlib.suppress(OSError, TimeoutError):
|
||||
client = WorkerClient(timeout=5.0)
|
||||
client.send_request({"action": "shutdown"})
|
||||
|
||||
# Force kill if still running after grace period
|
||||
time.sleep(0.5)
|
||||
status = get_worker_status()
|
||||
if status["state"] == "running":
|
||||
pid = read_pid()
|
||||
if pid is not None:
|
||||
with contextlib.suppress(OSError):
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
time.sleep(0.5)
|
||||
# Check again and use SIGKILL if still alive
|
||||
if _is_pid_alive_for_stop(pid):
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [
|
||||
{
|
||||
"severity": "INFO",
|
||||
"category": "worker",
|
||||
"message": "Worker stopped.",
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"status": "stopped",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def execute_status(args: argparse.Namespace) -> dict[str, Any]:
|
||||
"""Report the current worker status.
|
||||
|
||||
Returns state, pid, and uptime_seconds. PID is null when stopped.
|
||||
"""
|
||||
from binary_analysis.worker.client import get_worker_status
|
||||
|
||||
status = get_worker_status()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"partial": False,
|
||||
"warnings": [],
|
||||
"diagnostics": [],
|
||||
"data": status,
|
||||
}
|
||||
|
||||
|
||||
def _is_pid_alive_for_stop(pid: int) -> bool:
|
||||
"""Check if a PID is alive (used during stop sequence)."""
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
Reference in New Issue
Block a user