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 log::info;
144use serde_json::{Value, json};
145use tauri::{AppHandle, Manager, Wry};
146use uuid::Uuid;
147
148use crate::{ApplicationState::ApplicationState, Environment::MountainEnvironment::MountainEnvironment};
149
150/// Loads or generates a persistent machine ID.
151///
152/// The machine ID is stored in the app data directory as a simple text file.
153/// If the file doesn't exist, a new UUID is generated and saved.
154///
155/// # Arguments
156/// * `app_data_dir` - The application data directory path
157///
158/// # Returns
159/// The machine ID as a String
160fn get_or_generate_machine_id(app_data_dir:&PathBuf) -> String {
161	let machine_id_path = app_data_dir.join("machine-id.txt");
162
163	// Try to load existing machine ID
164	if let Ok(content) = fs::read_to_string(&machine_id_path) {
165		let trimmed = content.trim();
166		if !trimmed.is_empty() {
167			log::info!("[InitializationData] Loaded existing machine ID from disk");
168			return trimmed.to_string();
169		}
170	}
171
172	// Generate and save new machine ID
173	let new_machine_id = Uuid::new_v4().to_string();
174
175	// Ensure directory exists
176	if let Some(parent) = machine_id_path.parent() {
177		if let Err(e) = fs::create_dir_all(parent) {
178			log::warn!("[InitializationData] Failed to create machine ID directory: {}", e);
179		}
180	}
181
182	// Save to disk
183	if let Err(e) = fs::write(&machine_id_path, &new_machine_id) {
184		log::warn!("[InitializationData] Failed to persist machine ID to disk: {}", e);
185	} else {
186		log::info!("[InitializationData] Generated and persisted new machine ID");
187	}
188
189	new_machine_id
190}
191
192/// Constructs the `ISandboxConfiguration` payload needed by the `Sky` frontend.
193pub async fn ConstructSandboxConfiguration(
194	ApplicationHandle:&AppHandle<Wry>,
195
196	ApplicationState:&Arc<ApplicationState>,
197) -> Result<Value, CommonError> {
198	info!("[InitializationData] Constructing ISandboxConfiguration for Sky.");
199
200	let PathResolver = ApplicationHandle.path();
201
202	let AppRootUri = PathResolver.resource_dir().map_err(|Error| {
203		CommonError::ConfigurationLoad {
204			Description:format!("Failed to resolve resource directory (app root): {}", Error),
205		}
206	})?;
207
208	let AppDataDir = PathResolver.app_data_dir().map_err(|Error| {
209		CommonError::ConfigurationLoad { Description:format!("Failed to resolve app data directory: {}", Error) }
210	})?;
211
212	let HomeDir = PathResolver.home_dir().map_err(|Error| {
213		CommonError::ConfigurationLoad { Description:format!("Failed to resolve home directory: {}", Error) }
214	})?;
215
216	let TmpDir = env::temp_dir();
217
218	let BackupPath = AppDataDir.join("Backups").join(ApplicationState.GetWorkspaceIdentifier()?);
219
220	let Platform = match env::consts::OS {
221		"windows" => "win32",
222
223		"macos" => "darwin",
224
225		"linux" => "linux",
226
227		_ => "unknown",
228	};
229
230	let Arch = match env::consts::ARCH {
231		"x86_64" => "x64",
232
233		"aarch64" => "arm64",
234
235		"x86" => "ia32",
236
237		_ => "unknown",
238	};
239
240	let Versions = json!({
241		"mountain": ApplicationHandle.package_info().version.to_string(),
242
243		// Explicitly signal we are not in Electron
244		"electron": "0.0.0-tauri",
245
246		// Representative version
247		"chrome": "120.0.0.0",
248
249		// Representative version
250		"node": "18.18.2"
251	});
252
253	// Load or generate persistent machine ID
254	let machine_id = get_or_generate_machine_id(&AppDataDir);
255
256	Ok(json!({
257		"windowId": ApplicationHandle.get_webview_window("main").unwrap().label(),
258
259		// Persist the machineId to ApplicationState or persistent storage and load
260		// it on subsequent runs. A stable machine identifier is crucial for licensing
261		// validation, telemetry deduplication, and cross-session state consistency.
262		// Now implemented with persistent storage in app data directory.
263		"machineId": machine_id,
264
265		"sessionId": Uuid::new_v4().to_string(),
266
267		"logLevel": log::max_level() as i32,
268
269		"userEnv": env::vars().collect::<HashMap<_,_>>(),
270
271		"appRoot": url::Url::from_directory_path(AppRootUri).unwrap().to_string(),
272
273		"appName": ApplicationHandle.package_info().name.clone(),
274
275		"appUriScheme": "mountain",
276
277		"appLanguage": "en",
278
279		"appHost": "desktop",
280
281		"platform": Platform,
282
283		"arch": Arch,
284
285		"versions": Versions,
286
287		"execPath": env::current_exe().unwrap_or_default().to_string_lossy(),
288
289		"homeDir": url::Url::from_directory_path(HomeDir).unwrap().to_string(),
290
291		"tmpDir": url::Url::from_directory_path(TmpDir).unwrap().to_string(),
292
293		"userDataDir": url::Url::from_directory_path(AppDataDir).unwrap().to_string(),
294
295		"backupPath": url::Url::from_directory_path(BackupPath).unwrap().to_string(),
296
297		"nls": { "messages": {}, "language": "en", "availableLanguages": { "en": "English" } },
298
299		"productConfiguration": {
300
301			"nameShort": "Mountain",
302
303			"nameLong": "Mountain Editor",
304
305			"applicationName": "mountain",
306
307			"embedderIdentifier": "mountain-desktop"
308		},
309
310		"resourcesPath": PathResolver.resource_dir().unwrap_or_default().to_string_lossy(),
311
312		"VSCODE_CWD": env::current_dir().unwrap_or_default().to_string_lossy(),
313	}))
314}
315
316/// Constructs the `IExtensionHostInitData` payload sent to `Cocoon`.
317pub async fn ConstructExtensionHostInitializationData(Environment:&MountainEnvironment) -> Result<Value, CommonError> {
318	info!("[InitializationData] Constructing IExtensionHostInitData for Cocoon.");
319
320	let ApplicationState = &Environment.ApplicationState;
321
322	let ApplicationHandle = &Environment.ApplicationHandle;
323
324	let ExtensionManagementProvider:Arc<dyn ExtensionManagementService> = Environment.Require();
325
326	let ExtensionsDTO = ExtensionManagementProvider.GetExtensions().await?;
327
328	let WorkspaceProvider:Arc<dyn WorkspaceProvider> = Environment.Require();
329
330	let WorkspaceName = WorkspaceProvider
331		.GetWorkspaceName()
332		.await?
333		.unwrap_or_else(|| "Mountain Workspace".to_string());
334
335	let WorkspaceFoldersGuard = ApplicationState.Workspace.WorkspaceFolders.lock().unwrap();
336
337	let WorkspaceDTO = if WorkspaceFoldersGuard.is_empty() {
338		Value::Null
339	} else {
340		json!({
341
342			"id": ApplicationState.GetWorkspaceIdentifier()?,
343
344			"name": WorkspaceName,
345
346			"configuration": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().as_ref().map(|p| p.to_string_lossy()),
347
348			"isUntitled": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().is_none(),
349
350			"transient": false
351		})
352	};
353
354	let PathResolver = ApplicationHandle.path();
355
356	let AppRoot = PathResolver
357		.resource_dir()
358		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
359
360	let AppData = PathResolver
361		.app_data_dir()
362		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
363
364	let LogsLocation = PathResolver
365		.app_log_dir()
366		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
367
368	let GlobalStorage = AppData.join("User/globalStorage");
369
370	let WorkspaceStorage = AppData.join("User/workspaceStorage");
371
372	Ok(json!({
373
374		"commit": "dev-commit-hash",
375
376		"version": ApplicationHandle.package_info().version.to_string(),
377
378		"quality": "development",
379
380		"parentPid": std::process::id(),
381
382		"environment": {
383
384			"isExtensionDevelopmentDebug": false,
385
386			"appName": "Mountain",
387
388			"appHost": "desktop",
389
390			"appUriScheme": "mountain",
391
392			"appLanguage": "en",
393
394			"isExtensionTelemetryLoggingOnly": true,
395
396			"appRoot": url::Url::from_directory_path(AppRoot.clone()).unwrap(),
397
398			"globalStorageHome": url::Url::from_directory_path(GlobalStorage).unwrap(),
399
400			"workspaceStorageHome": url::Url::from_directory_path(WorkspaceStorage).unwrap(),
401
402			"extensionDevelopmentLocationURI": [],
403
404			"extensionTestsLocationURI": Value::Null,
405
406			"extensionLogLevel": [["info", "Default"]],
407
408		},
409
410		"workspace": WorkspaceDTO,
411
412		"remote": {
413
414			"isRemote": false,
415
416			"authority": Value::Null,
417
418			"connectionData": Value::Null,
419
420		},
421
422		"consoleForward": { "includeStack": true, "logNative": true },
423
424		"logLevel": log::max_level() as i32,
425
426		"logsLocation": url::Url::from_directory_path(LogsLocation).unwrap(),
427
428		"telemetryInfo": {
429
430			"sessionId": Uuid::new_v4().to_string(),
431
432			"machineId": get_or_generate_machine_id(&AppData),
433
434			"firstSessionDate": "2024-01-01T00:00:00.000Z",
435
436			"msftInternal": false
437		},
438
439		"extensions": ExtensionsDTO,
440
441		"autoStart": true,
442
443		// UIKind.Desktop
444		"uiKind": 1,
445	}))
446}