From 00e8c2d1b44da5ce079c3003475046e5302f9422 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:31:38 +0000 Subject: [PATCH] Remote jobs: cards_leave_off, a run job's --cards-off cards restored as OFF on any exit (persisted by the app's own card path) The way to hold a card out of mining past a job without a script on api/cards (the 6 October rule): the runner's hold is built with enabled=false (identities and cap kept), the report line says LEFT OFF, publish-jobs.sh carries --cards-leave-off. For the Arc B580 on PC 1 while its worker fix rides to the shipped app. Test: cards_leave_off_restores_the_card_as_off_with_its_settings_kept (igneum-app 157 of 157 on the box). Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 9e794503d2e9689ae3a0996e703cb97ea6d95cef) --- app/igneum-app/src/jobrun.rs | 33 ++++++++++++++++++++++++++++++++- app/igneum-app/src/jobs.rs | 4 +++- packaging/ota/publish-jobs.sh | 9 ++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/app/igneum-app/src/jobrun.rs b/app/igneum-app/src/jobrun.rs index a6a1d26f..c25e29eb 100644 --- a/app/igneum-app/src/jobrun.rs +++ b/app/igneum-app/src/jobrun.rs @@ -137,6 +137,12 @@ pub fn key_without_index(k: &str) -> String { } } +/// The restore list of a `cards_leave_off` job: the same entries (identities and cap kept) with enabled=false, so the +/// runner's restore on any exit leaves the card off, persisted by the app's own card path. +pub fn leave_off(restore: Vec) -> Vec { + restore.into_iter().map(|mut c| { c.enabled = false; c }).collect() +} + /// One live card as `cards_off_choices` sees it: key, enabled, identities, power_pct, present. pub type LiveCard = (String, bool, u32, u32, bool); @@ -605,6 +611,10 @@ impl Jobs { return None; } a.waiting_for_cards = false; + // cards_leave_off (7 October 2026, intel-arc): the hold still ends through the app's own card path on any + // exit, but with enabled=false, so the card stays off and persisted (settings.json) after the job: the way + // to hold a card out of mining past a job without a script touching api/cards (the 6 October rule). + let restore = if a.job.bool_param("cards_leave_off") { leave_off(restore) } else { restore }; a.cards = CardHold::hold(restore); if a.waiting_for_miners { return Some(Action::StopMiners(format!("job {}: {}", a.job.id, a.job.label()))); @@ -630,7 +640,11 @@ impl Jobs { if held.is_empty() { sink.line(&format!("cards-off: {} asked, no present card matched (nothing switched; the script's card may be loaded)", asked.join(","))); } else { - sink.line(&format!("cards-off: {} switched off by the runner before this script; cards restored: {} by the runner on any exit (done, failed, timeout, aborted, app quit), with their own enabled flag, identities and cap", held.keys(), held.keys())); + if job.bool_param("cards_leave_off") { + sink.line(&format!("cards-off: {} switched off by the runner before this script; cards LEFT OFF: {} by the runner on any exit (done, failed, timeout, aborted, app quit), enabled=false with their own identities and cap, persisted by the app (cards_leave_off)", held.keys(), held.keys())); + } else { + sink.line(&format!("cards-off: {} switched off by the runner before this script; cards restored: {} by the runner on any exit (done, failed, timeout, aborted, app quit), with their own enabled flag, identities and cap", held.keys(), held.keys())); + } } } let ctx = account_context(); @@ -1434,6 +1448,23 @@ fn collect_done(uploaded: u32, failed: u32, names: Vec, ran: Option mod tests { use super::*; + /// cards_leave_off (7 October 2026): the restore entries keep their identities and cap and carry enabled=false, + /// so the job's exit leaves the card off through the same path; without the param they keep their own flag. + #[test] + fn cards_leave_off_restores_the_card_as_off_with_its_settings_kept() { + let live: Vec = vec![("other:Intel(R) Arc(TM) B580 Graphics".into(), true, 8, 0, true), ("nvidia:NVIDIA GeForce RTX 5090".into(), true, 2, 80, true)]; + let (off, restore) = cards_off_choices(&["other:Intel(R) Arc(TM) B580 Graphics".to_string()], &live); + assert_eq!(off.len(), 1); + assert!(restore[0].enabled, "the plain restore puts the card back on"); + let left = leave_off(restore.clone()); + assert_eq!(left.len(), 1); + assert_eq!((left[0].key.as_str(), left[0].enabled, left[0].identities, left[0].power_pct), ("other:Intel(R) Arc(TM) B580 Graphics", false, 8, restore[0].power_pct)); + // the job's param decides, through the same hold + let j = jobs::parse(r#"{"jobs":[{"id":"x","kind":"run","expires_at":"2099-01-01T00:00:00Z","target":{"machine_ids":["ae432dc7"]},"params":{"script":"ls","cards_off":["other:Intel(R) Arc(TM) B580 Graphics"],"cards_leave_off":true}}]}"#).unwrap().jobs.remove(0); + assert!(j.bool_param("cards_leave_off")); + assert_eq!(j.list_param("cards_off"), vec!["other:Intel(R) Arc(TM) B580 Graphics".to_string()]); + } + #[test] fn cards_off_matches_keys_with_and_without_the_device_index_and_restores_exactly() { // PC 1, 6 October 2026: settings.json holds amd:1:gfx1201 and amd:3:gfx1201, the live state's key is amd:gfx1201 diff --git a/app/igneum-app/src/jobs.rs b/app/igneum-app/src/jobs.rs index e54805bf..20662e2a 100644 --- a/app/igneum-app/src/jobs.rs +++ b/app/igneum-app/src/jobs.rs @@ -22,7 +22,9 @@ //! //! Kinds and their params: //! run script (the body), shell powershell|bash (default per platform), elevated, stop_miners_first, -//! timeout_minutes (default 60, at most 600) +//! timeout_minutes (default 60, at most 600), cards_off [keys] (the runner switches them off before +//! the script and restores them on any exit), cards_leave_off (with cards_off: restored as OFF, so +//! the card stays off and persisted after the job; 7 October 2026, the Arc on PC 1) //! fetch url (https), sha256, size, to (file name), dir jobs|prove|packs|updates (default jobs, which is //! /app/jobs//), extract (tar -xf into the dir), fresh (empty extract_dir first), extract_dir //! collect globs ["logs/app-*.log", ...] relative to the app data root (* and ? per path component), command diff --git a/packaging/ota/publish-jobs.sh b/packaging/ota/publish-jobs.sh index 9f12b794..3d8146e2 100755 --- a/packaging/ota/publish-jobs.sh +++ b/packaging/ota/publish-jobs.sh @@ -10,7 +10,9 @@ # as run_id job-- (read back with tools/jobs.mjs). # # packaging/ota/publish-jobs.sh add --kind run --target 1ccfe586 --script path.ps1 [--elevated] [--stop-miners] \ -# [--cards-off key,key] [--timeout-minutes 60] [--shell powershell|bash] --title "..." [--expires-hours 48] [--deploy] +# [--cards-off key,key] [--cards-leave-off] [--timeout-minutes 60] [--shell powershell|bash] --title "..." [--expires-hours 48] [--deploy] +# (--cards-leave-off, with --cards-off: the runner restores the cards as OFF on any exit, so they stay off and +# persisted by the app after the job; the way to hold a card out of mining without a script on api/cards, 7 October 2026) # (--cards-off: the RUNNER switches these cards off through the app's own card path before the script and puts them # back exactly on any exit, done, failed, timeout, aborted or the app quitting; keys with or without the device # index; a script never posts to /api/cards itself: tools/ci/playbook-quit-check.sh, 6 October 2026) @@ -52,7 +54,7 @@ SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign" CMD="${1:-}"; [ $# -gt 0 ] && shift KIND="" TARGET="" PLATFORM="" REQUIRES="" REQUIRES_SET=0 ID="" TITLE="" EXPIRES_H="48" DEPLOY=0 BASE="" DEST="" TRIES=12 -SCRIPT="" SHELL_KIND="" ELEVATED=0 STOP_MINERS=0 TIMEOUT_MIN="" CARDS_OFF="" +SCRIPT="" SHELL_KIND="" ELEVATED=0 STOP_MINERS=0 TIMEOUT_MIN="" CARDS_OFF="" CARDS_LEAVE_OFF=0 FILE="" URL="" SHA="" SIZE="" DIR="" TO="" EXTRACT=0 EXTRACT_DIR="" FRESH=0 GLOBS=() COMMAND="" WHAT="" ZIP="" FIXTURES="" CAP_MIN="" DISTRO="" WSL_USER="" REMOVE_ID="" @@ -74,6 +76,7 @@ while [ $# -gt 0 ]; do --elevated) ELEVATED=1; shift ;; --stop-miners) STOP_MINERS=1; shift ;; --cards-off) CARDS_OFF="$2"; shift 2 ;; + --cards-leave-off) CARDS_LEAVE_OFF=1; shift ;; --timeout-minutes) TIMEOUT_MIN="$2"; shift 2 ;; --file) FILE="$2"; shift 2 ;; --url) URL="$2"; shift 2 ;; @@ -277,7 +280,7 @@ if [ "$CMD" = add ]; then fi out="$(bash "$ROOT/tools/ci/prover-socket-check.sh" "$SCRIPT" 2>&1)" || { echo "run: prover-socket-check.sh refuses $SCRIPT:" >&2; printf '%s\n' "$out" >&2; exit 1; } out="$(bash "$ROOT/tools/ci/kit-path-check.sh" "$SCRIPT" 2>&1)" || { echo "run: kit-path-check.sh refuses $SCRIPT:" >&2; printf '%s\n' "$out" >&2; exit 1; } - PARAMS="$(python3 -c 'import json,sys; print(json.dumps({"script": open(sys.argv[1]).read(), "shell": sys.argv[2], "elevated": sys.argv[3]=="1", "stop_miners_first": sys.argv[4]=="1", **({"timeout_minutes": int(sys.argv[5])} if sys.argv[5] else {}), **({"cards_off": [k.strip() for k in sys.argv[6].split(",") if k.strip()]} if sys.argv[6] else {})}))' "$SCRIPT" "$SHELL_KIND" "$ELEVATED" "$STOP_MINERS" "$TIMEOUT_MIN" "$CARDS_OFF")" + PARAMS="$(python3 -c 'import json,sys; print(json.dumps({"script": open(sys.argv[1]).read(), "shell": sys.argv[2], "elevated": sys.argv[3]=="1", "stop_miners_first": sys.argv[4]=="1", **({"timeout_minutes": int(sys.argv[5])} if sys.argv[5] else {}), **({"cards_off": [k.strip() for k in sys.argv[6].split(",") if k.strip()]} if sys.argv[6] else {}), **({"cards_leave_off": True} if sys.argv[7]=="1" else {})}))' "$SCRIPT" "$SHELL_KIND" "$ELEVATED" "$STOP_MINERS" "$TIMEOUT_MIN" "$CARDS_OFF" "$CARDS_LEAVE_OFF")" [ -n "$TITLE" ] || TITLE="run $(basename "$SCRIPT")" ;; fetch)