Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_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	// `logsPath` is a required field of `ISandboxConfiguration`. VS Code reads
234	// it via `NativeWorkbenchEnvironmentService.logsHome` → `URI.file(logsPath)`.
235	// Missing it leaves logsPath=undefined → URI.file(undefined).fsPath=undefined
236	// → path.join(undefined,"…") → "The path argument must be of type string".
237	let LogsPath = AppDataDir.join("logs").join(crate::IPC::DevLog::SessionTimestamp::Fn());
238	let _ = std::fs::create_dir_all(&LogsPath);
239
240	let Platform = match env::consts::OS {
241		"windows" => "win32",
242
243		"macos" => "darwin",
244
245		"linux" => "linux",
246
247		_ => "unknown",
248	};
249
250	let Arch = match env::consts::ARCH {
251		"x86_64" => "x64",
252
253		"aarch64" => "arm64",
254
255		"x86" => "ia32",
256
257		_ => "unknown",
258	};
259
260	let Versions = json!({
261		"mountain": ApplicationHandle.package_info().version.to_string(),
262
263		// Explicitly signal we are not in Electron
264		"electron": "0.0.0-tauri",
265
266		// Representative version
267		"chrome": "120.0.0.0",
268
269		// Representative version
270		"node": "18.18.2"
271	});
272
273	// Load or generate persistent machine ID
274	let machine_id = get_or_generate_machine_id(&AppDataDir);
275
276	Ok(json!({
277		"windowId": ApplicationHandle.get_webview_window("main").unwrap().label(),
278
279		// Persist the machineId to ApplicationState or persistent storage and load
280		// it on subsequent runs. A stable machine identifier is crucial for licensing
281		// validation, telemetry deduplication, and cross-session state consistency.
282		// Now implemented with persistent storage in app data directory.
283		"machineId": machine_id,
284
285		"sessionId": Uuid::new_v4().to_string(),
286
287		"logLevel": log::max_level() as i32,
288
289		"userEnv": env::vars().collect::<HashMap<_,_>>(),
290
291		// `INativeWindowConfiguration.appRoot` - plain OS filesystem path.
292		// VS Code's `AbstractNativeEnvironmentService.appRoot` returns this
293		// string directly and passes it to `path.join(appRoot, ...)`.
294		// Previously sent as a `file://` URL which caused `URI.file(fileUrl)`
295		// to construct a URI with path `/file:///…` (double-scheme), making
296		// every downstream `path.join` operate on a malformed base.
297		"appRoot": AppRootUri.to_string_lossy(),
298
299		"appName": ApplicationHandle.package_info().name.clone(),
300
301		"appUriScheme": "mountain",
302
303		"appLanguage": "en",
304
305		"appHost": "desktop",
306
307		"platform": Platform,
308
309		"arch": Arch,
310
311		"versions": Versions,
312
313		"execPath": env::current_exe().unwrap_or_default().to_string_lossy(),
314
315		// Plain OS paths for all home/data/tmp/backup.
316		// VS Code wraps these in `URI.file(path)` and `path.join(path, …)`;
317		// both require a real filesystem path, not a `file://` URL string.
318		"homeDir": HomeDir.to_string_lossy(),
319
320		"tmpDir": TmpDir.to_string_lossy(),
321
322		"userDataDir": AppDataDir.to_string_lossy(),
323
324		"backupPath": BackupPath.to_string_lossy(),
325
326		"logsPath": LogsPath.to_string_lossy(),
327
328		// Required non-optional fields in INativeWindowConfiguration.
329		// Missing these causes crashes in NativeWorkbenchEnvironmentService getters
330		// that access them without null-checks.
331		"perfMarks": [],
332
333		"colorScheme": { "dark": false, "highContrast": false },
334
335		"loggers": [],
336
337		"mainPid": std::process::id(),
338
339		"os": {
340			"release": "22.0.0",
341			"hostname": "land",
342			"arch": env::consts::ARCH,
343		},
344
345		"nls": { "messages": {}, "language": "en", "availableLanguages": { "en": "English" } },
346
347		"productConfiguration": {
348
349		// Atom I5: read from process env (populated from .env.Land at
350		// Mountain startup). Fallback strings keep a sensible identity
351		// if the env file is absent at a release-profile launch.
352		"nameShort": std::env::var("ProductNameShort").unwrap_or_else(|_| "FIDDEE".into()),
353		"nameLong": std::env::var("ProductNameLong").unwrap_or_else(|_| "FIDDEE".into()),
354		"applicationName": std::env::var("ProductApplicationName").unwrap_or_else(|_| "fiddee".into()),
355		"embedderIdentifier": std::env::var("ProductEmbedderIdentifier").unwrap_or_else(|_| "fiddee-desktop".into()),
356
357		// `dataFolderName` is used by VS Code's `AbstractNativeEnvironmentService
358		// .extensionsPath` via `URI.joinPath(userHome, dataFolderName, "extensions")`.
359		// Without it, `path.posix.join("...", undefined, "extensions")` throws
360		// "The path argument must be of type string. Received type undefined".
361		"dataFolderName": std::env::var("ProductDataFolderName").unwrap_or_else(|_| ".fiddee".into()),
362
363		// `sharedDataFolderName` is used by `appSharedDataHome` in the same way.
364		// Provide the same value to avoid a second undefined-path crash if accessed.
365		"sharedDataFolderName": std::env::var("ProductDataFolderName").unwrap_or_else(|_| ".fiddee".into()),
366
367		// `version` is used in extension compatibility checks and telemetry.
368		"version": std::env::var("ProductVersion").unwrap_or_else(|_| "1.0.0".into()),
369		},
370
371		"resourcesPath": PathResolver.resource_dir().unwrap_or_default().to_string_lossy(),
372
373		"VSCODE_CWD": env::current_dir().unwrap_or_default().to_string_lossy(),
374	}))
375}
376
377/// Constructs the `IExtensionHostInitData` payload sent to `Cocoon`.
378pub async fn ConstructExtensionHostInitializationData(Environment:&MountainEnvironment) -> Result<Value, CommonError> {
379	dev_log!("cocoon", "[InitializationData] Constructing IExtensionHostInitData for Cocoon.");
380
381	let ApplicationState = &Environment.ApplicationState;
382
383	let ApplicationHandle = &Environment.ApplicationHandle;
384
385	let ExtensionManagementProvider:Arc<dyn ExtensionManagementService> = Environment.Require();
386
387	let ExtensionsDTO = ExtensionManagementProvider.GetExtensions().await?;
388
389	let WorkspaceProvider:Arc<dyn WorkspaceProvider> = Environment.Require();
390
391	let WorkspaceName = WorkspaceProvider
392		.GetWorkspaceName()
393		.await?
394		.unwrap_or_else(|| "Mountain Workspace".to_string());
395
396	let WorkspaceFoldersGuard = ApplicationState.Workspace.WorkspaceFolders.lock().unwrap();
397
398	// Cocoon's `WorkspaceNamespace/Index.ts` reads
399	// `ExtensionHostInitData.workspace.folders` at shim construction time,
400	// then mutates the same array in place on `$deltaWorkspaceFolders`. If
401	// `folders` is missing from the init payload, every
402	// `vscode.workspace.workspaceFolders` read returns `[]` until a delta
403	// fires - which means the git extension boots with zero folders to
404	// scan and never calls `createSourceControl`. Emit the folder list
405	// inline so extensions that read `workspaceFolders` synchronously in
406	// their `activate()` (vscode.git, eamodio.gitlens, typescript) see
407	// the real folders.
408	let FoldersWire:Vec<Value> = WorkspaceFoldersGuard
409		.iter()
410		.map(|Folder| {
411			json!({
412				"uri": Folder.URI.to_string(),
413				"name": Folder.GetDisplayName(),
414				"index": Folder.Index,
415			})
416		})
417		.collect();
418
419	// Pair with the Cocoon-side PRE-ACTIVATE snapshot in
420	// ExtensionHostHandler.ts. If Cocoon prints `folders.length=0` while
421	// this log says `folders=1`, we have a wire-shape bug; if both say
422	// 0, ApplicationState was empty at InitData build time and we need
423	// to defer InitData construction past the workspace seeding.
424	dev_log!(
425		"cocoon",
426		"[InitializationData] FoldersWire count={} sample0={}",
427		FoldersWire.len(),
428		FoldersWire.first().map(|F| F.to_string()).unwrap_or_else(|| "<none>".into())
429	);
430
431	let WorkspaceDTO = if WorkspaceFoldersGuard.is_empty() {
432		Value::Null
433	} else {
434		json!({
435
436			"id": ApplicationState.GetWorkspaceIdentifier()?,
437
438			"name": WorkspaceName,
439
440			"folders": FoldersWire,
441
442			"configuration": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().as_ref().map(|p| p.to_string_lossy()),
443
444			"isUntitled": ApplicationState.Workspace.WorkspaceConfigurationPath.lock().unwrap().is_none(),
445
446			"transient": false
447		})
448	};
449
450	let PathResolver = ApplicationHandle.path();
451
452	let AppRoot = PathResolver
453		.resource_dir()
454		.ok()
455		.filter(|P| !P.as_os_str().is_empty() && P.exists())
456		.or_else(|| {
457			// Tauri's `resource_dir()` returns Err (or an empty/missing
458			// path) for raw-binary launches outside the bundle. Probe two
459			// fallback layouts so both `.app` and dev launches resolve:
460			//
461			//   1. `.app/Contents/MacOS/<bin>` → `Contents/Resources/` (shipped bundle,
462			//      raw-binary launch from inside the bundle tree).
463			//   2. `Element/Mountain/Target/<profile>/<bin>` → `Element/Sky/Target/`
464			//      (monorepo dev / raw release).
465			let ExeDir = std::env::current_exe()
466				.ok()
467				.and_then(|P| P.parent().map(|D| D.to_path_buf()))
468				.unwrap_or_default();
469			let BundleResources = ExeDir.join("../Resources");
470			if BundleResources.exists() {
471				return Some(BundleResources.canonicalize().unwrap_or(BundleResources));
472			}
473			let SkyTarget = ExeDir.join("../../../Sky/Target");
474			if SkyTarget.exists() {
475				return Some(SkyTarget.canonicalize().unwrap_or(SkyTarget));
476			}
477			None
478		})
479		.ok_or_else(|| {
480			CommonError::ConfigurationLoad {
481				Description:"Could not resolve AppRoot from resource_dir, ../Resources, or ../../../Sky/Target"
482					.to_string(),
483			}
484		})?;
485
486	let AppData = PathResolver
487		.app_data_dir()
488		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
489
490	let LogsLocation = PathResolver
491		.app_log_dir()
492		.map_err(|Error| CommonError::ConfigurationLoad { Description:Error.to_string() })?;
493
494	let GlobalStorage = AppData.join("User/globalStorage");
495
496	let WorkspaceStorage = AppData.join("User/workspaceStorage");
497
498	Ok(json!({
499
500		// Atom I5: product version + commit + quality come from .env.Land via
501		// process env. `Tauri's package_info().version` reads tauri.conf.json
502		// which still carries a placeholder "0.0.1" - we can't trust it for
503		// extension compat checks. `ProductVersion` from env is the canonical
504		// value shared with Wind and Cocoon.
505		"commit": std::env::var("ProductCommit").unwrap_or_else(|_| "dev".into()),
506
507		"version": std::env::var("ProductVersion").unwrap_or_else(|_| {
508			ApplicationHandle.package_info().version.to_string()
509		}),
510
511		"quality": std::env::var("ProductQuality").unwrap_or_else(|_| "development".into()),
512
513		"parentPid": std::process::id(),
514
515		"environment": {
516
517			"isExtensionDevelopmentDebug": false,
518
519			"appName": "Mountain",
520
521			"appHost": "desktop",
522
523			"appUriScheme": "mountain",
524
525			"appLanguage": "en",
526
527			"isExtensionTelemetryLoggingOnly": true,
528
529			"appRoot": url::Url::from_directory_path(AppRoot.clone()).unwrap(),
530
531			"globalStorageHome": url::Url::from_directory_path(GlobalStorage).unwrap(),
532
533			"workspaceStorageHome": url::Url::from_directory_path(WorkspaceStorage).unwrap(),
534
535			"extensionDevelopmentLocationURI": [],
536
537			"extensionTestsLocationURI": Value::Null,
538
539			"extensionLogLevel": [["info", "Default"]],
540
541		},
542
543		"workspace": WorkspaceDTO,
544
545		"remote": {
546
547			"isRemote": false,
548
549			"authority": Value::Null,
550
551			"connectionData": Value::Null,
552
553		},
554
555		"consoleForward": { "includeStack": true, "logNative": true },
556
557		"logLevel": log::max_level() as i32,
558
559		"logsLocation": url::Url::from_directory_path(LogsLocation).unwrap(),
560
561		"telemetryInfo": {
562
563			"sessionId": Uuid::new_v4().to_string(),
564
565			"machineId": get_or_generate_machine_id(&AppData),
566
567			"firstSessionDate": "2024-01-01T00:00:00.000Z",
568
569			"msftInternal": false
570		},
571
572		"extensions": ExtensionsDTO,
573
574		"autoStart": true,
575
576		// UIKind.Desktop
577		"uiKind": 1,
578	}))
579}