Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/IPC/WindServiceHandlers/Extensions/
ExtensionsGetInstalled.rs

1//! `extensions:getInstalled(type?)` - return scanned extensions reshaped as
2//! VS Code's `ILocalExtension[]` so `ExtensionManagementChannelClient
3//! .getInstalled` can destructure `extension.identifier.id`,
4//! `extension.manifest.*`, and `extension.location` without blowing up.
5//!
6//! ## Argument contract
7//!
8//! `Arguments[0]` is the optional `ExtensionType` filter VS Code passes:
9//! - `0` (System) → only built-ins.
10//! - `1` (User) → only VSIX-installed.
11//! - `null` / missing → every known extension.
12//!
13//! Without the filter the trusted-publishers boot migration iterates
14//! User-typed extensions over System manifests and crashes on
15//! `manifest.publisher.toLowerCase()`.
16//!
17//! ## Boot-time race
18//!
19//! The workbench fires `getInstalled` ~13 times within the first second.
20//! `ExtensionPopulate` runs in parallel and writes to `ScannedExtensions`
21//! 250-500 ms in. We await `ExtensionState.ScanReady` (a `tokio::sync::Notify`
22//! fired once the scan commits its results) with a 5 s hard cap, then return
23//! whatever is available. No 50 ms polling loop - we wake exactly when data
24//! arrives.
25//!
26//! ## Manifest skeleton
27//!
28//! VS Code unconditionally calls `manifest.publisher.toLowerCase()`. A `null`
29//! or non-object manifest crashes the webview before its first paint. We
30//! coerce to `{}` and inject `publisher`/`name`/`version` defaults.
31
32use std::{
33	sync::{Arc, OnceLock},
34	time::Duration,
35};
36
37use CommonLibrary::ExtensionManagement::ExtensionManagementService::ExtensionManagementService;
38use serde_json::{Value, json};
39
40use crate::{
41	IPC::UriComponents::Normalize::Fn as NormalizeUri,
42	RunTime::ApplicationRunTime::ApplicationRunTime,
43	dev_log,
44};
45
46const EXTENSION_TYPE_SYSTEM:u8 = 0;
47
48const EXTENSION_TYPE_USER:u8 = 1;
49
50const SCAN_WAIT_CAP_MS:u64 = 5000;
51
52// Per-type cached responses. Extensions don't change during a session so
53// building the ILocalExtension[] once per type and returning the cached Value
54// on subsequent calls avoids re-serializing ~1.8 MB on every getInstalled call.
55// Keyed by TypeFilter: index 0=None(all), 1=System(0), 2=User(1).
56static INSTALLED_CACHE:[OnceLock<Value>; 3] = [OnceLock::new(), OnceLock::new(), OnceLock::new()];
57
58fn CacheIndex(TypeFilter:Option<u8>) -> usize {
59	match TypeFilter {
60		None => 0,
61
62		Some(EXTENSION_TYPE_SYSTEM) => 1,
63
64		Some(EXTENSION_TYPE_USER) => 2,
65
66		Some(_) => 0,
67	}
68}
69
70pub async fn Fn(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
71	let TypeFilter:Option<u8> = Arguments.first().and_then(|V| V.as_u64()).map(|N| N as u8);
72
73	// Fast path: return cached response if available (built on first call per
74	// type).
75	let CacheSlot = CacheIndex(TypeFilter);
76
77	if let Some(Cached) = INSTALLED_CACHE[CacheSlot].get() {
78		let Count = Cached.as_array().map(|A| A.len()).unwrap_or(0);
79
80		dev_log!(
81			"extensions",
82			"extensions:getInstalled type={:?} returning {} entries (cache hit)",
83			TypeFilter,
84			Count
85		);
86
87		return Ok(Cached.clone());
88	}
89
90	// Subscribe to the scan-ready notify BEFORE calling GetExtensions() to
91	// close the TOCTOU window: if the scan completes between GetExtensions()
92	// returning empty and notified() being registered, the signal would be
93	// lost (Notify does not latch) and we'd wait the full 5 s timeout.
94	let ScanReady = RunTime.Environment.ApplicationState.Extension.ScanReady.clone();
95	let NotifyFuture = ScanReady.notified();
96
97	let mut Extensions = RunTime
98		.Environment
99		.GetExtensions()
100		.await
101		.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
102
103	if Extensions.is_empty() {
104		let Notified = tokio::time::timeout(Duration::from_millis(SCAN_WAIT_CAP_MS), NotifyFuture).await;
105
106		Extensions = RunTime
107			.Environment
108			.GetExtensions()
109			.await
110			.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
111
112		match Notified {
113			Ok(()) => {
114				dev_log!(
115					"extensions",
116					"extensions:getInstalled: scan-ready signal received, {} entries available",
117					Extensions.len()
118				);
119			},
120
121			Err(_) => {
122				dev_log!(
123					"extensions",
124					"warn: extensions:getInstalled: scan-ready timed out after {}ms; {} entries available",
125					SCAN_WAIT_CAP_MS,
126					Extensions.len()
127				);
128			},
129		}
130	}
131
132	let Wrapped:Vec<Value> = Extensions
133		.into_iter()
134		.filter_map(|Manifest| {
135			let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
136			let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
137
138			if let Some(Wanted) = TypeFilter
139				&& Wanted != ExtensionType
140			{
141				return None;
142			}
143
144			let Publisher = Manifest
145				.get("publisher")
146				.and_then(Value::as_str)
147				.filter(|S| !S.is_empty())
148				.unwrap_or("unknown")
149				.to_string();
150			let Name = Manifest
151				.get("name")
152				.and_then(Value::as_str)
153				.filter(|S| !S.is_empty())
154				.unwrap_or("unknown")
155				.to_string();
156			let Id = format!("{}.{}", Publisher, Name);
157
158			let Location = NormalizeUri(Manifest.get("extensionLocation"));
159
160			let mut Manifest = match Manifest {
161				Value::Object(_) => Manifest,
162				_ => json!({}),
163			};
164			if let Value::Object(ref mut Map) = Manifest {
165				Map.insert("extensionLocation".to_string(), Location.clone());
166				Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
167				Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
168				Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
169			}
170
171			Some(json!({
172				"type": ExtensionType,
173				"isBuiltin": IsBuiltin,
174				"identifier": { "id": Id },
175				"manifest": Manifest,
176				"location": Location,
177				"targetPlatform": "undefined",
178				"isValid": true,
179				"validations": [],
180				"preRelease": false,
181				"isWorkspaceScoped": false,
182				"isMachineScoped": false,
183				"isApplicationScoped": false,
184				"publisherId": null,
185				"isPreReleaseVersion": false,
186				"hasPreReleaseVersion": false,
187				"private": false,
188				"updated": false,
189				"pinned": false,
190				"forceAutoUpdate": false,
191				"source": if IsBuiltin { "system" } else { "vsix" },
192				"size": 0,
193			}))
194		})
195		.collect();
196
197	dev_log!(
198		"extensions",
199		"extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
200		TypeFilter,
201		Wrapped.len()
202	);
203
204	let Response = json!(Wrapped);
205
206	// Only cache non-empty responses - an empty response on first call (timeout)
207	// shouldn't poison the cache for subsequent calls that would get real data.
208	if !Wrapped.is_empty() {
209		let _ = INSTALLED_CACHE[CacheSlot].set(Response.clone());
210	}
211
212	Ok(Response)
213}