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;
7#[cfg(target_os = "macos")]
8mod menu_bar;
9mod provider;
10
11use std::ffi::OsString;
12#[cfg(any(unix, test))]
13use std::fs::{self, File, OpenOptions, TryLockError};
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(unix)]
30use crate::auth::PairingStatus;
31use crate::auth::{AuthStore, PairingGrant};
32use crate::client::{Endpoint, GatewayClient, MAX_PENDING_FRAMES};
33use crate::cloudflare::CloudflareTunnel;
34use crate::config::{
35    CloudflareConfig, ConfigStore, DEFAULT_LISTEN, GatewayConfig, TlsConfig, load_cloudflare_token,
36    state_dir,
37};
38use crate::server::GatewayServer;
39use crate::wire::{ClientKind, ClientMessage, ServerMessage};
40use crate::{Error, Result};
41#[cfg(unix)]
42use nix::sys::signal::{Signal, kill};
43#[cfg(unix)]
44use nix::unistd::Pid;
45#[cfg(any(unix, test))]
46use serde::Deserialize;
47use serde::Serialize;
48#[cfg(unix)]
49use tokio::process::{Child, Command as TokioCommand};
50#[cfg(unix)]
51use tokio::signal::unix::{Signal as TokioSignal, SignalKind, signal};
52use uuid::Uuid;
53
54#[cfg(test)]
55use self::args::parse;
56use self::args::{
57    CloudflareInit, Command, ConnectOptions, InitOptions, RegisterProviderOptions, parse_cli,
58};
59pub use self::args::{FrontendCommand, GatewayCli};
60use self::connection::*;
61use self::init::*;
62pub use self::init::{
63    initialize_named_cloudflare, initialize_quick_cloudflare, reset_gateway_state,
64};
65pub use self::lifecycle::ensure_background_gateway;
66use self::lifecycle::*;
67use self::provider::*;
68
69#[cfg(unix)]
70/// Terminates a process group created by gateway autostart.
71pub fn terminate_process_group(pid: u32) {
72    if pid <= 1 {
73        return;
74    }
75    let Ok(pid) = i32::try_from(pid) else {
76        return;
77    };
78    let Some(group) = pid.checked_neg() else {
79        return;
80    };
81    let _ = kill(Pid::from_raw(group), Signal::SIGKILL);
82}
83
84#[cfg(any(unix, test))]
85const PROCESS_FILE: &str = "gateway-process.json";
86#[cfg(unix)]
87const STARTUP_FILE: &str = "gateway-start.lock";
88#[cfg(unix)]
89const STATE_MARKER_FILE: &str = "gateway.toml";
90#[cfg(any(unix, test))]
91const MAX_PROCESS_RECORD_BYTES: usize = 4 * 1024;
92#[cfg(unix)]
93const EXIT_TIMEOUT: Duration = Duration::from_secs(5);
94#[cfg(unix)]
95const EXIT_POLL_INTERVAL: Duration = Duration::from_millis(100);
96#[cfg(unix)]
97const BACKGROUND_START_TIMEOUT: Duration = Duration::from_secs(40);
98#[cfg(unix)]
99const BACKGROUND_START_POLL_INTERVAL: Duration = Duration::from_millis(50);
100#[cfg(unix)]
101const MAX_BACKGROUND_ERROR_BYTES: u64 = 16 * 1024;
102#[cfg(unix)]
103const CONNECTION_POLL_INTERVAL: Duration = Duration::from_millis(100);
104
105/// Runs a gateway command with arguments excluding the executable name.
106pub async fn run(
107    arguments: Vec<OsString>,
108    save_local_client: fn(&Endpoint, String) -> Result<()>,
109    load_local_client: fn(&Endpoint) -> Result<Option<String>>,
110) -> Result<()> {
111    let cli = match parse_cli(arguments) {
112        Ok(cli) => cli,
113        Err(error)
114            if matches!(
115                error.kind(),
116                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
117            ) =>
118        {
119            error.print()?;
120            return Ok(());
121        }
122        Err(error) => return Err(Error::Config(error.to_string())),
123    };
124    run_cli(cli, save_local_client, load_local_client).await
125}
126
127/// Runs an already parsed gateway command.
128pub async fn run_cli(
129    cli: GatewayCli,
130    save_local_client: fn(&Endpoint, String) -> Result<()>,
131    load_local_client: fn(&Endpoint) -> Result<Option<String>>,
132) -> Result<()> {
133    match cli.into_command()? {
134        Command::Init(options) => initialize(options),
135        Command::Bootstrap { state_dir } => initialize_bootstrap(state_dir, save_local_client),
136        Command::ResetBotDefaults { state_dir } => reset_bot_defaults(state_dir),
137        Command::PairingCode { state_dir } => pairing_code(state_dir, load_local_client).await,
138        Command::ClearProviderCredential {
139            state_dir,
140            instance,
141        } => provider::clear_provider_credential(state_dir, instance, load_local_client).await,
142        Command::RegisterProvider(options) => {
143            register_provider_command(options, load_local_client).await
144        }
145        Command::Connect(options) => connect(options, load_local_client).await,
146        Command::Serve {
147            state_dir,
148            background,
149        } => {
150            if background {
151                serve_in_background(state_dir).await
152            } else {
153                serve(state_dir, true, save_local_client).await
154            }
155        }
156        Command::ServeChild { state_dir } => serve(state_dir, false, save_local_client).await,
157        Command::Exit { state_dir } => exit_gateway(state_dir),
158        #[cfg(target_os = "macos")]
159        Command::MenuBar { state_dir } => menu_bar::open(&state_dir),
160        #[cfg(target_os = "macos")]
161        Command::MenuBarConnect { state_dir } => {
162            menu_bar::connect(state_dir, load_local_client).await
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests;