Mountain/IPC/WindServiceHandlers/Extensions/
ExtensionsGetInstalled.rs1use 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
52static 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 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 let ScanReady = RunTime.Environment.ApplicationState.Extension.ScanReady.clone();
95
96 let NotifyFuture = ScanReady.notified();
97
98 let mut Extensions = RunTime
99 .Environment
100 .GetExtensions()
101 .await
102 .map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
103
104 if Extensions.is_empty() {
105 let Notified = tokio::time::timeout(Duration::from_millis(SCAN_WAIT_CAP_MS), NotifyFuture).await;
106
107 Extensions = RunTime
108 .Environment
109 .GetExtensions()
110 .await
111 .map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
112
113 match Notified {
114 Ok(()) => {
115 dev_log!(
116 "extensions",
117 "extensions:getInstalled: scan-ready signal received, {} entries available",
118 Extensions.len()
119 );
120 },
121
122 Err(_) => {
123 dev_log!(
124 "extensions",
125 "warn: extensions:getInstalled: scan-ready timed out after {}ms; {} entries available",
126 SCAN_WAIT_CAP_MS,
127 Extensions.len()
128 );
129 },
130 }
131 }
132
133 let Wrapped:Vec<Value> = Extensions
134 .into_iter()
135 .filter_map(|Manifest| {
136 let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
137
138 let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
139
140 if let Some(Wanted) = TypeFilter
141 && Wanted != ExtensionType
142 {
143 return None;
144 }
145
146 let Publisher = Manifest
147 .get("publisher")
148 .and_then(Value::as_str)
149 .filter(|S| !S.is_empty())
150 .unwrap_or("unknown")
151 .to_string();
152
153 let Name = Manifest
154 .get("name")
155 .and_then(Value::as_str)
156 .filter(|S| !S.is_empty())
157 .unwrap_or("unknown")
158 .to_string();
159
160 let Id = format!("{}.{}", Publisher, Name);
161
162 let Location = NormalizeUri(Manifest.get("extensionLocation"));
163
164 let mut Manifest = match Manifest {
165 Value::Object(_) => Manifest,
166 _ => json!({}),
167 };
168
169 if let Value::Object(ref mut Map) = Manifest {
170 Map.insert("extensionLocation".to_string(), Location.clone());
171
172 Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
173
174 Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
175
176 Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
177 }
178
179 Some(json!({
180 "type": ExtensionType,
181 "isBuiltin": IsBuiltin,
182 "identifier": { "id": Id },
183 "manifest": Manifest,
184 "location": Location,
185 "targetPlatform": "undefined",
186 "isValid": true,
187 "validations": [],
188 "preRelease": false,
189 "isWorkspaceScoped": false,
190 "isMachineScoped": false,
191 "isApplicationScoped": false,
192 "publisherId": null,
193 "isPreReleaseVersion": false,
194 "hasPreReleaseVersion": false,
195 "private": false,
196 "updated": false,
197 "pinned": false,
198 "forceAutoUpdate": false,
199 "source": if IsBuiltin { "system" } else { "vsix" },
200 "size": 0,
201 }))
202 })
203 .collect();
204
205 dev_log!(
206 "extensions",
207 "extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
208 TypeFilter,
209 Wrapped.len()
210 );
211
212 let Response = json!(Wrapped);
213
214 if !Wrapped.is_empty() {
217 let _ = INSTALLED_CACHE[CacheSlot].set(Response.clone());
218 }
219
220 Ok(Response)
221}