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::{
136	collections::HashMap,
137	env,
138	path::PathBuf,
139	sync::{Arc, OnceLock},
140};
141
142/// Stable session ID for the lifetime of this process. Generated once on
143/// first access and reused by every call to `ConstructSandboxConfiguration`
144/// and `ConstructExtensionHostInitializationData` so extensions comparing
145/// `telemetryInfo.sessionId` across calls see the same value.
146static SESSION_ID:OnceLock<String> = OnceLock::new();
147
148fn SessionId() -> &'static str { SESSION_ID.get_or_init(|| Uuid::new_v4().to_string()) }
149
150use CommonLibrary::{
151	Environment::Requires::Requires,
152	Error::CommonError::CommonError,
153	ExtensionManagement::ExtensionManagementService::ExtensionManagementService,
154	Workspace::WorkspaceProvider::WorkspaceProvider,
155};
156use serde_json::{Value, json};
157use tauri::{AppHandle, Manager, Wry};
158use uuid::Uuid;
159
160use crate::{
161	ApplicationState::State::ApplicationState::ApplicationState,
162	Environment::MountainEnvironment::MountainEnvironment,
163	dev_log,
164};
165
166/// Loads or generates a persistent machine ID.
167///
168/// The machine ID is stored in the app data directory as a simple text file.
169/// If the file doesn't exist, a new UUID is generated and saved.
170///
171/// # Arguments
172/// * `app_data_dir` - The application data directory path
173///
174/// # Returns
175/// The machine ID as a String
176async fn get_or_generate_machine_id(app_data_dir:&PathBuf) -> String {
177	let machine_id_path = app_data_dir.join("machine-id.txt");
178
179	// Try to load existing machine ID using async I/O so the Tokio
180	// worker thread is not parked during the disk read at boot.
181	if let Ok(content) = tokio::fs::read_to_string(&machine_id_path).await {
182		let trimmed = content.trim();
183
184		if !trimmed.is_empty() {
185			dev_log!("cocoon", "[InitializationData] Loaded existing machine ID from disk");
186
187			return trimmed.to_string();
188		}
189	}
190
191	// Generate and save new machine ID
192	let new_machine_id = Uuid::new_v4().to_string();
193
194	// Ensure directory exists
195	if let Some(parent) = machine_id_path.parent() {
196		if let Err(e) = tokio::fs::create_dir_all(parent).await {
197			dev_log!(
198				"cocoon",
199				"warn: [InitializationData] Failed to create machine ID directory: {}",
200				e
201			);
202		}
203	}
204
205	// Save to disk
206	if let Err(e) = tokio::fs::write(&machine_id_path, &new_machine_id).await {
207		dev_log!(
208			"cocoon",
209			"warn: [InitializationData] Failed to persist machine ID to disk: {}",
210			e
211		);
212	} else {
213		dev_log!("cocoon", "[InitializationData] Generated and persisted new machine ID");
214	}
215
216	new_machine_id
217}
218
219/// Constructs the `ISandboxConfiguration` payload needed by the `Sky` frontend.
220pub async fn ConstructSandboxConfiguration(
221	ApplicationHandle:&AppHandle<Wry>,
222
223	ApplicationState:&Arc<ApplicationState>,
224) -> Result<Value, CommonError> {
225	dev_log!("cocoon", "[InitializationData] Constructing ISandboxConfiguration for Sky.");
226
227	let PathResolver = ApplicationHandle.path();
228
229	let AppRootUri = PathResolver.resource_dir().map_err(|Error| {
230		CommonError::ConfigurationLoad {
231			Description:format!("Failed to resolve resource directory (app root): {}", Error),
232		}
233	})?;
234
235	let AppDataDir = PathResolver.app_data_dir().map_err(|Error| {
236		CommonError::ConfigurationLoad { Description:format!("Failed to resolve app data directory: {}", Error) }
237	})?;
238
239	let HomeDir = PathResolver.home_dir().map_err(|Error| {
240		CommonError::ConfigurationLoad { Description:format!("Failed to resolve home directory: {}", Error) }
241	})?;
242
243	let TmpDir = env::temp_dir();
244
245	let BackupPath = AppDataDir.join("Backups").join(ApplicationState.GetWorkspaceIdentifier()?);
246
247	// `logsPath` is a required field of `ISandboxConfiguration`. VS Code reads
248	// it via `NativeWorkbenchEnvironmentService.logsHome` → `URI.file(logsPath)`.
249	// Missing it leaves logsPath=undefined → URI.file(undefined).fsPath=undefined
250	// → path.join(undefined,"…") → "The path argument must be of type string".
251	let LogsPath = AppDataDir.join("logs").join(crate::IPC::DevLog::SessionTimestamp::Fn());
252
253	let _ = std::fs::create_dir_all(&LogsPath);
254
255	let Platform = match env::consts::OS {
256		"windows" => "win32",
257
258		"macos" => "darwin",
259
260		"linux" => "linux",
261
262		_ => "unknown",
263	};
264
265	let Arch = match env::consts::ARCH {
266		"x86_64" => "x64",
267
268		"aarch64" => "arm64",
269
270		"x86" => "ia32",
271
272		_ => "unknown",
273	};
274
275	let Versions = json!({
276		"mountain": ApplicationHandle.package_info().version.to_string(),
277
278		// Explicitly signal we are not in Electron
279		"electron": "0.0.0-tauri",
280
281		// Representative version
282		"chrome": "120.0.0.0",
283
284		// Representative version
285		"node": "18.18.2"
286	});
287
288	// Load or generate persistent machine ID
289	let machine_id = get_or_generate_machine_id(&AppDataDir).await;
290
291	// Build the `profiles` section outside the main `json!` call to avoid
292	// exceeding the macro's recursion limit (default 64). Each nested json!
293	// call here is shallow enough to compile without issue.
294	let UserProfile = AppDataDir.join("User");
295
296	// Helper closure: build a `UriComponents` JSON object for a filesystem path.
297	let FileUri = |P:std::path::PathBuf| -> serde_json::Value {
298		json!({
299			"scheme": "file",
300			"authority": "",
301			"path": P.to_string_lossy(),
302			"query": "",
303			"fragment": ""
304		})
305	};
306
307	let DefaultProfile = json!({
308		"id": "__default__profile__",
309		"name": "Default",
310		"location": FileUri(UserProfile.clone()),
311		"isDefault": true,
312		"globalStorageHome": FileUri(UserProfile.join("globalStorage")),
313		"settingsResource": FileUri(UserProfile.join("settings.json")),
314		"keybindingsResource": FileUri(UserProfile.join("keybindings.json")),
315		"tasksResource": FileUri(UserProfile.join("tasks.json")),
316		"snippetsHome": FileUri(UserProfile.join("snippets")),
317		"promptsHome": FileUri(UserProfile.join("prompts")),
318		"extensionsResource": FileUri(UserProfile.join("extensions.json")),
319		"mcpResource": FileUri(UserProfile.join("mcp.json")),
320		"languageModelsResource": FileUri(UserProfile.join("chatLanguageModels.json")),
321		"agentPluginsHome": FileUri(UserProfile.join("agent-plugins")),
322		"cacheHome": FileUri(UserProfile.join("profiles/.cache/__default__profile__"))
323	});
324
325	let ProfilesSection = json!({
326		"home": FileUri(UserProfile.join("profiles")),
327		"all": [DefaultProfile.clone()],
328		"profile": DefaultProfile
329	});
330
331	// Pre-build other nested sections that contribute heavily to the token
332	// count of the outer json! call and could push it past the limit.
333	let NlsSection = json!({
334		"messages": {},
335		"language": "en",
336		"availableLanguages": { "en": "English" }
337	});
338
339	let ProductConfig = json!({
340		"nameShort": std::env::var("ProductNameShort").unwrap_or_else(|_| "FIDDEE".into()),
341		"nameLong": std::env::var("ProductNameLong").unwrap_or_else(|_| "FIDDEE".into()),
342		"applicationName": std::env::var("ProductApplicationName").unwrap_or_else(|_| "fiddee".into()),
343		"embedderIdentifier": std::env::var("ProductEmbedderIdentifier").unwrap_or_else(|_| "fiddee-desktop".into()),
344		"dataFolderName": std::env::var("ProductDataFolderName").unwrap_or_else(|_| ".fiddee".into()),
345		"sharedDataFolderName": std::env::var("ProductDataFolderName").unwrap_or_else(|_| ".fiddee".into()),
346		"version": std::env::var("ProductVersion").unwrap_or_else(|_| "1.0.0".into()),
347	});
348
349	let OsSection = json!({
350		"release": "22.0.0",
351		"hostname": "land",
352		"arch": env::consts::ARCH,
353	});
354
355	Ok(json!({
356		"windowId": ApplicationHandle.get_webview_window("main").unwrap().label(),
357
358		// Persist the machineId to ApplicationState or persistent storage and load
359		// it on subsequent runs. A stable machine identifier is crucial for licensing
360		// validation, telemetry deduplication, and cross-session state consistency.
361		// Now implemented with persistent storage in app data directory.
362		"machineId": machine_id,
363
364		"sessionId": SessionId(),
365
366		"logLevel": log::max_level() as i32,
367
368		"userEnv": env::vars().collect::<HashMap<_,_>>(),
369
370		// `INativeWindowConfiguration.appRoot` - plain OS filesystem path.
371		// VS Code's `AbstractNativeEnvironmentService.appRoot` returns this
372		// string directly and passes it to `path.join(appRoot, ...)`.
373		// Previously sent as a `file://` URL which caused `URI.file(fileUrl)`
374		// to construct a URI with path `/file:///…` (double-scheme), making
375		// every downstream `path.join` operate on a malformed base.
376		"appRoot": AppRootUri.to_string_lossy(),
377
378		"appName": ApplicationHandle.package_info().name.clone(),
379
380		"appUriScheme": "mountain",
381
382		"appLanguage": "en",
383
384		"appHost": "desktop",
385
386		"platform": Platform,
387
388		"arch": Arch,
389
390		"versions": Versions,
391
392		"execPath": env::current_exe().unwrap_or_default().to_string_lossy(),
393
394		// Plain OS paths for all home/data/tmp/backup.
395		// VS Code wraps these in `URI.file(path)` and `path.join(path, …)`;
396		// both require a real filesystem path, not a `file://` URL string.
397		"homeDir": HomeDir.to_string_lossy(),
398
399		"tmpDir": TmpDir.to_string_lossy(),
400
401		"userDataDir": AppDataDir.to_string_lossy(),
402
403		"backupPath": BackupPath.to_string_lossy(),
404
405		"logsPath": LogsPath.to_string_lossy(),
406
407		// Required non-optional fields in INativeWindowConfiguration.
408		// Missing these causes crashes in NativeWorkbenchEnvironmentService getters
409		// that access them without null-checks.
410		"perfMarks": [],
411
412		"colorScheme": { "dark": false, "highContrast": false },
413
414		"loggers": [],
415
416		"mainPid": std::process::id(),
417
418		"os": OsSection,
419
420		"nls": NlsSection,
421
422		// Atom I5: read from process env. Pre-built above to reduce the
423		// token count inside this json! call.
424		"productConfiguration": ProductConfig,
425
426		"resourcesPath": PathResolver.resource_dir().unwrap_or_default().to_string_lossy(),
427
428		"VSCODE_CWD": env::current_dir().unwrap_or_default().to_string_lossy(),
429
430		// Pre-built outside json! to avoid macro recursion limit (see above).
431		"profiles": ProfilesSection,
432
433		// Required non-optional fields added defensively to avoid future
434		// workbench crashes on properties accessed without null-checks.
435		"sqmId": "",
436		"devDeviceId": "",
437		"isPortable": false,
438	}))
439}
440
441/// Constructs the `IExtensionHostInitData` payload sent to `Cocoon`.
442pub async fn ConstructExtensionHostInitializationData(Environment:&MountainEnvironment) -> Result<Value, CommonError> {
443	dev_log!("cocoon", "[InitializationData] Constructing IExtensionHostInitData for Cocoon.");
444
445	let ApplicationState = &Environment.ApplicationState;
446
447	let ApplicationHandle = &Environment.ApplicationHandle;
448
449	let ExtensionManagementProvider:Arc<dyn ExtensionManagementService> = Environment.Require();
450
451	let ExtensionsDTO = ExtensionManagementProvider.GetExtensions().await?;
452
453	let WorkspaceProvider:Arc<dyn WorkspaceProvider> = Environment.Require();
454
455	let WorkspaceName = WorkspaceProvider
456		.GetWorkspaceName()
457		.await?
458		.unwrap_or_else(|| "Mountain Workspace".to_string());
459
460	// Scope the MutexGuard so it is dropped before any `.await` below.
461	// `MutexGuard<T>` is not `Send`; holding it across an await makes the
462	// future non-Send, breaking `tauri::async_runtime::spawn`. Extract the
463	// two scalars needed for logging before moving `FoldersWire` into
464	// `WorkspaceDTO` - no clone required.
465	let WorkspaceDTO = {
466		let Guard = ApplicationState.Workspace.WorkspaceFolders.lock().unwrap();
467
468		// Cocoon's `WorkspaceNamespace/Index.ts` reads
469		// `ExtensionHostInitData.workspace.folders` at shim construction time,
470		// then mutates the same array in place on `$deltaWorkspaceFolders`. If
471		// `folders` is missing from the init payload, every
472		// `vscode.workspace.workspaceFolders` read returns `[]` until a delta
473		// fires - which means the git extension boots with zero folders to
474		// scan and never calls `createSourceControl`. Emit the folder list
475		// inline so extensions that read `workspaceFolders` synchronously in
476		// their `activate()` (vscode.git, eamodio.gitlens, typescript) see
477		// the real folders.
478		let FoldersWire:Vec<Value> = Guard
479			.iter()
480			.map(|Folder| {
481				json!({
482					"uri": Folder.URI.to_string(),
483					"name": Folder.GetDisplayName(),
484					"index": Folder.Index,
485				})
486			})
487			.collect();
488
489		// Extract logging scalars before FoldersWire is moved - avoids clone.
490		let FolderCount = FoldersWire.len();
491
492		let FolderSample = FoldersWire.first().map(|F| F.to_string()).unwrap_or_else(|| "<none>".into());
493
494		let IsEmpty = Guard.is_empty();
495
496		drop(Guard); // guard released; no await points follow inside this block
497
498		dev_log!(
499			"cocoon",
500			"[InitializationData] FoldersWire count={} sample0={}",
501			FolderCount,
502			FolderSample
503		);
504
505		if IsEmpty {
506			Value::Null
507		} else {
508			json!({
509				"id": ApplicationState.GetWorkspaceIdentifier()?,
510				"name": WorkspaceName,
511				"folders": FoldersWire, // moved in - zero extra allocation
512				"configuration": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().as_ref().map(|p| p.to_string_lossy()),
513				"isUntitled": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().is_none(),
514				"transient": false
515			})
516		}
517	};
518
519	let PathResolver = ApplicationHandle.path();
520
521	let AppRoot = PathResolver
522		.resource_dir()
523		.ok()
524		.filter(|P| !P.as_os_str().is_empty() && P.exists())
525		.or_else(|| {
526			// Tauri's `resource_dir()` returns Err (or an empty/missing
527			// path) for raw-binary launches outside the bundle. Probe two
528			// fallback layouts so both `.app` and dev launches resolve:
529			//
530			//   1. `.app/Contents/MacOS/<bin>` → `Contents/Resources/` (shipped bundle,
531			//      raw-binary launch from inside the bundle tree).
532			//   2. `Element/Mountain/Target/<profile>/<bin>` → `Element/Sky/Target/`
533			//      (monorepo dev / raw release).
534			let ExeDir = std::env::current_exe()
535				.ok()
536				.and_then(|P| P.parent().map(|D| D.to_path_buf()))
537				.unwrap_or_default();
538			let BundleResources = ExeDir.join("../Resources");
539			if BundleResources.exists() {
540				return Some(BundleResources.canonicalize().unwrap_or(BundleResources));
541			}
542			let SkyTarget = ExeDir.join("../../../Sky/Target");
543			if SkyTarget.exists() {
544				return Some(SkyTarget.canonicalize().unwrap_or(SkyTarget));
545			}
546			None
547		})
548		.ok_or_else(|| {
549			CommonError::ConfigurationLoad {
550				Description:"Could not resolve AppRoot from resource_dir, ../Resources, or ../../../Sky/Target"
551					.to_string(),
552			}
553		})?;
554
555	let AppData = PathResolver
556		.app_data_dir()
557		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
558
559	let LogsLocation = PathResolver
560		.app_log_dir()
561		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
562
563	let GlobalStorage = AppData.join("User/globalStorage");
564
565	let WorkspaceStorage = AppData.join("User/workspaceStorage");
566
567	Ok(json!({
568
569		// Atom I5: product version + commit + quality come from .env.Land via
570		// process env. `Tauri's package_info().version` reads tauri.conf.json
571		// which still carries a placeholder "0.0.1" - we can't trust it for
572		// extension compat checks. `ProductVersion` from env is the canonical
573		// value shared with Wind and Cocoon.
574		"commit": std::env::var("ProductCommit").unwrap_or_else(|_| "dev".into()),
575
576		"version": std::env::var("ProductVersion").unwrap_or_else(|_| {
577			ApplicationHandle.package_info().version.to_string()
578		}),
579
580		"quality": std::env::var("ProductQuality").unwrap_or_else(|_| "development".into()),
581
582		"parentPid": std::process::id(),
583
584		"environment": {
585
586			"isExtensionDevelopmentDebug": false,
587
588			"appName": "Mountain",
589
590			"appHost": "desktop",
591
592			"appUriScheme": "mountain",
593
594			"appLanguage": "en",
595
596			"isExtensionTelemetryLoggingOnly": true,
597
598			"appRoot": url::Url::from_directory_path(AppRoot.clone()).unwrap(),
599
600			"globalStorageHome": url::Url::from_directory_path(GlobalStorage).unwrap(),
601
602			"workspaceStorageHome": url::Url::from_directory_path(WorkspaceStorage).unwrap(),
603
604			"extensionDevelopmentLocationURI": [],
605
606			"extensionTestsLocationURI": Value::Null,
607
608			"extensionLogLevel": [["info", "Default"]],
609
610		},
611
612		"workspace": WorkspaceDTO,
613
614		"remote": {
615
616			"isRemote": false,
617
618			"authority": Value::Null,
619
620			"connectionData": Value::Null,
621
622		},
623
624		"consoleForward": { "includeStack": true, "logNative": true },
625
626		"logLevel": log::max_level() as i32,
627
628		"logsLocation": url::Url::from_directory_path(LogsLocation).unwrap(),
629
630		"telemetryInfo": {
631
632			"sessionId": SessionId(),
633
634			"machineId": get_or_generate_machine_id(&AppData).await,
635
636			"firstSessionDate": "2024-01-01T00:00:00.000Z",
637
638			"msftInternal": false
639		},
640
641		"extensions": ExtensionsDTO,
642
643		"autoStart": true,
644
645		// UIKind.Desktop
646		"uiKind": 1,
647	}))
648}