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
170		if !trimmed.is_empty() {
171			dev_log!("cocoon", "[InitializationData] Loaded existing machine ID from disk");
172
173			return trimmed.to_string();
174		}
175	}
176
177	// Generate and save new machine ID
178	let new_machine_id = Uuid::new_v4().to_string();
179
180	// Ensure directory exists
181	if let Some(parent) = machine_id_path.parent() {
182		if let Err(e) = fs::create_dir_all(parent) {
183			dev_log!(
184				"cocoon",
185				"warn: [InitializationData] Failed to create machine ID directory: {}",
186				e
187			);
188		}
189	}
190
191	// Save to disk
192	if let Err(e) = fs::write(&machine_id_path, &new_machine_id) {
193		dev_log!(
194			"cocoon",
195			"warn: [InitializationData] Failed to persist machine ID to disk: {}",
196			e
197		);
198	} else {
199		dev_log!("cocoon", "[InitializationData] Generated and persisted new machine ID");
200	}
201
202	new_machine_id
203}
204
205/// Constructs the `ISandboxConfiguration` payload needed by the `Sky` frontend.
206pub async fn ConstructSandboxConfiguration(
207	ApplicationHandle:&AppHandle<Wry>,
208
209	ApplicationState:&Arc<ApplicationState>,
210) -> Result<Value, CommonError> {
211	dev_log!("cocoon", "[InitializationData] Constructing ISandboxConfiguration for Sky.");
212
213	let PathResolver = ApplicationHandle.path();
214
215	let AppRootUri = PathResolver.resource_dir().map_err(|Error| {
216		CommonError::ConfigurationLoad {
217			Description:format!("Failed to resolve resource directory (app root): {}", Error),
218		}
219	})?;
220
221	let AppDataDir = PathResolver.app_data_dir().map_err(|Error| {
222		CommonError::ConfigurationLoad { Description:format!("Failed to resolve app data directory: {}", Error) }
223	})?;
224
225	let HomeDir = PathResolver.home_dir().map_err(|Error| {
226		CommonError::ConfigurationLoad { Description:format!("Failed to resolve home directory: {}", Error) }
227	})?;
228
229	let TmpDir = env::temp_dir();
230
231	let BackupPath = AppDataDir.join("Backups").join(ApplicationState.GetWorkspaceIdentifier()?);
232
233	let Platform = match env::consts::OS {
234		"windows" => "win32",
235
236		"macos" => "darwin",
237
238		"linux" => "linux",
239
240		_ => "unknown",
241	};
242
243	let Arch = match env::consts::ARCH {
244		"x86_64" => "x64",
245
246		"aarch64" => "arm64",
247
248		"x86" => "ia32",
249
250		_ => "unknown",
251	};
252
253	let Versions = json!({
254		"mountain": ApplicationHandle.package_info().version.to_string(),
255
256		// Explicitly signal we are not in Electron
257		"electron": "0.0.0-tauri",
258
259		// Representative version
260		"chrome": "120.0.0.0",
261
262		// Representative version
263		"node": "18.18.2"
264	});
265
266	// Load or generate persistent machine ID
267	let machine_id = get_or_generate_machine_id(&AppDataDir);
268
269	Ok(json!({
270		"windowId": ApplicationHandle.get_webview_window("main").unwrap().label(),
271
272		// Persist the machineId to ApplicationState or persistent storage and load
273		// it on subsequent runs. A stable machine identifier is crucial for licensing
274		// validation, telemetry deduplication, and cross-session state consistency.
275		// Now implemented with persistent storage in app data directory.
276		"machineId": machine_id,
277
278		"sessionId": Uuid::new_v4().to_string(),
279
280		"logLevel": log::max_level() as i32,
281
282		"userEnv": env::vars().collect::<HashMap<_,_>>(),
283
284		"appRoot": url::Url::from_directory_path(AppRootUri).unwrap().to_string(),
285
286		"appName": ApplicationHandle.package_info().name.clone(),
287
288		"appUriScheme": "mountain",
289
290		"appLanguage": "en",
291
292		"appHost": "desktop",
293
294		"platform": Platform,
295
296		"arch": Arch,
297
298		"versions": Versions,
299
300		"execPath": env::current_exe().unwrap_or_default().to_string_lossy(),
301
302		"homeDir": url::Url::from_directory_path(HomeDir).unwrap().to_string(),
303
304		"tmpDir": url::Url::from_directory_path(TmpDir).unwrap().to_string(),
305
306		"userDataDir": url::Url::from_directory_path(AppDataDir).unwrap().to_string(),
307
308		"backupPath": url::Url::from_directory_path(BackupPath).unwrap().to_string(),
309
310		"nls": { "messages": {}, "language": "en", "availableLanguages": { "en": "English" } },
311
312		"productConfiguration": {
313
314			// Atom I5: read from process env (populated from .env.Land at
315			// Mountain startup). Fallback strings keep a sensible identity
316			// if the env file is absent at a release-profile launch.
317			"nameShort": std::env::var("ProductNameShort").unwrap_or_else(|_| "Land".into()),
318
319			"nameLong": std::env::var("ProductNameLong").unwrap_or_else(|_| "Land Editor".into()),
320
321			"applicationName": std::env::var("ProductApplicationName").unwrap_or_else(|_| "land".into()),
322
323			"embedderIdentifier": std::env::var("ProductEmbedderIdentifier").unwrap_or_else(|_| "land-desktop".into())
324		},
325
326		"resourcesPath": PathResolver.resource_dir().unwrap_or_default().to_string_lossy(),
327
328		"VSCODE_CWD": env::current_dir().unwrap_or_default().to_string_lossy(),
329	}))
330}
331
332/// Constructs the `IExtensionHostInitData` payload sent to `Cocoon`.
333pub async fn ConstructExtensionHostInitializationData(Environment:&MountainEnvironment) -> Result<Value, CommonError> {
334	dev_log!("cocoon", "[InitializationData] Constructing IExtensionHostInitData for Cocoon.");
335
336	let ApplicationState = &Environment.ApplicationState;
337
338	let ApplicationHandle = &Environment.ApplicationHandle;
339
340	let ExtensionManagementProvider:Arc<dyn ExtensionManagementService> = Environment.Require();
341
342	let ExtensionsDTO = ExtensionManagementProvider.GetExtensions().await?;
343
344	let WorkspaceProvider:Arc<dyn WorkspaceProvider> = Environment.Require();
345
346	let WorkspaceName = WorkspaceProvider
347		.GetWorkspaceName()
348		.await?
349		.unwrap_or_else(|| "Mountain Workspace".to_string());
350
351	let WorkspaceFoldersGuard = ApplicationState.Workspace.WorkspaceFolders.lock().unwrap();
352
353	// Cocoon's `WorkspaceNamespace/Index.ts` reads
354	// `ExtensionHostInitData.workspace.folders` at shim construction time,
355	// then mutates the same array in place on `$deltaWorkspaceFolders`. If
356	// `folders` is missing from the init payload, every
357	// `vscode.workspace.workspaceFolders` read returns `[]` until a delta
358	// fires - which means the git extension boots with zero folders to
359	// scan and never calls `createSourceControl`. Emit the folder list
360	// inline so extensions that read `workspaceFolders` synchronously in
361	// their `activate()` (vscode.git, eamodio.gitlens, typescript) see
362	// the real folders.
363	let FoldersWire:Vec<Value> = WorkspaceFoldersGuard
364		.iter()
365		.map(|Folder| {
366			json!({
367				"uri": Folder.URI.to_string(),
368				"name": Folder.GetDisplayName(),
369				"index": Folder.Index,
370			})
371		})
372		.collect();
373
374	// Pair with the Cocoon-side PRE-ACTIVATE snapshot in
375	// ExtensionHostHandler.ts. If Cocoon prints `folders.length=0` while
376	// this log says `folders=1`, we have a wire-shape bug; if both say
377	// 0, ApplicationState was empty at InitData build time and we need
378	// to defer InitData construction past the workspace seeding.
379	dev_log!(
380		"cocoon",
381		"[InitializationData] FoldersWire count={} sample0={}",
382		FoldersWire.len(),
383		FoldersWire.first().map(|F| F.to_string()).unwrap_or_else(|| "<none>".into())
384	);
385
386	let WorkspaceDTO = if WorkspaceFoldersGuard.is_empty() {
387		Value::Null
388	} else {
389		json!({
390
391			"id": ApplicationState.GetWorkspaceIdentifier()?,
392
393			"name": WorkspaceName,
394
395			"folders": FoldersWire,
396
397			"configuration": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().as_ref().map(|p| p.to_string_lossy()),
398
399			"isUntitled": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().is_none(),
400
401			"transient": false
402		})
403	};
404
405	let PathResolver = ApplicationHandle.path();
406
407	let AppRoot = PathResolver
408		.resource_dir()
409		.ok()
410		.filter(|P| !P.as_os_str().is_empty() && P.exists())
411		.or_else(|| {
412			// Tauri's `resource_dir()` returns Err (or an empty/missing
413			// path) for raw-binary launches outside the bundle. Probe two
414			// fallback layouts so both `.app` and dev launches resolve:
415			//
416			//   1. `.app/Contents/MacOS/<bin>` → `Contents/Resources/` (shipped bundle,
417			//      raw-binary launch from inside the bundle tree).
418			//   2. `Element/Mountain/Target/<profile>/<bin>` → `Element/Sky/Target/`
419			//      (monorepo dev / raw release).
420			let ExeDir = std::env::current_exe()
421				.ok()
422				.and_then(|P| P.parent().map(|D| D.to_path_buf()))
423				.unwrap_or_default();
424			let BundleResources = ExeDir.join("../Resources");
425			if BundleResources.exists() {
426				return Some(BundleResources.canonicalize().unwrap_or(BundleResources));
427			}
428			let SkyTarget = ExeDir.join("../../../Sky/Target");
429			if SkyTarget.exists() {
430				return Some(SkyTarget.canonicalize().unwrap_or(SkyTarget));
431			}
432			None
433		})
434		.ok_or_else(|| {
435			CommonError::ConfigurationLoad {
436				Description:"Could not resolve AppRoot from resource_dir, ../Resources, or ../../../Sky/Target"
437					.to_string(),
438			}
439		})?;
440
441	let AppData = PathResolver
442		.app_data_dir()
443		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
444
445	let LogsLocation = PathResolver
446		.app_log_dir()
447		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
448
449	let GlobalStorage = AppData.join("User/globalStorage");
450
451	let WorkspaceStorage = AppData.join("User/workspaceStorage");
452
453	Ok(json!({
454
455		// Atom I5: product version + commit + quality come from .env.Land via
456		// process env. `Tauri's package_info().version` reads tauri.conf.json
457		// which still carries a placeholder "0.0.1" - we can't trust it for
458		// extension compat checks. `ProductVersion` from env is the canonical
459		// value shared with Wind and Cocoon.
460		"commit": std::env::var("ProductCommit").unwrap_or_else(|_| "dev".into()),
461
462		"version": std::env::var("ProductVersion").unwrap_or_else(|_| {
463			ApplicationHandle.package_info().version.to_string()
464		}),
465
466		"quality": std::env::var("ProductQuality").unwrap_or_else(|_| "development".into()),
467
468		"parentPid": std::process::id(),
469
470		"environment": {
471
472			"isExtensionDevelopmentDebug": false,
473
474			"appName": "Mountain",
475
476			"appHost": "desktop",
477
478			"appUriScheme": "mountain",
479
480			"appLanguage": "en",
481
482			"isExtensionTelemetryLoggingOnly": true,
483
484			"appRoot": url::Url::from_directory_path(AppRoot.clone()).unwrap(),
485
486			"globalStorageHome": url::Url::from_directory_path(GlobalStorage).unwrap(),
487
488			"workspaceStorageHome": url::Url::from_directory_path(WorkspaceStorage).unwrap(),
489
490			"extensionDevelopmentLocationURI": [],
491
492			"extensionTestsLocationURI": Value::Null,
493
494			"extensionLogLevel": [["info", "Default"]],
495
496		},
497
498		"workspace": WorkspaceDTO,
499
500		"remote": {
501
502			"isRemote": false,
503
504			"authority": Value::Null,
505
506			"connectionData": Value::Null,
507
508		},
509
510		"consoleForward": { "includeStack": true, "logNative": true },
511
512		"logLevel": log::max_level() as i32,
513
514		"logsLocation": url::Url::from_directory_path(LogsLocation).unwrap(),
515
516		"telemetryInfo": {
517
518			"sessionId": Uuid::new_v4().to_string(),
519
520			"machineId": get_or_generate_machine_id(&AppData),
521
522			"firstSessionDate": "2024-01-01T00:00:00.000Z",
523
524			"msftInternal": false
525		},
526
527		"extensions": ExtensionsDTO,
528
529		"autoStart": true,
530
531		// UIKind.Desktop
532		"uiKind": 1,
533	}))
534}