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 Fn(_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
48		return Ok(());
49	}
50
51	#[cfg(feature = "AirIntegration")]
52	{
53		LaunchAndConnectAir(_ApplicationHandle.clone(), _Environment.clone()).await
54	}
55
56	#[cfg(not(feature = "AirIntegration"))]
57	{
58		dev_log!(
59			"grpc",
60			"[AirStart] AirIntegration feature disabled; skipping spawn (workbench runs without Air)"
61		);
62
63		Ok(())
64	}
65}
66
67#[cfg(feature = "AirIntegration")]
68async fn LaunchAndConnectAir(ApplicationHandle:AppHandle, _Environment:Arc<MountainEnvironment>) -> Result<(), String> {
69	use std::path::PathBuf;
70
71	use tauri::Manager;
72
73	dev_log!("grpc", "[AirStart] Resolving Air sidecar binary path...");
74
75	// Try the Tauri sidecar resolver first (release / bundled).
76	// Falls back to the Cargo target dir for dev builds.
77	let BinaryPath:Option<PathBuf> = ApplicationHandle
78		.path()
79		.resolve("Air", tauri::path::BaseDirectory::Resource)
80		.ok()
81		.filter(|P| P.exists())
82		.or_else(|| {
83			let CargoTarget = std::env::var("CARGO_TARGET_DIR")
84				.map(PathBuf::from)
85				.unwrap_or_else(|_| PathBuf::from("Element/Air/Target/debug"));
86
87			let Candidate = CargoTarget.join("Air");
88
89			Candidate.exists().then_some(Candidate)
90		});
91
92	let BinaryPath = match BinaryPath {
93		Some(P) => P,
94
95		None => {
96			dev_log!(
97				"grpc",
98				"warn: [AirStart] Air binary not found in resources or target/debug; running without Air"
99			);
100
101			return Ok(());
102		},
103	};
104
105	dev_log!("grpc", "[AirStart] Spawning Air binary at: {}", BinaryPath.display());
106
107	// Spawn detached so Air's lifecycle is independent of Mountain's
108	// boot path. Mountain holds no Child handle - Air manages its own
109	// shutdown via SIGTERM from the OS or its own gRPC `Shutdown` RPC.
110	let SpawnResult = tokio::process::Command::new(&BinaryPath)
111		.env("AIR_GRPC_ADDRESS", AIR_GRPC_ADDRESS)
112		.env(
113			"AIR_LOG_DIR",
114			std::env::var("AIR_LOG_DIR").unwrap_or_else(|_| "/tmp/air-log".to_string()),
115		)
116		.stdin(std::process::Stdio::null())
117		.stdout(std::process::Stdio::piped())
118		.stderr(std::process::Stdio::piped())
119		.spawn();
120
121	let mut Child = match SpawnResult {
122		Ok(C) => C,
123
124		Err(Error) => {
125			dev_log!("grpc", "warn: [AirStart] Failed to spawn Air ({}); running without Air", Error);
126
127			return Ok(());
128		},
129	};
130
131	let AirPid = Child.id();
132
133	dev_log!("grpc", "[AirStart] Air spawned successfully (pid={:?})", AirPid);
134
135	// Drain Air's stdout/stderr into Mountain's dev log so the user
136	// can diagnose Air-side issues from a single log stream.
137	if let Some(Stdout) = Child.stdout.take() {
138		tokio::spawn(async move {
139			use tokio::io::{AsyncBufReadExt, BufReader};
140
141			let mut Reader = BufReader::new(Stdout).lines();
142
143			while let Ok(Some(Line)) = Reader.next_line().await {
144				dev_log!("grpc", "[Air stdout] {}", Line);
145			}
146		});
147	}
148
149	if let Some(Stderr) = Child.stderr.take() {
150		tokio::spawn(async move {
151			use tokio::io::{AsyncBufReadExt, BufReader};
152
153			let mut Reader = BufReader::new(Stderr).lines();
154
155			while let Ok(Some(Line)) = Reader.next_line().await {
156				dev_log!("grpc", "[Air stderr] {}", Line);
157			}
158		});
159	}
160
161	// Reap the child in a detached task so the OS doesn't keep a
162	// zombie around when Air exits.
163	tokio::spawn(async move {
164		match Child.wait().await {
165			Ok(Status) => dev_log!("grpc", "[AirStart] Air exited (status={:?})", Status),
166			Err(Error) => dev_log!("grpc", "warn: [AirStart] Air wait error: {}", Error),
167		}
168	});
169
170	// Connect via Vine. Air's gRPC server takes ~150 ms to become
171	// listenable; ConnectToSideCar handles the retry loop.
172	let SideCarIdentifier = "air-main".to_string();
173
174	let Address = format!("http://{}", AIR_GRPC_ADDRESS);
175
176	match ::Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), Address.clone()).await {
177		Ok(()) => {
178			dev_log!("grpc", "[AirStart] Air gRPC connection established at {}", Address);
179		},
180
181		Err(Error) => {
182			dev_log!(
183				"grpc",
184				"warn: [AirStart] Air spawned but gRPC connect failed ({}); workbench continues in degraded mode",
185				Error
186			);
187		},
188	}
189
190	Ok(())
191}