Skip to main content

Mountain/ApplicationState/Internal/ExtensionScanner/
ScanAndPopulateExtensions.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use CommonLibrary::Error::CommonError::CommonError;
4use serde_json::Value;
5use tauri::AppHandle;
6
7use crate::{
8	ApplicationState::DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO,
9	ExtensionManagement,
10	dev_log,
11};
12
13pub async fn Fn(
14	ApplicationHandle:AppHandle,
15
16	_State:&crate::ApplicationState::State::ExtensionState::State::State,
17) -> Result<(), CommonError> {
18	dev_log!("extensions", "[ExtensionScanner] Starting extension scan...");
19
20	// --- Fast path: pre-baked manifest cache (B7.P08) ---
21	// `Maintain/Build/Manifest/PreBake.ts` writes a single JSON blob to
22	// `Target/debug/extensions.manifest.json` as part of the debug build.
23	// Loading it avoids the N×disk-read scan and cuts ~1200 ms from boot.
24	if let Ok(ExecutablePath) = std::env::current_exe() {
25		if let Some(BinaryDir) = ExecutablePath.parent() {
26			match super::LoadFromCache::Fn(&BinaryDir.to_path_buf()).await {
27				Ok(Some(CachedMap)) => {
28					let CachedLen = CachedMap.len();
29
30					let PostWriteCount = {
31						let mut Guard = _State
32							.ScannedExtensions
33							.ScannedExtensions
34							.lock()
35							.map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
36
37						*Guard = CachedMap;
38
39						Guard.len()
40					};
41
42					dev_log!(
43						"extensions",
44						"[ExtensionScanner] Cache hit: {} extensions loaded in <50ms (live scan skipped). State has \
45						 {} entries.",
46						CachedLen,
47						PostWriteCount
48					);
49
50					// Unblock any callers waiting for the first scan result.
51					_State.ScanReady.notify_waiters();
52
53					return Ok(());
54				},
55
56				Ok(None) => {
57					dev_log!("extensions", "[ExtensionScanner] Cache miss - falling back to live disk scan");
58				},
59
60				Err(E) => {
61					dev_log!(
62						"extensions",
63						"warn: [ExtensionScanner] Cache load error: {}; continuing with live scan",
64						E
65					);
66				},
67			}
68		}
69	}
70
71	let ScanPaths:Vec<PathBuf> = _State.Registry.GetExtensionScanPaths();
72
73	dev_log!(
74		"extensions",
75		"[ExtensionScanner] Scanning {} paths in parallel",
76		ScanPaths.len()
77	);
78
79	// Scan all paths concurrently; each spawns its own tokio task so slow
80	// directories (e.g. a network-mounted extensions folder) don't stall the
81	// others.
82	let Futures:Vec<_> = ScanPaths
83		.into_iter()
84		.map(|Path| {
85			let Handle = ApplicationHandle.clone();
86
87			async move {
88				let Display = Path.display().to_string();
89
90				match ExtensionManagement::Scanner::ScanDirectoryForExtensions(Handle, Path).await {
91					Ok(Found) => {
92						dev_log!(
93							"extensions",
94							"[ExtensionScanner] Path '{}' → {} extensions",
95							Display,
96							Found.len()
97						);
98
99						(Display, Ok(Found))
100					},
101
102					Err(E) => {
103						dev_log!("extensions", "warn: [ExtensionScanner] Path '{}' failed: {}", Display, E);
104
105						(Display, Err(E))
106					},
107				}
108			}
109		})
110		.collect();
111
112	let Results = futures::future::join_all(Futures).await;
113
114	let mut All:HashMap<String, ExtensionDescriptionStateDTO> = HashMap::new();
115
116	let mut SuccessfulScans = 0usize;
117
118	let mut FailedScans = 0usize;
119
120	for (_Path, Result) in Results {
121		match Result {
122			Ok(Found) => {
123				SuccessfulScans += 1;
124
125				for Extension in Found {
126					let Identifier = Extension
127						.Identifier
128						.get("value")
129						.and_then(Value::as_str)
130						.unwrap_or_default()
131						.to_string();
132
133					if !Identifier.is_empty() {
134						All.insert(Identifier, Extension);
135					}
136				}
137			},
138
139			Err(_) => {
140				FailedScans += 1;
141			},
142		}
143	}
144
145	// Single-swap replace: build the full map first (above), then take the
146	// lock once for a pointer-swap so concurrent GetExtensions calls are not
147	// blocked during the N-insert loop (which was previously holding the lock
148	// for 100-500 ms on a cold filesystem with 94+ extensions).
149	let AllLen = All.len();
150
151	let PostWriteCount = {
152		let mut Guard = _State
153			.ScannedExtensions
154			.ScannedExtensions
155			.lock()
156			.map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
157
158		*Guard = All; // move - no clone needed
159
160		Guard.len()
161	};
162
163	dev_log!(
164		"extensions",
165		"[ExtensionScanner] Complete: {} extensions ({} paths ok, {} failed). State has {} entries.",
166		AllLen,
167		SuccessfulScans,
168		FailedScans,
169		PostWriteCount
170	);
171
172	// Unblock any callers waiting for the first scan result.
173	_State.ScanReady.notify_waiters();
174
175	Ok(())
176}
177
178/// Robust extension scanning - clears state first, retries once on failure.
179pub(crate) async fn ScanExtensionsWithRecovery(
180	ApplicationHandle:AppHandle,
181
182	State:&crate::ApplicationState::State::ExtensionState::State::State,
183) -> Result<(), CommonError> {
184	dev_log!("extensions", "[ExtensionScanner] Starting robust extension scan...");
185
186	match Fn(ApplicationHandle.clone(), State).await {
187		Ok(()) => {
188			dev_log!("extensions", "[ExtensionScanner] Robust scan completed successfully");
189
190			Ok(())
191		},
192
193		Err(Error) => {
194			dev_log!("extensions", "error: [ExtensionScanner] Scan failed: {}; retrying once", Error);
195
196			Fn(ApplicationHandle, State).await
197		},
198	}
199}