release: public alpha 0.6.0 baseline
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""Single-request JSON-line entrypoint for the JWeBot extension host."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
|
||||
from jwebot_kernel import RuntimeTelemetry
|
||||
|
||||
from .runtime import (
|
||||
CapabilityUnavailable,
|
||||
ExtensionRuntime,
|
||||
HostError,
|
||||
process_request,
|
||||
)
|
||||
|
||||
|
||||
MAX_REQUEST_BYTES = 256 * 1024
|
||||
MAX_SERVICE_CALLS = 8
|
||||
|
||||
|
||||
def _scrub_environment() -> None:
|
||||
retained = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
if key in {"LANG", "LC_ALL", "TZ"}
|
||||
}
|
||||
os.environ.clear()
|
||||
os.environ.update(retained)
|
||||
|
||||
|
||||
def _read_message(stream: object) -> dict[str, object]:
|
||||
raw = stream.readline(MAX_REQUEST_BYTES + 1)
|
||||
if not raw or len(raw) > MAX_REQUEST_BYTES:
|
||||
raise HostError("request payload is empty or too large")
|
||||
try:
|
||||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as error:
|
||||
raise HostError("request payload is invalid") from error
|
||||
if not isinstance(value, dict):
|
||||
raise HostError("request payload must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _write_message(
|
||||
value: Mapping[str, object],
|
||||
stream: object,
|
||||
) -> None:
|
||||
payload = (
|
||||
json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
encoded = payload.encode("utf-8")
|
||||
if len(encoded) > MAX_REQUEST_BYTES:
|
||||
raise HostError("response payload is too large")
|
||||
stream.write(encoded)
|
||||
stream.flush()
|
||||
|
||||
|
||||
class _ProtocolServiceBroker:
|
||||
def __init__(
|
||||
self,
|
||||
input_stream: object,
|
||||
output_stream: object,
|
||||
telemetry: RuntimeTelemetry,
|
||||
) -> None:
|
||||
self.calls = 0
|
||||
self.input_stream = input_stream
|
||||
self.output_stream = output_stream
|
||||
self.telemetry = telemetry
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
profile: str,
|
||||
operation: str,
|
||||
payload: Mapping[str, object],
|
||||
) -> Mapping[str, object]:
|
||||
started = time.monotonic()
|
||||
self.calls += 1
|
||||
if self.calls > MAX_SERVICE_CALLS:
|
||||
raise CapabilityUnavailable("service call limit exceeded")
|
||||
call_id = str(self.calls)
|
||||
_write_message(
|
||||
{
|
||||
"status": "service_request",
|
||||
"call_id": call_id,
|
||||
"profile": profile,
|
||||
"operation": operation,
|
||||
"payload": dict(payload),
|
||||
},
|
||||
self.output_stream,
|
||||
)
|
||||
response = _read_message(self.input_stream)
|
||||
if response.get("call_id") != call_id:
|
||||
raise CapabilityUnavailable("service response id mismatch")
|
||||
if response.get("op") == "service_error":
|
||||
self.telemetry.elapsed(
|
||||
"host.service.wait",
|
||||
started,
|
||||
status="error",
|
||||
plugin_id=profile,
|
||||
operation=operation,
|
||||
error_type=str(response.get("error", "ServiceError")),
|
||||
)
|
||||
raise CapabilityUnavailable("kernel service call failed")
|
||||
if response.get("op") != "service_result":
|
||||
raise CapabilityUnavailable("kernel service response is invalid")
|
||||
result = response.get("result")
|
||||
if not isinstance(result, Mapping):
|
||||
raise CapabilityUnavailable("kernel service result is invalid")
|
||||
self.telemetry.elapsed(
|
||||
"host.service.wait",
|
||||
started,
|
||||
plugin_id=profile,
|
||||
operation=operation,
|
||||
)
|
||||
return dict(result)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--release-dir", type=Path, required=True)
|
||||
parser.add_argument("--allowed-signers", type=Path)
|
||||
parser.add_argument("--publisher")
|
||||
parser.add_argument("--allow-unsigned-local", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.allow_unsigned_local:
|
||||
if args.allowed_signers is not None or args.publisher is not None:
|
||||
parser.error(
|
||||
"--allow-unsigned-local cannot include publisher trust options"
|
||||
)
|
||||
elif args.allowed_signers is None:
|
||||
parser.error("signed host requires --allowed-signers")
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
_scrub_environment()
|
||||
telemetry = RuntimeTelemetry(component="extension-host")
|
||||
process_started = time.monotonic()
|
||||
input_stream = sys.stdin.buffer
|
||||
output_stream = sys.stdout.buffer
|
||||
try:
|
||||
service_broker = _ProtocolServiceBroker(
|
||||
input_stream,
|
||||
output_stream,
|
||||
telemetry,
|
||||
)
|
||||
load_started = time.monotonic()
|
||||
runtime = ExtensionRuntime.load(
|
||||
args.release_dir,
|
||||
allowed_signers=args.allowed_signers,
|
||||
expected_publisher=args.publisher,
|
||||
allow_unsigned=args.allow_unsigned_local,
|
||||
service_broker=service_broker,
|
||||
)
|
||||
telemetry.elapsed(
|
||||
"host.runtime.load",
|
||||
load_started,
|
||||
plugin_id=runtime.manifest.plugin_id,
|
||||
process="extension-host",
|
||||
)
|
||||
request = _read_message(input_stream)
|
||||
event_value = request.get("event")
|
||||
event_id = (
|
||||
event_value.get("event_id", "")
|
||||
if isinstance(event_value, Mapping)
|
||||
else ""
|
||||
)
|
||||
telemetry.emit(
|
||||
"host.request.received",
|
||||
event_id=event_id,
|
||||
plugin_id=runtime.manifest.plugin_id,
|
||||
operation=str(request.get("op", "")),
|
||||
process="extension-host",
|
||||
)
|
||||
response = process_request(runtime, request)
|
||||
except HostError as error:
|
||||
logging.warning("extension host request failed error_type=%s", type(error).__name__)
|
||||
telemetry.elapsed(
|
||||
"host.process.complete",
|
||||
process_started,
|
||||
status="error",
|
||||
process="extension-host",
|
||||
error_type=type(error).__name__,
|
||||
)
|
||||
response = {"status": "error", "error": type(error).__name__}
|
||||
except Exception as error:
|
||||
logging.error("extension host failed error_type=%s", type(error).__name__)
|
||||
telemetry.elapsed(
|
||||
"host.process.complete",
|
||||
process_started,
|
||||
status="error",
|
||||
process="extension-host",
|
||||
error_type=type(error).__name__,
|
||||
)
|
||||
response = {"status": "error", "error": "HostUnavailable"}
|
||||
else:
|
||||
telemetry.elapsed(
|
||||
"host.process.complete",
|
||||
process_started,
|
||||
plugin_id=str(response.get("plugin_id", "")),
|
||||
operation=str(request.get("op", "")),
|
||||
process="extension-host",
|
||||
)
|
||||
_write_message(response, output_stream)
|
||||
return 0 if response.get("status") == "ok" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user