{"record":{"id":"05958e757782b3c4","repo":"Hmbown/CodeWhale","slug":"another-pet-owner-is-running","errorCode":null,"errorMessage":"Another pet owner is running","messagePattern":"Another pet owner is running","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/owner.rs","lineNumber":301,"sourceCode":"// startup line and stop reason are the operator's only output.\n#[allow(clippy::print_stdout, clippy::print_stderr)]\npub fn serve(root: PathBuf, requested_port: u16) -> anyhow::Result<()> {\n    std::fs::create_dir_all(&root)?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::{MetadataExt, PermissionsExt};\n        let metadata = std::fs::symlink_metadata(&root)?;\n        if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {\n            anyhow::bail!(\"Pet directory must belong to this user and cannot be a link\");\n        }\n        std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700))?;\n    }\n    let lock_path = WorkspaceFile::open(&root, Path::new(\"owner.lock\"), true)?;\n    let original = lock_path.open_update(true, false)?;\n    let mut lifetime = fd_lock::RwLock::new(original.try_clone()?);\n    let _guard = lifetime\n        .try_write()\n        .map_err(|_| anyhow::anyhow!(\"Another pet owner is running\"))?;\n    let mut store = Store::at(&root)?;\n    let previous = store.load()?;\n    let mut saved = if let Some(text) = previous {\n        let value: Saved = serde_json::from_str(&text)?;\n        if value.version != 1\n            || uuid::Uuid::parse_str(&value.identity).is_err()\n            || value.token.len() != 64\n            || !value.token.bytes().all(|b| b.is_ascii_hexdigit())\n            || !valid_source(&value.source)\n            || value.clients.len() > MAX_CLIENTS\n            || !value.appearance.valid()\n        {\n            anyhow::bail!(\"Invalid shared habitat; the existing file was kept\");\n        }\n        value\n    } else {\n        Saved {\n            version: 1,","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/tui/pet_watch/owner.rs#L283-L319","documentation":"`pet serve` enforces single ownership of a shared pet habitat via an advisory file lock (`fd_lock::RwLock::try_write`) on `owner.lock` inside the pet directory. `try_write` fails immediately if another process already holds the write lock, and the code converts that into \"Another pet owner is running\" rather than blocking. It exists so two `pet serve` processes (two HTTP servers, two tick loops, two checkpoint writers) never mutate the same habitat state file concurrently.","triggerScenarios":"Running `pet serve` (or attaching a view that starts an owner) while another `pet serve` process holding the `owner.lock` write lock is still alive — even if that process is idle or hung. Any successful `lifetime.try_write()` holder (i.e. the first serve) causes all later serves against the same root to fail with this error.","commonSituations":"A previous `pet serve` left running in another terminal, tmux pane, or detached session; a crashed serve whose lock file persists — note on most Unix filesystems the flock is released on process death, so this usually means a live process, but a stale `owner.lock` file plus an inherited/shared file descriptor (child process, another session in the same container) can also hold it; two TUI instances attaching to the same pet root simultaneously.","solutions":["Find the existing owner: `fuser -v <root>/owner.lock` or `lsof <root>/owner.lock`, then stop that `pet serve` process (Ctrl-C or kill).","If you need a second habitat, point the new serve at a different pet root directory instead of sharing one.","Only after confirming no live process holds the lock, remove the stale `owner.lock` file and retry `pet serve`.","Do not bypass by force-deleting while an owner runs — the running owner checks the lock file identity each tick and will abort with \"Owner lock was replaced\"."],"exampleFix":"// before: second serve on the same root fails\n$ pet serve ~/.local/share/codewhale/pet\nError: Another pet owner is running\n// after\n$ fuser -v ~/.local/share/codewhale/pet/owner.lock   # find PID\n$ kill <pid>                                          # stop the old owner\n$ pet serve ~/.local/share/codewhale/pet","handlingStrategy":"try-catch","validationCode":"// Check for a live owner before starting serve:\nlet lock_alive = std::fs::File::open(root.join(\"owner.lock\"))\n    .ok()\n    .and_then(|f| fd_lock::RwLock::new(f).try_write().ok())\n    .is_none();\nif lock_alive {\n    eprintln!(\"A pet owner is already running for this root; stop it first.\");\n    std::process::exit(1);\n}","typeGuard":null,"tryCatchPattern":"match serve(root, port) {\n    Err(e) if e.to_string() == \"Another pet owner is running\" => {\n        eprintln!(\"Pet already served; attach to the existing instance instead.\");\n    }\n    Err(e) => return Err(e),\n    Ok(()) => {}\n}","preventionTips":["Use a process supervisor or pidfile check so only one `pet serve` per root runs.","Find the lock holder with `fuser`/`lsof` before assuming the lock file is stale.","Never delete `owner.lock` while an owner lives — it triggers the replacement check and aborts the running owner.","Point concurrent sessions at separate pet roots."],"tags":["file-lock","concurrency","single-instance","pet-watch"],"backgroundTag":"file-lock-conflict","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}