Skip to main content

Mountain/IPC/WindServiceHandlers/Extensions/
ExtensionsGetInstalled.rs

1#![allow(non_snake_case)]
2
3//! `extensions:getInstalled(type?)` - return scanned extensions
4//! reshaped as VS Code's `ILocalExtension[]` so
5//! `ExtensionManagementChannelClient.getInstalled` can
6//! destructure `extension.identifier.id`,
7//! `extension.manifest.*`, and `extension.location` without
8//! blowing up.
9//!
10//! ## Argument contract
11//!
12//! `Arguments[0]` is the optional `ExtensionType` filter VS
13//! Code passes:
14//!
15//! - `0` (System) → only built-ins.
16//! - `1` (User) → only VSIX-installed.
17//! - `null` / missing → every known extension.
18//!
19//! Without the filter the trusted-publishers boot migration
20//! iterates User-typed extensions over System manifests and
21//! crashes on `manifest.publisher.toLowerCase()`.
22//!
23//! ## Boot-time race
24//!
25//! The workbench fires `getInstalled` ~13 times within the
26//! first second. `ExtensionPopulate` runs in parallel and only
27//! writes to ScannedExtensions ~250-500 ms in. If we returned
28//! `[]` early, the workbench cached it forever and the activity
29//! bar lost every extension-contributed icon. We poll for ≤5 s
30//! before returning empty.
31//!
32//! ## Manifest skeleton
33//!
34//! VS Code unconditionally calls
35//! `manifest.publisher.toLowerCase()`. A `null` or non-object
36//! manifest crashes the webview before its first paint. We
37//! coerce to `{}` and inject `publisher`/`name`/`version`
38//! defaults so the renderer always has shape.
39
40use std::sync::Arc;
41
42use CommonLibrary::ExtensionManagement::ExtensionManagementService::ExtensionManagementService;
43use serde_json::{Value, json};
44
45use crate::{
46	IPC::UriComponents::Normalize::Fn as NormalizeUri,
47	RunTime::ApplicationRunTime::ApplicationRunTime,
48	dev_log,
49};
50
51const EXTENSION_TYPE_SYSTEM:u8 = 0;
52
53const EXTENSION_TYPE_USER:u8 = 1;
54
55pub async fn ExtensionsGetInstalled(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
56	let TypeFilter:Option<u8> = Arguments.first().and_then(|V| V.as_u64()).map(|N| N as u8);
57
58	let mut Extensions = RunTime
59		.Environment
60		.GetExtensions()
61		.await
62		.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
63
64	if Extensions.is_empty() {
65		const POLL_INTERVAL_MS:u64 = 50;
66
67		const MAX_WAIT_MS:u64 = 5000;
68
69		let mut Elapsed:u64 = 0;
70
71		while Extensions.is_empty() && Elapsed < MAX_WAIT_MS {
72			tokio::time::sleep(std::time::Duration::from_millis(POLL_INTERVAL_MS)).await;
73
74			Elapsed += POLL_INTERVAL_MS;
75
76			Extensions = RunTime
77				.Environment
78				.GetExtensions()
79				.await
80				.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
81		}
82
83		if !Extensions.is_empty() {
84			dev_log!(
85				"extensions",
86				"extensions:getInstalled awaited scan completion ({}ms) - now has {} entries",
87				Elapsed,
88				Extensions.len()
89			);
90		} else {
91			dev_log!(
92				"extensions",
93				"warn: extensions:getInstalled timed out after {}ms; returning empty list",
94				Elapsed
95			);
96		}
97	}
98
99	let Wrapped:Vec<Value> = Extensions
100		.into_iter()
101		.filter_map(|Manifest| {
102			let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
103			let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
104
105			if let Some(Wanted) = TypeFilter
106				&& Wanted != ExtensionType
107			{
108				return None;
109			}
110
111			let Publisher = Manifest
112				.get("publisher")
113				.and_then(Value::as_str)
114				.filter(|S| !S.is_empty())
115				.unwrap_or("unknown")
116				.to_string();
117			let Name = Manifest
118				.get("name")
119				.and_then(Value::as_str)
120				.filter(|S| !S.is_empty())
121				.unwrap_or("unknown")
122				.to_string();
123			let Id = format!("{}.{}", Publisher, Name);
124
125			let Location = NormalizeUri(Manifest.get("extensionLocation"));
126
127			let mut Manifest = match Manifest {
128				Value::Object(_) => Manifest,
129				_ => json!({}),
130			};
131			if let Value::Object(ref mut Map) = Manifest {
132				Map.insert("extensionLocation".to_string(), Location.clone());
133				Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
134				Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
135				Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
136			}
137
138			Some(json!({
139				"type": ExtensionType,
140				"isBuiltin": IsBuiltin,
141				"identifier": { "id": Id },
142				"manifest": Manifest,
143				"location": Location,
144				"targetPlatform": "undefined",
145				"isValid": true,
146				"validations": [],
147				"preRelease": false,
148				"isWorkspaceScoped": false,
149				"isMachineScoped": false,
150				"isApplicationScoped": false,
151				"publisherId": null,
152				"isPreReleaseVersion": false,
153				"hasPreReleaseVersion": false,
154				"private": false,
155				"updated": false,
156				"pinned": false,
157				"forceAutoUpdate": false,
158				"source": if IsBuiltin { "system" } else { "vsix" },
159				"size": 0,
160			}))
161		})
162		.collect();
163
164	dev_log!(
165		"extensions",
166		"extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
167		TypeFilter,
168		Wrapped.len()
169	);
170
171	Ok(json!(Wrapped))
172}