Skip to main content

Mountain/Binary/Service/
AirStart.rs

1//! # Air Start Module
2//!
3//! Spawns and connects to the Air daemon sidecar.
4//!
5//! Mirror of `CocoonStart.rs`. Air is the long-lived background daemon
6//! responsible for updates, downloads, crypto signing, file indexing,
7//! system monitoring, and other off-the-hot-path work that should not
8//! tax the workbench. Mountain spawns Air at boot, connects via gRPC
9//! over `[::1]:50053`, and registers it as a sidecar in the same
10//! Vine pool as Cocoon.
11//!
12//! Lifecycle parity with Cocoon:
13//!
14//!   - Resolve binary path from the Tauri sidecar resolver. The Air binary
15//!     ships next to Mountain in the bundle (release builds); in dev mode the
16//!     Cargo target dir is searched.
17//!   - Spawn as a background tokio task with stdout/stderr captured.
18//!   - Wait for the gRPC server to become available, then create an `AirClient`
19//!     and store it in the environment for handlers to consume.
20//!   - On failure: log a degraded-mode warning and return Ok(()) - the
21//!     workbench works without Air, just without update / index /
22//!     system-monitor capability.
23
24use std::sync::Arc;
25
26use tauri::AppHandle;
27
28use crate::{Environment::MountainEnvironment::MountainEnvironment, dev_log};
29
30/// Default gRPC address used by Air. Mirror of
31/// `AirClient::DEFAULT_AIR_SERVER_ADDRESS` for the connect step.
32const AIR_GRPC_ADDRESS:&str = "[::1]:50053";
33
34/// Spawn and connect to the Air daemon. Returns Ok(()) regardless of
35/// outcome - Air is non-essential for workbench operation; Mountain
36/// gracefully degrades when Air is unavailable.
37///
38/// Spawn is gated on:
39///   - The `AirIntegration` Cargo feature (compile-time).
40///   - The `Spawn` env var (runtime; mirrors `CocoonStart` semantics).
41pub async fn AirStart(_ApplicationHandle:&AppHandle, _Environment:&Arc<MountainEnvironment>) -> Result<(), String> {
42	// Atom N1 mirror: respect the `Spawn=false` env that disables
43	// sidecar spawn for tests and the smallest-shippable-surface
44	// Mountain-only profile.
45	if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
46		dev_log!("grpc", "[AirStart] Skipping Air spawn (Spawn=false)");
47		return Ok(());
48	}
49
50	#[cfg(feature = "AirIntegration")]
51	{
52		LaunchAndConnectAir(_ApplicationHandle.clone(), _Environment.clone()).await
53	}
54
55	#[cfg(not(feature = "AirIntegration"))]
56	{
57		dev_log!(
58			"grpc",
59			"[AirStart] AirIntegration feature disabled; skipping spawn (workbench runs without Air)"
60		);
61		Ok(())
62	}
63}
64
65#[cfg(feature = "AirIntegration")]
66async fn LaunchAndConnectAir(ApplicationHandle:AppHandle, _Environment:Arc<MountainEnvironment>) -> Result<(), String> {
67	use std::path::PathBuf;
68
69	use tauri::Manager;
70
71	dev_log!("grpc", "[AirStart] Resolving Air sidecar binary path...");
72
73	// Try the Tauri sidecar resolver first (release / bundled).
74	// Falls back to the Cargo target dir for dev builds.
75	let BinaryPath:Option<PathBuf> = ApplicationHandle
76		.path()
77		.resolve("Air", tauri::path::BaseDirectory::Resource)
78		.ok()
79		.filter(|P| P.exists())
80		.or_else(|| {
81			let CargoTarget = std::env::var("CARGO_TARGET_DIR")
82				.map(PathBuf::from)
83				.unwrap_or_else(|_| PathBuf::from("Element/Air/Target/debug"));
84			let Candidate = CargoTarget.join("Air");
85			Candidate.exists().then_some(Candidate)
86		});
87
88	let BinaryPath = match BinaryPath {
89		Some(P) => P,
90		None => {
91			dev_log!(
92				"grpc",
93				"warn: [AirStart] Air binary not found in resources or target/debug; running without Air"
94			);
95			return Ok(());
96		},
97	};
98
99	dev_log!("grpc", "[AirStart] Spawning Air binary at: {}", BinaryPath.display());
100
101	// Spawn detached so Air's lifecycle is independent of Mountain's
102	// boot path. Mountain holds no Child handle - Air manages its own
103	// shutdown via SIGTERM from the OS or its own gRPC `Shutdown` RPC.
104	let SpawnResult = tokio::process::Command::new(&BinaryPath)
105		.env("AIR_GRPC_ADDRESS", AIR_GRPC_ADDRESS)
106		.env(
107			"AIR_LOG_DIR",
108			std::env::var("AIR_LOG_DIR").unwrap_or_else(|_| "/tmp/air-log".to_string()),
109		)
110		.stdin(std::process::Stdio::null())
111		.stdout(std::process::Stdio::piped())
112		.stderr(std::process::Stdio::piped())
113		.spawn();
114
115	let mut Child = match SpawnResult {
116		Ok(C) => C,
117		Err(Error) => {
118			dev_log!("grpc", "warn: [AirStart] Failed to spawn Air ({}); running without Air", Error);
119			return Ok(());
120		},
121	};
122
123	let AirPid = Child.id();
124	dev_log!("grpc", "[AirStart] Air spawned successfully (pid={:?})", AirPid);
125
126	// Drain Air's stdout/stderr into Mountain's dev log so the user
127	// can diagnose Air-side issues from a single log stream.
128	if let Some(Stdout) = Child.stdout.take() {
129		tokio::spawn(async move {
130			use tokio::io::{AsyncBufReadExt, BufReader};
131			let mut Reader = BufReader::new(Stdout).lines();
132			while let Ok(Some(Line)) = Reader.next_line().await {
133				dev_log!("grpc", "[Air stdout] {}", Line);
134			}
135		});
136	}
137	if let Some(Stderr) = Child.stderr.take() {
138		tokio::spawn(async move {
139			use tokio::io::{AsyncBufReadExt, BufReader};
140			let mut Reader = BufReader::new(Stderr).lines();
141			while let Ok(Some(Line)) = Reader.next_line().await {
142				dev_log!("grpc", "[Air stderr] {}", Line);
143			}
144		});
145	}
146
147	// Reap the child in a detached task so the OS doesn't keep a
148	// zombie around when Air exits.
149	tokio::spawn(async move {
150		match Child.wait().await {
151			Ok(Status) => dev_log!("grpc", "[AirStart] Air exited (status={:?})", Status),
152			Err(Error) => dev_log!("grpc", "warn: [AirStart] Air wait error: {}", Error),
153		}
154	});
155
156	// Connect via Vine. Air's gRPC server takes ~150 ms to become
157	// listenable; ConnectToSideCar handles the retry loop.
158	let SideCarIdentifier = "air-main".to_string();
159	let Address = format!("http://{}", AIR_GRPC_ADDRESS);
160	match crate::Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), Address.clone()).await {
161		Ok(()) => {
162			dev_log!("grpc", "[AirStart] Air gRPC connection established at {}", Address);
163		},
164		Err(Error) => {
165			dev_log!(
166				"grpc",
167				"warn: [AirStart] Air spawned but gRPC connect failed ({}); workbench continues in degraded mode",
168				Error
169			);
170		},
171	}
172
173	Ok(())
174}