mobius_gateway/
command.rs1mod 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)]
70pub 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
105pub 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
127pub 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;