| 6 | 6 | | //! |
| 7 | 7 | | //! - **When.** On by default; settings turn it off ("Run jobs on this |
| 8 | 8 | | //! computer while g1t is open"), and Stop in settings or the tray stops |
| 9 | | − | //! it until Start, or until g1t starts again. It starts a few seconds |
| 10 | | − | //! after the window, never before. |
| 9 | + | //! it until Start, or until g1t starts again. It starts once the site's |
| 10 | + | //! page has loaded (or 20 s have passed) and 5 s more, never before, on a |
| 11 | + | //! thread of its own that a panic restarts rather than ending the app. |
| 11 | 12 | | //! - **How.** The backend settings choose (backend.rs): Sandboxed, the |
| 12 | 13 | | //! default, where nothing a job does reaches this computer; Docker; or |
| 13 | 14 | | //! directly on this computer. The two that are not sandboxes are only |
| ⋯ |
| 65 | 66 | | use std::io::{BufRead, BufReader, Read, Write}; |
| 66 | 67 | | use std::path::PathBuf; |
| 67 | 68 | | use std::process::{Child, Stdio}; |
| 68 | | − | use std::sync::Mutex; |
| 69 | + | use std::panic::{AssertUnwindSafe, catch_unwind}; |
| 69 | 70 | | use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender}; |
| 71 | + | use std::sync::{Mutex, MutexGuard, PoisonError}; |
| 70 | 72 | | use std::time::{Duration, Instant, SystemTime}; |
| 71 | 73 | | |
| 72 | 74 | | use serde::{Deserialize, Serialize}; |
| ⋯ |
| 86 | 88 | | /// What the site's page listens for: the app needs a registration token for a workspace. |
| 87 | 89 | | pub const WANTED_EVENT: &str = "g1t-runner-wanted"; |
| 88 | 90 | | |
| 89 | | − | /// How long after the window the runner starts. |
| 90 | | − | const AFTER_START: Duration = Duration::from_secs(4); |
| 91 | + | /// How long after the site's page has loaded the runner starts: the |
| 92 | + | /// window, the page and its first calls to the app come first. |
| 93 | + | const AFTER_START: Duration = Duration::from_secs(5); |
| 94 | + | /// The longest the runner waits for the site's page before starting |
| 95 | + | /// anyway (offline, the server picker, a site too old to say it loaded). |
| 96 | + | const SITE_WAIT: Duration = Duration::from_secs(20); |
| 97 | + | /// How long after the runner's thread failed it starts again. |
| 98 | + | const AFTER_PANIC: Duration = Duration::from_secs(15); |
| 99 | + | |
| 100 | + | /// A lock that a failed runner thread can't take the app down with: a |
| 101 | + | /// panic while holding one leaves it poisoned, and `unwrap` on the main |
| 102 | + | /// thread would then end the app. The data is plain state, never half |
| 103 | + | /// written in a way that matters, so it is used as it is. |
| 104 | + | trait Held<T> { |
| 105 | + | fn held(&self) -> MutexGuard<'_, T>; |
| 106 | + | } |
| 107 | + | |
| 108 | + | impl<T> Held<T> for Mutex<T> { |
| 109 | + | fn held(&self) -> MutexGuard<'_, T> { |
| 110 | + | self.lock().unwrap_or_else(PoisonError::into_inner) |
| 111 | + | } |
| 112 | + | } |
| 91 | 113 | | /// How often the thread looks at the runner when nothing has happened. |
| 92 | 114 | | const TICK: Duration = Duration::from_secs(5); |
| 93 | 115 | | /// How long after starting the first look for an update is, then how often. |
| ⋯ |
| 331 | 353 | | if let Some(registration) = Registration::load(app).filter(|r| r.server == server) { |
| 332 | 354 | | return Some(registration.workspace); |
| 333 | 355 | | } |
| 334 | | − | let site = app.state::<Shared>().site.lock().unwrap().clone(); |
| 356 | + | let site = app.state::<Shared>().site.held().clone(); |
| 335 | 357 | | site.current.filter(|slug| site.owned.iter().any(|o| &o.slug == slug)).or_else(|| site.owned.first().map(|o| o.slug.clone())) |
| 336 | 358 | | } |
| 337 | 359 | | |
| ⋯ |
| 359 | 381 | | live: now.live, |
| 360 | 382 | | starting: now.starting.clone(), |
| 361 | 383 | | download: now.download.clone(), |
| 362 | | − | held: !settings.runner || *app.state::<Shared>().paused.lock().unwrap(), |
| 384 | + | held: !settings.runner || *app.state::<Shared>().paused.held(), |
| 363 | 385 | | update: update_facts(app, now, backend), |
| 364 | 386 | | } |
| 365 | 387 | | } |
| ⋯ |
| 369 | 391 | | /// its program while they run; a runner with no kinds of job takes it |
| 370 | 392 | | /// itself. |
| 371 | 393 | | fn update_facts(app: &AppHandle, now: &Now, backend: Backend) -> diagnosis::Update { |
| 372 | | − | let updates = app.state::<Shared>().updates.lock().unwrap().clone(); |
| 394 | + | let updates = app.state::<Shared>().updates.held().clone(); |
| 373 | 395 | | let sandbox = now.sandbox.as_ref().filter(|_| backend == Backend::Sandboxed); |
| 374 | 396 | | let mut waiting = Vec::new(); |
| 375 | 397 | | if updates.image { |
| ⋯ |
| 417 | 439 | | |
| 418 | 440 | | pub fn view(app: &AppHandle) -> View { |
| 419 | 441 | | let shared = app.state::<Shared>(); |
| 420 | | − | let now = shared.now.lock().unwrap().clone(); |
| 442 | + | let now = shared.now.held().clone(); |
| 421 | 443 | | let settings = config::settings(app); |
| 422 | 444 | | let facts = facts(app, &now); |
| 423 | 445 | | let server = config::server(app); |
| ⋯ |
| 431 | 453 | | backend: settings.runner_backend, |
| 432 | 454 | | link: facts.workspace.as_ref().and_then(|w| server.join(&format!("{w}/-/runners")).ok()).map(|u| u.to_string()), |
| 433 | 455 | | workspace: facts.workspace, |
| 434 | | − | workspaces: shared.site.lock().unwrap().owned.clone(), |
| 456 | + | workspaces: shared.site.held().owned.clone(), |
| 435 | 457 | | enabled: settings.runner, |
| 436 | | − | paused: *shared.paused.lock().unwrap(), |
| 458 | + | paused: *shared.paused.held(), |
| 437 | 459 | | machine: facts.machine, |
| 438 | 460 | | platform: facts.platform, |
| 439 | | − | log: shared.tail.lock().unwrap().iter().cloned().collect(), |
| 461 | + | log: shared.tail.held().iter().cloned().collect(), |
| 440 | 462 | | } |
| 441 | 463 | | } |
| 442 | 464 | | |
| 443 | 465 | | /// Whether the runner is running a job: what restarts g1t waits for it, |
| 444 | 466 | | /// since restarting stops the runner and would cut the job short. |
| 445 | 467 | | pub fn busy(app: &AppHandle) -> bool { |
| 446 | | − | let now = app.state::<Shared>().now.lock().unwrap().clone(); |
| 468 | + | let now = app.state::<Shared>().now.held().clone(); |
| 447 | 469 | | now.code == Code::Busy || now.sandbox.as_ref().is_some_and(|status| status.job().is_some()) |
| 448 | 470 | | } |
| 449 | 471 | | |
| ⋯ |
| 464 | 486 | | } |
| 465 | 487 | | |
| 466 | 488 | | fn ask(app: &AppHandle, ask: Ask) { |
| 467 | | − | if let Some(tx) = app.state::<Shared>().tx.lock().unwrap().as_ref() { |
| 489 | + | if let Some(tx) = app.state::<Shared>().tx.held().as_ref() { |
| 468 | 490 | | let _ = tx.send(ask); |
| 469 | 491 | | } |
| 470 | 492 | | } |
| ⋯ |
| 472 | 494 | | /// Starts the thread that keeps the runner, as the app starts. |
| 473 | 495 | | pub fn install(app: &AppHandle) { |
| 474 | 496 | | let (tx, rx) = mpsc::channel(); |
| 475 | | − | *app.state::<Shared>().tx.lock().unwrap() = Some(tx.clone()); |
| 497 | + | *app.state::<Shared>().tx.held() = Some(tx.clone()); |
| 476 | 498 | | let handle = app.clone(); |
| 477 | | − | let spawned = std::thread::Builder::new().name("g1t-runner".into()).spawn(move || Keeper::new(handle, tx).keep(rx)); |
| 499 | + | let spawned = std::thread::Builder::new().name("g1t-runner".into()).spawn(move || supervise(handle, tx, rx)); |
| 478 | 500 | | if let Err(error) = spawned { |
| 479 | 501 | | eprintln!("g1t: the runner's thread could not start: {error}"); |
| 480 | 502 | | } |
| 481 | 503 | | } |
| 482 | 504 | | |
| 505 | + | /// The runner's thread, kept apart from the app: it starts only once the |
| 506 | + | /// window and the site's page are up, and a panic in it ends neither the |
| 507 | + | /// app nor the runner for good: the keeper starts again a little later, |
| 508 | + | /// reaping what the last one left, as it does after a crash of the app. |
| 509 | + | fn supervise(app: AppHandle, tx: Sender<Ask>, rx: Receiver<Ask>) { |
| 510 | + | let mut held = match settle(&app, &rx) { |
| 511 | + | Ok(held) => held, |
| 512 | + | Err(done) => { |
| 513 | + | let _ = done.send(()); |
| 514 | + | return; |
| 515 | + | } |
| 516 | + | }; |
| 517 | + | loop { |
| 518 | + | let keeper = Keeper::new(app.clone(), tx.clone()); |
| 519 | + | let asks = std::mem::take(&mut held); |
| 520 | + | match catch_unwind(AssertUnwindSafe(|| keeper.keep(&rx, asks))) { |
| 521 | + | Ok(()) => return, |
| 522 | + | Err(panic) => { |
| 523 | + | let why = panic.downcast_ref::<&str>().map(|s| (*s).to_owned()).or_else(|| panic.downcast_ref::<String>().cloned()).unwrap_or_default(); |
| 524 | + | eprintln!("g1t: the runner's thread failed ({why}); starting it again in {}s", AFTER_PANIC.as_secs()); |
| 525 | + | // Asks that come meanwhile wait for the next keeper; Quit ends it all. |
| 526 | + | let until = Instant::now() + AFTER_PANIC; |
| 527 | + | while let Some(left) = until.checked_duration_since(Instant::now()).filter(|d| !d.is_zero()) { |
| 528 | + | match rx.recv_timeout(left) { |
| 529 | + | Ok(Ask::Quit(done)) => { |
| 530 | + | let _ = done.send(()); |
| 531 | + | return; |
| 532 | + | } |
| 533 | + | Ok(ask) => held.push(ask), |
| 534 | + | Err(RecvTimeoutError::Timeout) => break, |
| 535 | + | Err(RecvTimeoutError::Disconnected) => return, |
| 536 | + | } |
| 537 | + | } |
| 538 | + | } |
| 539 | + | } |
| 540 | + | } |
| 541 | + | } |
| 542 | + | |
| 543 | + | /// Waits, doing nothing the window could notice, until the site's page has |
| 544 | + | /// loaded (or SITE_WAIT has passed) and AFTER_START more: the runner starts |
| 545 | + | /// with the app, but never in its way. What is asked meanwhile is kept for |
| 546 | + | /// after; Quit returns its sender at once. |
| 547 | + | fn settle(app: &AppHandle, rx: &Receiver<Ask>) -> Result<Vec<Ask>, Sender<()>> { |
| 548 | + | let began = Instant::now(); |
| 549 | + | let mut loaded: Option<Instant> = None; |
| 550 | + | let mut held = Vec::new(); |
| 551 | + | loop { |
| 552 | + | if loaded.is_none() && (config::site_ready(app) || began.elapsed() >= SITE_WAIT) { |
| 553 | + | loaded = Some(Instant::now()); |
| 554 | + | } |
| 555 | + | if loaded.is_some_and(|at| at.elapsed() >= AFTER_START) { |
| 556 | + | return Ok(held); |
| 557 | + | } |
| 558 | + | match rx.recv_timeout(Duration::from_millis(250)) { |
| 559 | + | Ok(Ask::Quit(done)) => return Err(done), |
| 560 | + | Ok(ask) => held.push(ask), |
| 561 | + | Err(RecvTimeoutError::Timeout) => {} |
| 562 | + | Err(RecvTimeoutError::Disconnected) => return Ok(held), |
| 563 | + | } |
| 564 | + | } |
| 565 | + | } |
| 566 | + | |
| 483 | 567 | | /// Settings changed: the thread looks again. |
| 484 | 568 | | pub fn settings_changed(app: &AppHandle) { |
| 485 | 569 | | ask(app, Ask::Look); |
| ⋯ |
| 488 | 572 | | |
| 489 | 573 | | /// Start or Stop, from settings or the tray. |
| 490 | 574 | | pub fn set_paused(app: &AppHandle, paused: bool) { |
| 491 | | − | *app.state::<Shared>().paused.lock().unwrap() = paused; |
| 575 | + | *app.state::<Shared>().paused.held() = paused; |
| 492 | 576 | | // Start with the setting off turns the setting on: that is what was asked. |
| 493 | 577 | | if !paused && !config::settings(app).runner { |
| 494 | 578 | | let mut settings = config::settings(app); |
| ⋯ |
| 508 | 592 | | } |
| 509 | 593 | | if on { |
| 510 | 594 | | settings.runner = true; |
| 511 | | − | *app.state::<Shared>().paused.lock().unwrap() = false; |
| 595 | + | *app.state::<Shared>().paused.held() = false; |
| 512 | 596 | | } |
| 513 | 597 | | settings::apply(app, settings)?; |
| 514 | 598 | | if on { |
| ⋯ |
| 526 | 610 | | |
| 527 | 611 | | /// Retry and Restart: now, rather than when the wait is over. |
| 528 | 612 | | pub fn retry(app: &AppHandle) { |
| 529 | | − | *app.state::<Shared>().paused.lock().unwrap() = false; |
| 613 | + | *app.state::<Shared>().paused.held() = false; |
| 530 | 614 | | ask(app, Ask::Retry); |
| 531 | 615 | | } |
| 532 | 616 | | |
| ⋯ |
| 536 | 620 | | let next = Site { signed_in: Some(signed_in), owned, current: current.map(|c| c.to_ascii_lowercase()) }; |
| 537 | 621 | | let changed = { |
| 538 | 622 | | let shared = app.state::<Shared>(); |
| 539 | | − | let mut site = shared.site.lock().unwrap(); |
| 623 | + | let mut site = shared.site.held(); |
| 540 | 624 | | let changed = *site != next; |
| 541 | 625 | | *site = next; |
| 542 | 626 | | changed |
| ⋯ |
| 572 | 656 | | "stop" => set_paused(app, true), |
| 573 | 657 | | "retry" | "restart" => retry(app), |
| 574 | 658 | | "remove_sandbox" => { |
| 575 | | − | *app.state::<Shared>().paused.lock().unwrap() = false; |
| 659 | + | *app.state::<Shared>().paused.held() = false; |
| 576 | 660 | | ask(app, Ask::RemoveSandbox); |
| 577 | 661 | | } |
| 578 | 662 | | "check_updates" => ask(app, Ask::Check), |
| ⋯ |
| 657 | 741 | | |
| 658 | 742 | | /// The workspace a token is wanted for now, if one is: the site asks as it loads. |
| 659 | 743 | | pub fn wanted(app: &AppHandle) -> Option<String> { |
| 660 | | − | app.state::<Shared>().wanted.lock().unwrap().clone() |
| 744 | + | app.state::<Shared>().wanted.held().clone() |
| 661 | 745 | | } |
| 662 | 746 | | |
| 663 | 747 | | /// The runner's log, for the settings window to open. |
| ⋯ |
| 667 | 751 | | |
| 668 | 752 | | /// Stops the runner as g1t quits or restarts, and waits for it. |
| 669 | 753 | | pub fn shutdown(app: &AppHandle) { |
| 670 | | − | let tx = app.state::<Shared>().tx.lock().unwrap().take(); |
| 754 | + | let tx = app.state::<Shared>().tx.held().take(); |
| 671 | 755 | | if let Some(tx) = tx { |
| 672 | 756 | | let (done, wait) = mpsc::channel(); |
| 673 | 757 | | if tx.send(Ask::Quit(done)).is_ok() { |
| ⋯ |
| 825 | 909 | | } |
| 826 | 910 | | } |
| 827 | 911 | | |
| 828 | | − | fn keep(mut self, rx: Receiver<Ask>) { |
| 912 | + | fn keep(mut self, rx: &Receiver<Ask>, asks: Vec<Ask>) { |
| 829 | 913 | | self.reap(); |
| 830 | 914 | | self.set(Code::Starting, detail("Starting after g1t's window.")); |
| 915 | + | for ask in asks { |
| 916 | + | self.handle(ask); |
| 917 | + | } |
| 831 | 918 | | loop { |
| 832 | 919 | | match rx.recv_timeout(TICK) { |
| 833 | 920 | | Ok(Ask::Quit(done)) => { |
| ⋯ |
| 860 | 947 | | } |
| 861 | 948 | | |
| 862 | 949 | | fn with_updates<R>(&self, change: impl FnOnce(&mut Updates) -> R) -> R { |
| 863 | | − | change(&mut self.app.state::<Shared>().updates.lock().unwrap()) |
| 950 | + | change(&mut self.app.state::<Shared>().updates.held()) |
| 864 | 951 | | } |
| 865 | 952 | | |
| 866 | 953 | | /// What an idle runner shows: why its last update failed, until one works. |
| ⋯ |
| 874 | 961 | | fn set(&mut self, code: Code, extra: Extra) { |
| 875 | 962 | | let changed = { |
| 876 | 963 | | let shared = self.app.state::<Shared>(); |
| 877 | | − | let mut now = shared.now.lock().unwrap(); |
| 964 | + | let mut now = shared.now.held(); |
| 878 | 965 | | let job = if code == Code::Busy { now.job.clone() } else { None }; |
| 879 | 966 | | let next = Now { |
| 880 | 967 | | code, |
| ⋯ |
| 896 | 983 | | } |
| 897 | 984 | | |
| 898 | 985 | | fn code(&self) -> Code { |
| 899 | | − | self.app.state::<Shared>().now.lock().unwrap().code |
| 986 | + | self.app.state::<Shared>().now.held().code |
| 900 | 987 | | } |
| 901 | 988 | | |
| 902 | 989 | | fn note(&mut self, job: Option<String>, version: Option<String>) { |
| 903 | 990 | | let shared = self.app.state::<Shared>(); |
| 904 | | − | let mut now = shared.now.lock().unwrap(); |
| 991 | + | let mut now = shared.now.held(); |
| 905 | 992 | | if job.is_some() { |
| 906 | 993 | | now.job = job; |
| 907 | 994 | | } |
| ⋯ |
| 914 | 1001 | | fn write_log(&mut self, line: &str) { |
| 915 | 1002 | | { |
| 916 | 1003 | | let shared = self.app.state::<Shared>(); |
| 917 | | − | let mut tail = shared.tail.lock().unwrap(); |
| 1004 | + | let mut tail = shared.tail.held(); |
| 918 | 1005 | | if tail.len() == TAIL { |
| 919 | 1006 | | tail.pop_front(); |
| 920 | 1007 | | } |
| ⋯ |
| 969 | 1056 | | Ask::Checked(found) => self.checked(found), |
| 970 | 1057 | | Ask::Token { workspace, token } => { |
| 971 | 1058 | | let shared = self.app.state::<Shared>(); |
| 972 | | − | let mut wanted = shared.wanted.lock().unwrap(); |
| 1059 | + | let mut wanted = shared.wanted.held(); |
| 973 | 1060 | | if wanted.as_deref() == Some(workspace.as_str()) { |
| 974 | 1061 | | *wanted = None; |
| 975 | 1062 | | self.token = Some((workspace, token)); |
| ⋯ |
| 979 | 1066 | | Ask::Refused { workspace, allowed, why } => { |
| 980 | 1067 | | let matched = { |
| 981 | 1068 | | let shared = self.app.state::<Shared>(); |
| 982 | | − | let mut wanted = shared.wanted.lock().unwrap(); |
| 1069 | + | let mut wanted = shared.wanted.held(); |
| 983 | 1070 | | let matched = wanted.as_deref() == Some(workspace.as_str()); |
| 984 | 1071 | | if matched { |
| 985 | 1072 | | *wanted = None; |
| ⋯ |
| 1007 | 1094 | | if let Some(download) = launcher::download(line) { |
| 1008 | 1095 | | let changed = { |
| 1009 | 1096 | | let shared = self.app.state::<Shared>(); |
| 1010 | | − | let mut now = shared.now.lock().unwrap(); |
| 1097 | + | let mut now = shared.now.held(); |
| 1011 | 1098 | | let changed = now.download.as_ref() != Some(&download); |
| 1012 | 1099 | | now.download = Some(download); |
| 1013 | 1100 | | changed |
| ⋯ |
| 1117 | 1204 | | } |
| 1118 | 1205 | | }; |
| 1119 | 1206 | | let shared = self.app.state::<Shared>(); |
| 1120 | | − | let mut now = shared.now.lock().unwrap(); |
| 1207 | + | let mut now = shared.now.held(); |
| 1121 | 1208 | | now.sandbox = stopped.filter(|_| how != Down::Delete); |
| 1122 | 1209 | | now.starting.clear(); |
| 1123 | 1210 | | now.download = None; |
| ⋯ |
| 1192 | 1279 | | |
| 1193 | 1280 | | /// Asks the site for a registration token for `workspace`. |
| 1194 | 1281 | | fn want(&mut self, workspace: &str) { |
| 1195 | | − | *self.app.state::<Shared>().wanted.lock().unwrap() = Some(workspace.to_owned()); |
| 1282 | + | *self.app.state::<Shared>().wanted.held() = Some(workspace.to_owned()); |
| 1196 | 1283 | | let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned()); |
| 1197 | 1284 | | self.not_before = Instant::now() + ASK_AGAIN; |
| 1198 | | − | let signed_out = self.app.state::<Shared>().site.lock().unwrap().signed_in == Some(false); |
| 1285 | + | let signed_out = self.app.state::<Shared>().site.held().signed_in == Some(false); |
| 1199 | 1286 | | self.set(if signed_out { Code::SignedOut } else { Code::Registering }, Extra::default()); |
| 1200 | 1287 | | } |
| 1201 | 1288 | | |
| 1202 | 1289 | | /// The thread's look round: what should run, and whether it does. |
| 1203 | 1290 | | fn look(&mut self) { |
| 1204 | 1291 | | let settings = config::settings(&self.app); |
| 1205 | | − | let paused = *self.app.state::<Shared>().paused.lock().unwrap(); |
| 1292 | + | let paused = *self.app.state::<Shared>().paused.held(); |
| 1206 | 1293 | | if !settings.runner || paused { |
| 1207 | 1294 | | if self.run.is_some() { |
| 1208 | 1295 | | self.halt(); |
| 1209 | 1296 | | } |
| 1210 | | − | *self.app.state::<Shared>().wanted.lock().unwrap() = None; |
| 1297 | + | *self.app.state::<Shared>().wanted.held() = None; |
| 1211 | 1298 | | self.set(if paused { Code::Stopped } else { Code::Off }, Extra::default()); |
| 1212 | 1299 | | return; |
| 1213 | 1300 | | } |
| ⋯ |
| 1264 | 1351 | | }; |
| 1265 | 1352 | | let Some(workspace) = workspace else { |
| 1266 | 1353 | | self.not_before = Instant::now() + LOOK_AGAIN; |
| 1267 | | − | let code = match app.state::<Shared>().site.lock().unwrap().signed_in { |
| 1354 | + | let code = match app.state::<Shared>().site.held().signed_in { |
| 1268 | 1355 | | Some(false) => Code::SignedOut, |
| 1269 | 1356 | | Some(true) => Code::NoWorkspace, |
| 1270 | 1357 | | None => Code::Starting, |
| ⋯ |
| 1449 | 1536 | | self.program = None; |
| 1450 | 1537 | | } |
| 1451 | 1538 | | // The sandbox's image is the release's version; one older takes the newest. |
| 1452 | | − | let sandbox = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone(); |
| 1539 | + | let sandbox = self.app.state::<Shared>().now.held().sandbox.clone(); |
| 1453 | 1540 | | let launcher = self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher); |
| 1454 | 1541 | | let image = launcher |
| 1455 | 1542 | | && found.images |
| ⋯ |
| 1510 | 1597 | | self.not_before = Instant::now(); |
| 1511 | 1598 | | return true; |
| 1512 | 1599 | | } |
| 1513 | | − | let Some(status) = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone() else { return false }; |
| 1600 | + | let Some(status) = self.app.state::<Shared>().now.held().sandbox.clone() else { return false }; |
| 1514 | 1601 | | if self.program.is_some() { |
| 1515 | 1602 | | let running = native_kinds(&status); |
| 1516 | 1603 | | if running.is_empty() { |
| ⋯ |
| 1694 | 1781 | | fn now_starting(&self, kinds: Vec<String>) { |
| 1695 | 1782 | | let changed = { |
| 1696 | 1783 | | let shared = self.app.state::<Shared>(); |
| 1697 | | − | let mut now = shared.now.lock().unwrap(); |
| 1784 | + | let mut now = shared.now.held(); |
| 1698 | 1785 | | let changed = now.starting != kinds; |
| 1699 | 1786 | | now.starting = kinds; |
| 1700 | 1787 | | changed |
| ⋯ |
| 1713 | 1800 | | if !self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher) { |
| 1714 | 1801 | | return; |
| 1715 | 1802 | | } |
| 1716 | | − | let Some(status) = self.app.state::<Shared>().now.lock().unwrap().sandbox.clone() else { return }; |
| 1803 | + | let Some(status) = self.app.state::<Shared>().now.held().sandbox.clone() else { return }; |
| 1717 | 1804 | | if !status.per_kind() { |
| 1718 | 1805 | | return; |
| 1719 | 1806 | | } |
| ⋯ |
| 1767 | 1854 | | other => { |
| 1768 | 1855 | | self.token = other; |
| 1769 | 1856 | | if wanted(&self.app).is_none() { |
| 1770 | | − | *self.app.state::<Shared>().wanted.lock().unwrap() = Some(workspace.to_owned()); |
| 1857 | + | *self.app.state::<Shared>().wanted.held() = Some(workspace.to_owned()); |
| 1771 | 1858 | | let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned()); |
| 1772 | 1859 | | } |
| 1773 | 1860 | | return; |
| ⋯ |
| 1884 | 1971 | | /// Keeps what a status says for the tray, settings and the site. |
| 1885 | 1972 | | fn seen(&mut self, status: &Status) { |
| 1886 | 1973 | | let shared = self.app.state::<Shared>(); |
| 1887 | | − | let mut now = shared.now.lock().unwrap(); |
| 1974 | + | let mut now = shared.now.held(); |
| 1888 | 1975 | | now.sandbox = Some(status.clone()); |
| 1889 | 1976 | | if let Some(version) = status.version() { |
| 1890 | 1977 | | now.version = Some(version); |
| ⋯ |
| 1894 | 1981 | | |
| 1895 | 1982 | | /// The sandbox as it is now, while it is the run. |
| 1896 | 1983 | | fn sandbox_now(&mut self, status: Status) { |
| 1897 | | − | let had = self.app.state::<Shared>().now.lock().unwrap().job.clone(); |
| 1984 | + | let had = self.app.state::<Shared>().now.held().job.clone(); |
| 1898 | 1985 | | let Some(run) = self.run.as_mut() else { return }; |
| 1899 | 1986 | | let step = run.step(); |
| 1900 | 1987 | | let job = status.job().map(launcher::Job::said); |
| ⋯ |
| 1990 | 2077 | | let status = launcher::parse(&printed); |
| 1991 | 2078 | | { |
| 1992 | 2079 | | let shared = self.app.state::<Shared>(); |
| 1993 | | − | let mut now = shared.now.lock().unwrap(); |
| 2080 | + | let mut now = shared.now.held(); |
| 1994 | 2081 | | now.starting.clear(); |
| 1995 | 2082 | | now.download = None; |
| 1996 | 2083 | | } |
| ⋯ |
| 2059 | 2146 | | self.not_before = Instant::now() + wait; |
| 2060 | 2147 | | // The cards say why from what `up` said, not from the stopped sandbox. |
| 2061 | 2148 | | if let Some(failed) = status { |
| 2062 | | − | self.app.state::<Shared>().now.lock().unwrap().sandbox = Some(failed); |
| 2149 | + | self.app.state::<Shared>().now.held().sandbox = Some(failed); |
| 2063 | 2150 | | } |
| 2064 | 2151 | | return self.set(code, Extra { detail: Some(cause), fix, wait_seconds: Some(wait.as_secs()), ..Extra::default() }); |
| 2065 | 2152 | | } |