Merge main (management REST API) into m1-lumen-core
ci / rust (push) Has been cancelled

Resolutions: serve() keeps main's AppState::new() with our persisted-pairing load folded
into it; main.rs keeps both the m3 and mgmt modules; mgmt's test LaunchSessions gain the
new appid field; Cargo.lock re-resolved. Full gate green (92 tests, clippy, fmt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 07:03:41 +00:00
11 changed files with 1969 additions and 59 deletions
+64 -5
View File
@@ -20,10 +20,10 @@ mod gamestream;
mod inject;
mod m0;
mod m3;
mod mgmt;
mod pipeline;
mod pwinit;
mod vdisplay;
mod web;
#[cfg(target_os = "linux")]
mod zerocopy;
@@ -33,10 +33,12 @@ use m0::{Options, Source};
use std::path::PathBuf;
fn main() {
// Logs go to stderr so stdout stays machine-readable (`lumen-host openapi > spec.json`).
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.with_writer(std::io::stderr)
.init();
if let Err(e) = real_main() {
@@ -50,8 +52,13 @@ fn real_main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
// M2 GameStream host control plane (P1.1: mDNS + serverinfo).
Some("serve") => gamestream::serve(),
// M2 GameStream host control plane (P1.1: mDNS + serverinfo) + management API.
Some("serve") => gamestream::serve(parse_serve(&args[1..])?),
// Print the management API's OpenAPI document (for client codegen).
Some("openapi") => {
print!("{}", mgmt::openapi_json());
Ok(())
}
// Standalone input-injection smoke test (no client needed): open the session's input
// backend and inject a scripted mouse/keyboard pattern. Watch a focused app / `wev`.
Some("input-test") => input_test(),
@@ -140,6 +147,51 @@ fn input_test() -> Result<()> {
bail!("input-test requires Linux")
}
/// `serve` options — all about the management API; the GameStream ports are protocol-fixed.
fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
let mut opts = mgmt::Options::default();
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
let mut next = || {
i += 1;
args.get(i)
.cloned()
.ok_or_else(|| anyhow::anyhow!("missing value for {arg}"))
};
match arg {
"--mgmt-bind" => {
opts.bind = next()?
.parse()
.map_err(|_| anyhow::anyhow!("bad --mgmt-bind (want IP:PORT)"))?
}
"--mgmt-token" => {
let token = next()?;
// An empty token would satisfy the non-loopback "token required" guard
// while authenticating nobody (or, worse, everybody) — refuse it loudly
// rather than letting `--mgmt-token "$UNSET_VAR"` ship a dead credential.
if token.trim().is_empty() {
bail!("--mgmt-token must not be empty");
}
opts.token = Some(token);
}
"-h" | "--help" => {
print_usage();
std::process::exit(0);
}
other => bail!("unknown argument '{other}' (try --help)"),
}
i += 1;
}
// Flag wins over the environment so a unit file can set a default and a shell override it.
if opts.token.is_none() {
opts.token = std::env::var("LUMEN_MGMT_TOKEN")
.ok()
.filter(|t| !t.is_empty());
}
Ok(opts)
}
fn parse_m0(args: &[String]) -> Result<Options> {
let mut source = Source::Portal;
let mut width = 1920u32;
@@ -242,10 +294,17 @@ fn print_usage() {
"lumen-host — Linux streaming host
USAGE:
lumen-host serve GameStream host control plane (M2: mDNS + serverinfo …)
lumen-host serve [OPTIONS] GameStream host control plane (M2: mDNS + serverinfo …)
+ the management REST API
lumen-host openapi print the management API's OpenAPI document (codegen)
lumen-host m0 [OPTIONS] M0 capture→encode→file pipeline spike
OPTIONS:
SERVE OPTIONS:
--mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990)
--mgmt-token <TOKEN> bearer token for the management API (or LUMEN_MGMT_TOKEN);
required when --mgmt-bind is not loopback
M0 OPTIONS:
--source <synthetic|portal|kwin-virtual>
frame source (default: portal). 'kwin-virtual' creates a
KWin virtual output at --width x --height and captures it