Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_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					// Supplementary live-scan of user-writable paths only.
51					// PreBake at build time walks the bundled extension trees
52					// (Mountain/Target/Resources/extensions,
53					// Sky/Target/Static/Application/extensions, VS Code
54					// Dependency/.../extensions) - it does NOT walk
55					// `~/.fiddee/extensions` or `~/.land/extensions`.
56					// Without this supplementary scan, the cache hit hides
57					// every VSIX-installed extension from the workbench:
58					// `extensions:scanUserExtensions` returns 0 and the
59					// `@installed` view stays empty.
60					//
61					// We re-use `Registry.GetExtensionScanPaths()` and filter
62					// through `Scanner::IsUserExtensionScanPath` so the
63					// classifier and the scan set stay coherent (Lodge
64					// override, ~/.fiddee/extensions, ~/.land/extensions).
65					// Found entries overwrite cache entries with the same ID
66					// - matches stock VS Code semantics where an installed
67					// VSIX shadows a built-in of the same identifier.
68					let UserScanPaths:Vec<PathBuf> = _State
69						.Registry
70						.GetExtensionScanPaths()
71						.into_iter()
72						.filter(|P| ExtensionManagement::Scanner::IsUserExtensionScanPath(P))
73						.collect();
74
75					if !UserScanPaths.is_empty() {
76						dev_log!(
77							"extensions",
78							"[ExtensionScanner] Cache hit supplement: live-scanning {} user-writable path(s)",
79							UserScanPaths.len()
80						);
81
82						let UserFutures:Vec<_> = UserScanPaths
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] User path '{}' → {} extensions (supplement)",
95												Display,
96												Found.len()
97											);
98
99											Found
100										},
101
102										Err(E) => {
103											dev_log!(
104												"extensions",
105												"warn: [ExtensionScanner] User path '{}' failed (supplement): {}",
106												Display,
107												E
108											);
109
110											Vec::new()
111										},
112									}
113								}
114							})
115							.collect();
116
117						let UserResults = futures::future::join_all(UserFutures).await;
118
119						let mut UserMerged = 0usize;
120
121						{
122							let mut Guard = _State
123								.ScannedExtensions
124								.ScannedExtensions
125								.lock()
126								.map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
127
128							for Found in UserResults {
129								for Extension in Found {
130									let Identifier = Extension
131										.Identifier
132										.get("value")
133										.and_then(Value::as_str)
134										.unwrap_or_default()
135										.to_string();
136
137									if !Identifier.is_empty() {
138										Guard.insert(Identifier, Extension);
139
140										UserMerged += 1;
141									}
142								}
143							}
144						}
145
146						dev_log!(
147							"extensions",
148							"[ExtensionScanner] Cache hit supplement: merged {} user extension(s) into state",
149							UserMerged
150						);
151					}
152
153					// Unblock any callers waiting for the first scan result.
154					_State.ScanReady.notify_waiters();
155
156					return Ok(());
157				},
158
159				Ok(None) => {
160					dev_log!("extensions", "[ExtensionScanner] Cache miss - falling back to live disk scan");
161				},
162
163				Err(E) => {
164					dev_log!(
165						"extensions",
166						"warn: [ExtensionScanner] Cache load error: {}; continuing with live scan",
167						E
168					);
169				},
170			}
171		}
172	}
173
174	let ScanPaths:Vec<PathBuf> = _State.Registry.GetExtensionScanPaths();
175
176	dev_log!(
177		"extensions",
178		"[ExtensionScanner] Scanning {} paths in parallel",
179		ScanPaths.len()
180	);
181
182	// Scan all paths concurrently; each spawns its own tokio task so slow
183	// directories (e.g. a network-mounted extensions folder) don't stall the
184	// others.
185	let Futures:Vec<_> = ScanPaths
186		.into_iter()
187		.map(|Path| {
188			let Handle = ApplicationHandle.clone();
189
190			async move {
191				let Display = Path.display().to_string();
192
193				match ExtensionManagement::Scanner::ScanDirectoryForExtensions(Handle, Path).await {
194					Ok(Found) => {
195						dev_log!(
196							"extensions",
197							"[ExtensionScanner] Path '{}' → {} extensions",
198							Display,
199							Found.len()
200						);
201
202						(Display, Ok(Found))
203					},
204
205					Err(E) => {
206						dev_log!("extensions", "warn: [ExtensionScanner] Path '{}' failed: {}", Display, E);
207
208						(Display, Err(E))
209					},
210				}
211			}
212		})
213		.collect();
214
215	let Results = futures::future::join_all(Futures).await;
216
217	let mut All:HashMap<String, ExtensionDescriptionStateDTO> = HashMap::new();
218
219	let mut SuccessfulScans = 0usize;
220
221	let mut FailedScans = 0usize;
222
223	for (_Path, Result) in Results {
224		match Result {
225			Ok(Found) => {
226				SuccessfulScans += 1;
227
228				for Extension in Found {
229					let Identifier = Extension
230						.Identifier
231						.get("value")
232						.and_then(Value::as_str)
233						.unwrap_or_default()
234						.to_string();
235
236					if !Identifier.is_empty() {
237						All.insert(Identifier, Extension);
238					}
239				}
240			},
241
242			Err(_) => {
243				FailedScans += 1;
244			},
245		}
246	}
247
248	// Single-swap replace: build the full map first (above), then take the
249	// lock once for a pointer-swap so concurrent GetExtensions calls are not
250	// blocked during the N-insert loop (which was previously holding the lock
251	// for 100-500 ms on a cold filesystem with 94+ extensions).
252	let AllLen = All.len();
253
254	let PostWriteCount = {
255		let mut Guard = _State
256			.ScannedExtensions
257			.ScannedExtensions
258			.lock()
259			.map_err(|Error| CommonError::StateLockPoisoned { Context:Error.to_string() })?;
260
261		*Guard = All; // move - no clone needed
262
263		Guard.len()
264	};
265
266	dev_log!(
267		"extensions",
268		"[ExtensionScanner] Complete: {} extensions ({} paths ok, {} failed). State has {} entries.",
269		AllLen,
270		SuccessfulScans,
271		FailedScans,
272		PostWriteCount
273	);
274
275	// Unblock any callers waiting for the first scan result.
276	_State.ScanReady.notify_waiters();
277
278	Ok(())
279}
280
281/// Robust extension scanning - clears state first, retries once on failure.
282pub(crate) async fn ScanExtensionsWithRecovery(
283	ApplicationHandle:AppHandle,
284
285	State:&crate::ApplicationState::State::ExtensionState::State::State,
286) -> Result<(), CommonError> {
287	dev_log!("extensions", "[ExtensionScanner] Starting robust extension scan...");
288
289	match Fn(ApplicationHandle.clone(), State).await {
290		Ok(()) => {
291			dev_log!("extensions", "[ExtensionScanner] Robust scan completed successfully");
292
293			Ok(())
294		},
295
296		Err(Error) => {
297			dev_log!("extensions", "error: [ExtensionScanner] Scan failed: {}; retrying once", Error);
298
299			Fn(ApplicationHandle, State).await
300		},
301	}
302}