Skip to main content

mobius_gateway/
command.rs

1//! Command-line entrypoint shared by the gateway and CLI packages.
2
3mod args;
4mod connection;
5mod init;
6mod lifecycle;
7mod provider;
8
9use std::ffi::OsString;
10#[cfg(any(unix, test))]
11use std::fs::{self, File, OpenOptions, TryLockError};
12#[cfg(unix)]
13use std::io::IsTerminal as _;
14#[cfg(any(unix, test))]
15use std::io::Write;
16#[cfg(any(unix, test))]
17use std::io::{Read as _, Seek as _, SeekFrom};
18use std::net::SocketAddr;
19#[cfg(unix)]
20use std::os::unix::fs::PermissionsExt as _;
21#[cfg(unix)]
22use std::os::unix::process::CommandExt as _;
23use std::path::{Path, PathBuf};
24#[cfg(unix)]
25use std::process::Stdio;
26#[cfg(unix)]
27use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
28
29#[cfg(any(unix, test))]
30use qrcode::QrCode;
31#[cfg(any(unix, test))]
32use qrcode::render::unicode::Dense1x2;
33#[cfg(any(unix, test))]
34use url::Url;
35
36#[cfg(unix)]
37use crate::auth::PairingStatus;
38use crate::auth::{AuthStore, PairingGrant};
39use crate::client::{Endpoint, GatewayClient, MAX_PENDING_FRAMES};
40use crate::cloudflare::CloudflareTunnel;
41use crate::config::{
42    CloudflareConfig, ConfigStore, DEFAULT_LISTEN, GatewayConfig, TlsConfig, load_cloudflare_token,
43    state_dir,
44};
45use crate::server::GatewayServer;
46use crate::wire::{ClientKind, ClientMessage, ServerMessage};
47use crate::{Error, Result};
48#[cfg(unix)]
49use nix::sys::signal::{Signal, kill};
50#[cfg(unix)]
51use nix::unistd::Pid;
52#[cfg(any(unix, test))]
53use serde::Deserialize;
54use serde::Serialize;
55#[cfg(unix)]
56use tokio::process::{Child, Command as TokioCommand};
57#[cfg(unix)]
58use tokio::signal::unix::{Signal as TokioSignal, SignalKind, signal};
59use uuid::Uuid;
60
61use self::args::*;
62use self::connection::*;
63use self::init::*;
64pub use self::init::{
65    initialize_named_cloudflare, initialize_quick_cloudflare, reset_gateway_state,
66};
67pub use self::lifecycle::ensure_background_gateway;
68use self::lifecycle::*;
69use self::provider::*;
70
71pub const USAGE: &str = "usage: mobius-gateway [--state-dir PATH]\n       \
72                     mobius-gateway provider [--state-dir PATH]\n       \
73                     mobius-gateway init [--state-dir PATH] [--listen ADDR] \
74                     [--tls-cert PATH --tls-key PATH] \
75                     [--cloudflare-hostname HOST --cloudflare-token-file PATH]\n       \
76                     mobius-gateway bootstrap [--state-dir PATH]\n       \
77                     mobius-gateway reset-default-agent [--state-dir PATH]\n       \
78                     mobius-gateway pairing-code [--state-dir PATH] --json\n       \
79                     mobius-gateway register-provider [--state-dir PATH] --provider ID \
80                     --model ID [--instance ID] [--label TEXT] \
81                     [--reasoning-efforts CSV] [--web-search off|cached|live] \
82                     [--base-url URL] \
83                     [--credentialless | --credential-stdin]\n       \
84                     mobius-gateway connect [--state-dir PATH] [--endpoint ENDPOINT]\n       \
85                     mobius-gateway serve [--state-dir PATH] [--background]\n       \
86                     mobius-gateway exit [--state-dir PATH]";
87
88#[cfg(any(unix, test))]
89const PROCESS_FILE: &str = "gateway-process.json";
90#[cfg(unix)]
91const STARTUP_FILE: &str = "gateway-start.lock";
92#[cfg(unix)]
93const STATE_MARKER_FILE: &str = "gateway.toml";
94#[cfg(any(unix, test))]
95const MAX_PROCESS_RECORD_BYTES: usize = 4 * 1024;
96#[cfg(unix)]
97const EXIT_TIMEOUT: Duration = Duration::from_secs(5);
98#[cfg(unix)]
99const EXIT_POLL_INTERVAL: Duration = Duration::from_millis(100);
100#[cfg(unix)]
101const BACKGROUND_START_TIMEOUT: Duration = Duration::from_secs(40);
102#[cfg(unix)]
103const BACKGROUND_START_POLL_INTERVAL: Duration = Duration::from_millis(50);
104#[cfg(unix)]
105const MAX_BACKGROUND_ERROR_BYTES: u64 = 16 * 1024;
106#[cfg(unix)]
107const CONNECTION_POLL_INTERVAL: Duration = Duration::from_millis(100);
108
109/// Runs a gateway command with arguments excluding the executable name.
110pub async fn run(
111    arguments: Vec<OsString>,
112    save_local_client: fn(&Endpoint, String) -> Result<()>,
113    load_local_client: fn(&Endpoint) -> Result<Option<String>>,
114) -> Result<()> {
115    if matches!(arguments.as_slice(), [flag] if flag == "--help" || flag == "-h") {
116        println!("{USAGE}");
117        return Ok(());
118    }
119    if matches!(arguments.as_slice(), [flag] if flag == "--version" || flag == "-V") {
120        println!("mobius-gateway {}", env!("CARGO_PKG_VERSION"));
121        return Ok(());
122    }
123    match parse(arguments)? {
124        Command::Init(options) => initialize(options),
125        Command::Bootstrap { state_dir } => initialize_bootstrap(state_dir, save_local_client),
126        Command::ResetDefaultAgent { state_dir } => reset_default_agent(state_dir),
127        Command::PairingCode { state_dir } => pairing_code(state_dir, load_local_client).await,
128        Command::RegisterProvider(options) => {
129            register_provider_command(options, load_local_client).await
130        }
131        Command::Connect(options) => connect(options, load_local_client).await,
132        Command::Serve {
133            state_dir,
134            background,
135        } => {
136            if background {
137                serve_in_background(state_dir).await
138            } else {
139                serve(state_dir, true, save_local_client).await
140            }
141        }
142        Command::ServeChild { state_dir } => serve(state_dir, false, save_local_client).await,
143        Command::Exit { state_dir } => exit_gateway(state_dir),
144    }
145}
146
147#[cfg(test)]
148mod tests;