Implement Remote Client Shares Phase 2 browse support and unify remote agent HTTP + heartbeat
This commit is contained in:
@@ -3,12 +3,15 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib import error, request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import uvicorn
|
||||
|
||||
|
||||
AGENT_VERSION = "1.1.0-phase1"
|
||||
@@ -106,7 +109,7 @@ def post_json(url: str, token: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def run(config: AgentConfig) -> None:
|
||||
def run_heartbeat_loop(config: AgentConfig, stop_event: threading.Event) -> None:
|
||||
require_non_empty(config.webmanager_base_url, "webmanager_base_url")
|
||||
require_non_empty(config.registration_token, "registration_token")
|
||||
require_non_empty(config.agent_access_token, "agent_access_token")
|
||||
@@ -117,9 +120,10 @@ def run(config: AgentConfig) -> None:
|
||||
heartbeat_url = f"{config.normalized_base_url}/api/clients/heartbeat"
|
||||
|
||||
print(f"Starting remote client agent for {config.display_name} ({config.client_id})", flush=True)
|
||||
print("agent_access_token is configured for future authenticated agent endpoints", flush=True)
|
||||
print(f"Using config: {config.config_path}", flush=True)
|
||||
print("agent_access_token is configured for authenticated agent endpoints", flush=True)
|
||||
|
||||
while True:
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
post_json(register_url, config.registration_token, build_register_payload(config))
|
||||
print("register ok", flush=True)
|
||||
@@ -128,9 +132,10 @@ def run(config: AgentConfig) -> None:
|
||||
print(f"register failed: HTTP {exc.code}", file=sys.stderr, flush=True)
|
||||
except error.URLError as exc:
|
||||
print(f"register failed: {exc.reason}", file=sys.stderr, flush=True)
|
||||
time.sleep(config.heartbeat_interval_seconds)
|
||||
if stop_event.wait(config.heartbeat_interval_seconds):
|
||||
return
|
||||
|
||||
while True:
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
post_json(heartbeat_url, config.registration_token, build_heartbeat_payload(config))
|
||||
print("heartbeat ok", flush=True)
|
||||
@@ -138,7 +143,52 @@ def run(config: AgentConfig) -> None:
|
||||
print(f"heartbeat failed: HTTP {exc.code}", file=sys.stderr, flush=True)
|
||||
except error.URLError as exc:
|
||||
print(f"heartbeat failed: {exc.reason}", file=sys.stderr, flush=True)
|
||||
time.sleep(config.heartbeat_interval_seconds)
|
||||
if stop_event.wait(config.heartbeat_interval_seconds):
|
||||
return
|
||||
|
||||
|
||||
def resolve_bind_host(config: AgentConfig, requested_host: str | None) -> str:
|
||||
normalized = (requested_host or "").strip()
|
||||
if normalized:
|
||||
return normalized
|
||||
return "0.0.0.0"
|
||||
|
||||
|
||||
def resolve_bind_port(config: AgentConfig, requested_port: int | None) -> int:
|
||||
if requested_port and requested_port > 0:
|
||||
return requested_port
|
||||
parsed = urlparse(config.endpoint)
|
||||
if parsed.port:
|
||||
return parsed.port
|
||||
if parsed.scheme == "https":
|
||||
return 443
|
||||
if parsed.scheme == "http":
|
||||
return 80
|
||||
return 8765
|
||||
|
||||
|
||||
def run(config: AgentConfig, requested_host: str | None, requested_port: int | None) -> None:
|
||||
stop_event = threading.Event()
|
||||
heartbeat_thread = threading.Thread(
|
||||
target=run_heartbeat_loop,
|
||||
args=(config, stop_event),
|
||||
daemon=True,
|
||||
name="remote-client-heartbeat",
|
||||
)
|
||||
heartbeat_thread.start()
|
||||
|
||||
bind_host = resolve_bind_host(config, requested_host)
|
||||
bind_port = resolve_bind_port(config, requested_port)
|
||||
print(f"Starting HTTP agent on {bind_host}:{bind_port}", flush=True)
|
||||
print(f"Advertised endpoint: {config.endpoint}", flush=True)
|
||||
try:
|
||||
import os
|
||||
|
||||
os.environ["FINDER_COMMANDER_REMOTE_AGENT_CONFIG"] = str(config.config_path)
|
||||
uvicorn.run("app.main:app", host=bind_host, port=bind_port)
|
||||
finally:
|
||||
stop_event.set()
|
||||
heartbeat_thread.join(timeout=2)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -148,6 +198,8 @@ def parse_args() -> argparse.Namespace:
|
||||
default=str(Path(__file__).resolve().with_name("remote_client_agent.example.json")),
|
||||
help="Path to remote client agent config JSON",
|
||||
)
|
||||
parser.add_argument("--host", default="", help="Bind host for the HTTP agent, defaults to 0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=0, help="Bind port for the HTTP agent, defaults to endpoint port")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -155,7 +207,7 @@ def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
config = load_config(Path(args.config).resolve())
|
||||
run(config)
|
||||
run(config, requested_host=args.host, requested_port=args.port)
|
||||
except KeyboardInterrupt:
|
||||
return 130
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user