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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! Shared lock utilities for single-instance enforcement.
//!
//! [`try_flock`] is a non-blocking exclusive lock on the WHOLE file, over std's
//! [`File::try_lock`]: `flock(LOCK_EX|LOCK_NB)` on Unix, `LockFileEx` over
//! `u32::MAX` bytes low and high (the whole addressable range) from offset 0 on
//! Windows. Std owning both the byte range and the platform's contention mapping
//! is deliberate — a whole-file Windows range cannot overlap whatever another
//! handle holds, and std knows which code that comes back as.
//!
//! [`lock_file_path`] gives the standard `mahbot.lock` path. The probes at the
//! bottom answer "does an instance hold this storage location?" for the
//! read-only CLI paths: they report a tri-state, because "the probe could not
//! tell" must never be read as "no instance holds the location".
//!
//! # Release
//!
//! The lock is released by an explicit [`File::unlock`], or at the latest when
//! the last handle to the file is closed: on `Drop`, on `process::exit` (Rust
//! destructors do not run, but the O.S. closes all handles), and on abrupt
//! termination. There is no stale-lock scenario. The explicit unlock is what
//! [`crate::self_update`] uses before spawning the replacement instance: it must
//! be able to take the lock as soon as the release returns, and neither platform
//! guarantees that a close-only release has landed by then — Windows documents
//! the O.S. unlock on close as taking an unspecified amount of time, depending
//! on available system resources.
use ;
use io;
use ;
/// Path to the standard `mahbot.lock` under the given storage root.
///
/// Used by [`crate::self_update`] for the instance lock.
pub
/// Try to acquire an exclusive whole-file lock non-blockingly.
///
/// Returns:
/// - `Ok(true)` — lock acquired.
/// - `Ok(false)` — lock held by another process.
/// - `Err(io::Error)` — non-retryable OS error.
/// What the instance lock says about a storage location.
pub
/// Refusal wording for the read-only CLI paths (`mahbot debug`,
/// `bench-openrouter`) when the probe reported
/// [`Unknown`](InstanceLockState::Unknown): whether an instance holds the
/// storage location could not be established, so the probe — the thing that
/// decides whether opening a store is safe — refuses, and both halves show in
/// the sentence. Shared by both consumers so they cannot drift apart, and
/// returned as a clause without terminal punctuation: each appends its own
/// continuation (`mahbot debug` its re-run advice, the bench what it uses
/// instead).
pub
/// Probe the instance lock once for the given storage location.
///
/// The lock file is opened read+write and [`try_flock`]ed once:
///
/// * A `NotFound` open error is [`InstanceLockState::Free`] — no instance ever
/// took this location (the lock file is created before the stores are opened).
/// * Any other open error is [`InstanceLockState::Unknown`].
/// * `Ok(true)` from [`try_flock`] is [`InstanceLockState::Free`] — the probe
/// acquires and immediately releases the lock, so it never leaves it held.
/// * `Ok(false)` is [`InstanceLockState::Held`].
/// * `Err(e)` from [`try_flock`] is [`InstanceLockState::Unknown`].
///
/// A held lock means an instance is running: the caller must NOT open the live
/// stores directly (single-process mode would conflict with that instance's
/// writer) and must route read-only queries through the debug IPC endpoint
/// instead. A free lock means no instance holds the location, so a direct
/// single-process read-only open is safe. [`InstanceLockState::Unknown`] is
/// deliberately never read as "free" — a lock file the inspecting user cannot
/// open read+write makes the read-only CLI refuse where it would otherwise have
/// read the store directly.
/// Re-check count/interval for [`instance_lock_state_settled`].
const LOCK_SETTLE_RECHECKS: usize = 3;
const LOCK_SETTLE_INTERVAL: Duration = from_millis;
/// Conservative instance-up probe for read-only CLI fallbacks (`mahbot debug`,
/// `bench-openrouter`).
///
/// Re-probes the instance lock over a short window.
/// [`Held`](InstanceLockState::Held) wins outright and is returned as soon as it
/// is seen — a held lock means an instance is running, so there is nothing left
/// to settle. This prevents the caller from direct-opening a live store during
/// the self-update handoff, when the outgoing instance has released the lock but
/// the incoming one has not yet re-acquired it (a sub-millisecond window).
///
/// [`Free`](InstanceLockState::Free) is returned only after the window
/// consistently shows the lock free — no instance holds the location. An
/// [`Unknown`](InstanceLockState::Unknown) observed in the window is remembered
/// and returned if no `Held` was ever seen: the probe may have failed on an
/// ordinary racing window, and a probe that could not tell must never be
/// downgraded to `Free`. The extra ~300 ms latency is a CLI diagnostic tool, not
/// a hot path.
pub