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
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			let Candidate = CargoTarget.join("Air");
87			Candidate.exists().then_some(Candidate)
88		});
89
90	let BinaryPath = match BinaryPath {
91		Some(P) => P,
92
93		None => {
94			dev_log!(
95				"grpc",
96				"warn: [AirStart] Air binary not found in resources or target/debug; running without Air"
97			);
98
99			return Ok(());
100		},
101	};
102
103	dev_log!("grpc", "[AirStart] Spawning Air binary at: {}", BinaryPath.display());
104
105	// Spawn detached so Air's lifecycle is independent of Mountain's
106	// boot path. Mountain holds no Child handle - Air manages its own
107	// shutdown via SIGTERM from the OS or its own gRPC `Shutdown` RPC.
108	let SpawnResult = tokio::process::Command::new(&BinaryPath)
109		.env("AIR_GRPC_ADDRESS", AIR_GRPC_ADDRESS)
110		.env(
111			"AIR_LOG_DIR",
112			std::env::var("AIR_LOG_DIR").unwrap_or_else(|_| "/tmp/air-log".to_string()),
113		)
114		.stdin(std::process::Stdio::null())
115		.stdout(std::process::Stdio::piped())
116		.stderr(std::process::Stdio::piped())
117		.spawn();
118
119	let mut Child = match SpawnResult {
120		Ok(C) => C,
121
122		Err(Error) => {
123			dev_log!("grpc", "warn: [AirStart] Failed to spawn Air ({}); running without Air", Error);
124
125			return Ok(());
126		},
127	};
128
129	let AirPid = Child.id();
130
131	dev_log!("grpc", "[AirStart] Air spawned successfully (pid={:?})", AirPid);
132
133	// Drain Air's stdout/stderr into Mountain's dev log so the user
134	// can diagnose Air-side issues from a single log stream.
135	if let Some(Stdout) = Child.stdout.take() {
136		tokio::spawn(async move {
137			use tokio::io::{AsyncBufReadExt, BufReader};
138			let mut Reader = BufReader::new(Stdout).lines();
139			while let Ok(Some(Line)) = Reader.next_line().await {
140				dev_log!("grpc", "[Air stdout] {}", Line);
141			}
142		});
143	}
144
145	if let Some(Stderr) = Child.stderr.take() {
146		tokio::spawn(async move {
147			use tokio::io::{AsyncBufReadExt, BufReader};
148			let mut Reader = BufReader::new(Stderr).lines();
149			while let Ok(Some(Line)) = Reader.next_line().await {
150				dev_log!("grpc", "[Air stderr] {}", Line);
151			}
152		});
153	}
154
155	// Reap the child in a detached task so the OS doesn't keep a
156	// zombie around when Air exits.
157	tokio::spawn(async move {
158		match Child.wait().await {
159			Ok(Status) => dev_log!("grpc", "[AirStart] Air exited (status={:?})", Status),
160			Err(Error) => dev_log!("grpc", "warn: [AirStart] Air wait error: {}", Error),
161		}
162	});
163
164	// Connect via Vine. Air's gRPC server takes ~150 ms to become
165	// listenable; ConnectToSideCar handles the retry loop.
166	let SideCarIdentifier = "air-main".to_string();
167
168	let Address = format!("http://{}", AIR_GRPC_ADDRESS);
169
170	match crate::Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), Address.clone()).await {
171		Ok(()) => {
172			dev_log!("grpc", "[AirStart] Air gRPC connection established at {}", Address);
173		},
174
175		Err(Error) => {
176			dev_log!(
177				"grpc",
178				"warn: [AirStart] Air spawned but gRPC connect failed ({}); workbench continues in degraded mode",
179				Error
180			);
181		},
182	}
183
184	Ok(())
185}