A computer whose runner is registered again takes its own runner's name back instead of being refused. When the desktop app's sandbox was made again (a new VM, its registration gone), its runners registered under the names the old sandbox's still held on g1t, were refused with a 409 asking for --replace, and tried again forever. A runner now says which computer it is when it registers: a fingerprint g1t-runner works out from the operating system's machine id (Windows' MachineGuid, macOS's platform UUID, Linux's /etc/machine-id), hashed so the id never leaves the computer, or from --machine / G1T_RUNNER_MACHINE; a container sends none, since its machine id is its image's, and g1t-runner sandbox passes the computer's to the runner in the VM. The actions service keeps it hashed again with the workspace (migration 0022 adds runners.machine_hash), and a registration of a name already taken replaces that runner when it was registered from the same computer, or, for a runner from before fingerprints, when the same person made both tokens and the old runner has been offline for 90 seconds; a runner of the same name from another computer is still refused without --replace, and the refusal says so. The self-hosted runners guide covers registering again and --machine.
9 files+185−90/9 viewed
| 5 | 5 | //! | |
| 6 | 6 | //! | Route | Body | | |
| 7 | 7 | //! | --- | --- | | |
| 8 | − | //! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace`, `isolation` | | |
| 8 | + | //! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace`, `isolation`, `machine` | | |
| 9 | 9 | //! | `POST /runners/{id}/poll` | `version`, `running`, `wait_ms` | | |
| 10 | 10 | //! | `POST /runners/{id}/finished` | `id`, `exit_code`, `reason` | | |
| 11 | 11 | //! | `POST /runners/{id}/remove` | nothing | | |
| ⋯ | |||
| 57 | 57 | "group": body["group"].as_str(), | |
| 58 | 58 | "replace": body["replace"].as_bool().unwrap_or(false), | |
| 59 | 59 | "isolation": body["isolation"].as_str(), | |
| 60 | + | "machine": body["machine"].as_str(), | |
| 60 | 61 | }); | |
| 61 | 62 | return Ok(Some(answer(services, "runner_register", &args).await?)); | |
| 62 | 63 | } | |
| 551 | 551 | | `--job-docker-image` | The image a job's own Docker Engine runs. `docker:28-dind` if left out. | | |
| 552 | 552 | | `--work-dir` | Where jobs run with `--no-docker` keep their files. `~/.g1t-runner/work` if left out. | | |
| 553 | 553 | | `--dir` | Where the runner keeps its configuration and credential. `~/.g1t-runner`, or `G1T_RUNNER_DIR`, if left out. Every command takes it. | | |
| 554 | − | | `--replace` | Take the place of a runner with the same name. | | |
| 554 | + | | `--replace` | Take the place of a runner with the same name, registered from any computer. A runner registering again from the computer it registered from takes its name back without it ([registering again](#registering-again)). | | |
| 555 | + | | `--machine` | Which computer this is, as 32 to 128 hex digits, for a runner that can't tell ([registering again](#registering-again)). Worked out from the operating system if left out. | | |
| 555 | 556 | | `--no-auto-update` | Never update itself. | | |
| 556 | 557 | ||
| 558 | + | ### Registering again | |
| 559 | + | ||
| 560 | + | A runner says which computer it is when it registers: a fingerprint made | |
| 561 | + | from the operating system's machine id (Windows' `MachineGuid`, macOS's | |
| 562 | + | platform UUID, Linux's `/etc/machine-id`), hashed so the id itself never | |
| 563 | + | leaves the computer, and kept by g1t hashed again with your workspace. When | |
| 564 | + | a computer registers a runner under a name one of its own runners already | |
| 565 | + | has, because its sandbox was made again or its `--dir` was lost, the new | |
| 566 | + | registration takes the old one's place, with no `--replace`. A runner of | |
| 567 | + | the same name from another computer is left alone. | |
| 568 | + | ||
| 569 | + | A runner in a container can't tell which computer it is on, since its | |
| 570 | + | machine id is its image's, so it sends no fingerprint unless given one | |
| 571 | + | with `--machine` or `G1T_RUNNER_MACHINE`. `g1t-runner sandbox` and the | |
| 572 | + | desktop app pass the computer's to the runner in the sandbox. | |
| 573 | + | ||
| 574 | + | A runner registered before runners sent a fingerprint is taken over when | |
| 575 | + | the person who made the new registration token made its token too, and it | |
| 576 | + | has been offline for more than 90 seconds. | |
| 577 | + | ||
| 557 | 578 | To run several runners on one machine, give each its own `--dir` and | |
| 558 | 579 | `--name`. | |
| 559 | 580 | ||
| ⋯ | |||
| 793 | 814 | | The job says *Waiting for a self-hosted runner with labels …* | No online runner has every one of those labels in a group that allows the repository. Compare the labels on the **Runners** page, check the runner's group, or start the runner. | | |
| 794 | 815 | | *Pull requests from forks do not run on self-hosted runners here.* | The run is from a fork. Allow it under **Where work runs**, if you trust everyone who can open a pull request. | | |
| 795 | 816 | | `register` says the token is not valid | It expired after an hour, or was mistyped. Make a new one. | | |
| 796 | − | | `register` says a runner with that name is registered | Choose another `--name`, or add `--replace`. | | |
| 817 | + | | `register` says a runner with that name is registered | Another computer's runner has that name, or one registered before the computer was known. Choose another `--name`, or add `--replace`. | | |
| 797 | 818 | | `run` says *g1t no longer knows this runner* | It was removed, or its credential was. Register it again. | | |
| 798 | 819 | | *this runner is not sandboxed from its machine, so it will not take work* | The runner's container can reach the machine: it lists why. Remove the machine's Docker socket and `--privileged`, and run it with `--runtime=sysbox-runc`. See [run it as a container](#run-it-as-a-container). | | |
| 799 | 820 | | *the runner's own Docker Engine could not start* | The container is not running under Sysbox. Add `--runtime=sysbox-runc`, and check that Sysbox is installed (`docker info` lists `sysbox-runc` under runtimes). | | |
| 525 | 525 | /// runners leave it out. | |
| 526 | 526 | #[serde(default)] | |
| 527 | 527 | pub isolation: Option<String>, | |
| 528 | + | /// Which computer it is: a fingerprint the runner derives from its | |
| 529 | + | /// operating system's machine id, never the id itself. A runner of the | |
| 530 | + | /// same name registered from the same computer is taken over without | |
| 531 | + | /// `replace`, so a computer whose sandbox was made again registers | |
| 532 | + | /// again by itself. Older runners, and those in a container that can't | |
| 533 | + | /// tell, leave it out. | |
| 534 | + | #[serde(default)] | |
| 535 | + | pub machine: Option<String>, | |
| 528 | 536 | } | |
| 529 | 537 | ||
| 530 | 538 | /// The isolation levels a runner may say it has (`RegisterArgs::isolation`). |
| 79 | 79 | pub labels: Vec<String>, | |
| 80 | 80 | #[serde(default)] | |
| 81 | 81 | pub group: Option<String>, | |
| 82 | + | /// The computer's fingerprint (`machine_fingerprint`), which the | |
| 83 | + | /// runner in the VM can't work out for itself. | |
| 84 | + | #[serde(default)] | |
| 85 | + | pub machine: Option<String>, | |
| 82 | 86 | /// The runner's container's share of the VM, and so its jobs'. | |
| 83 | 87 | #[serde(default)] | |
| 84 | 88 | pub cpus: u32, | |
| ⋯ | |||
| 237 | 241 | if let Some(group) = &settings.group { | |
| 238 | 242 | lines.push(format!("G1T_RUNNER_GROUP={group}")); | |
| 239 | 243 | } | |
| 244 | + | if let Some(machine) = &settings.machine { | |
| 245 | + | lines.push(format!("G1T_RUNNER_MACHINE={machine}")); | |
| 246 | + | } | |
| 240 | 247 | if lines.iter().any(|l| l.contains('\n') || l.contains('\r')) { | |
| 241 | 248 | bail!("a registration setting has a line break in it"); | |
| 242 | 249 | } | |
| ⋯ | |||
| 388 | 395 | if given.group.is_some() { | |
| 389 | 396 | settings.group = given.group; | |
| 390 | 397 | } | |
| 398 | + | if given.machine.is_some() { | |
| 399 | + | settings.machine = given.machine; | |
| 400 | + | } | |
| 391 | 401 | if given.cpus > 0 { | |
| 392 | 402 | settings.cpus = given.cpus; | |
| 393 | 403 | } | |
| ⋯ | |||
| 916 | 926 | }; | |
| 917 | 927 | let env = register_env(&settings).unwrap(); | |
| 918 | 928 | assert_eq!(env, "G1T_RUNNER_URL=https://g1t.sh\nG1T_RUNNER_TOKEN=g1trt_x\nG1T_RUNNER_NAME=box-linux\nG1T_RUNNER_LABELS=gpu\n"); | |
| 929 | + | // The computer's fingerprint, which the runner in the VM can't work out. | |
| 930 | + | settings.machine = Some("ab".repeat(32)); | |
| 931 | + | assert!(register_env(&settings).unwrap().ends_with(&format!("G1T_RUNNER_MACHINE={}\n", "ab".repeat(32)))); | |
| 919 | 932 | settings.name = Some("a\nG1T_RUNNER_URL=https://evil".into()); | |
| 920 | 933 | assert!(register_env(&settings).is_err()); | |
| 921 | 934 | assert!(register_env(&Settings::default()).is_err()); | |
| 970 | 970 | name: Some(format!("{}-linux", base_name(saved))), | |
| 971 | 971 | labels: saved.labels.clone(), | |
| 972 | 972 | group: saved.group.clone(), | |
| 973 | + | machine: super::machine_fingerprint(), | |
| 973 | 974 | cpus: saved.resources.cpus, | |
| 974 | 975 | memory_mib: saved.resources.memory_mib, | |
| 975 | 976 | runner_image, | |
| ⋯ | |||
| 1161 | 1162 | name: Some(format!("{}-linux", base_name(saved))), | |
| 1162 | 1163 | labels: saved.labels.clone(), | |
| 1163 | 1164 | group: saved.group.clone(), | |
| 1165 | + | machine: super::machine_fingerprint(), | |
| 1164 | 1166 | ..Settings::default() | |
| 1165 | 1167 | })?; | |
| 1166 | 1168 | if was_running { | |
| 108 | 108 | name: given.name.or(merged.name), | |
| 109 | 109 | labels: if given.labels.is_empty() { merged.labels } else { given.labels }, | |
| 110 | 110 | group: given.group.or(merged.group), | |
| 111 | + | machine: given.machine.or(merged.machine), | |
| 111 | 112 | cpus: if given.cpus > 0 { given.cpus } else { merged.cpus }, | |
| 112 | 113 | memory_mib: if given.memory_mib > 0 { given.memory_mib } else { merged.memory_mib }, | |
| 113 | 114 | runner_image: if given.runner_image.is_empty() { merged.runner_image } else { given.runner_image }, |
| 148 | 148 | } | |
| 149 | 149 | ||
| 150 | 150 | /// The options that take a value. | |
| 151 | − | const VALUE_OPTIONS: [&str; 13] = | |
| 152 | − | ["url", "api", "token", "name", "labels", "group", "work-dir", "image", "harness", "memory", "cpus", "pids-limit", "job-docker-image"]; | |
| 151 | + | const VALUE_OPTIONS: [&str; 14] = | |
| 152 | + | ["url", "api", "token", "name", "labels", "group", "work-dir", "image", "harness", "memory", "cpus", "pids-limit", "job-docker-image", "machine"]; | |
| 153 | 153 | ||
| 154 | 154 | const HELP: &str = "g1t-runner: a self-hosted runner for g1t. | |
| 155 | 155 | ||
| ⋯ | |||
| 259 | 259 | .unwrap_or_else(|| "runner".to_owned()) | |
| 260 | 260 | } | |
| 261 | 261 | ||
| 262 | + | /// Which computer this is, for g1t to know it when it registers again (its | |
| 263 | + | /// sandbox made again, its state lost) and let it take its runners' names | |
| 264 | + | /// back: a hash of the operating system's machine id, never the id itself. | |
| 265 | + | /// None in a container, whose machine id is its image's and so shared by | |
| 266 | + | /// every container made from it (the sandbox's launcher passes the | |
| 267 | + | /// computer's as `G1T_RUNNER_MACHINE`), and where the OS has none. | |
| 268 | + | pub fn machine_fingerprint() -> Option<String> { | |
| 269 | + | use sha2::{Digest, Sha256}; | |
| 270 | + | let id = os_machine_id()?; | |
| 271 | + | Some(hex::encode(Sha256::digest(format!("g1t-runner machine\n{id}")))) | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | fn os_machine_id() -> Option<String> { | |
| 275 | + | let id = if cfg!(windows) { | |
| 276 | + | // HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid, set when Windows is installed. | |
| 277 | + | let out = hidden(std::process::Command::new("reg").args(["query", r"HKLM\SOFTWARE\Microsoft\Cryptography", "/v", "MachineGuid"])).output().ok()?; | |
| 278 | + | String::from_utf8_lossy(&out.stdout).lines().find(|l| l.contains("MachineGuid"))?.split_whitespace().last()?.to_owned() | |
| 279 | + | } else if cfg!(target_os = "macos") { | |
| 280 | + | let out = std::process::Command::new("ioreg").args(["-rd1", "-c", "IOPlatformExpertDevice"]).output().ok()?; | |
| 281 | + | let text = String::from_utf8_lossy(&out.stdout).into_owned(); | |
| 282 | + | text.lines().find(|l| l.contains("\"IOPlatformUUID\""))?.rsplit('"').nth(1)?.to_owned() | |
| 283 | + | } else { | |
| 284 | + | if Path::new("/.dockerenv").exists() || Path::new("/run/.containerenv").exists() { | |
| 285 | + | return None; | |
| 286 | + | } | |
| 287 | + | ["/etc/machine-id", "/var/lib/dbus/machine-id"].iter().find_map(|path| std::fs::read_to_string(path).ok())? | |
| 288 | + | }; | |
| 289 | + | let id = id.trim().to_ascii_lowercase(); | |
| 290 | + | (id.len() >= 8 && id.chars().any(|c| c != '0' && c != '-')).then_some(id) | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | /// No console window for a helper the runner runs on Windows. | |
| 294 | + | fn hidden(command: &mut std::process::Command) -> &mut std::process::Command { | |
| 295 | + | #[cfg(windows)] | |
| 296 | + | { | |
| 297 | + | use std::os::windows::process::CommandExt; | |
| 298 | + | const CREATE_NO_WINDOW: u32 = 0x0800_0000; | |
| 299 | + | command.creation_flags(CREATE_NO_WINDOW); | |
| 300 | + | } | |
| 301 | + | command | |
| 302 | + | } | |
| 303 | + | ||
| 262 | 304 | /// A runner's name from a machine's: what g1t accepts. | |
| 263 | 305 | fn runner_name(given: &str) -> String { | |
| 264 | 306 | let name: String = given | |
| ⋯ | |||
| 325 | 367 | "group": options.value("group"), | |
| 326 | 368 | "replace": options.flag("replace"), | |
| 327 | 369 | "isolation": isolation.as_str(), | |
| 370 | + | "machine": options.value("machine").map(str::to_owned).or_else(machine_fingerprint), | |
| 328 | 371 | }), | |
| 329 | 372 | )?; | |
| 330 | 373 | let work_dir = options.value("work-dir").map(PathBuf::from).unwrap_or_else(|| folder.join("work")); | |
| 1 | + | -- Which computer a self-hosted runner registered from, as a hash of the | |
| 2 | + | -- fingerprint it sent: a runner of the same name registered again from the | |
| 3 | + | -- same computer takes the old one's place without --replace. Null for | |
| 4 | + | -- runners that registered before runners said so. | |
| 5 | + | ALTER TABLE runners ADD COLUMN machine_hash TEXT; |
| 82 | 82 | /// for a runner that never said. | |
| 83 | 83 | #[serde(default)] | |
| 84 | 84 | pub isolation: Option<String>, | |
| 85 | + | /// Which computer it registered from (migration 0022): a hash of the | |
| 86 | + | /// fingerprint it sent, with its workspace; none for a runner that | |
| 87 | + | /// never said. | |
| 88 | + | #[serde(default)] | |
| 89 | + | pub machine_hash: Option<String>, | |
| 85 | 90 | } | |
| 86 | 91 | ||
| 87 | 92 | impl RunnerRow { | |
| ⋯ | |||
| 194 | 199 | sha256_hex(secret) | |
| 195 | 200 | } | |
| 196 | 201 | ||
| 202 | + | /// What a runner's machine fingerprint is kept as: hashed with its | |
| 203 | + | /// workspace, so the same computer is not recognisable across workspaces. | |
| 204 | + | /// A fingerprint is 32 to 128 hex digits; anything else counts as none. | |
| 205 | + | fn machine_hash(workspace: &str, machine: Option<&str>) -> Option<String> { | |
| 206 | + | let machine = machine.map(str::trim).filter(|m| (32..=128).contains(&m.len()) && m.chars().all(|c| c.is_ascii_hexdigit()))?; | |
| 207 | + | Some(hash(&format!("{workspace}\n{}", machine.to_ascii_lowercase()))) | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | /// Whether a registration takes the place of `existing`, a runner of the | |
| 211 | + | /// same name: when asked to (`--replace`); when it is the same computer | |
| 212 | + | /// registering again (its sandbox made again, its state lost); or, for a | |
| 213 | + | /// runner from before runners said which computer they are, when the same | |
| 214 | + | /// person registered it and it is no longer heard from. | |
| 215 | + | fn takes_over(existing: &RunnerRow, replace: bool, machine_hash: Option<&str>, created_by: Option<&str>, now: u64) -> bool { | |
| 216 | + | let same_machine = machine_hash.is_some() && existing.machine_hash.as_deref() == machine_hash; | |
| 217 | + | let left_behind = existing.machine_hash.is_none() && created_by.is_some() && existing.created_by.as_deref() == created_by && !existing.online(now); | |
| 218 | + | replace || same_machine || left_behind | |
| 219 | + | } | |
| 220 | + | ||
| 197 | 221 | /// The actor of a runner's own entries in the audit log. | |
| 198 | 222 | fn runner_actor(runner: &RunnerRow) -> AuditActor { | |
| 199 | 223 | AuditActor { | |
| ⋯ | |||
| 1010 | 1034 | .bind(&[workspace.as_str().into(), registration.repo_id.clone().unwrap_or_default().into(), name.as_str().into()])? | |
| 1011 | 1035 | .first::<RunnerRow>(None) | |
| 1012 | 1036 | .await?; | |
| 1037 | + | let machine_hash = machine_hash(&workspace, a.machine.as_deref()); | |
| 1013 | 1038 | if let Some(existing) = &existing { | |
| 1014 | − | if !a.replace { | |
| 1039 | + | if !takes_over(existing, a.replace, machine_hash.as_deref(), registration.created_by.as_deref(), at) { | |
| 1040 | + | let elsewhere = if existing.machine_hash.is_some() && machine_hash.is_some() { " from another computer" } else { "" }; | |
| 1015 | 1041 | return Ok(fail( | |
| 1016 | 1042 | FailureCode::Conflict, | |
| 1017 | − | format!("A runner called {name} is already registered here. Register with --replace to take its place, or choose another --name."), | |
| 1043 | + | format!("A runner called {name} is already registered here{elsewhere}. Register with --replace to take its place, or choose another --name."), | |
| 1018 | 1044 | )); | |
| 1019 | 1045 | } | |
| 1020 | 1046 | self.forget_runner(existing, &format!("The runner {name} was replaced by a new registration.")).await?; | |
| ⋯ | |||
| 1029 | 1055 | self.db | |
| 1030 | 1056 | .prepare( | |
| 1031 | 1057 | "INSERT INTO runners (id, workspace, repo_id, repo, group_id, name, labels, os, arch, version, ephemeral, credential_hash, | |
| 1032 | − | rotated_at, last_seen_at, created_at, created_by, isolation) | |
| 1033 | − | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
| 1058 | + | rotated_at, last_seen_at, created_at, created_by, isolation, machine_hash) | |
| 1059 | + | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
| 1034 | 1060 | ) | |
| 1035 | 1061 | .bind(&[ | |
| 1036 | 1062 | id.as_str().into(), | |
| ⋯ | |||
| 1051 | 1077 | // The person who made the token: a desktop's runner is theirs. | |
| 1052 | 1078 | optional(registration.created_by.as_deref()), | |
| 1053 | 1079 | optional(isolation), | |
| 1080 | + | optional(machine_hash.as_deref()), | |
| 1054 | 1081 | ])? | |
| 1055 | 1082 | .run() | |
| 1056 | 1083 | .await?; | |
| ⋯ | |||
| 1985 | 2012 | created_at: String::new(), | |
| 1986 | 2013 | created_by: None, | |
| 1987 | 2014 | isolation: isolation.map(str::to_owned), | |
| 2015 | + | machine_hash: None, | |
| 1988 | 2016 | }; | |
| 1989 | 2017 | let wanted = Wanted::of(&["self-hosted".to_owned(), "linux".to_owned()], None); | |
| 1990 | 2018 | let takes = |row: &RunnerRow| takes_public_job(row, &[], "rep_1", "acme/web", &wanted, at); | |
| ⋯ | |||
| 2021 | 2049 | created_at: String::new(), | |
| 2022 | 2050 | created_by: None, | |
| 2023 | 2051 | isolation: None, | |
| 2052 | + | machine_hash: None, | |
| 2024 | 2053 | }; | |
| 2025 | 2054 | assert!(row.online(1_000_000_000 + 60_000)); | |
| 2026 | 2055 | assert!(!row.online(1_000_000_000 + 120_000)); | |
| 2027 | 2056 | assert_eq!(row.workspace, "acme"); | |
| 2028 | 2057 | } | |
| 2058 | + | ||
| 2059 | + | #[test] | |
| 2060 | + | fn a_machine_fingerprint_is_kept_per_workspace_and_only_when_it_is_one() { | |
| 2061 | + | let print = "ab".repeat(32); | |
| 2062 | + | assert_eq!(machine_hash("acme", Some(&print)), machine_hash("acme", Some(&print.to_ascii_uppercase()))); | |
| 2063 | + | assert_ne!(machine_hash("acme", Some(&print)), machine_hash("other", Some(&print))); | |
| 2064 | + | assert_eq!(machine_hash("acme", Some("SYNTAQX")), None); | |
| 2065 | + | assert_eq!(machine_hash("acme", Some("abc")), None); | |
| 2066 | + | assert_eq!(machine_hash("acme", None), None); | |
| 2067 | + | } | |
| 2068 | + | ||
| 2069 | + | #[test] | |
| 2070 | + | fn the_same_computer_takes_its_runners_name_back() { | |
| 2071 | + | let at = 1_000_000_000; | |
| 2072 | + | let ours = machine_hash("acme", Some(&"ab".repeat(32))); | |
| 2073 | + | let theirs = machine_hash("acme", Some(&"cd".repeat(32))); | |
| 2074 | + | let row = |machine_hash: Option<String>, created_by: Option<&str>, seen: u64| RunnerRow { | |
| 2075 | + | id: "rnr_1".into(), | |
| 2076 | + | workspace: "acme".into(), | |
| 2077 | + | repo_id: None, | |
| 2078 | + | repo: None, | |
| 2079 | + | group_id: None, | |
| 2080 | + | name: "box-linux".into(), | |
| 2081 | + | labels: "[]".into(), | |
| 2082 | + | os: "linux".into(), | |
| 2083 | + | arch: "x64".into(), | |
| 2084 | + | version: String::new(), | |
| 2085 | + | ephemeral: 0, | |
| 2086 | + | credential_hash: String::new(), | |
| 2087 | + | previous_hash: None, | |
| 2088 | + | rotated_at: String::new(), | |
| 2089 | + | work_id: None, | |
| 2090 | + | work_kind: None, | |
| 2091 | + | spent: 0, | |
| 2092 | + | last_seen_at: Some(rfc3339(seen)), | |
| 2093 | + | created_at: String::new(), | |
| 2094 | + | created_by: created_by.map(str::to_owned), | |
| 2095 | + | isolation: None, | |
| 2096 | + | machine_hash, | |
| 2097 | + | }; | |
| 2098 | + | // The same computer, even while the old one still looks online. | |
| 2099 | + | assert!(takes_over(&row(ours.clone(), Some("u1"), at), false, ours.as_deref(), Some("u1"), at)); | |
| 2100 | + | // Another computer of the same name: only when asked to. | |
| 2101 | + | assert!(!takes_over(&row(theirs.clone(), Some("u1"), 0), false, ours.as_deref(), Some("u1"), at)); | |
| 2102 | + | assert!(takes_over(&row(theirs, Some("u1"), at), true, ours.as_deref(), Some("u1"), at)); | |
| 2103 | + | // One from before fingerprints: the same person's, gone quiet. | |
| 2104 | + | assert!(takes_over(&row(None, Some("u1"), 0), false, ours.as_deref(), Some("u1"), at)); | |
| 2105 | + | assert!(!takes_over(&row(None, Some("u1"), at), false, ours.as_deref(), Some("u1"), at)); | |
| 2106 | + | assert!(!takes_over(&row(None, Some("u2"), 0), false, ours.as_deref(), Some("u1"), at)); | |
| 2107 | + | assert!(!takes_over(&row(None, None, 0), false, ours.as_deref(), None, at)); | |
| 2108 | + | // A runner without a fingerprint never matches one without. | |
| 2109 | + | assert!(!takes_over(&row(None, Some("u1"), at), false, None, Some("u1"), at)); | |
| 2110 | + | } | |
| 2029 | 2111 | } | |