fierr's picture
Deploy chatgpt-register runner
7868d50 verified
Raw
History Blame Contribute Delete
4.68 kB
"""HTTP entrypoint for the Cloudflare Container runner.
The Worker creates a queued job in D1, then calls this container's /run-once
endpoint. The container reuses cloudflare_runner.run_once(), so the actual
registration flow stays in Python and runs inside Cloudflare Containers rather
than on the operator's local machine.
"""
from __future__ import annotations
import json
import hmac
import os
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from chatgpt_register_sub2api.runner.cloudflare_runner import run_once
_RUN_LOCK = threading.Lock()
class ContainerRunnerHandler(BaseHTTPRequestHandler):
server_version = "ChatGPTRegisterCloudRunner/0.1"
def do_GET(self) -> None: # noqa: N802 - http.server hook
if self.path in {"/", "/health", "/ping"}:
self._json({"ok": True, "service": "registration-runner"})
return
self._json({"error": "not_found"}, status=404)
def do_POST(self) -> None: # noqa: N802 - http.server hook
if self.path != "/run-once":
self._json({"error": "not_found"}, status=404)
return
if not self._authorized():
self._json({"ok": False, "error": "unauthorized"}, status=401)
return
if not _RUN_LOCK.acquire(blocking=False):
self._json({"ok": False, "busy": True, "error": "runner_busy"}, status=409)
return
try:
body = self._read_json()
result = self._run_once(body)
self._json(result)
except Exception as error: # keep HTTP service alive after one failed job
self._json({"ok": False, "error": str(error)}, status=500)
finally:
_RUN_LOCK.release()
def log_message(self, fmt: str, *args: Any) -> None:
print("[container-runner] " + (fmt % args), flush=True)
def _run_once(self, body: dict[str, Any]) -> dict[str, Any]:
base_url = str(body.get("base_url") or os.environ.get("WORKER_BASE_URL") or "").strip()
runner_token = str(body.get("runner_token") or os.environ.get("RUNNER_TOKEN") or "").strip()
config_yaml = str(body.get("config_yaml") or os.environ.get("RUNNER_CONFIG_YAML") or "").strip()
timeout = float(body.get("timeout") or os.environ.get("RUNNER_TIMEOUT") or 30)
if not base_url:
raise ValueError("base_url is required")
if not runner_token:
raise ValueError("runner_token is required")
with tempfile.TemporaryDirectory(prefix="cf-container-runner-") as temp_dir:
temp = Path(temp_dir)
config_path = temp / "config.yaml"
if config_yaml:
config_path.write_text(config_yaml, encoding="utf-8")
else:
config_path.write_text("mail:\n providers: []\n", encoding="utf-8")
args = SimpleNamespace(
base_url=base_url,
token=runner_token,
config=str(config_path),
timeout=timeout,
prune_local_config=False,
no_prune_local_backup=True,
)
did_work = run_once(args)
return {"ok": True, "did_work": did_work}
def _read_json(self) -> dict[str, Any]:
size = int(self.headers.get("content-length") or "0")
if size <= 0:
return {}
raw = self.rfile.read(min(size, 2_000_000))
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, dict):
raise ValueError("JSON body must be an object")
return data
def _authorized(self) -> bool:
expected = str(os.environ.get("RUNNER_INVOKE_TOKEN") or "").strip()
if not expected:
return True
actual = str(self.headers.get("X-Runner-Invoke-Token") or "").strip()
return hmac.compare_digest(actual, expected)
def _json(self, payload: dict[str, Any], status: int = 200) -> None:
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def main() -> int:
port = int(os.environ.get("PORT") or "8080")
server = ThreadingHTTPServer(("0.0.0.0", port), ContainerRunnerHandler)
print(f"[container-runner] listening on 0.0.0.0:{port}", flush=True)
server.serve_forever()
return 0
if __name__ == "__main__":
raise SystemExit(main())