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;
8mod telemetry;
9
10use std::ffi::OsString;
11#[cfg(any(unix, test))]
12use std::fs::{self, File, OpenOptions, TryLockError};
13#[cfg(any(unix, test))]
14use std::io::Write;
15#[cfg(any(unix, test))]
16use std::io::{Read as _, Seek as _, SeekFrom};
17use std::net::SocketAddr;
18#[cfg(unix)]
19use std::os::unix::process::CommandExt as _;
20use std::path::{Path, PathBuf};
21#[cfg(unix)]
22use std::process::Stdio;
23#[cfg(unix)]
24use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
25
26#[cfg(unix)]
27use crate::auth::PairingStatus;
28use crate::auth::{AuthStore, PairingGrant};
29use crate::client::{Endpoint, GatewayClient, MAX_PENDING_FRAMES};
30use crate::cloudflare::CloudflareTunnel;
31use crate::config::{
32    CloudflareConfig, ConfigStore, DEFAULT_LISTEN, GatewayConfig, TlsConfig, load_secret_file,
33    state_dir,
34};
35use crate::server::GatewayServer;
36use crate::wire::{ClientKind, ClientMessage, ServerMessage};
37use crate::{Error, Result};
38#[cfg(unix)]
39use nix::sys::signal::{Signal, kill};
40#[cfg(unix)]
41use nix::unistd::Pid;
42#[cfg(any(unix, test))]
43use serde::Deserialize;
44use serde::Serialize;
45#[cfg(unix)]
46use tokio::process::{Child, Command as TokioCommand};
47#[cfg(unix)]
48use tokio::signal::unix::{Signal as TokioSignal, SignalKind, signal};
49use uuid::Uuid;
50
51#[cfg(test)]
52use self::args::parse;
53pub use self::args::{CloudflareInit, FrontendCommand, GatewayCli};
54use self::args::{Command, ConnectOptions, InitOptions, RegisterProviderOptions, parse_cli};
55use self::connection::*;
56use self::init::*;
57pub use self::init::{
58    initialize_named_cloudflare, initialize_quick_cloudflare, reset_gateway_state,
59};
60use self::lifecycle::*;
61pub use self::lifecycle::{ensure_background_gateway, startup_error};
62use self::provider::*;
63
64#[cfg(unix)]
65/// Terminates a process group created by gateway autostart.
66pub fn terminate_process_group(pid: u32) {
67    if pid <= 1 {
68        return;
69    }
70    let Ok(pid) = i32::try_from(pid) else {
71        return;
72    };
73    let Some(group) = pid.checked_neg() else {
74        return;
75    };
76    let _ = kill(Pid::from_raw(group), Signal::SIGKILL);
77}
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.
101/// # Errors
102///
103/// Returns an error if validation or an operation required to complete the request fails.
104pub async fn run(
105    arguments: Vec<OsString>,
106    save_local_client: fn(&Endpoint, String) -> Result<()>,
107    load_local_client: fn(&Endpoint) -> Result<Option<String>>,
108) -> Result<()> {
109    let cli = match parse_cli(arguments) {
110        Ok(cli) => cli,
111        Err(error)
112            if matches!(
113                error.kind(),
114                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
115            ) =>
116        {
117            error.print()?;
118            return Ok(());
119        }
120        Err(error) => return Err(Error::Config(error.to_string())),
121    };
122    run_cli(cli, save_local_client, load_local_client).await
123}
124
125/// Runs an already parsed gateway command.
126/// # Errors
127///
128/// Returns an error if validation or an operation required to complete the request fails.
129pub async fn run_cli(
130    cli: GatewayCli,
131    save_local_client: fn(&Endpoint, String) -> Result<()>,
132    load_local_client: fn(&Endpoint) -> Result<Option<String>>,
133) -> Result<()> {
134    match cli.into_command()? {
135        Command::PrintDefaultConfig => {
136            println!(
137                "{}",
138                toml::to_string_pretty(&GatewayConfig::new(crate::config::DEFAULT_LISTEN, None)?)
139                    .map_err(|error| Error::Config(format!(
140                    "cannot encode default gateway configuration: {error}"
141                )))?
142            );
143            Ok(())
144        }
145        Command::CheckConfig { state_dir } => {
146            ConfigStore::open(state_dir)?;
147            println!("gateway configuration is valid");
148            Ok(())
149        }
150        Command::ExportComputerResources { directory } => {
151            crate::computer_runtime::export_resources(&directory)
152        }
153        Command::Telemetry { state_dir, command } => {
154            telemetry::run(state_dir, command, load_local_client).await
155        }
156        Command::SetRuntime {
157            state_dir,
158            idle_exit_seconds,
159            storage_limit_bytes,
160            ingress,
161            clear_ingress,
162            clear_storage_limit,
163        } => {
164            let (store, mut config) = ConfigStore::open(state_dir)?;
165            ensure_gateway_stopped(&store, &config)?;
166            if let Some(seconds) = idle_exit_seconds {
167                config.runtime.idle_exit_seconds = seconds;
168            }
169            if let Some(bytes) = storage_limit_bytes {
170                config.runtime.storage_limit_bytes = Some(bytes);
171            }
172            if let Some(address) = ingress {
173                config.runtime.ingress = Some(address);
174            }
175            if clear_ingress {
176                config.runtime.ingress = None;
177            }
178            if clear_storage_limit {
179                config.runtime.storage_limit_bytes = None;
180            }
181            config.validate()?;
182            store.save(&config)
183        }
184        Command::Init(options) => initialize(options),
185        Command::Bootstrap { state_dir } => initialize_bootstrap(state_dir, save_local_client),
186        Command::ResetBotDefaults { state_dir } => reset_bot_defaults(state_dir),
187        Command::SetDesktop { state_dir, enabled } => set_desktop(state_dir, enabled),
188        Command::PairingCode { state_dir } => pairing_code(state_dir, load_local_client).await,
189        Command::ClearProviderCredential {
190            state_dir,
191            instance,
192        } => provider::clear_provider_credential(state_dir, instance, load_local_client).await,
193        Command::RegisterProvider(options) => {
194            register_provider_command(options, load_local_client).await
195        }
196        Command::Connect(options) => connect(options, load_local_client).await,
197        Command::Serve {
198            state_dir,
199            background,
200        } => {
201            if background {
202                serve_in_background(state_dir, load_local_client).await
203            } else {
204                serve(state_dir, true, save_local_client, load_local_client).await
205            }
206        }
207        Command::ServeChild { state_dir } => {
208            serve(state_dir, false, save_local_client, load_local_client).await
209        }
210        Command::Exit { state_dir } => exit_gateway(state_dir),
211    }
212}
213
214#[cfg(test)]
215mod tests;