1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//! Cross-process exclusive advisory lock around a whole-file critical section.
//!
//! Why: [`crate::json_rmw`] already owned this lock, but only for JSON
//! documents it also serialises and publishes itself. `trusty-search`'s
//! `indexes.toml` is a TOML registry with its own loader, its own
//! `skip_serializing_if` shape, and its own fail-closed parse contract, so it
//! cannot route through `json_rmw::update` — yet it has the identical failure
//! mode: several independent PROCESSES (the daemon, `trusty-search prune`,
//! `trusty-search prune-orphans`) each run load → mutate → save-the-whole-file,
//! and a write landing between another writer's load and its save is discarded
//! with both callers reporting success (#5344). Extracting the lock here means
//! there is still exactly ONE implementation of the critical section; `json_rmw`
//! now calls it rather than owning it.
//! What: [`with_exclusive_lock`] runs a closure while holding an exclusive
//! `flock(2)`-style advisory lock on a `<path>.lock` sidecar, releasing it by
//! RAII on every exit path including a panic. [`lock_path`] names that sidecar.
//! Test: `cargo test -p trusty-common --features unconditional-only --
//! file_lock::tests`.
//!
//! # Contract
//!
//! - **Serialisation.** The lock is held by the open file description, so it
//! serialises separate PROCESSES and separate threads that each call
//! [`with_exclusive_lock`], on Unix and Windows alike.
//! - **Advisory, not mandatory.** A process that writes the guarded file
//! without going through this entry point is not blocked. Every writer of a
//! given file must use it.
//! - **Never fail open.** A lock that cannot be created or acquired is an
//! `Err`; the closure never runs. Proceeding unlocked is the lost-update bug
//! this module exists to remove.
//! - **Not reentrant.** Nesting two [`with_exclusive_lock`] calls on the same
//! path self-deadlocks: the second acquisition uses a different descriptor.
//! - **Blocking.** Acquisition blocks the calling thread. Async callers must
//! run it on a blocking-safe thread (e.g. `tokio::task::spawn_blocking`).
//!
//! [`with_exclusive_lock`]: crate::file_lock::with_exclusive_lock
//! [`lock_path`]: crate::file_lock::lock_path
use OpenOptions;
use ;
/// Sidecar lock-file path for `path`.
///
/// Why: locking the guarded file itself would mean opening it for write before
/// we know whether the update will succeed, and the lock would be lost across
/// the `rename` that publishes a new version (the renamed-over inode, and any
/// lock on it, is discarded). A stable sidecar survives every publish.
/// What: appends `.lock` to the file name, keeping it in the same directory.
/// Test: `lock_path_is_a_sidecar`.
/// Run `f` while holding the exclusive cross-process lock guarding `path`.
///
/// Why: see the module docs — this is the one place the load → mutate → save
/// critical section is made safe against writers in other processes.
/// What: creates (if needed) and opens the [`lock_path`] sidecar, blocks until
/// the exclusive advisory lock is available, runs `f`, and releases the lock by
/// RAII. `f`'s own return value — commonly a `Result` — passes through
/// untouched; the `Err` returned here is only ever a lock-acquisition failure,
/// so a caller can never confuse "could not lock" with "the work failed".
/// Test: `with_exclusive_lock_serialises_separate_descriptors`,
/// `with_exclusive_lock_releases_on_panic`, `with_exclusive_lock_unopenable_errors`.