Skip to main content

Module single_instance

Module single_instance 

Source
Expand description

Single-instance boot guard.

forge guarantees one instance per config dir. crate::Workspace::new calls acquire at startup, taking a non-blocking exclusive flock on a never-renamed lockfile. If another forge already holds it, boot refuses with an AcquireError::AlreadyRunning: a clean stderr message + non-zero exit, not a panic. Otherwise the caller’s PID is written and the lock is held for the process lifetime (flock auto-releases on exit/crash, so there is no stale-lock cleanup). One instance per config dir is what lets the rest of forge - the cron store especially - coordinate with just an in-process mutex instead of cross-process locking.

The lockfile is MACHINE-LOCAL: it lives under forge’s app-support base (forge_sdk::app_support_dir, beside the logs) at locks/<hash>.lock, where <hash> is derived from the config-dir path so distinct profiles / config dirs on one machine get distinct, independently-held locks.

Why there and not beside the other forge state in <config_dir>/forge/: flock binds the open file description (the inode), not the path, and rewriting a file by renaming a temp over it swaps the inode, so the lock must be a dedicated file that is only ever opened + flocked, never renamed. The config dir is Syncthing-synced across the user’s Macs, and Syncthing applies incoming changes by rename - a lock synced in from another Mac would swap the inode out from under a running forge, orphaning its flock and letting a second instance start. A machine-local lock outside the synced dir sidesteps it.

A v0.18.0 boot may have left a stray <config_dir>/forge/forge.lock; it is now unused and harmless (a fresh boot writes the machine-local lock instead), so there is no migration.

Re-exportsΒ§

use std::fs::File; πŸ”’
use std::fs::OpenOptions; πŸ”’
use std::io::Read; πŸ”’
use std::io::Seek; πŸ”’
use std::io::Write; πŸ”’
use std::path::Path; πŸ”’
use std::path::PathBuf; πŸ”’
use rustix::fs::FlockOperation; πŸ”’
use rustix::fs::flock; πŸ”’

EnumsΒ§

AcquireError πŸ”’
Why acquire refused.

ConstantsΒ§

LOCK_DIR_NAME πŸ”’
Subdirectory of the app-support base that holds the per-config-dir lockfiles.

FunctionsΒ§

acquire πŸ”’
Acquire the per-config-dir single-instance lock under app_support.
another_instance_running πŸ”’
Whether another live forge holds a lock under app_support for a config dir other than config_dir.
lock_path πŸ”’
Machine-local lock path for config_dir under app_support: <app_support>/locks/<hex-hash-of-config-dir>.lock. The config-dir path is canonicalised first (best-effort) so symlinked / trailing-slash variants resolve to one lock.
read_pid πŸ”’
Read the holder’s PID from the lockfile. None when empty or unparseable.
record_pid πŸ”’
Stamp our PID into the (now exclusively-held) lockfile so a would-be second instance can name us in its refusal. Best-effort: a failed write doesn’t void the lock - the flock is what enforces single-instance; the PID is only for the message.