Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

The desktop app no longer freezes as it opens (#49). On Windows it was marked Not Responding a second or two after starting, about one launch in two, and Windows closed it, which looked like a crash. The runner's thread held the tray's runner rows locked while changing their text, which off the main thread waits for the main thread; the main thread meanwhile, answering the site's runner_offer, waited for the same lock. The tray's rows now change on the main thread, handed over without waiting, and the Restart and Developer tools rows are copied out of their locks before they change.

The runner is also kept apart from the app: it starts once the site's page has loaded (or after 20 seconds without it) and 5 seconds more, doing nothing the window could notice before; a panic on its thread restarts it 15 seconds later rather than ending the app; and its state locks are read even after such a panic, where unwrap on a poisoned lock would have ended the app. The desktop guide says when it starts.

Checked:
a memory dump of the hung 0.3.7 (main thread waiting on the static lock inside a WebView2 IPC callback, the g1t-runner thread waiting on the main thread); the installed 0.3.7 hung on 2 of 4 launches, this build on none of 6, its runner starting 9 s after launch; cargo test (47 passing).
G1t-Session:
ses_01m4yd1fj1e7mt7x8kd2hhk00m
syntaqxcommitted Parented6a77aBrowse files
5 files+167−610/5 viewed
+4−1
6363 /// system can close it (not on Windows, whose WebView2 has no way to).
6464 pub fn setting_changed(app: &AppHandle) {
6565 let on = on(app);
66− for item in ITEMS.lock().unwrap().iter() {
66+ // Out of the lock first, as in tray::runner_changed: off the main thread
67+ // set_enabled waits for the main thread.
68+ let items = ITEMS.lock().unwrap().clone();
69+ for item in &items {
6770 let _ = item.set_enabled(usable(app));
6871 }
6972 allow(app, on);
+1−1
152152 tray::install(&handle)?;
153153 links::install(&handle);
154154 updates::schedule(handle.clone());
155− // The runner starts on a thread of its own, a few seconds after the window.
155+ // The runner starts on a thread of its own, once the site's page is up, apart from the app (runner::supervise).
156156 runner::install(&handle);
157157 Ok(())
158158 })
+134−47
66 //!
77 //! - **When.** On by default; settings turn it off ("Run jobs on this
88 //! 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.
1112 //! - **How.** The backend settings choose (backend.rs): Sandboxed, the
1213 //! default, where nothing a job does reaches this computer; Docker; or
1314 //! directly on this computer. The two that are not sandboxes are only
6566 use std::io::{BufRead, BufReader, Read, Write};
6667 use std::path::PathBuf;
6768 use std::process::{Child, Stdio};
68−use std::sync::Mutex;
69+use std::panic::{AssertUnwindSafe, catch_unwind};
6970 use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
71+use std::sync::{Mutex, MutexGuard, PoisonError};
7072 use std::time::{Duration, Instant, SystemTime};
7173
7274 use serde::{Deserialize, Serialize};
8688 /// What the site's page listens for: the app needs a registration token for a workspace.
8789 pub const WANTED_EVENT: &str = "g1t-runner-wanted";
8890
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+}
91113 /// How often the thread looks at the runner when nothing has happened.
92114 const TICK: Duration = Duration::from_secs(5);
93115 /// How long after starting the first look for an update is, then how often.
331353 if let Some(registration) = Registration::load(app).filter(|r| r.server == server) {
332354 return Some(registration.workspace);
333355 }
334− let site = app.state::<Shared>().site.lock().unwrap().clone();
356+ let site = app.state::<Shared>().site.held().clone();
335357 site.current.filter(|slug| site.owned.iter().any(|o| &o.slug == slug)).or_else(|| site.owned.first().map(|o| o.slug.clone()))
336358 }
337359
359381 live: now.live,
360382 starting: now.starting.clone(),
361383 download: now.download.clone(),
362− held: !settings.runner || *app.state::<Shared>().paused.lock().unwrap(),
384+ held: !settings.runner || *app.state::<Shared>().paused.held(),
363385 update: update_facts(app, now, backend),
364386 }
365387 }
369391 /// its program while they run; a runner with no kinds of job takes it
370392 /// itself.
371393 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();
373395 let sandbox = now.sandbox.as_ref().filter(|_| backend == Backend::Sandboxed);
374396 let mut waiting = Vec::new();
375397 if updates.image {
417439
418440 pub fn view(app: &AppHandle) -> View {
419441 let shared = app.state::<Shared>();
420− let now = shared.now.lock().unwrap().clone();
442+ let now = shared.now.held().clone();
421443 let settings = config::settings(app);
422444 let facts = facts(app, &now);
423445 let server = config::server(app);
431453 backend: settings.runner_backend,
432454 link: facts.workspace.as_ref().and_then(|w| server.join(&format!("{w}/-/runners")).ok()).map(|u| u.to_string()),
433455 workspace: facts.workspace,
434− workspaces: shared.site.lock().unwrap().owned.clone(),
456+ workspaces: shared.site.held().owned.clone(),
435457 enabled: settings.runner,
436− paused: *shared.paused.lock().unwrap(),
458+ paused: *shared.paused.held(),
437459 machine: facts.machine,
438460 platform: facts.platform,
439− log: shared.tail.lock().unwrap().iter().cloned().collect(),
461+ log: shared.tail.held().iter().cloned().collect(),
440462 }
441463 }
442464
443465 /// Whether the runner is running a job: what restarts g1t waits for it,
444466 /// since restarting stops the runner and would cut the job short.
445467 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();
447469 now.code == Code::Busy || now.sandbox.as_ref().is_some_and(|status| status.job().is_some())
448470 }
449471
464486 }
465487
466488 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() {
468490 let _ = tx.send(ask);
469491 }
470492 }
472494 /// Starts the thread that keeps the runner, as the app starts.
473495 pub fn install(app: &AppHandle) {
474496 let (tx, rx) = mpsc::channel();
475− *app.state::<Shared>().tx.lock().unwrap() = Some(tx.clone());
497+ *app.state::<Shared>().tx.held() = Some(tx.clone());
476498 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));
478500 if let Err(error) = spawned {
479501 eprintln!("g1t: the runner's thread could not start: {error}");
480502 }
481503 }
482504
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+
483567 /// Settings changed: the thread looks again.
484568 pub fn settings_changed(app: &AppHandle) {
485569 ask(app, Ask::Look);
488572
489573 /// Start or Stop, from settings or the tray.
490574 pub fn set_paused(app: &AppHandle, paused: bool) {
491− *app.state::<Shared>().paused.lock().unwrap() = paused;
575+ *app.state::<Shared>().paused.held() = paused;
492576 // Start with the setting off turns the setting on: that is what was asked.
493577 if !paused && !config::settings(app).runner {
494578 let mut settings = config::settings(app);
508592 }
509593 if on {
510594 settings.runner = true;
511− *app.state::<Shared>().paused.lock().unwrap() = false;
595+ *app.state::<Shared>().paused.held() = false;
512596 }
513597 settings::apply(app, settings)?;
514598 if on {
526610
527611 /// Retry and Restart: now, rather than when the wait is over.
528612 pub fn retry(app: &AppHandle) {
529− *app.state::<Shared>().paused.lock().unwrap() = false;
613+ *app.state::<Shared>().paused.held() = false;
530614 ask(app, Ask::Retry);
531615 }
532616
536620 let next = Site { signed_in: Some(signed_in), owned, current: current.map(|c| c.to_ascii_lowercase()) };
537621 let changed = {
538622 let shared = app.state::<Shared>();
539− let mut site = shared.site.lock().unwrap();
623+ let mut site = shared.site.held();
540624 let changed = *site != next;
541625 *site = next;
542626 changed
572656 "stop" => set_paused(app, true),
573657 "retry" | "restart" => retry(app),
574658 "remove_sandbox" => {
575− *app.state::<Shared>().paused.lock().unwrap() = false;
659+ *app.state::<Shared>().paused.held() = false;
576660 ask(app, Ask::RemoveSandbox);
577661 }
578662 "check_updates" => ask(app, Ask::Check),
657741
658742 /// The workspace a token is wanted for now, if one is: the site asks as it loads.
659743 pub fn wanted(app: &AppHandle) -> Option<String> {
660− app.state::<Shared>().wanted.lock().unwrap().clone()
744+ app.state::<Shared>().wanted.held().clone()
661745 }
662746
663747 /// The runner's log, for the settings window to open.
667751
668752 /// Stops the runner as g1t quits or restarts, and waits for it.
669753 pub fn shutdown(app: &AppHandle) {
670− let tx = app.state::<Shared>().tx.lock().unwrap().take();
754+ let tx = app.state::<Shared>().tx.held().take();
671755 if let Some(tx) = tx {
672756 let (done, wait) = mpsc::channel();
673757 if tx.send(Ask::Quit(done)).is_ok() {
825909 }
826910 }
827911
828− fn keep(mut self, rx: Receiver<Ask>) {
912+ fn keep(mut self, rx: &Receiver<Ask>, asks: Vec<Ask>) {
829913 self.reap();
830914 self.set(Code::Starting, detail("Starting after g1t's window."));
915+ for ask in asks {
916+ self.handle(ask);
917+ }
831918 loop {
832919 match rx.recv_timeout(TICK) {
833920 Ok(Ask::Quit(done)) => {
860947 }
861948
862949 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())
864951 }
865952
866953 /// What an idle runner shows: why its last update failed, until one works.
874961 fn set(&mut self, code: Code, extra: Extra) {
875962 let changed = {
876963 let shared = self.app.state::<Shared>();
877− let mut now = shared.now.lock().unwrap();
964+ let mut now = shared.now.held();
878965 let job = if code == Code::Busy { now.job.clone() } else { None };
879966 let next = Now {
880967 code,
896983 }
897984
898985 fn code(&self) -> Code {
899− self.app.state::<Shared>().now.lock().unwrap().code
986+ self.app.state::<Shared>().now.held().code
900987 }
901988
902989 fn note(&mut self, job: Option<String>, version: Option<String>) {
903990 let shared = self.app.state::<Shared>();
904− let mut now = shared.now.lock().unwrap();
991+ let mut now = shared.now.held();
905992 if job.is_some() {
906993 now.job = job;
907994 }
9141001 fn write_log(&mut self, line: &str) {
9151002 {
9161003 let shared = self.app.state::<Shared>();
917− let mut tail = shared.tail.lock().unwrap();
1004+ let mut tail = shared.tail.held();
9181005 if tail.len() == TAIL {
9191006 tail.pop_front();
9201007 }
9691056 Ask::Checked(found) => self.checked(found),
9701057 Ask::Token { workspace, token } => {
9711058 let shared = self.app.state::<Shared>();
972− let mut wanted = shared.wanted.lock().unwrap();
1059+ let mut wanted = shared.wanted.held();
9731060 if wanted.as_deref() == Some(workspace.as_str()) {
9741061 *wanted = None;
9751062 self.token = Some((workspace, token));
9791066 Ask::Refused { workspace, allowed, why } => {
9801067 let matched = {
9811068 let shared = self.app.state::<Shared>();
982− let mut wanted = shared.wanted.lock().unwrap();
1069+ let mut wanted = shared.wanted.held();
9831070 let matched = wanted.as_deref() == Some(workspace.as_str());
9841071 if matched {
9851072 *wanted = None;
10071094 if let Some(download) = launcher::download(line) {
10081095 let changed = {
10091096 let shared = self.app.state::<Shared>();
1010− let mut now = shared.now.lock().unwrap();
1097+ let mut now = shared.now.held();
10111098 let changed = now.download.as_ref() != Some(&download);
10121099 now.download = Some(download);
10131100 changed
11171204 }
11181205 };
11191206 let shared = self.app.state::<Shared>();
1120− let mut now = shared.now.lock().unwrap();
1207+ let mut now = shared.now.held();
11211208 now.sandbox = stopped.filter(|_| how != Down::Delete);
11221209 now.starting.clear();
11231210 now.download = None;
11921279
11931280 /// Asks the site for a registration token for `workspace`.
11941281 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());
11961283 let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned());
11971284 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);
11991286 self.set(if signed_out { Code::SignedOut } else { Code::Registering }, Extra::default());
12001287 }
12011288
12021289 /// The thread's look round: what should run, and whether it does.
12031290 fn look(&mut self) {
12041291 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();
12061293 if !settings.runner || paused {
12071294 if self.run.is_some() {
12081295 self.halt();
12091296 }
1210− *self.app.state::<Shared>().wanted.lock().unwrap() = None;
1297+ *self.app.state::<Shared>().wanted.held() = None;
12111298 self.set(if paused { Code::Stopped } else { Code::Off }, Extra::default());
12121299 return;
12131300 }
12641351 };
12651352 let Some(workspace) = workspace else {
12661353 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 {
12681355 Some(false) => Code::SignedOut,
12691356 Some(true) => Code::NoWorkspace,
12701357 None => Code::Starting,
14491536 self.program = None;
14501537 }
14511538 // 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();
14531540 let launcher = self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher);
14541541 let image = launcher
14551542 && found.images
15101597 self.not_before = Instant::now();
15111598 return true;
15121599 }
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 };
15141601 if self.program.is_some() {
15151602 let running = native_kinds(&status);
15161603 if running.is_empty() {
16941781 fn now_starting(&self, kinds: Vec<String>) {
16951782 let changed = {
16961783 let shared = self.app.state::<Shared>();
1697− let mut now = shared.now.lock().unwrap();
1784+ let mut now = shared.now.held();
16981785 let changed = now.starting != kinds;
16991786 now.starting = kinds;
17001787 changed
17131800 if !self.run.as_ref().is_some_and(|run| run.engine == Engine::Launcher) {
17141801 return;
17151802 }
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 };
17171804 if !status.per_kind() {
17181805 return;
17191806 }
17671854 other => {
17681855 self.token = other;
17691856 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());
17711858 let _ = self.app.emit_to(EventTarget::webview(frame::SITE), WANTED_EVENT, workspace.to_owned());
17721859 }
17731860 return;
18841971 /// Keeps what a status says for the tray, settings and the site.
18851972 fn seen(&mut self, status: &Status) {
18861973 let shared = self.app.state::<Shared>();
1887− let mut now = shared.now.lock().unwrap();
1974+ let mut now = shared.now.held();
18881975 now.sandbox = Some(status.clone());
18891976 if let Some(version) = status.version() {
18901977 now.version = Some(version);
18941981
18951982 /// The sandbox as it is now, while it is the run.
18961983 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();
18981985 let Some(run) = self.run.as_mut() else { return };
18991986 let step = run.step();
19001987 let job = status.job().map(launcher::Job::said);
19902077 let status = launcher::parse(&printed);
19912078 {
19922079 let shared = self.app.state::<Shared>();
1993− let mut now = shared.now.lock().unwrap();
2080+ let mut now = shared.now.held();
19942081 now.starting.clear();
19952082 now.download = None;
19962083 }
20592146 self.not_before = Instant::now() + wait;
20602147 // The cards say why from what `up` said, not from the stopped sandbox.
20612148 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);
20632150 }
20642151 return self.set(code, Extra { detail: Some(cause), fix, wait_seconds: Some(wait.as_secs()), ..Extra::default() });
20652152 }
+22−8
7979 }
8080
8181 /// The runner changed: its rows, and the icon's tooltip, say what it is doing now.
82+///
83+/// The change is handed to the main thread and not waited for: off the
84+/// main thread `set_text` waits for the main thread to make it, so the
85+/// runner's thread would stall on a busy window, and holding `RUNNER`
86+/// across it while the main thread came here too (the site's
87+/// `runner_offer`) froze the app, the window marked Not Responding.
8288 pub fn runner_changed(app: &AppHandle, view: &crate::runner::View) {
83− if let Some((status, toggle)) = RUNNER.lock().unwrap().as_ref() {
84− let _ = status.set_text(crate::runner::headline(view));
85− let _ = toggle.set_text(toggle_text(view));
86− }
87− if let Some(tray) = app.tray_by_id(TRAY) {
88− let _ = tray.set_tooltip(Some(format!("g1t · {}", crate::runner::headline(view))));
89− }
89+ let headline = crate::runner::headline(view);
90+ let toggle_line = toggle_text(view);
91+ let handle = app.clone();
92+ let _ = app.run_on_main_thread(move || {
93+ let rows = RUNNER.lock().unwrap().clone();
94+ if let Some((status, toggle)) = rows {
95+ let _ = status.set_text(&headline);
96+ let _ = toggle.set_text(toggle_line);
97+ }
98+ if let Some(tray) = handle.tray_by_id(TRAY) {
99+ let _ = tray.set_tooltip(Some(format!("g1t · {headline}")));
100+ }
101+ });
90102 }
91103
92104 /// The update is downloaded: the menus say so, and offer the restart.
93105 pub fn update_ready(version: &str) {
94− for item in RESTART.lock().unwrap().iter() {
106+ // Out of the lock first, as in runner_changed: this runs off the main thread.
107+ let items = RESTART.lock().unwrap().clone();
108+ for item in &items {
95109 let _ = item.set_text(format!("Restart to update to {version}"));
96110 let _ = item.set_enabled(true);
97111 }
+6−4
208208 The app runs a [self-hosted runner](/guides/self-hosted-runners/) for
209209 you while it is open: your workspace's jobs that ask for `self-hosted`
210210 run on your computer, at no cost, and nothing has to be downloaded,
211−registered or started by hand. It starts a few seconds after the window
212−opens, and stops when g1t quits. Time on it is free, as on any of your
211+registered or started by hand. It starts a few seconds after g1t's page has loaded
212+(or after 20 seconds without it), and stops when g1t quits. It runs apart
213+from the window: a runner that fails starts again on its own, and never takes
214+the app down with it. Time on it is free, as on any of your
213215 runners.
214216
215217 To stop it, press **Stop** in [the runner menu](#the-runner-menu), or
261263
262264 | Setting | Default | What it does |
263265 | --- | --- | --- |
264−| **Run jobs on this computer while g1t is open** | On | Starts the runner a few seconds after the window. Off, nothing runs. |
266+| **Run jobs on this computer while g1t is open** | On | Starts the runner a few seconds after g1t's page loads. Off, nothing runs. |
265267 | **How jobs run** | Sandboxed | Sandboxed, in Docker, or directly on this computer. See [how jobs run](#how-jobs-run). |
266268 | **Linux jobs**, **Windows jobs** (sandboxed) | Both on | Each kind of job's card has its own **Stop** and **Start**, which leave the other kind as it is. |
267269 | **Workspace** | The one you were in when it first registered | The workspace whose jobs the runner runs. Lists the workspaces you own. |
320322
321323 | When | The app runs |
322324 | --- | --- |
323−| A few seconds after g1t opens | `g1t-runner sandbox up --json --state-dir <data folder>/sandbox --url <server> --name <computer> --labels desktop`, with the registration token the first time only, in its environment as `G1T_RUNNER_TOKEN`, never on its command line, where anyone on the computer could read it. The first `up` downloads and checks the sandbox's image and makes the VM, which takes a few minutes. |
325+| A few seconds after g1t's page loads | `g1t-runner sandbox up --json --state-dir <data folder>/sandbox --url <server> --name <computer> --labels desktop`, with the registration token the first time only, in its environment as `G1T_RUNNER_TOKEN`, never on its command line, where anyone on the computer could read it. The first `up` downloads and checks the sandbox's image and makes the VM, which takes a few minutes. |
324326 | Every few seconds while it runs | `g1t-runner sandbox status --json`, for the tray, the settings and the Runners page. |
325327 | When g1t quits, or you press **Stop running jobs** | `g1t-runner sandbox down`: a job that is running is cut short. |
326328 | When you press **Stop** on one kind of job's card | `g1t-runner sandbox down --only windows` (or `linux`): that kind stops, a job of its own cut short, and stays off; the other runs on. Every `up` names the kinds that are on, `--only linux,windows`, so one you stopped never starts by itself. |

Comments

Sign in to comment.