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