Skip to main content

Mountain/Binary/Main/
Entry.rs

1//! # Entry (Binary/Main)
2//!
3//! ## RESPONSIBILITIES
4//!
5//! Main application entry point that orchestrates the complete application
6//! lifecycle. This function coordinates:
7//! - Tokio runtime creation and management
8//! - CLI argument parsing
9//! - Application state initialization
10//! - Tauri application builder setup
11//! - Service initialization (Vine, Cocoon, Configuration)
12//! - Graceful shutdown handling
13//!
14//! ## ARCHITECTURAL ROLE
15//!
16//! The Entry module is the **primary entry point** in Mountain's architecture:
17//!
18//! ```text
19//! main.rs ──► Binary::Main::Entry::Fn()
20//!                                    │
21//!                                    ▼
22//! AppLifecycle ──► Service Initialization ──► Tauri App Run
23//!                                           │
24//!                                           ▼
25//!                                   Graceful Shutdown
26//! ```
27//!
28//! ## KEY COMPONENTS
29//!
30//! - **Fn()**: Main entry point exported as `Binary::Main::Fn()`
31//! - Tokio runtime management
32//! - Application state initialization via StateBuild
33//! - Tauri builder configuration via TauriBuild
34//! - Service orchestration (Vine, Cocoon, Configuration)
35//! - Event-driven lifecycle management
36//!
37//! ## ERROR HANDLING
38//!
39//! - Panics on fatal errors (Tokio runtime failure, Tauri build failure)
40//! - Logs errors for service initialization failures
41//! - Graceful degradation for non-critical service failures
42//!
43//! ## LOGGING
44//!
45//! Uses the TraceStep! macro for checkpoint logging at TRACE level.
46//! Additional logging at DEBUG, INFO, WARN, and ERROR levels throughout.
47//!
48//! ## PERFORMANCE CONSIDERATIONS
49//!
50//! - Tokio multi-threaded runtime for optimal performance
51//! - Asynchronous service initialization
52//! - Lazy initialization where possible
53//!
54//! ## TODO
55//! - [ ] Add crash recovery mechanism
56//! - [ ] Implement proper error dialog for startup failures
57//! - [ ] Add startup performance metrics
58
59use std::sync::{
60	Arc,
61	atomic::{AtomicBool, Ordering},
62};
63
64use tauri::{App, Manager, RunEvent, Wry};
65use Echo::Scheduler::{Scheduler::Scheduler, SchedulerBuilder::SchedulerBuilder};
66
67use crate::dev_log;
68use crate::{
69	// Crate root imports
70	ApplicationState::State::ApplicationState::ApplicationState,
71	Binary::Build::DnsCommands::{
72		StartupTime::init_dns_startup_time,
73		dns_get_forward_allowlist::dns_get_forward_allowlist,
74		dns_get_health_status::dns_get_health_status,
75		dns_get_server_info::dns_get_server_info,
76		dns_get_zone_info::dns_get_zone_info,
77		dns_health_check::dns_health_check,
78		dns_resolve::dns_resolve,
79		dns_test_resolution::dns_test_resolution,
80	},
81	// Binary submodule imports
82	Binary::Build::LocalhostPlugin::LocalhostPlugin as LocalhostPluginFn,
83	Binary::Build::LoggingPlugin::LoggingPlugin as LoggingPluginFn,
84	Binary::Build::Scheme::{self, DnsPort, init_service_registry, land_scheme_handler, register_land_service},
85	Binary::Build::ServiceRegistry::ServiceRegistry as ServiceRegistryFn,
86	Binary::Build::TauriBuild::TauriBuild as TauriBuildFn,
87	Binary::Build::WindowBuild::WindowBuild as WindowBuildFn,
88	Binary::Extension::ExtensionPopulate::Fn as ExtensionPopulateFn,
89	Binary::Extension::ScanPathConfigure::ScanPathConfigure as ScanPathConfigureFn,
90	Binary::Initialize::CliParse::Parse as CliParseFn,
91	Binary::Initialize::LogLevel::Resolve as ResolveLogLevel,
92	Binary::Initialize::PortSelector::BuildUrl as BuildPortUrl,
93	Binary::Initialize::PortSelector::Select as SelectPort,
94	Binary::Initialize::StateBuild::Build as BuildStateFn,
95	Binary::Register::AdvancedFeaturesRegister::AdvancedFeaturesRegister as AdvancedFeaturesRegisterFn,
96	Binary::Register::CommandRegister::CommandRegister as CommandRegisterFn,
97	Binary::Register::IPCServerRegister::IPCServerRegister as IPCServerRegisterFn,
98	Binary::Register::StatusReporterRegister::StatusReporterRegister as StatusReporterRegisterFn,
99	Binary::Register::WindSyncRegister::WindSyncRegister as WindSyncRegisterFn,
100	Binary::Service::CocoonStart::Fn as CocoonStartFn,
101	Binary::Service::ConfigurationInitialize::Fn as ConfigurationInitializeFn,
102	Binary::Service::VineStart::Fn as VineStartFn,
103	Binary::Shutdown::RuntimeShutdown::RuntimeShutdown as RuntimeShutdownFn,
104	Binary::Shutdown::SchedulerShutdown::SchedulerShutdown as SchedulerShutdownFn,
105	Command,
106	Environment::MountainEnvironment::MountainEnvironment,
107	ProcessManagement::InitializationData,
108	RunTime::ApplicationRunTime::ApplicationRunTime,
109	Track,
110};
111use super::AppLifecycle::AppLifecycleSetup;
112
113// Note: Tauri commands are used with fully qualified paths in generate_handler
114// because the __cmd_* macros generated by #[tauri::command] are module-local.
115
116/// Logs a checkpoint message at TRACE level.
117macro_rules! TraceStep {
118
119	($($arg:tt)*) => {{
120
121		dev_log!("lifecycle", $($arg)*);
122	}};
123}
124
125/// The main function that orchestrates the application lifecycle.
126///
127/// This function:
128/// 1. Creates a Tokio runtime
129/// 2. Parses CLI arguments
130/// 3. Builds application state
131/// 4. Creates a scheduler
132/// 5. Selects a port for the local server
133/// 6. Resolves the log level
134/// 7. Sets up the Tauri builder
135/// 8. Configures the application lifecycle
136/// 9. Runs the Tauri application
137/// 10. Handles graceful shutdown
138pub fn Fn() {
139	// Initialize the native keyring store (Keychain on macOS) before any
140	// code path that calls SecretProvider. keyring-core 1.0 requires an
141	// explicit store set via set_default_store() before Entry::new() can
142	// create or look up credentials. omitting this causes "No default
143	// store has been set, so cannot search or create entries" on every
144	// secrets.get call, which in turn prevents extensions (e.g. Roo Code)
145	// from completing their webview initialisation.
146	//
147	// The `not_keyutils` parameter only matters on Linux - macOS ignores
148	// it and always routes to the native Keychain.
149	match keyring::use_native_store(false) {
150		Ok(()) => dev_log!("lifecycle", "[Boot] [Keyring] Native store initialized for secret management"),
151
152		Err(E) => {
153			dev_log!(
154				"lifecycle",
155				"warn: [Boot] [Keyring] Failed to initialize native store ({}); secret operations will fall back to \
156				 no-op",
157				E
158			)
159		},
160	}
161
162	// Open `Mountain.dev.log` up front. Forces `InitFileSink` to create
163	// the session log header on disk before any other code can panic, so
164	// an early crash still leaves a file with a timestamp + pid + tag
165	// context for post-mortem. Env vars are read from the shell here (the
166	// `.env.Land` load below may add MORE keys but never overrides
167	// Trace / Record because `set_var` only runs when a
168	// key is currently unset). Harmless to call: the inner `OnceLock`
169	// gates repeat invocations.
170	crate::IPC::DevLog::InitEager::Fn();
171
172	// -------------------------------------------------------------------------
173	// [Boot] [Env] Enhance the process environment with the user's
174	// interactive-shell PATH / NVM_DIR / HOMEBREW_PREFIX / JAVA_HOME / …
175	// before any child process is spawned. macOS GUI launches (Finder,
176	// Dock, Spotlight, `open <bundle>.app`) start the app with a minimal
177	// env where Homebrew, NVM, and similar are not on PATH; without this
178	// step the Cocoon node binary, language servers, and `git` calls
179	// from extensions all fall back to system defaults (or fail).
180	//
181	// Skip entirely when launched from a TTY (terminal already has the
182	// right env). On macOS, `std::io::stdin().is_terminal()` is the
183	// canonical check - waits for is-terminal 0.5; in the interim,
184	// probe `TERM_PROGRAM` env var which macOS Terminal.app and iTerm2
185	// both set. `TERM=xterm-256color` alone is unreliable (pipelines
186	// set it too). No-op when skip or the shell probe fails/times out.
187	// -------------------------------------------------------------------------
188	let IsTtyLaunch =
189		std::env::var("TERM_PROGRAM").is_ok() || std::env::var("TERM").map_or(false, |V| V != "dumb" && V != "unknown");
190
191	if !IsTtyLaunch {
192		crate::Environment::Utility::EnhanceShellEnvironment::Fn();
193	}
194
195	// -------------------------------------------------------------------------
196	// [Boot] [Env] Load .env.Land into process env so standalone binary
197	// invocations pick up Product*, Tier*, Network* vars without requiring
198	// the shell to pre-source the env file. Skip when launched from a TTY
199	// (terminal already has the right env).
200	// -------------------------------------------------------------------------
201	if !IsTtyLaunch {
202		{
203			fn LoadEnvFile(Path:&std::path::Path) -> bool {
204				let Ok(Content) = std::fs::read_to_string(Path) else {
205					return false;
206				};
207
208				for Line in Content.lines() {
209					let Trimmed = Line.trim();
210
211					if Trimmed.is_empty() || Trimmed.starts_with('#') {
212						continue;
213					}
214
215					if let Some((Key, Value)) = Trimmed.split_once('=') {
216						let CleanKey = Key.trim();
217
218						let CleanValue = Value.trim().trim_matches('"').trim_matches('\'');
219
220						if std::env::var_os(CleanKey).is_none() {
221							// SAFETY: set_var is called once per key during bootstrap
222							// before any threads read env (Tokio runtime starts later
223							// in this function).
224							unsafe { std::env::set_var(CleanKey, CleanValue) };
225						}
226					}
227				}
228
229				true
230			}
231
232			let mut Candidates:Vec<std::path::PathBuf> = Vec::new();
233
234			if let Ok(Cwd) = std::env::current_dir() {
235				Candidates.push(Cwd.join(".env.Land"));
236
237				if let Some(Parent) = Cwd.parent() {
238					Candidates.push(Parent.join(".env.Land"));
239				}
240
241				Candidates.push(Cwd.join(".env.Land.Sample"));
242
243				if let Some(Parent) = Cwd.parent() {
244					Candidates.push(Parent.join(".env.Land.Sample"));
245				}
246			}
247
248			// Repo-layout probe: Target/debug/<bin> → four hops up lands at Land/.
249			if let Ok(Exe) = std::env::current_exe() {
250				let Ancestors:Vec<&std::path::Path> = Exe.ancestors().collect();
251
252				for Candidate in Ancestors.iter().take(6) {
253					Candidates.push(Candidate.join(".env.Land"));
254
255					Candidates.push(Candidate.join(".env.Land.Sample"));
256				}
257			}
258
259			let mut Loaded = false;
260
261			for Candidate in Candidates {
262				if Candidate.exists() && LoadEnvFile(&Candidate) {
263					crate::dev_log!("lifecycle", "[Boot] [Env] Loaded env from {}", Candidate.display());
264
265					Loaded = true;
266
267					break;
268				}
269			}
270
271			if !Loaded {
272				crate::dev_log!(
273					"lifecycle",
274					"[Boot] [Env] No .env.Land / .env.Land.Sample found - using defaults"
275				);
276			}
277		}
278	}
279
280	// -------------------------------------------------------------------------
281	// [Boot] [Tier] Resolved tier banner (Plan A Wave 1.7 runtime banner)
282	// -------------------------------------------------------------------------
283	crate::LandFixTier::LogResolvedTiers();
284
285	// -------------------------------------------------------------------------
286	// [Boot] [Profile] Self-report (BATCH-13 step 6)
287	//
288	// Build.sh exports `Browser`/`Mountain`/`Electron`/`Bundle`/`Compiler`/
289	// `Profile` into the shell that invokes cargo. `build.rs` captures
290	// those into `cargo:rustc-env=LAND_*` so they're baked into the binary -
291	// runtime env lookups don't survive launching the binary from Finder /
292	// another shell. `option_env!` falls back to "unknown" when the build
293	// ran outside Build.sh (e.g. plain `cargo build`).
294	// -------------------------------------------------------------------------
295	{
296		let NamedProfile = option_env!("Profile").unwrap_or("unknown");
297
298		let Workbench = option_env!("Pack").unwrap_or("Unknown");
299
300		let Bundle = option_env!("Bundle").unwrap_or("");
301
302		let Compiler = option_env!("Compiler").unwrap_or("default");
303
304		dev_log!(
305			"lifecycle",
306			"[LandFix:Profile] Active profile={} workbench={} bundle={} compiler={}",
307			NamedProfile,
308			Workbench,
309			Bundle,
310			Compiler
311		);
312	}
313
314	// -------------------------------------------------------------------------
315	// [Boot] [Runtime] Tokio runtime creation
316	// -------------------------------------------------------------------------
317	TraceStep!("[Boot] [Runtime] Building Tokio runtime...");
318
319	let Runtime = tokio::runtime::Builder::new_multi_thread()
320		.enable_all()
321		.build()
322		.expect("FATAL: Cannot build Tokio runtime.");
323
324	TraceStep!("[Boot] [Runtime] Tokio runtime built.");
325
326	Runtime.block_on(async {
327		// ---------------------------------------------------------------------
328		// [Boot] [Telemetry] Hydrate runtime env from compile-baked
329		// Constants so spawned children (Cocoon Node, Sky webview)
330		// see the same telemetry config Mountain itself was built
331		// with - even when the user invokes the bare binary without
332		// sourcing `.env.Land.PostHog`. Idempotent + debug-only.
333		// Must run BEFORE PostHogPlugin::Initialize so the client
334		// reads the same effective env as the children.
335		// ---------------------------------------------------------------------
336		crate::Binary::Build::PostHogPlugin::HydrateRuntimeEnvironment::Fn();
337
338		// ---------------------------------------------------------------------
339		// [Boot] [PostHog] Initialize telemetry client first so any
340		// error captured during the rest of boot lands in the project.
341		// No-op in release builds or when Report=false.
342		// ---------------------------------------------------------------------
343		crate::Binary::Build::PostHogPlugin::Initialize::Fn().await;
344
345		// ---------------------------------------------------------------------
346		// [Boot] [Common::Telemetry] Initialize the shared dual-pipe
347		// stack so library crates linked into Mountain (Echo, Mist,
348		// Common) emit through the same client. The HydrateRuntime
349		// Environment step above populated the env so this picks up
350		// the same Authorize/Beam/Capture values Mountain's plugin
351		// already loaded. Idempotent.
352		// ---------------------------------------------------------------------
353		CommonLibrary::Telemetry::Initialize::Fn(CommonLibrary::Telemetry::Tier::Tier::Mountain).await;
354
355		// ---------------------------------------------------------------------
356		// [Boot] [Args] CLI parsing (using CliParse module)
357		// ---------------------------------------------------------------------
358		let _WorkspaceConfigurationPath = CliParseFn();
359
360		let _InitialFolders:Vec<String> = vec![];
361
362		// ---------------------------------------------------------------------
363		// [Boot] [State] ApplicationState (using StateBuild module)
364		// ---------------------------------------------------------------------
365		dev_log!("lifecycle", "[Boot] [State] Building ApplicationState...");
366
367		// Create application state directly (StateBuild::Build with default config)
368		let AppState = ApplicationState::default();
369
370		// -------------------------------------------------------------------
371		// [Boot] [Workspace] Seed initial workspace folders so every extension
372		// that calls `vscode.workspace.findFiles(...)` at activation has
373		// something to walk. Precedence: --folder flags → positional dirs →
374		// Open env → CWD fallback. See
375		// CliParse::ParseWorkspaceFolders.
376		// -------------------------------------------------------------------
377		{
378			let InitialFolderPaths = crate::Binary::Initialize::CliParse::ParseWorkspaceFolders();
379
380			if InitialFolderPaths.is_empty() {
381				dev_log!(
382					"lifecycle",
383					"[Boot] [Workspace] No initial folders resolved - editor will open in \"no folder\" mode."
384				);
385			} else {
386				use crate::ApplicationState::DTO::WorkspaceFolderStateDTO::WorkspaceFolderStateDTO;
387
388				let mut Folders:Vec<WorkspaceFolderStateDTO> = Vec::new();
389
390				for (Index, Path) in InitialFolderPaths.iter().enumerate() {
391					let Uri = match url::Url::from_directory_path(Path) {
392						Ok(U) => U,
393						Err(()) => {
394							dev_log!(
395								"lifecycle",
396								"warn: [Boot] [Workspace] Failed to build URL for {}; skipping",
397								Path.display()
398							);
399
400							continue;
401						},
402					};
403
404					let Name = Path
405						.file_name()
406						.and_then(|N| N.to_str())
407						.map(str::to_string)
408						.unwrap_or_else(|| Path.display().to_string());
409
410					match WorkspaceFolderStateDTO::New(Uri, Name, Index) {
411						Ok(Dto) => Folders.push(Dto),
412						Err(Error) => {
413							dev_log!(
414								"lifecycle",
415								"warn: [Boot] [Workspace] Failed to build folder DTO for {}: {}",
416								Path.display(),
417								Error
418							);
419						},
420					}
421				}
422
423				if !Folders.is_empty() {
424					// Seed state directly; Cocoon is not yet spawned at this
425					// point, so there is no sidecar to notify. The initial
426					// workspace makes it to Cocoon via `InitializeExtensionHost`'s
427					// `workspace` payload during its handshake instead.
428					AppState.Workspace.SetWorkspaceFolders(Folders);
429
430					dev_log!(
431						"lifecycle",
432						"[Boot] [Workspace] Seeded {} workspace folder(s).",
433						InitialFolderPaths.len()
434					);
435				}
436			}
437		}
438
439		dev_log!(
440			"lifecycle",
441			"[Boot] [State] ApplicationState created with {} workspace folders.",
442			AppState.Workspace.WorkspaceFolders.lock().map(|f| f.len()).unwrap_or(0)
443		);
444
445		// Create Arc for application state to be managed by Tauri
446		let AppStateArcForClosure = Arc::new(AppState.clone());
447
448		// ---------------------------------------------------------------------
449		// [Boot] [Runtime] Scheduler handles (using RuntimeBuild module)
450		// ---------------------------------------------------------------------
451		let Scheduler = Arc::new(SchedulerBuilder::Create().Build());
452
453		let SchedulerForClosure = Scheduler.clone();
454
455		TraceStep!("[Boot] [Echo] Scheduler handles prepared.");
456
457		// ---------------------------------------------------------------------
458		// [Boot] [Localhost] Port selection (using PortSelector module)
459		// ---------------------------------------------------------------------
460		let ServerPort = SelectPort();
461
462		let LocalhostUrl = BuildPortUrl(ServerPort);
463
464		// ---------------------------------------------------------------------
465		// [Boot] [Logging] Log level resolution (using LogLevel module)
466		// ---------------------------------------------------------------------
467		let log_level = ResolveLogLevel();
468
469		// ---------------------------------------------------------------------
470		// [Boot] [Tauri] Builder setup (using TauriBuild module)
471		// ---------------------------------------------------------------------
472		let Builder = TauriBuildFn();
473
474		Builder
475			.plugin(LoggingPluginFn(log_level))
476			.plugin(LocalhostPluginFn(ServerPort))
477			.manage(AppStateArcForClosure.clone())
478			.setup({
479				let LocalhostUrl = LocalhostUrl.clone();
480
481				let ServerPortForClosure = ServerPort;
482
483				move |app:&mut App| {
484					dev_log!("lifecycle", "[Lifecycle] [Setup] Setup hook started.");
485
486					dev_log!("lifecycle", "[Lifecycle] [Setup] LocalhostUrl={}", LocalhostUrl);
487
488					// ---------------------------------------------------------
489					// [Service Registry] Initialize service registry for land:// routing
490					// ---------------------------------------------------------
491					dev_log!(
492						"lifecycle",
493						"[Lifecycle] [Setup] Initializing ServiceRegistry for land:// scheme..."
494					);
495
496					let service_registry = ServiceRegistryFn::new();
497
498					init_service_registry(service_registry.clone());
499
500					// ---------------------------------------------------------
501					// [Service Registry] Register local HTTP services
502					// ---------------------------------------------------------
503					// Register the main code editor service
504					dev_log!(
505						"lifecycle",
506						"[Lifecycle] [Setup] Registering code.land.playform.cloud service on port {}",
507						ServerPortForClosure
508					);
509
510					register_land_service("code.land.playform.cloud", ServerPortForClosure);
511
512					// Register API editor service (same port for now, can be separate later)
513					register_land_service("api.land.playform.cloud", ServerPortForClosure);
514
515					// Register assets editor service (same port for now, can be separate later)
516					register_land_service("assets.editor.land", ServerPortForClosure);
517
518					// Make the registry available as managed state for Tauri commands
519					app.manage(service_registry);
520
521					dev_log!(
522						"lifecycle",
523						"[Lifecycle] [Setup] ServiceRegistry initialized and services registered."
524					);
525
526					// ---------------------------------------------------------
527					// [DNS Server] Start the Hickory DNS server
528					// ---------------------------------------------------------
529					// The DNS server must start BEFORE any webview loads to ensure
530					// that land:// protocol_resolution is available
531					dev_log!("lifecycle", "[Lifecycle] [Setup] Starting DNS server on preferred port 5380...");
532
533					let dns_port = Mist::start(5380).unwrap_or_else(|e| {
534						dev_log!(
535							"lifecycle",
536							"warn: [Lifecycle] [Setup] Failed to start DNS server on port 5380: {}",
537							e
538						);
539
540						// Fallback to random port if preferred port fails
541						Mist::start(0).unwrap_or_else(|e| {
542							dev_log!(
543								"lifecycle",
544								"error: [Lifecycle] [Setup] Completely failed to start DNS server: {}",
545								e
546							);
547
548							0 // Return 0 as error indicator
549						})
550					});
551
552					if dns_port == 0 {
553						dev_log!(
554							"lifecycle",
555							"warn: [Lifecycle] [Setup] DNS server failed to start, land:// protocol will not be \
556							 available"
557						);
558					} else {
559						dev_log!(
560							"lifecycle",
561							"[Lifecycle] [Setup] DNS server started successfully on port {}",
562							dns_port
563						);
564
565						// Initialize DNS startup time for tracking
566						crate::Binary::Build::DnsCommands::StartupTime::init_dns_startup_time();
567					}
568
569					// ---------------------------------------------------------
570					// [Mist WebSocket] Optional Sky↔Mountain direct transport
571					// ---------------------------------------------------------
572					// `TierWebSocket=Mist` activates a localhost JSON-RPC
573					// WebSocket on port 5051 that Sky's TauriMainProcessService
574					// can use for high-frequency IPC (`storage:updateItems`,
575					// decoration updates, model changes) instead of the
576					// Tauri-invoke + Mountain-gRPC double hop. `Disabled` (the
577					// default) skips the bind entirely so the surface stays
578					// pure Tauri. Wiring of the actual handler registry is
579					// staged - this boot-time gate establishes the port and
580					// secret so subsequent atom batches can register handlers
581					// against the existing HandlerRegistry without revisiting
582					// the Mountain boot path.
583					let TierWebSocketSetting = std::env::var("TierWebSocket")
584						.unwrap_or_else(|_| env!("TierWebSocket", "Disabled").to_string());
585
586					if TierWebSocketSetting == "Mist" {
587						dev_log!(
588							"lifecycle",
589							"[Lifecycle] [Setup] TierWebSocket=Mist - starting WebSocket transport on 127.0.0.1:5051"
590						);
591
592						let MistRegistry = Mist::WebSocket::HandlerRegistry::new();
593
594						let MistSecret = Mist::WebSocket::SharedSecret::random();
595
596						// Expose the secret to Cocoon/Sky via env. The startup
597						// helper (`MountainGetWorkbenchConfiguration`) reads
598						// MountainWebSocketSecret + Port to surface in the
599						// workbench configuration payload that Sky consumes.
600						unsafe {
601							std::env::set_var("MountainWebSocketSecret", MistSecret.as_hex());
602
603							std::env::set_var("MountainWebSocketPort", "5051");
604						}
605
606						tokio::spawn(async move {
607							if let Err(Error) = Mist::WebSocket::ServeLocal(5051, MistSecret, MistRegistry).await {
608								dev_log!("lifecycle", "warn: [Lifecycle] [Mist] WebSocket server exited: {:?}", Error);
609							}
610						});
611					} else {
612						dev_log!(
613							"lifecycle",
614							"[Lifecycle] [Setup] TierWebSocket={} - WebSocket transport disabled",
615							TierWebSocketSetting
616						);
617					}
618
619					// Register DnsPort as managed state for Tauri commands
620					app.manage(DnsPort(dns_port));
621
622					let AppHandle = app.handle().clone();
623
624					TraceStep!("[Lifecycle] [Setup] AppHandle acquired.");
625
626					// ---------------------------------------------------------
627					// Setup application lifecycle through AppLifecycle module
628					// ---------------------------------------------------------
629					let AppStateArcFromClosure = AppStateArcForClosure.clone();
630
631					if let Err(e) = AppLifecycleSetup(
632						app,
633						AppHandle.clone(),
634						LocalhostUrl.clone(),
635						SchedulerForClosure.clone(),
636						AppStateArcFromClosure,
637					) {
638						dev_log!("lifecycle", "error: [Lifecycle] [Setup] Failed to setup lifecycle: {}", e);
639					}
640
641					Ok(())
642				}
643			})
644			.register_asynchronous_uri_scheme_protocol("fiddee", |_ctx, request, responder| {
645				// Implemented: delegate to synchronous scheme handler
646				let response = crate::Binary::Build::Scheme::land_scheme_handler(&request);
647
648				responder.respond(response);
649			})
650			.register_asynchronous_uri_scheme_protocol("vscode-file", |ctx, request, responder| {
651				// VS Code Electron workbench uses vscode-file:// to load assets.
652				// Maps to embedded frontend assets from Sky/Target.
653				let AppHandle = ctx.app_handle().clone();
654
655				std::thread::spawn(move || {
656					let response = crate::Binary::Build::Scheme::VscodeFileSchemeHandler(&AppHandle, &request);
657
658					responder.respond(response);
659				});
660			})
661			.register_asynchronous_uri_scheme_protocol("vscode-webview", |ctx, request, responder| {
662				// VS Code's `WebviewElement` wraps every extension webview in
663				// an iframe whose `src` is `vscode-webview://<authority>/index.html?...`.
664				// Without this handler the iframe stays blank and every
665				// extension that uses `webviewView` / `WebviewPanel` /
666				// `CustomEditor` (Roo Code, Claude, GitLens, custom editors)
667				// is dead. The handler serves the three-file `pre/`
668				// directory (`index.html`, `service-worker.js`, `fake.html`);
669				// extension HTML itself comes through later via the workbench's
670				// `swMessage` postMessage channel, not this scheme.
671				let AppHandle = ctx.app_handle().clone();
672
673				std::thread::spawn(move || {
674					let response = crate::Binary::Build::Scheme::VscodeWebviewSchemeHandler(&AppHandle, &request);
675
676					responder.respond(response);
677				});
678			})
679			.register_asynchronous_uri_scheme_protocol("vscode-webview-resource", |ctx, request, responder| {
680				// `vscode-webview-resource://<auth>/<path>` is the URI shape
681				// stock VS Code emits from `webview.asWebviewUri(...)`. The
682				// service worker inside `pre/index.html` would normally
683				// intercept these and proxy through the host. Land disables
684				// that SW (see Output `PatchWebviewIframeServiceWorker`)
685				// because WKWebView refuses SW registration on the
686				// `vscode-webview://` custom protocol; instead we register
687				// this scheme directly so any extension that hard-codes
688				// the URI form (or didn't go through Cocoon's `asWebviewUri`
689				// rewrite) still resolves. Strip the `<auth>` and forward
690				// the path to `VscodeFileSchemeHandler` by rewriting the
691				// URI to `vscode-file://vscode-app/<path>`.
692				let AppHandle = ctx.app_handle().clone();
693
694				std::thread::spawn(move || {
695					let Original = request.uri().to_string();
696
697					let RewrittenUri = match Original.strip_prefix("vscode-webview-resource://") {
698						Some(After) => {
699							let Rest = After.find('/').map(|I| &After[I..]).unwrap_or("/");
700
701							format!("vscode-file://vscode-app{}", Rest)
702						},
703						None => "vscode-file://vscode-app/".to_string(),
704					};
705
706					crate::dev_log!(
707						"scheme-assets",
708						"[LandFix:VscodeWebviewResource] {} -> {}",
709						Original,
710						RewrittenUri
711					);
712
713					let mut Builder = tauri::http::request::Request::builder().uri(&RewrittenUri);
714
715					for (Name, Value) in request.headers().iter() {
716						Builder = Builder.header(Name, Value);
717					}
718
719					let Forwarded = Builder
720						.method(request.method().clone())
721						.body(request.body().clone())
722						.unwrap_or_else(|_| request.clone());
723
724					let response = crate::Binary::Build::Scheme::VscodeFileSchemeHandler(&AppHandle, &Forwarded);
725
726					responder.respond(response);
727				});
728			})
729			.register_asynchronous_uri_scheme_protocol("vscode-resource", |ctx, request, responder| {
730				// Legacy stock-VS Code resource scheme. Same handling as
731				// `vscode-webview-resource` - rewrite to `vscode-file://`
732				// and dispatch through the existing file handler.
733				let AppHandle = ctx.app_handle().clone();
734
735				std::thread::spawn(move || {
736					let Original = request.uri().to_string();
737
738					let RewrittenUri = match Original.strip_prefix("vscode-resource://") {
739						Some(After) => {
740							let Rest = After.find('/').map(|I| &After[I..]).unwrap_or("/");
741
742							format!("vscode-file://vscode-app{}", Rest)
743						},
744						None => "vscode-file://vscode-app/".to_string(),
745					};
746
747					crate::dev_log!("scheme-assets", "[LandFix:VscodeResource] {} -> {}", Original, RewrittenUri);
748
749					let mut Builder = tauri::http::request::Request::builder().uri(&RewrittenUri);
750
751					for (Name, Value) in request.headers().iter() {
752						Builder = Builder.header(Name, Value);
753					}
754
755					let Forwarded = Builder
756						.method(request.method().clone())
757						.body(request.body().clone())
758						.unwrap_or_else(|_| request.clone());
759
760					let response = crate::Binary::Build::Scheme::VscodeFileSchemeHandler(&AppHandle, &Forwarded);
761
762					responder.respond(response);
763				});
764			})
765			.plugin(tauri_plugin_dialog::init())
766			.plugin(tauri_plugin_fs::init())
767			.invoke_handler(tauri::generate_handler![
768				crate::Binary::Tray::SwitchTrayIcon::SwitchTrayIcon,
769
770				crate::Binary::IPC::WorkbenchConfigurationCommand::MountainGetWorkbenchConfiguration,
771
772				Command::TreeView::GetTreeViewChildren::GetTreeViewChildren,
773
774				Command::LanguageFeature::MountainProvideHover::MountainProvideHover,
775
776				Command::LanguageFeature::MountainProvideCompletions::MountainProvideCompletions,
777
778				Command::LanguageFeature::MountainProvideDefinition::MountainProvideDefinition,
779
780				Command::LanguageFeature::MountainProvideReferences::MountainProvideReferences,
781
782				Command::SourceControlManagement::GetAllSourceControlManagementState::GetAllSourceControlManagementState,
783
784				Command::Keybinding::GetResolvedKeybinding::GetResolvedKeybinding,
785
786				Track::FrontendCommand::DispatchFrontendCommand::DispatchFrontendCommand,
787
788				Track::UIRequest::ResolveUIRequest::ResolveUIRequest,
789
790				Track::Webview::MountainWebviewPostMessageFromGuest::MountainWebviewPostMessageFromGuest,
791
792				crate::Binary::IPC::MessageReceiveCommand::MountainIPCReceiveMessage,
793
794				crate::Binary::IPC::StatusGetCommand::MountainIPCGetStatus,
795
796				crate::Binary::IPC::InvokeCommand::MountainIPCInvoke,
797
798				crate::Binary::IPC::WindConfigurationCommand::MountainGetWindDesktopConfiguration,
799
800				crate::Binary::IPC::ConfigurationUpdateCommand::MountainUpdateConfigurationFromWind,
801
802				crate::Binary::IPC::ConfigurationSyncCommand::MountainSynchronizeConfiguration,
803
804				crate::Binary::IPC::ConfigurationStatusCommand::MountainGetConfigurationStatus,
805
806				crate::Binary::IPC::IPCStatusCommand::MountainGetIPCStatus,
807
808				crate::Binary::IPC::IPCStatusHistoryCommand::MountainGetIPCStatusHistory,
809
810				crate::Binary::IPC::IPCStatusReportingStartCommand::MountainStartIPCStatusReporting,
811
812				crate::Binary::IPC::PerformanceStatsCommand::MountainGetPerformanceStats,
813
814				crate::Binary::IPC::CacheStatsCommand::MountainGetCacheStats,
815
816				crate::Binary::IPC::CollaborationSessionCommand::MountainCreateCollaborationSession,
817
818				crate::Binary::IPC::CollaborationSessionCommand::MountainGetCollaborationSessions,
819
820				crate::Binary::IPC::DocumentSyncCommand::MountainAddDocumentForSync,
821
822				crate::Binary::IPC::DocumentSyncCommand::MountainGetSyncStatus,
823
824				crate::Binary::IPC::UpdateSubscriptionCommand::MountainSubscribeToUpdates,
825
826				crate::Binary::IPC::ConfigurationDataCommand::GetConfigurationData,
827
828				crate::Binary::IPC::ConfigurationDataCommand::SaveConfigurationData,
829
830				crate::Binary::IPC::WorkspaceFolderCommand::MountainWorkspaceOpenFolder,
831
832				crate::Binary::IPC::WorkspaceFolderCommand::MountainWorkspaceListFolders,
833
834				crate::Binary::IPC::WorkspaceFolderCommand::MountainWorkspaceCloseAllFolders,
835
836				crate::Binary::Build::DnsCommands::dns_get_server_info::dns_get_server_info,
837
838				crate::Binary::Build::DnsCommands::dns_get_zone_info::dns_get_zone_info,
839
840				crate::Binary::Build::DnsCommands::dns_get_forward_allowlist::dns_get_forward_allowlist,
841
842				crate::Binary::Build::DnsCommands::dns_get_health_status::dns_get_health_status,
843
844				crate::Binary::Build::DnsCommands::dns_resolve::dns_resolve,
845
846				crate::Binary::Build::DnsCommands::dns_test_resolution::dns_test_resolution,
847
848				crate::Binary::Build::DnsCommands::dns_health_check::dns_health_check,
849
850				// Process commands (direct Tauri invoke from ProcessPolyfill)
851				crate::Binary::IPC::ProcessCommand::process_get_exec_path::process_get_exec_path,
852
853				crate::Binary::IPC::ProcessCommand::process_get_platform::process_get_platform,
854
855				crate::Binary::IPC::ProcessCommand::process_get_arch::process_get_arch,
856
857				crate::Binary::IPC::ProcessCommand::process_get_pid::process_get_pid,
858
859				crate::Binary::IPC::ProcessCommand::process_get_shell_env::process_get_shell_env,
860
861				crate::Binary::IPC::ProcessCommand::process_get_memory_info::process_get_memory_info,
862
863				// Health check commands (direct Tauri invoke from SharedProcessProxy)
864				crate::Binary::IPC::HealthCommand::cocoon_extension_host_health::cocoon_extension_host_health,
865
866				crate::Binary::IPC::HealthCommand::cocoon_search_service_health::cocoon_search_service_health,
867
868				crate::Binary::IPC::HealthCommand::cocoon_debug_service_health::cocoon_debug_service_health,
869
870				crate::Binary::IPC::HealthCommand::shared_process_service_health::shared_process_service_health,
871
872				crate::Binary::IPC::RenderDevLogCommand::RenderDevLog,
873
874				// LAND-PATCH B7-S6 P14.5: Vine notification broadcast
875				// subscription. `vine_subscribe_notifications` opens a
876				// Tauri Channel that drains the process-wide
877				// `Vine::Client` broadcast into the webview; Effect-TS
878				// `VineNotificationsLive` Layer wraps it as a
879				// `Stream<NotificationFrame>`. `vine_subscriber_count`
880				// is a diagnostic for verifying registrations didn't
881				// leak across reloads.
882				crate::Binary::IPC::VineSubscribeCommand::vine_subscribe_notifications,
883
884				crate::Binary::IPC::VineSubscribeCommand::vine_subscriber_count,
885			])
886			.build(tauri::generate_context!())
887			.expect("FATAL: Error while building Mountain Tauri application")
888			.run(move |app_handle:&tauri::AppHandle, event:tauri::RunEvent| {
889				// Debug-only: log selected lifecycle events
890				if cfg!(debug_assertions) {
891					match &event {
892						RunEvent::MainEventsCleared => {},
893						RunEvent::WindowEvent { .. } => {},
894						_ => dev_log!("lifecycle", "[Lifecycle] [RunEvent] {:?}", event),
895					}
896				}
897
898				if let RunEvent::ExitRequested { api, .. } = event {
899					// Shutdown runs once. The graceful path ends with
900					// `app_handle.exit(0)`, which Tauri re-delivers as a
901					// second `ExitRequested { code: Some(0) }`. On re-entry
902					// we must NOT `prevent_exit` or spawn the shutdown task
903					// again - Cocoon has already been SIGKILLed and the
904					// second pass would log spurious "tcp connect error"
905					// warnings trying to notify a dead sidecar.
906					static SHUTTING_DOWN:AtomicBool = AtomicBool::new(false);
907
908					if SHUTTING_DOWN.swap(true, Ordering::SeqCst) {
909						return;
910					}
911
912					dev_log!(
913						"lifecycle",
914						"warn: [Lifecycle] [Shutdown] Exit requested. Starting graceful shutdown..."
915					);
916
917					api.prevent_exit();
918
919					let SchedulerHandle = Scheduler.clone();
920
921					let app_handle_clone = app_handle.clone();
922
923					tokio::spawn(async move {
924						dev_log!("lifecycle", "[Lifecycle] [Shutdown] Shutting down ApplicationRunTime...");
925
926						let _ = RuntimeShutdownFn(&app_handle_clone).await;
927
928						dev_log!("lifecycle", "[Lifecycle] [Shutdown] Stopping Echo scheduler...");
929
930						let _ = SchedulerShutdownFn(SchedulerHandle).await;
931
932						dev_log!("lifecycle", "[Lifecycle] [Shutdown] Done. Exiting process.");
933
934						app_handle_clone.exit(0);
935					});
936				}
937			});
938
939		dev_log!("lifecycle", "[Lifecycle] [Exit] Mountain application has shut down.");
940	});
941}