Skip to main content

Mountain/ProcessManagement/
InitializationData.rs

1//! # InitializationData (ProcessManagement)
2//!
3//! Constructs the initial data payloads that are sent to the `Sky` frontend
4//! and the `Cocoon` sidecar to bootstrap their states during application
5//! startup.
6//!
7//! ## RESPONSIBILITIES
8//!
9//! ### 1. Frontend Sandbox Configuration
10//! - Gather host environment data (paths, platform, versions)
11//! - Construct `ISandboxConfiguration` payload for Sky
12//! - Include machine ID, session ID, and user environment
13//! - Provide appRoot, homeDir, tmpDir, and userDataDir URIs
14//!
15//! ### 2. Extension Host Initialization
16//! - Assemble data for extension host (Cocoon) startup
17//! - Include discovered extensions list
18//! - Provide workspace information (folders, configuration)
19//! - Set up storage paths (globalStorage, workspaceStorage)
20//! - Configure logging and telemetry settings
21//!
22//! ### 3. Path Resolution
23//! - Resolve application root from Tauri resources
24//! - Resolve app data directory for persistence
25//! - Resolve home directory and temp directory
26//! - Handle path errors with descriptive `CommonError` types
27//!
28//! ## ARCHITECTURAL ROLE
29//!
30//! InitializationData is the **bootstrap orchestrator** for Mountain's
31//! startup sequence:
32//!
33//! ```text
34//! Binary::Main ──► InitializationData ──► Sky (Frontend)
35//! │
36//! └─► Cocoon (Extension Host)
37//! ```
38//!
39//! ### Position in Mountain
40//! - `ProcessManagement` module: Process lifecycle and initialization
41//! - Called during `Binary::Main` startup and `CocoonManagement` initialization
42//! - Provides complete environment snapshot for all processes
43//!
44//! ### Dependencies
45//! - `tauri::AppHandle`: Path resolution and package info
46//! - `CommonLibrary::Environment::Requires`: DI for services
47//! - `CommonLibrary::Error::CommonError`: Error handling
48//! - `uuid::Uuid`: Generate machine/session IDs
49//! - `serde_json::json`: Payload construction
50//!
51//! ### Dependents
52//! - `Binary::Main::Fn`: Calls `ConstructSandboxConfiguration` for UI
53//! - `CocoonManagement::InitializeCocoon`: Calls
54//!   `ConstructExtensionHostInitializationData`
55//!
56//! ## PAYLOAD FORMATS
57//!
58//! ### ISandboxConfiguration (for Sky)
59//! ```json
60//! {
61//!   "windowId": "main",
62//!   "machineId": "uuid",
63//!   "sessionId": "uuid",
64//!   "logLevel": 2,
65//!   "userEnv": { ... },
66//!   "appRoot": "file:///...",
67//!   "appName": "Mountain",
68//!   "platform": "darwin|win32|linux",
69//!   "arch": "x64|arm64",
70//!   "versions": { "mountain": "x.y.z", "electron": "0.0.0-tauri", ... },
71//!   "homeDir": "file:///...",
72//!   "tmpDir": "file:///...",
73//!   "userDataDir": "file:///...",
74//!   "backupPath": "file:///...",
75//!   "productConfiguration": { ... }
76//! }
77//! ```
78//!
79//! ### IExtensionHostInitData (for Cocoon)
80//! ```json
81//! {
82//!   "commit": "dev-commit-hash",
83//!   "version": "x.y.z",
84//!   "parentPid": 12345,
85//!   "environment": {
86//!     "appName": "Mountain",
87//!     "appRoot": "file:///...",
88//!     "globalStorageHome": "file:///...",
89//!     "workspaceStorageHome": "file:///...",
90//!     "extensionLogLevel": [["info", "Default"]]
91//!   },
92//!   "workspace": { "id": "...", "name": "...", ... },
93//!   "logsLocation": "file:///...",
94//!   "telemetryInfo": { ... },
95//!   "extensions": [ ... ],
96//!   "autoStart": true,
97//!   "uiKind": 1
98//! }
99//! ```
100//!
101//! ## ERROR HANDLING
102//!
103//! - Path resolution failures return `CommonError::ConfigurationLoad`
104//! - Workspace identifier errors propagate from
105//!   `ApplicationState::GetWorkspaceIdentifier`
106//! - JSON serialization errors should not occur (using `json!` macro)
107//!
108//! ## PLATFORM DETECTION
109//!
110//! Platform strings match VS Code conventions:
111//! - `"win32"` for Windows
112//! - `"darwin"` for macOS
113//! - `"linux"` for Linux
114//!
115//! Architecture mapping:
116//! - `"x64"` for x86_64
117//! - `"arm64"` for aarch64
118//! - `"ia32"` for x86
119//!
120//! ## TODO
121//!
122//! - [ ] Persist machineId across sessions (currently generated new each
123//!   launch)
124//! - [ ] Add environment variable overrides for development
125//! - [ ] Implement workspace cache for faster startup
126//! - [ ] Add telemetry for initialization performance
127//! - [ ] Support remote workspace URIs
128//!
129//! ## MODULE CONTENTS
130//!
131//! - [`ConstructSandboxConfiguration`]: Build ISandboxConfiguration for Sky
132//! - [`ConstructExtensionHostInitializationData`]: Build IExtensionHostInitData
133//!   for Cocoon
134
135use std::{collections::HashMap, env, fs, path::PathBuf, sync::Arc};
136
137use CommonLibrary::{
138	Environment::Requires::Requires,
139	Error::CommonError::CommonError,
140	ExtensionManagement::ExtensionManagementService::ExtensionManagementService,
141	Workspace::WorkspaceProvider::WorkspaceProvider,
142};
143use serde_json::{Value, json};
144use tauri::{AppHandle, Manager, Wry};
145use uuid::Uuid;
146
147use crate::{
148	ApplicationState::State::ApplicationState::ApplicationState,
149	Environment::MountainEnvironment::MountainEnvironment,
150	dev_log,
151};
152
153/// Loads or generates a persistent machine ID.
154///
155/// The machine ID is stored in the app data directory as a simple text file.
156/// If the file doesn't exist, a new UUID is generated and saved.
157///
158/// # Arguments
159/// * `app_data_dir` - The application data directory path
160///
161/// # Returns
162/// The machine ID as a String
163fn get_or_generate_machine_id(app_data_dir:&PathBuf) -> String {
164	let machine_id_path = app_data_dir.join("machine-id.txt");
165
166	// Try to load existing machine ID
167	if let Ok(content) = fs::read_to_string(&machine_id_path) {
168		let trimmed = content.trim();
169		if !trimmed.is_empty() {
170			dev_log!("cocoon", "[InitializationData] Loaded existing machine ID from disk");
171			return trimmed.to_string();
172		}
173	}
174
175	// Generate and save new machine ID
176	let new_machine_id = Uuid::new_v4().to_string();
177
178	// Ensure directory exists
179	if let Some(parent) = machine_id_path.parent() {
180		if let Err(e) = fs::create_dir_all(parent) {
181			dev_log!(
182				"cocoon",
183				"warn: [InitializationData] Failed to create machine ID directory: {}",
184				e
185			);
186		}
187	}
188
189	// Save to disk
190	if let Err(e) = fs::write(&machine_id_path, &new_machine_id) {
191		dev_log!(
192			"cocoon",
193			"warn: [InitializationData] Failed to persist machine ID to disk: {}",
194			e
195		);
196	} else {
197		dev_log!("cocoon", "[InitializationData] Generated and persisted new machine ID");
198	}
199
200	new_machine_id
201}
202
203/// Constructs the `ISandboxConfiguration` payload needed by the `Sky` frontend.
204pub async fn ConstructSandboxConfiguration(
205	ApplicationHandle:&AppHandle<Wry>,
206
207	ApplicationState:&Arc<ApplicationState>,
208) -> Result<Value, CommonError> {
209	dev_log!("cocoon", "[InitializationData] Constructing ISandboxConfiguration for Sky.");
210
211	let PathResolver = ApplicationHandle.path();
212
213	let AppRootUri = PathResolver.resource_dir().map_err(|Error| {
214		CommonError::ConfigurationLoad {
215			Description:format!("Failed to resolve resource directory (app root): {}", Error),
216		}
217	})?;
218
219	let AppDataDir = PathResolver.app_data_dir().map_err(|Error| {
220		CommonError::ConfigurationLoad { Description:format!("Failed to resolve app data directory: {}", Error) }
221	})?;
222
223	let HomeDir = PathResolver.home_dir().map_err(|Error| {
224		CommonError::ConfigurationLoad { Description:format!("Failed to resolve home directory: {}", Error) }
225	})?;
226
227	let TmpDir = env::temp_dir();
228
229	let BackupPath = AppDataDir.join("Backups").join(ApplicationState.GetWorkspaceIdentifier()?);
230
231	let Platform = match env::consts::OS {
232		"windows" => "win32",
233
234		"macos" => "darwin",
235
236		"linux" => "linux",
237
238		_ => "unknown",
239	};
240
241	let Arch = match env::consts::ARCH {
242		"x86_64" => "x64",
243
244		"aarch64" => "arm64",
245
246		"x86" => "ia32",
247
248		_ => "unknown",
249	};
250
251	let Versions = json!({
252		"mountain": ApplicationHandle.package_info().version.to_string(),
253
254		// Explicitly signal we are not in Electron
255		"electron": "0.0.0-tauri",
256
257		// Representative version
258		"chrome": "120.0.0.0",
259
260		// Representative version
261		"node": "18.18.2"
262	});
263
264	// Load or generate persistent machine ID
265	let machine_id = get_or_generate_machine_id(&AppDataDir);
266
267	Ok(json!({
268		"windowId": ApplicationHandle.get_webview_window("main").unwrap().label(),
269
270		// Persist the machineId to ApplicationState or persistent storage and load
271		// it on subsequent runs. A stable machine identifier is crucial for licensing
272		// validation, telemetry deduplication, and cross-session state consistency.
273		// Now implemented with persistent storage in app data directory.
274		"machineId": machine_id,
275
276		"sessionId": Uuid::new_v4().to_string(),
277
278		"logLevel": log::max_level() as i32,
279
280		"userEnv": env::vars().collect::<HashMap<_,_>>(),
281
282		"appRoot": url::Url::from_directory_path(AppRootUri).unwrap().to_string(),
283
284		"appName": ApplicationHandle.package_info().name.clone(),
285
286		"appUriScheme": "mountain",
287
288		"appLanguage": "en",
289
290		"appHost": "desktop",
291
292		"platform": Platform,
293
294		"arch": Arch,
295
296		"versions": Versions,
297
298		"execPath": env::current_exe().unwrap_or_default().to_string_lossy(),
299
300		"homeDir": url::Url::from_directory_path(HomeDir).unwrap().to_string(),
301
302		"tmpDir": url::Url::from_directory_path(TmpDir).unwrap().to_string(),
303
304		"userDataDir": url::Url::from_directory_path(AppDataDir).unwrap().to_string(),
305
306		"backupPath": url::Url::from_directory_path(BackupPath).unwrap().to_string(),
307
308		"nls": { "messages": {}, "language": "en", "availableLanguages": { "en": "English" } },
309
310		"productConfiguration": {
311
312			// Atom I5: read from process env (populated from .env.Land at
313			// Mountain startup). Fallback strings keep a sensible identity
314			// if the env file is absent at a release-profile launch.
315			"nameShort": std::env::var("ProductNameShort").unwrap_or_else(|_| "Land".into()),
316
317			"nameLong": std::env::var("ProductNameLong").unwrap_or_else(|_| "Land Editor".into()),
318
319			"applicationName": std::env::var("ProductApplicationName").unwrap_or_else(|_| "land".into()),
320
321			"embedderIdentifier": std::env::var("ProductEmbedderIdentifier").unwrap_or_else(|_| "land-desktop".into())
322		},
323
324		"resourcesPath": PathResolver.resource_dir().unwrap_or_default().to_string_lossy(),
325
326		"VSCODE_CWD": env::current_dir().unwrap_or_default().to_string_lossy(),
327	}))
328}
329
330/// Constructs the `IExtensionHostInitData` payload sent to `Cocoon`.
331pub async fn ConstructExtensionHostInitializationData(Environment:&MountainEnvironment) -> Result<Value, CommonError> {
332	dev_log!("cocoon", "[InitializationData] Constructing IExtensionHostInitData for Cocoon.");
333
334	let ApplicationState = &Environment.ApplicationState;
335
336	let ApplicationHandle = &Environment.ApplicationHandle;
337
338	let ExtensionManagementProvider:Arc<dyn ExtensionManagementService> = Environment.Require();
339
340	let ExtensionsDTO = ExtensionManagementProvider.GetExtensions().await?;
341
342	let WorkspaceProvider:Arc<dyn WorkspaceProvider> = Environment.Require();
343
344	let WorkspaceName = WorkspaceProvider
345		.GetWorkspaceName()
346		.await?
347		.unwrap_or_else(|| "Mountain Workspace".to_string());
348
349	let WorkspaceFoldersGuard = ApplicationState.Workspace.WorkspaceFolders.lock().unwrap();
350
351	// Cocoon's `WorkspaceNamespace/Index.ts` reads
352	// `ExtensionHostInitData.workspace.folders` at shim construction time,
353	// then mutates the same array in place on `$deltaWorkspaceFolders`. If
354	// `folders` is missing from the init payload, every
355	// `vscode.workspace.workspaceFolders` read returns `[]` until a delta
356	// fires - which means the git extension boots with zero folders to
357	// scan and never calls `createSourceControl`. Emit the folder list
358	// inline so extensions that read `workspaceFolders` synchronously in
359	// their `activate()` (vscode.git, eamodio.gitlens, typescript) see
360	// the real folders.
361	let FoldersWire:Vec<Value> = WorkspaceFoldersGuard
362		.iter()
363		.map(|Folder| {
364			json!({
365				"uri": Folder.URI.to_string(),
366				"name": Folder.GetDisplayName(),
367				"index": Folder.Index,
368			})
369		})
370		.collect();
371
372	// Pair with the Cocoon-side PRE-ACTIVATE snapshot in
373	// ExtensionHostHandler.ts. If Cocoon prints `folders.length=0` while
374	// this log says `folders=1`, we have a wire-shape bug; if both say
375	// 0, ApplicationState was empty at InitData build time and we need
376	// to defer InitData construction past the workspace seeding.
377	dev_log!(
378		"cocoon",
379		"[InitializationData] FoldersWire count={} sample0={}",
380		FoldersWire.len(),
381		FoldersWire.first().map(|F| F.to_string()).unwrap_or_else(|| "<none>".into())
382	);
383
384	let WorkspaceDTO = if WorkspaceFoldersGuard.is_empty() {
385		Value::Null
386	} else {
387		json!({
388
389			"id": ApplicationState.GetWorkspaceIdentifier()?,
390
391			"name": WorkspaceName,
392
393			"folders": FoldersWire,
394
395			"configuration": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().as_ref().map(|p| p.to_string_lossy()),
396
397			"isUntitled": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().is_none(),
398
399			"transient": false
400		})
401	};
402
403	let PathResolver = ApplicationHandle.path();
404
405	let AppRoot = PathResolver
406		.resource_dir()
407		.ok()
408		.filter(|P| !P.as_os_str().is_empty() && P.exists())
409		.or_else(|| {
410			// Tauri's `resource_dir()` returns Err (or an empty/missing
411			// path) for raw-binary launches outside the bundle. Probe two
412			// fallback layouts so both `.app` and dev launches resolve:
413			//
414			//   1. `.app/Contents/MacOS/<bin>` → `Contents/Resources/`
415			//      (shipped bundle, raw-binary launch from inside the
416			//      bundle tree).
417			//   2. `Element/Mountain/Target/<profile>/<bin>` →
418			//      `Element/Sky/Target/` (monorepo dev / raw release).
419			let ExeDir = std::env::current_exe()
420				.ok()
421				.and_then(|P| P.parent().map(|D| D.to_path_buf()))
422				.unwrap_or_default();
423			let BundleResources = ExeDir.join("../Resources");
424			if BundleResources.exists() {
425				return Some(BundleResources.canonicalize().unwrap_or(BundleResources));
426			}
427			let SkyTarget = ExeDir.join("../../../Sky/Target");
428			if SkyTarget.exists() {
429				return Some(SkyTarget.canonicalize().unwrap_or(SkyTarget));
430			}
431			None
432		})
433		.ok_or_else(|| CommonError::ConfigurationLoad {
434			Description:"Could not resolve AppRoot from resource_dir, ../Resources, or ../../../Sky/Target".to_string(),
435		})?;
436
437	let AppData = PathResolver
438		.app_data_dir()
439		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
440
441	let LogsLocation = PathResolver
442		.app_log_dir()
443		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
444
445	let GlobalStorage = AppData.join("User/globalStorage");
446
447	let WorkspaceStorage = AppData.join("User/workspaceStorage");
448
449	Ok(json!({
450
451		// Atom I5: product version + commit + quality come from .env.Land via
452		// process env. `Tauri's package_info().version` reads tauri.conf.json
453		// which still carries a placeholder "0.0.1" - we can't trust it for
454		// extension compat checks. `ProductVersion` from env is the canonical
455		// value shared with Wind and Cocoon.
456		"commit": std::env::var("ProductCommit").unwrap_or_else(|_| "dev".into()),
457
458		"version": std::env::var("ProductVersion").unwrap_or_else(|_| {
459			ApplicationHandle.package_info().version.to_string()
460		}),
461
462		"quality": std::env::var("ProductQuality").unwrap_or_else(|_| "development".into()),
463
464		"parentPid": std::process::id(),
465
466		"environment": {
467
468			"isExtensionDevelopmentDebug": false,
469
470			"appName": "Mountain",
471
472			"appHost": "desktop",
473
474			"appUriScheme": "mountain",
475
476			"appLanguage": "en",
477
478			"isExtensionTelemetryLoggingOnly": true,
479
480			"appRoot": url::Url::from_directory_path(AppRoot.clone()).unwrap(),
481
482			"globalStorageHome": url::Url::from_directory_path(GlobalStorage).unwrap(),
483
484			"workspaceStorageHome": url::Url::from_directory_path(WorkspaceStorage).unwrap(),
485
486			"extensionDevelopmentLocationURI": [],
487
488			"extensionTestsLocationURI": Value::Null,
489
490			"extensionLogLevel": [["info", "Default"]],
491
492		},
493
494		"workspace": WorkspaceDTO,
495
496		"remote": {
497
498			"isRemote": false,
499
500			"authority": Value::Null,
501
502			"connectionData": Value::Null,
503
504		},
505
506		"consoleForward": { "includeStack": true, "logNative": true },
507
508		"logLevel": log::max_level() as i32,
509
510		"logsLocation": url::Url::from_directory_path(LogsLocation).unwrap(),
511
512		"telemetryInfo": {
513
514			"sessionId": Uuid::new_v4().to_string(),
515
516			"machineId": get_or_generate_machine_id(&AppData),
517
518			"firstSessionDate": "2024-01-01T00:00:00.000Z",
519
520			"msftInternal": false
521		},
522
523		"extensions": ExtensionsDTO,
524
525		"autoStart": true,
526
527		// UIKind.Desktop
528		"uiKind": 1,
529	}))
530}