Mountain/IPC/WindServiceHandlers/Extensions/
ExtensionsGetInstalled.rs1
2use std::{
34 sync::{Arc, OnceLock},
35 time::Duration,
36};
37
38use CommonLibrary::ExtensionManagement::ExtensionManagementService::ExtensionManagementService;
39use serde_json::{Value, json};
40
41use crate::{
42 IPC::UriComponents::Normalize::Fn as NormalizeUri,
43 RunTime::ApplicationRunTime::ApplicationRunTime,
44 dev_log,
45};
46
47const EXTENSION_TYPE_SYSTEM:u8 = 0;
48
49const EXTENSION_TYPE_USER:u8 = 1;
50
51const SCAN_WAIT_CAP_MS:u64 = 5000;
52
53static INSTALLED_CACHE:[OnceLock<Value>; 3] = [OnceLock::new(), OnceLock::new(), OnceLock::new()];
58
59fn CacheIndex(TypeFilter:Option<u8>) -> usize {
60 match TypeFilter {
61 None => 0,
62 Some(EXTENSION_TYPE_SYSTEM) => 1,
63 Some(EXTENSION_TYPE_USER) => 2,
64 Some(_) => 0,
65 }
66}
67
68pub async fn Fn(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
69 let TypeFilter:Option<u8> = Arguments.first().and_then(|V| V.as_u64()).map(|N| N as u8);
70
71 let CacheSlot = CacheIndex(TypeFilter);
74 if let Some(Cached) = INSTALLED_CACHE[CacheSlot].get() {
75 let Count = Cached.as_array().map(|A| A.len()).unwrap_or(0);
76 dev_log!(
77 "extensions",
78 "extensions:getInstalled type={:?} returning {} entries (cache hit)",
79 TypeFilter,
80 Count
81 );
82 return Ok(Cached.clone());
83 }
84
85 let mut Extensions = RunTime
86 .Environment
87 .GetExtensions()
88 .await
89 .map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
90
91 if Extensions.is_empty() {
92 let ScanReady = RunTime.Environment.ApplicationState.Extension.ScanReady.clone();
93
94 let Notified = tokio::time::timeout(Duration::from_millis(SCAN_WAIT_CAP_MS), ScanReady.notified()).await;
95
96 Extensions = RunTime
97 .Environment
98 .GetExtensions()
99 .await
100 .map_err(|Error| format!("extensions:getInstalled failed: {}", Error))?;
101
102 match Notified {
103 Ok(()) => {
104 dev_log!(
105 "extensions",
106 "extensions:getInstalled: scan-ready signal received, {} entries available",
107 Extensions.len()
108 );
109 },
110
111 Err(_) => {
112 dev_log!(
113 "extensions",
114 "warn: extensions:getInstalled: scan-ready timed out after {}ms; {} entries available",
115 SCAN_WAIT_CAP_MS,
116 Extensions.len()
117 );
118 },
119 }
120 }
121
122 let Wrapped:Vec<Value> = Extensions
123 .into_iter()
124 .filter_map(|Manifest| {
125 let IsBuiltin = Manifest.get("isBuiltin").and_then(Value::as_bool).unwrap_or(true);
126 let ExtensionType = if IsBuiltin { EXTENSION_TYPE_SYSTEM } else { EXTENSION_TYPE_USER };
127
128 if let Some(Wanted) = TypeFilter
129 && Wanted != ExtensionType
130 {
131 return None;
132 }
133
134 let Publisher = Manifest
135 .get("publisher")
136 .and_then(Value::as_str)
137 .filter(|S| !S.is_empty())
138 .unwrap_or("unknown")
139 .to_string();
140 let Name = Manifest
141 .get("name")
142 .and_then(Value::as_str)
143 .filter(|S| !S.is_empty())
144 .unwrap_or("unknown")
145 .to_string();
146 let Id = format!("{}.{}", Publisher, Name);
147
148 let Location = NormalizeUri(Manifest.get("extensionLocation"));
149
150 let mut Manifest = match Manifest {
151 Value::Object(_) => Manifest,
152 _ => json!({}),
153 };
154 if let Value::Object(ref mut Map) = Manifest {
155 Map.insert("extensionLocation".to_string(), Location.clone());
156 Map.entry("publisher".to_string()).or_insert_with(|| json!(Publisher.clone()));
157 Map.entry("name".to_string()).or_insert_with(|| json!(Name.clone()));
158 Map.entry("version".to_string()).or_insert_with(|| json!("0.0.0"));
159 }
160
161 Some(json!({
162 "type": ExtensionType,
163 "isBuiltin": IsBuiltin,
164 "identifier": { "id": Id },
165 "manifest": Manifest,
166 "location": Location,
167 "targetPlatform": "undefined",
168 "isValid": true,
169 "validations": [],
170 "preRelease": false,
171 "isWorkspaceScoped": false,
172 "isMachineScoped": false,
173 "isApplicationScoped": false,
174 "publisherId": null,
175 "isPreReleaseVersion": false,
176 "hasPreReleaseVersion": false,
177 "private": false,
178 "updated": false,
179 "pinned": false,
180 "forceAutoUpdate": false,
181 "source": if IsBuiltin { "system" } else { "vsix" },
182 "size": 0,
183 }))
184 })
185 .collect();
186
187 dev_log!(
188 "extensions",
189 "extensions:getInstalled type={:?} returning {} ILocalExtension-shaped entries",
190 TypeFilter,
191 Wrapped.len()
192 );
193
194 let Response = json!(Wrapped);
195 if !Wrapped.is_empty() {
198 let _ = INSTALLED_CACHE[CacheSlot].set(Response.clone());
199 }
200 Ok(Response)
201}