#![expect(
clippy::print_stdout,
clippy::print_stderr,
reason = "a command-line tool reports to its terminal"
)]
use std::net::{IpAddr, SocketAddr};
use std::process::ExitCode;
use std::time::Duration;
use scaleset::testing::Emulator;
const USAGE: &str = "\
usage: scaleset-emulator [options]
--host <addr> address to bind (default 127.0.0.1)
--port <port> port to bind (default 4100; 0 picks one)
--pat <token> personal access token to accept (default ghp_emulator)
--app-installation-id <id> accept GitHub App JWTs for this installation
--long-poll-ms <ms> how long idle polls are held (default 50000)
--auto-run-ms <ms> start assigned jobs on idle runners and finish them after <ms>
--admin-token-ttl-secs <s> admin token lifetime (default 3600)
--runner-group <name> add a runner group (repeatable)
";
fn main() -> ExitCode {
match parse(std::env::args().skip(1)) {
Ok(Some((builder, addr))) => {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime");
runtime.block_on(async move {
let emu = match builder.bind(addr).try_start().await {
Ok(emu) => emu,
Err(e) => {
eprintln!("error: cannot listen on {addr}: {e}");
return ExitCode::FAILURE;
}
};
println!(
"{}",
serde_json::json!({
"url": emu.uri(),
"config_url": emu.config_url(),
"control_url": format!("{}/_emulator", emu.uri()),
})
);
let _ = tokio::signal::ctrl_c().await;
ExitCode::SUCCESS
})
}
Ok(None) => {
print!("{USAGE}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}\n\n{USAGE}");
ExitCode::from(2)
}
}
}
fn parse(
mut args: impl Iterator<Item = String>,
) -> Result<Option<(scaleset::testing::EmulatorBuilder, SocketAddr)>, String> {
let mut builder = Emulator::builder();
let mut host: IpAddr = [127, 0, 0, 1].into();
let mut port = 4100u16;
while let Some(flag) = args.next() {
if flag == "-h" || flag == "--help" {
return Ok(None);
}
let value = args.next().ok_or_else(|| format!("{flag} needs a value"))?;
let number = |v: &str| {
v.parse::<u64>()
.map_err(|_| format!("{flag}: not a number: {v}"))
};
match flag.as_str() {
"--host" => {
host = value
.parse()
.map_err(|_| format!("invalid host: {value}"))?;
}
"--port" => {
port = value
.parse()
.map_err(|_| format!("invalid port: {value}"))?;
}
"--pat" => builder = builder.pat(value),
"--app-installation-id" => {
builder = builder.github_app(
value
.parse()
.map_err(|_| format!("invalid installation ID: {value}"))?,
);
}
"--long-poll-ms" => builder = builder.long_poll(Duration::from_millis(number(&value)?)),
"--auto-run-ms" => {
builder = builder.auto_run_jobs(Duration::from_millis(number(&value)?));
}
"--admin-token-ttl-secs" => {
builder = builder.admin_token_ttl(Duration::from_secs(number(&value)?));
}
"--runner-group" => builder = builder.runner_group(value),
other => return Err(format!("unknown option {other}")),
}
}
Ok(Some((builder, SocketAddr::new(host, port))))
}