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;
52const EXTENSION_TYPE_USER:u8 = 1;
53
54pub async fn ExtensionsGetInstalled(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
55	let TypeFilter:Option<u8> = Arguments.first().and_then(|V| V.as_u64()).map(|N| N as u8);
56
57	let mut Extensions = RunTime
58		.Environment
59		.GetExtensions()
60		.await
61		.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
62
63	if Extensions.is_empty() {
64		const POLL_INTERVAL_MS:u64 = 50;
65		const MAX_WAIT_MS:u64 = 5000;
66		let mut Elapsed:u64 = 0;
67		while Extensions.is_empty() && Elapsed < MAX_WAIT_MS {
68			tokio::time::sleep(std::time::Duration::from_millis(POLL_INTERVAL_MS)).await;
69			Elapsed += POLL_INTERVAL_MS;
70			Extensions = RunTime
71				.Environment
72				.GetExtensions()
73				.await
74				.map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
75		}
76		if !Extensions.is_empty() {
77			dev_log!(
78				"extensions",
79				"extensions:getInstalled awaited scan completion ({}ms) - now has {} entries",
80				Elapsed,
81				Extensions.len()
82			);
83		} else {
84			dev_log!(
85				"extensions",
86				"warn: extensions:getInstalled timed out after {}ms; returning empty list",
87				Elapsed
88			);
89		}
90	}
91
92	let Wrapped:Vec<Value> = Extensions
93		.into_iter()
94		.filter_map(|Manifest| {
95			let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
96			let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
97
98			if let Some(Wanted) = TypeFilter
99				&& Wanted != ExtensionType
100			{
101				return None;
102			}
103
104			let Publisher = Manifest
105				.get("publisher")
106				.and_then(Value::as_str)
107				.filter(|S| !S.is_empty())
108				.unwrap_or("unknown")
109				.to_string();
110			let Name = Manifest
111				.get("name")
112				.and_then(Value::as_str)
113				.filter(|S| !S.is_empty())
114				.unwrap_or("unknown")
115				.to_string();
116			let Id = format!("{}.{}", Publisher, Name);
117
118			let Location = NormalizeUri(Manifest.get("extensionLocation"));
119
120			let mut Manifest = match Manifest {
121				Value::Object(_) => Manifest,
122				_ => json!({}),
123			};
124			if let Value::Object(ref mut Map) = Manifest {
125				Map.insert("extensionLocation".to_string(), Location.clone());
126				Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
127				Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
128				Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
129			}
130
131			Some(json!({
132				"type": ExtensionType,
133				"isBuiltin": IsBuiltin,
134				"identifier": { "id": Id },
135				"manifest": Manifest,
136				"location": Location,
137				"targetPlatform": "undefined",
138				"isValid": true,
139				"validations": [],
140				"preRelease": false,
141				"isWorkspaceScoped": false,
142				"isMachineScoped": false,
143				"isApplicationScoped": false,
144				"publisherId": null,
145				"isPreReleaseVersion": false,
146				"hasPreReleaseVersion": false,
147				"private": false,
148				"updated": false,
149				"pinned": false,
150				"forceAutoUpdate": false,
151				"source": if IsBuiltin { "system" } else { "vsix" },
152				"size": 0,
153			}))
154		})
155		.collect();
156
157	dev_log!(
158		"extensions",
159		"extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
160		TypeFilter,
161		Wrapped.len()
162	);
163
164	Ok(json!(Wrapped))
165}