CH-J Toolsserverbeheer over SSH
Menu
Gepubliceerde bron

CH-J Tools

Blader door mappen en bestanden voor een specifieke toepassing release.

Download bron ZIP
CH-J / Server Manager 1704 bestanden
0.1.2-beta Toegevoegd: · Gebouwd:
service.rs Toegevoegd: Datum niet geregistreerd 29,3 KB · 633 regels
Rubriek Bestand downloaden
1//! Module service and window runtime (port of `plugins/pluginService.js` and the window
2//! part of `plugins/pluginRuntime.js`): catalog state, install/uninstall, isolated module
3//! windows (one per module, child of the main window), collapse to the Core taskbar.
5use crate::core::app::{Core, MODULE_API_VERSION};
6use crate::core::error::{CoreError, CoreResult};
7use crate::core::modules::catalog::CatalogProvider;
8use crate::core::modules::installer::ModuleInstaller;
9use crate::core::modules::registry::ModuleRegistry;
10use crate::core::version::compare_versions;
11use crate::security::caller::{ModuleContext, MAIN_LABEL};
12use parking_lot::Mutex;
13use serde_json::{json, Value};
14use std::cmp::Ordering;
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
17use std::sync::Arc;
18use tauri::Manager;
20pub const MODULE_CSP: &str = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; worker-src 'self'; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'";
22/// Windows that hold a detached module. The WebView inside keeps its `module-N` label.
23pub const DETACHED_LABEL_PREFIX: &str = "detached-";
25#[derive(Clone)]
26struct WindowState {
27 label: String,
28 name: String,
29 version: String,
30 minimized: bool,
31 /// Label of the separate window; `None` while the module is embedded in the main window.
32 detached: Option<String>,
35/// Module area of the main page in logical pixels.
36#[derive(Clone, Copy)]
37pub struct Area {
38 pub x: f64,
39 pub y: f64,
40 pub width: f64,
41 pub height: f64,
44pub struct ModuleService {
45 pub registry: Arc<ModuleRegistry>,
46 installer: ModuleInstaller,
47 catalog: Mutex<Vec<Value>>,
48 source_base_url: Mutex<Option<String>>,
49 operation: Mutex<Option<&'static str>>,
50 windows: Mutex<HashMap<String, WindowState>>,
51 counter: AtomicU64,
52 close_gate: Mutex<bool>,
53 close_guard_installed: Mutex<bool>,
54 close_queries: Mutex<HashMap<String, (u64, tokio::sync::oneshot::Sender<bool>)>>,
55 exit_approved: std::sync::atomic::AtomicBool,
56 /// The embedded module shown in the module area and that area; no area means another view is open.
57 active: Mutex<Option<String>>,
58 area: Mutex<Option<Area>>,
61struct OperationGuard<'a>(&'a Mutex<Option<&'static str>>);
62impl Drop for OperationGuard<'_> {
63 fn drop(&mut self) {
64 *self.0.lock() = None;
65 }
68async fn confirm_windows<F, Fut>(labels: &[String], mut confirm: F) -> bool
69where F: FnMut(String) -> Fut, Fut: std::future::Future<Output = bool> {
70 for label in labels {
71 if !confirm(label.clone()).await { return false; }
72 }
73 true
76impl ModuleService {
77 pub fn new(registry: Arc<ModuleRegistry>, installer: ModuleInstaller) -> Self {
78 Self {
79 registry,
80 installer,
81 catalog: Mutex::new(Vec::new()),
82 source_base_url: Mutex::new(None),
83 operation: Mutex::new(None),
84 windows: Mutex::new(HashMap::new()),
85 counter: AtomicU64::new(0),
86 close_gate: Mutex::new(false),
87 close_guard_installed: Mutex::new(false),
88 close_queries: Mutex::new(HashMap::new()),
89 exit_approved: Default::default(),
90 active: Mutex::new(None),
91 area: Mutex::new(None),
92 }
93 }
95 fn begin(&self, name: &'static str) -> CoreResult<OperationGuard<'_>> {
96 let mut op = self.operation.lock();
97 if op.is_some() {
98 return Err(CoreError::plain("Another plugin operation is already running."));
99 }
100 *op = Some(name);
101 Ok(OperationGuard(&self.operation))
102 }
104 /// Install once the main bridge is ready, before any module can hold a draft.
105 pub fn install_close_guard(&self, core: &Core) -> CoreResult<()> {
106 let mut installed = self.close_guard_installed.lock();
107 if *installed { return Ok(()); }
108 let plugin = tauri::plugin::Builder::<tauri::Wry>::new("draft-close-guard")
109 .on_event(|app, event| {
110 let Some(core) = app.state::<crate::core::app::CoreSlot>().try_get() else { return };
111 match event {
112 tauri::RunEvent::WindowEvent { label, event: tauri::WindowEvent::CloseRequested { api, .. }, .. }
113 if label == MAIN_LABEL || core.services.modules.module_of_window(label).is_some() => {
114 api.prevent_close();
115 let target = if label == MAIN_LABEL { label.clone() } else { core.services.modules.module_of_window(label).unwrap_or_else(|| label.clone()) };
116 ModuleService::request_close(core, Some(target), None);
117 }
118 tauri::RunEvent::ExitRequested { api, code, .. } if !core.services.modules.exit_approved() => {
119 api.prevent_exit();
120 ModuleService::request_close(core, None, *code);
121 }
122 _ => {}
123 }
124 }).build();
125 core.app.plugin(plugin).map_err(|error| CoreError::plain(format!("Close guard could not be installed: {error}")))?;
126 *installed = true;
127 Ok(())
128 }
130 pub fn exit_approved(&self) -> bool {
131 self.exit_approved.load(AtomicOrdering::SeqCst)
132 }
134 /// Replies are scoped to the calling WebView and a fresh query ID.
135 pub fn close_query_reply(&self, label: &str, id: u64, dirty: bool) {
136 let mut queries = self.close_queries.lock();
137 if queries.get(label).is_some_and(|(expected, _)| *expected == id) {
138 if let Some((_, reply)) = queries.remove(label) { let _ = reply.send(dirty); }
139 }
140 }
142 async fn confirm_close(&self, core: &Core, label: &str) -> bool {
143 use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
144 let Some(webview) = core.app.get_webview(label) else { return true };
145 let window = webview.window();
146 let id = self.counter.fetch_add(1, AtomicOrdering::SeqCst) + 1;
147 let (tx, rx) = tokio::sync::oneshot::channel();
148 self.close_queries.lock().insert(label.to_string(), (id, tx));
149 let script = format!("window.dispatchEvent(new CustomEvent('chj:query-dirty', {{ detail: {id} }}))");
150 if webview.eval(&script).is_err() {
151 self.close_queries.lock().remove(label);
152 return false;
153 }
154 let dirty = tokio::time::timeout(std::time::Duration::from_secs(5), rx).await;
155 self.close_queries.lock().remove(label);
156 match dirty {
157 Ok(Ok(false)) => true,
158 Ok(Ok(true)) => {
159 let (tx, rx) = tokio::sync::oneshot::channel();
160 core.app.dialog().message(core.t("chrome.discardChanges")).title(window.title().unwrap_or_default())
161 .parent(&window).kind(MessageDialogKind::Warning)
162 .buttons(MessageDialogButtons::OkCancelCustom(core.t("chrome.discard"), core.t("chrome.cancel")))
163 .show(move |discard| { let _ = tx.send(discard); });
164 rx.await.unwrap_or(false)
165 }
166 _ => false,
167 }
168 }
170 /// Called by native CloseRequested and ExitRequested before any window is destroyed.
171 pub fn request_close(core: Arc<Core>, label: Option<String>, exit_code: Option<i32>) {
172 {
173 let mut active = core.services.modules.close_gate.lock();
174 if *active { return; }
175 *active = true;
176 }
177 tauri::async_runtime::spawn(async move {
178 let modules = &core.services.modules;
179 let all = label.as_deref().is_none_or(|l| l == MAIN_LABEL);
180 let mut labels: Vec<String> = if all {
181 core.windows.modules().iter().map(|ctx| ctx.label.clone()).collect()
182 } else { vec![label.clone().unwrap()] };
183 // Confirm every draft before destroying anything, so Cancel preserves the whole app.
184 if all && core.app.get_window(MAIN_LABEL).is_some() { labels.push(MAIN_LABEL.to_string()); }
185 if !confirm_windows(&labels, |target| { let core = &core; async move { modules.confirm_close(core, &target).await } }).await {
186 *modules.close_gate.lock() = false;
187 return;
188 }
189 if label.is_none() || (all && !cfg!(target_os = "macos")) {
190 modules.exit_approved.store(true, AtomicOrdering::SeqCst);
191 }
192 for target in labels {
193 if target == MAIN_LABEL {
194 if let Some(window) = core.app.get_window(&target) { let _ = window.destroy(); }
195 } else {
196 modules.destroy_module(&core, &target);
197 }
198 }
199 *modules.close_gate.lock() = false;
200 if label.is_none() { core.app.exit(exit_code.unwrap_or(0)); }
201 });
202 }
204 pub fn provider(&self, core: &Core) -> CoreResult<CatalogProvider> {
205 let config = core.config.get();
206 let base_urls: Vec<String> = config["updates"]["baseUrls"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
207 CatalogProvider::new(&base_urls, core.services.https.clone(), core.paths.in_data("plugins/.downloads"), core.logger.clone())
208 }
210 pub fn list_windows(&self) -> Vec<Value> {
211 let active = self.active.lock().clone();
212 let mut list: Vec<Value> = self
213 .windows
214 .lock()
215 .iter()
216 .map(|(id, w)| json!({ "pluginId": id, "moduleId": id, "name": w.name, "version": w.version, "minimized": w.minimized, "detached": w.detached.is_some(), "active": active.as_deref() == Some(id.as_str()) }))
217 .collect();
218 list.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
219 list
220 }
222 pub fn get_state(&self) -> CoreResult<Value> {
223 let installed = self.registry.list_installed()?;
224 let catalog: Vec<Value> = self
225 .catalog
226 .lock()
227 .iter()
228 .map(|module| {
229 let current = installed.iter().find(|m| m["id"] == module["id"]);
230 let comparison = current
231 .map(|c| compare_versions(module["version"].as_str().unwrap_or("0.0"), c["version"].as_str().unwrap_or("0.0")).unwrap_or(Ordering::Equal))
232 .unwrap_or(Ordering::Greater);
233 let same_artifact = current.and_then(|c| c["installedSha512"].as_str()).map(|s| Some(s) == module["sha512"].as_str()).unwrap_or(false);
234 let mut entry = module.clone();
235 entry["installedVersion"] = current.map(|c| c["version"].clone()).unwrap_or(Value::Null);
236 entry["installedReleaseId"] = current.map(|c| c["installedReleaseId"].clone()).unwrap_or(Value::Null);
237 entry["updateAvailable"] = Value::Bool(current.is_some() && (comparison == Ordering::Greater || (comparison == Ordering::Equal && !same_artifact)));
238 entry["installed"] = Value::Bool(current.is_some() && (comparison == Ordering::Less || (comparison == Ordering::Equal && same_artifact)));
239 entry
240 })
241 .collect();
242 Ok(json!({
243 "status": self.operation.lock().unwrap_or("idle"),
244 "installed": installed,
245 "windows": self.list_windows(),
246 "catalog": catalog,
247 "sourceBaseUrl": *self.source_base_url.lock()
248 }))
249 }
251 pub async fn check_catalog(&self, core: &Core) -> CoreResult<Value> {
252 let _guard = self.begin("checking")?;
253 let config = core.config.get();
254 let channel = config["plugins"]["channel"].as_str().unwrap_or("alpha").to_string();
255 let (list, base) = self.provider(core)?.list(&core.build.version, MODULE_API_VERSION, &channel).await?;
256 core.logger.info("Plugin catalog checked.", json!({ "count": list.len(), "channel": channel, "sourceBaseUrl": base }));
257 *self.catalog.lock() = list;
258 *self.source_base_url.lock() = Some(base);
259 drop(_guard);
260 self.get_state()
261 }
263 pub async fn install(&self, core: &Core, release_id: &str) -> CoreResult<Value> {
264 let module = self.catalog.lock().iter().find(|m| m["releaseId"].as_str() == Some(release_id)).cloned().ok_or_else(|| CoreError::plain("Selected plugin release is not in the current catalog."))?;
265 let guard = self.begin("installing")?;
266 let provider = self.provider(core)?;
267 let package = provider.download_and_verify(&module).await?;
268 let result = self.installer.install(&package, &module)?;
269 match provider.remove_downloaded(&package) {
270 Ok(removed) => core.logger.info("Installed plugin package removed from download cache.", json!({ "pluginId": module["id"], "releaseId": module["releaseId"], "removed": removed })),
271 Err(error) => core.logger.warn("Installed plugin package could not be removed from download cache.", json!({ "pluginId": module["id"], "releaseId": module["releaseId"], "message": error.message })),
272 }
273 core.logger.info("Plugin installed.", json!({ "pluginId": result.manifest["id"], "version": result.manifest["version"], "permissions": result.manifest["permissions"] }));
274 drop(guard);
275 let mut state = self.get_state()?;
276 state["installedPlugin"] = result.manifest;
277 state["reused"] = Value::Bool(result.reused);
278 Ok(state)
279 }
281 pub async fn uninstall(&self, core: &Core, module_id: &str) -> CoreResult<Value> {
282 let guard = self.begin("uninstalling")?;
283 let label = self.windows.lock().get(module_id).map(|window| window.label.clone());
284 if let Some(label) = label {
285 if !self.confirm_close(core, &label).await {
286 drop(guard);
287 let mut state = self.get_state()?;
288 state["canceled"] = json!(true);
289 return Ok(state);
290 }
291 self.destroy_module(core, &label);
292 }
293 let removed = self.registry.remove_installed(module_id)?;
294 drop(guard);
295 let mut state = self.get_state()?;
296 state["removedPluginId"] = if removed { Value::String(module_id.to_string()) } else { Value::Null };
297 Ok(state)
298 }
300 pub fn cleanup_downloads(&self, core: &Core) -> CoreResult<bool> {
301 self.provider(core)?.cleanup_downloads()
302 }
304 pub fn open(&self, core: &Core, module_id: &str) -> CoreResult<Value> {
305 self.install_close_guard(core)?;
306 let resolved = self.registry.resolve(module_id)?;
307 let id = resolved.manifest["id"].as_str().unwrap_or("").to_string();
308 let existing = self.windows.lock().get(&id).cloned();
309 if let Some(state) = existing {
310 if core.app.get_webview(&state.label).is_some() {
311 if let Some(entry) = self.windows.lock().get_mut(&id) {
312 entry.minimized = false;
313 }
314 match state.detached.as_ref().and_then(|window| core.app.get_window(window)) {
315 Some(window) => {
316 let _ = window.unminimize();
317 let _ = window.show();
318 let _ = window.set_focus();
319 }
320 None => self.activate(core, &id),
321 }
322 self.emit_state(core);
323 return Ok(resolved.manifest);
324 }
325 self.windows.lock().remove(&id);
326 }
327 let label = format!("module-{}", self.counter.fetch_add(1, AtomicOrdering::SeqCst) + 1);
328 let permissions = resolved.manifest["permissions"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
329 core.windows.register_module(ModuleContext { id: id.clone(), label: label.clone(), manifest: resolved.manifest.clone(), permissions, root: resolved.root.clone() });
330 let name = resolved.manifest["name"].as_str().unwrap_or(&id).to_string();
331 self.windows.lock().insert(id.clone(), WindowState { label: label.clone(), name: name.clone(), version: resolved.manifest["version"].as_str().unwrap_or("").to_string(), minimized: false, detached: None });
332 let created = crate::windows::create_module_webview(
333 &core.app,
334 &core.i18n,
335 crate::windows::ModuleWindowSpec {
336 label: &label,
337 module_id: &id,
338 entry: resolved.manifest["entry"].as_str().unwrap_or("ui/index.html"),
339 root: &resolved.root,
340 },
341 );
342 if let Err(error) = created {
343 core.windows.unregister_module(&label);
344 self.windows.lock().remove(&id);
345 core.logger.error("Plugin window failed to load.", json!({ "pluginId": id, "message": error.to_string() }));
346 return Err(CoreError::plain(format!("The module window could not be opened: {error}")));
347 }
348 core.logger.info("Plugin window opened.", json!({ "pluginId": id, "version": resolved.manifest["version"] }));
349 self.activate(core, &id);
350 self.emit_state(core);
351 Ok(resolved.manifest)
352 }
354 /// The module page finished loading: show it unless it was collapsed meanwhile.
355 pub fn on_loaded(&self, core: &Core, _label: &str) {
356 self.apply_layout(core);
357 self.emit_state(core);
358 }
360 /// Shows an embedded module in the module area of the main window.
361 fn activate(&self, core: &Core, id: &str) {
362 *self.active.lock() = Some(id.to_string());
363 core.emit_main("core:moduleActivate", id.to_string());
364 self.apply_layout(core);
365 }
367 /// The main page reports its module area; `None` means another view covers the window.
368 pub fn set_area(&self, core: &Core, area: Option<Area>) {
369 *self.area.lock() = area;
370 self.apply_layout(core);
371 }
373 /// Only the active embedded module is visible, and only while the module area is on screen.
374 fn apply_layout(&self, core: &Core) {
375 let area = *self.area.lock();
376 let active = self.active.lock().clone();
377 let states: Vec<(String, WindowState)> = self.windows.lock().iter().map(|(id, w)| (id.clone(), w.clone())).collect();
378 for (id, state) in states {
379 if state.detached.is_some() {
380 continue;
381 }
382 let Some(webview) = core.app.get_webview(&state.label) else { continue };
383 match area.filter(|_| active.as_deref() == Some(id.as_str()) && !state.minimized) {
384 Some(area) => {
385 crate::windows::place_embedded(&webview, area.x, area.y, area.width, area.height);
386 let _ = webview.show();
387 let _ = webview.set_focus();
388 }
389 None => {
390 let _ = webview.hide();
391 }
392 }
393 }
394 }
396 /// The module whose separate window has this label, or the module WebView itself.
397 pub fn module_of_window(&self, window_label: &str) -> Option<String> {
398 self.windows.lock().values().find(|w| w.detached.as_deref() == Some(window_label) || w.label == window_label).map(|w| w.label.clone())
399 }
401 pub fn label_of(&self, module_id: &str) -> CoreResult<String> {
402 self.windows.lock().get(module_id).map(|w| w.label.clone()).ok_or_else(|| CoreError::plain("The module is not open."))
403 }
405 /// Whether a module runs in its own window; the module page uses this for its window controls.
406 pub fn host_state(&self, label: &str) -> Value {
407 let detached = self.windows.lock().values().any(|w| w.label == label && w.detached.is_some());
408 json!({ "embedded": !detached })
409 }
411 /// Moves a running module to a separate window. The WebView keeps its page, state and identity.
412 pub fn detach(&self, core: &Core, label: &str) -> CoreResult<Value> {
413 let failed = |error: tauri::Error| CoreError::plain(format!("The module window could not be opened: {error}"));
414 let (id, state) = self.windows.lock().iter().find(|(_, w)| w.label == label).map(|(id, w)| (id.clone(), w.clone())).ok_or_else(|| CoreError::plain("The module is not open."))?;
415 if state.detached.is_some() {
416 return Ok(self.host_state(label));
417 }
418 let webview = core.app.get_webview(label).ok_or_else(|| CoreError::plain("The module is not open."))?;
419 let window_label = format!("{DETACHED_LABEL_PREFIX}{}", self.counter.fetch_add(1, AtomicOrdering::SeqCst) + 1);
420 let window = crate::windows::create_detached_window(&core.app, &window_label, &state.name).map_err(failed)?;
421 crate::windows::release_from_main(&webview);
422 if let Err(error) = webview.reparent(&window) {
423 crate::windows::embed_in_main(&webview);
424 let _ = window.destroy();
425 self.apply_layout(core);
426 return Err(failed(error));
427 }
428 if let Some(entry) = self.windows.lock().get_mut(&id) {
429 entry.detached = Some(window_label);
430 entry.minimized = false;
431 }
432 {
433 let mut active = self.active.lock();
434 if active.as_deref() == Some(id.as_str()) {
435 *active = None;
436 }
437 }
438 let _ = webview.show();
439 let _ = window.show();
440 let _ = window.set_focus();
441 self.fit_detached(core, label);
442 core.emit_window(label, "module:ui:hostChanged", self.host_state(label));
443 self.emit_state(core);
444 Ok(self.host_state(label))
445 }
447 /// A detached module fills its window. Tauri's automatic resize keeps the old position after a reparent, so the
448 /// bounds are set from the window size; on Linux the window's GtkBox already does this.
449 pub fn fit_detached(&self, core: &Core, label: &str) {
450 #[cfg(not(target_os = "linux"))]
451 {
452 let Some(webview) = core.app.get_webview(label) else { return };
453 // On macOS the inner size follows the first WebView, here the module's old size; the content view fills the frame.
454 #[cfg(target_os = "macos")]
455 let size = webview.window().outer_size();
456 #[cfg(not(target_os = "macos"))]
457 let size = webview.window().inner_size();
458 let Ok(size) = size else { return };
459 let _ = webview.set_bounds(tauri::Rect { position: tauri::PhysicalPosition::new(0, 0).into(), size: size.into() });
460 }
461 #[cfg(target_os = "linux")]
462 let _ = (core, label);
463 }
465 /// Moves a detached module back into the main window and shows it there.
466 pub fn attach(&self, core: &Core, label: &str) -> CoreResult<Value> {
467 let failed = |error: tauri::Error| CoreError::plain(format!("The module could not be returned: {error}"));
468 let (id, state) = self.windows.lock().iter().find(|(_, w)| w.label == label).map(|(id, w)| (id.clone(), w.clone())).ok_or_else(|| CoreError::plain("The module is not open."))?;
469 let Some(window_label) = state.detached else { return Ok(self.host_state(label)) };
470 let webview = core.app.get_webview(label).ok_or_else(|| CoreError::plain("The module is not open."))?;
471 let main = core.app.get_window(MAIN_LABEL).ok_or_else(|| CoreError::plain("The main window is not available."))?;
472 let _ = webview.hide();
473 if let Err(error) = webview.reparent(&main) {
474 let _ = webview.show();
475 return Err(failed(error));
476 }
477 // Cleared before the separate window closes, which must not take the module down with it.
478 if let Some(entry) = self.windows.lock().get_mut(&id) {
479 entry.detached = None;
480 entry.minimized = false;
481 }
482 crate::windows::embed_in_main(&webview);
483 if let Some(window) = core.app.get_window(&window_label) {
484 let _ = window.destroy();
485 }
486 let _ = main.unminimize();
487 let _ = main.show();
488 let _ = main.set_focus();
489 core.emit_window(label, "module:ui:hostChanged", self.host_state(label));
490 self.activate(core, &id);
491 self.emit_state(core);
492 Ok(self.host_state(label))
493 }
495 /// Ends a module without asking; callers confirmed drafts or are locking the vault.
496 fn destroy_module(&self, core: &Core, label: &str) {
497 let window = self.windows.lock().values().find(|w| w.label == label).and_then(|w| w.detached.clone());
498 if let Some(webview) = core.app.get_webview(label) {
499 let _ = webview.close();
500 }
501 if let Some(window) = window.and_then(|window| core.app.get_window(&window)) {
502 let _ = window.destroy();
503 }
504 self.on_destroyed(core, label);
505 }
507 pub fn minimize_label(&self, core: &Core, label: &str, focus_main: bool) {
508 let id = self.windows.lock().iter().find(|(_, w)| w.label == label).map(|(id, _)| id.clone());
509 let Some(id) = id else { return };
510 let detached = self.windows.lock().get(&id).and_then(|w| w.detached.clone());
511 if let Some(window) = detached.and_then(|window| core.app.get_window(&window)) {
512 let _ = window.hide();
513 }
514 if let Some(entry) = self.windows.lock().get_mut(&id) {
515 entry.minimized = true;
516 }
517 {
518 let mut active = self.active.lock();
519 if active.as_deref() == Some(id.as_str()) {
520 *active = None;
521 }
522 }
523 self.apply_layout(core);
524 if focus_main {
525 if let Some(main) = core.app.get_window(MAIN_LABEL) {
526 let _ = main.unminimize();
527 let _ = main.show();
528 let _ = main.set_focus();
529 }
530 }
531 self.emit_state(core);
532 core.logger.info("Plugin window minimized to the Core taskbar.", json!({ "pluginId": id }));
533 }
535 pub fn minimize_all(&self, core: &Core) {
536 // Embedded modules minimize together with the main window.
537 let labels: Vec<String> = self.windows.lock().values().filter(|w| w.detached.is_some()).map(|w| w.label.clone()).collect();
538 for label in labels {
539 self.minimize_label(core, &label, false);
540 }
541 }
543 pub fn close(&self, core: &Arc<Core>, module_id: &str) {
544 let label = self.windows.lock().get(module_id).map(|w| w.label.clone());
545 if let Some(label) = label {
546 ModuleService::request_close(core.clone(), Some(label), None);
547 }
548 }
550 pub fn close_label(&self, core: &Arc<Core>, label: &str) {
551 if core.app.get_webview(label).is_some() {
552 ModuleService::request_close(core.clone(), Some(label.to_string()), None);
553 }
554 }
556 pub fn close_all(&self, core: &Core) {
557 let labels: Vec<String> = self.windows.lock().values().map(|w| w.label.clone()).collect();
558 // Vault lock/reset wipes keys before this call; those security closes cannot wait
559 // for a renderer or leave sensitive windows visible. Main close/quit already confirmed.
560 for label in labels {
561 self.destroy_module(core, &label);
562 }
563 }
565 /// Window destroyed: drop its context, release per-module resources, update the taskbar.
566 pub fn on_destroyed(&self, core: &Core, label: &str) {
567 let context = core.windows.unregister_module(label);
568 {
569 let mut active = self.active.lock();
570 let closing = self.windows.lock().iter().find(|(_, w)| w.label == label).map(|(id, _)| id.clone());
571 if closing.is_some() && *active == closing {
572 *active = None;
573 }
574 }
575 self.windows.lock().retain(|_, w| w.label != label);
576 if let Some(context) = context {
577 core.services.on_module_closed(&context.id);
578 }
579 self.emit_state(core);
580 }
582 pub fn notify_language(&self, core: &Core, language: &str) {
583 for context in core.windows.modules() {
584 core.emit_window(&context.label, "module:ui:languageChanged", language.to_string());
585 }
586 }
588 pub fn emit_state(&self, core: &Core) {
589 core.emit_main("core:pluginWindowState", self.list_windows());
590 }
593#[cfg(test)]
594mod close_tests {
595 use super::*;
597 #[tokio::test]
598 async fn cancel_stops_confirmation_and_discard_confirms_every_window() {
599 let labels = vec!["files".into(), "nginx".into(), MAIN_LABEL.into()];
600 let visited = Mutex::new(Vec::new());
601 assert!(!confirm_windows(&labels, |label| {
602 let accept = label != "nginx";
603 visited.lock().push(label);
604 std::future::ready(accept)
605 }).await);
606 assert_eq!(*visited.lock(), vec!["files", "nginx"]);
607 visited.lock().clear();
608 assert!(confirm_windows(&labels, |label| {
609 visited.lock().push(label);
610 std::future::ready(true)
611 }).await);
612 assert_eq!(*visited.lock(), labels);
613 }
615 #[tokio::test]
616 async fn dirty_reply_cannot_answer_for_another_window_or_old_query() {
617 let dir = tempfile::tempdir().unwrap();
618 let registry = Arc::new(ModuleRegistry::new(dir.path(), None, Arc::new(crate::core::logging::Logger::new(dir.path()))));
619 let service = ModuleService::new(registry, ModuleInstaller::new(dir.path(), "0.1.1", "1.7.0"));
620 let (tx, mut rx) = tokio::sync::oneshot::channel();
621 service.close_queries.lock().insert("module-1".into(), (42, tx));
622 service.close_query_reply("module-2", 42, false);
623 service.close_query_reply("module-1", 41, false);
624 assert!(matches!(rx.try_recv(), Err(tokio::sync::oneshot::error::TryRecvError::Empty)));
625 service.close_query_reply("module-1", 42, true);
626 assert!(rx.await.unwrap());
627 assert!(service.close_queries.lock().is_empty());
628 }
631#[cfg(test)]
632#[path = "../../../tests/support/module_runtime_safety.rs"]
633mod safety_tests;
SHA-256f7323342e2d835561106b3a537d7106a387d64143d47805c9612f06d8f17b47f

CH-J Proprietary Software License 1.14 · Bron wordt geleverd onder de CH-J Proprietary Software License 1.14. De beschikbaarheid verandert de licentievoorwaarden niet of verleent geen extra rechten.

Archief SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Bestanden