leindex 1.9.5

LeIndex MCP and semantic code search engine for AI tools and large codebases
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
//! Advisory per-project single-instance lock for `leindex mcp` (D-3).
//!
//! Memory-pressure remediation: the swap-saturated workstation showed **8+
//! concurrent `leindex mcp` instances** for the same projects — every agent
//! session spawned its own server, each holding a ~2.4 GiB loaded engine
//! resident forever. This module writes a project-scoped lockfile
//! (`~/.leindex/run/leindex-mcp-<project-hash>.{lock,start}`) so a second
//! server for the same canonical project can *at least* know a live sibling
//! exists.
//!
//! **Advisory only — deliberately NOT a hard exit.** GrayHill flagged the
//! original D-3 design as a flaw: stdio MCP servers are 1:1 with the agent's
//! pipe, so a second instance hard-exiting 0 would break that agent's client.
//! The agreed design logs the overlap and continues; the *real* dedup lever
//! is the D-1 idle self-exit + D-2 engine eviction, which make duplicate
//! servers self-terminate and drop their engines.
//!
//! **Platform scope:** the ownership liveness check is Linux-only (it reads
//! `/proc/<pid>/stat` for process start time). On macOS/Windows the lock is a
//! documented **advisory no-op** — [`McpProjectLock::try_acquire`] returns
//! `NotAvailable` *before any file is written*, so the half-written sidecar
//! that a failed start-time write would otherwise leave can never be produced.
//! The dup-instance advisory warning therefore only fires on Linux; the real
//! dedup levers (D-1 idle self-exit, D-2 engine eviction) are platform-
//! independent.

use std::io;
use std::path::{Path, PathBuf};

/// Outcome of [`McpProjectLock::try_acquire`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockOutcome {
    /// This process now owns the lock (freshly written or stale-stolen).
    Acquired,
    /// A live sibling `leindex mcp` serves the same canonical project. The
    /// caller should log a warning and continue (advisory semantics).
    AlreadyOwned {
        /// PID of the live sibling instance owning the lock.
        pid: u32,
    },
    /// The lock could not be established (no home dir / write failure).
    /// Treat as advisory-no-op: never block the server on a lock problem.
    NotAvailable,
}

/// Held lock guard. Removing the sidecars on `Drop` keeps `~/.leindex/run/`
/// self-healing when a server exits cleanly (the D-7 GC covers SIGKILL debris).
#[derive(Debug)]
pub struct McpProjectLock {
    run_dir: PathBuf,
    stem: String,
}

impl McpProjectLock {
    /// Try to acquire the advisory lock for a canonical project path.
    ///
    /// Steals the lock when the owning PID is dead (start-time mismatch or
    /// missing `/proc` entry), mirroring `daemon_pid_is_owned` semantics.
    /// Never fails the server: all error paths degrade to `NotAvailable`.
    pub fn try_acquire(canonical: &Path) -> (LockOutcome, Option<McpProjectLock>) {
        let Some(home) = crate::config::resolve_leindex_home() else {
            return (LockOutcome::NotAvailable, None);
        };
        let run_dir = home.join("run");
        Self::try_acquire_in_dir(canonical, &run_dir)
    }

    /// Testable core: acquires the lock under an explicit run directory.
    ///
    /// On non-Linux platforms this is a **documented advisory no-op**: returns
    /// `NotAvailable` without touching the filesystem, so no partial sidecar is
    /// ever written. See the module doc for the rationale.
    pub fn try_acquire_in_dir(
        canonical: &Path,
        run_dir: &Path,
    ) -> (LockOutcome, Option<McpProjectLock>) {
        #[cfg(target_os = "linux")]
        {
            Self::try_acquire_in_dir_linux(canonical, run_dir)
        }
        #[cfg(not(target_os = "linux"))]
        {
            // Advisory no-op: the liveness check that makes stale-steal safe is
            // Linux-only (/proc), so on other platforms the lock must not write
            // a sidecar it cannot later validate (a half-written sidecar would
            // silently bypass the dup-instance warning AND leave debris).
            let _ = (canonical, run_dir);
            (LockOutcome::NotAvailable, None)
        }
    }

    /// Linux implementation of [`McpProjectLock::try_acquire_in_dir`]. Kept
    /// as a separate helper so the non-Linux no-op stays a one-liner above.
    #[cfg(target_os = "linux")]
    fn try_acquire_in_dir_linux(
        canonical: &Path,
        run_dir: &Path,
    ) -> (LockOutcome, Option<McpProjectLock>) {
        let stem = lock_stem(canonical);
        if std::fs::create_dir_all(run_dir).is_err() {
            return (LockOutcome::NotAvailable, None);
        }
        let lock_path = run_dir.join(format!("{stem}.lock"));
        let start_path = run_dir.join(format!("{stem}.start"));

        // Fast path: a live sibling already owns the lock.
        if let Some(pid) = read_lock_owner(&lock_path) {
            if pid_is_owned(pid, &start_path) {
                return (LockOutcome::AlreadyOwned { pid }, None);
            }
        }

        // Atomic arbitration (Codex P2): exclusive-create the lock file so two
        // concurrent starters cannot both pass the ownership check and then
        // truncate each other's sidecars. Only one process can win `create_new`;
        // the loser re-reads ownership and reports AlreadyOwned (or steals a
        // provably-stale lock, see below).
        match create_lock_exclusive(&lock_path) {
            Ok(()) => {}
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
                let owner = read_lock_owner(&lock_path);
                match owner {
                    // A sibling won the race and is live.
                    Some(pid) if pid_is_owned(pid, &start_path) => {
                        return (LockOutcome::AlreadyOwned { pid }, None);
                    }
                    // The recorded owner may be dead (stale, safe to steal) OR
                    // alive-but-mid-initialization (lock PID written, `.start`
                    // not yet). Codex P2 (publication TOCTOU): a loser must
                    // NEVER unlink a live owner's lock — that would let two
                    // servers both return `Acquired` and defeat the duplicate
                    // instance warning. Only a provably-dead owner (or a
                    // recycled PID that is not a leindex process) may be stolen.
                    Some(pid) => match crate::cli::cleanup::pid_is_alive(pid) {
                        // Live leindex/MCP sibling — possibly between `write_pid`
                        // and `write_start_time`. Do not touch its sidecars;
                        // degrade to advisory no-op (the real dedup levers are
                        // D-1 idle self-exit + D-2 engine eviction).
                        Some(true) => return (LockOutcome::NotAvailable, None),
                        // Provably dead, recycled PID, or unknown → stale. Remove
                        // it and retry the exclusive create exactly once. (`None`
                        // is unreachable here — `pid_is_alive` always returns
                        // `Some` on Linux and this branch is Linux-gated.)
                        _ => {
                            let _ = std::fs::remove_file(&lock_path);
                            if create_lock_exclusive(&lock_path).is_err() {
                                return (LockOutcome::NotAvailable, None);
                            }
                        }
                    },
                    // Unparseable/empty file: a concurrent acquirer is mid-write.
                    // Never remove or steal it — treat as a temporary no-op.
                    None => return (LockOutcome::NotAvailable, None),
                }
            }
            Err(_) => return (LockOutcome::NotAvailable, None),
        }

        // We hold the lock file exclusively. Write pid + start-time sidecars;
        // on failure remove our own artifacts so no partial lock is left behind.
        let my_pid = std::process::id();
        if write_pid(&lock_path, my_pid).is_err() || write_start_time(&start_path, my_pid).is_err()
        {
            let _ = std::fs::remove_file(&lock_path);
            let _ = std::fs::remove_file(&start_path);
            return (LockOutcome::NotAvailable, None);
        }

        (
            LockOutcome::Acquired,
            Some(McpProjectLock {
                run_dir: run_dir.to_path_buf(),
                stem,
            }),
        )
    }

    /// Explicitly release the sidecars (also called by `Drop`).
    pub fn release(&self) {
        let lock_path = self.run_dir.join(format!("{}.lock", self.stem));
        // Only remove sidecars this guard still owns: if a new process stole
        // the lock (our pid dead) between our exit and this drop, deleting the
        // files would silence its dup-instance warning and leave it without a
        // lock record. Compare the recorded owner before removing (Codex P2).
        if read_lock_owner(&lock_path) == Some(std::process::id()) {
            let _ = std::fs::remove_file(&lock_path);
            let _ = std::fs::remove_file(self.run_dir.join(format!("{}.start", self.stem)));
        }
    }
}

impl Drop for McpProjectLock {
    fn drop(&mut self) {
        self.release();
    }
}

/// Deterministic per-project lock stem: `leindex-mcp-<16-hex-hash>`.
///
/// Uses blake3 (an existing workspace dependency) instead of
/// `DefaultHasher`, whose SipHash algorithm is documented as **not stable
/// across Rust compiler versions** — a toolchain bump would silently change
/// every project's lock stem, leaving stale locks behind (Kilo #3) and letting
/// duplicate instances stop colliding. blake3 output is stable forever.
fn lock_stem(canonical: &Path) -> String {
    // Byte-exact (OsStr::as_encoded_bytes, 1.74+): `to_string_lossy` could let
    // two distinct non-UTF8 paths collide onto the same stem.
    let hash = blake3::hash(canonical.as_os_str().as_encoded_bytes());
    // First 8 bytes → u64 little-endian → 16 hex chars, matching the prior
    // `{:016x}` formatting of the lockfile name shape.
    let mut bytes = [0u8; 8];
    bytes.copy_from_slice(&hash.as_bytes()[..8]);
    format!("leindex-mcp-{:016x}", u64::from_le_bytes(bytes))
}

/// True when `pid` is alive AND matches the `start` sidecar (a reused PID for
/// a dead process fails the start-time comparison, so stale locks are stolen).
/// Linux-only: the `/proc` start-time + cmdline reads do not exist on
/// macOS/Windows, and this function is only reachable from the Linux
/// implementation above.
#[cfg(target_os = "linux")]
fn pid_is_owned(pid: u32, start_path: &Path) -> bool {
    let expected = std::fs::read_to_string(start_path)
        .ok()
        .and_then(|value| value.trim().parse::<u64>().ok());
    let Some(expected) = expected else {
        return false;
    };
    let actual = proc_start_time(pid);
    if actual != Some(expected) {
        return false;
    }
    // Secondary sanity: the owning process should be a leindex mcp server.
    let cmdline = std::fs::read(format!("/proc/{pid}/cmdline")).ok();
    cmdline.is_some_and(|raw| {
        let command = String::from_utf8_lossy(&raw);
        command
            .split('\0')
            .any(|arg| arg.contains("leindex") || arg.contains("mcp"))
    })
}

#[cfg(target_os = "linux")]
fn proc_start_time(pid: u32) -> Option<u64> {
    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    let fields = stat.rsplit_once(") ")?.1;
    fields.split_whitespace().nth(19)?.parse::<u64>().ok()
}

/// Create the lock file exclusively (`create_new`): fails with
/// `AlreadyExists` when another process already holds it. This is the atomic
/// arbitration primitive that makes concurrent acquisition race-free.
fn create_lock_exclusive(path: &Path) -> io::Result<()> {
    std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .open(path)
        .map(|_| ())
}

/// Read the PID recorded in a lock file, if parseable.
fn read_lock_owner(path: &Path) -> Option<u32> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|value| value.trim().parse::<u32>().ok())
}

fn write_pid(path: &Path, pid: u32) -> io::Result<()> {
    std::fs::write(path, format!("{pid}\n"))
}

#[cfg(target_os = "linux")]
fn write_start_time(path: &Path, pid: u32) -> io::Result<()> {
    let start = proc_start_time(pid)
        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no /proc stat for pid"))?;
    std::fs::write(path, format!("{start}\n"))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_run_dir() -> tempfile::TempDir {
        tempfile::tempdir().expect("tempdir")
    }

    #[test]
    fn test_lock_stem_is_deterministic_and_stable() {
        // Same canonical path → same stem every call (blake3, not DefaultHasher).
        let a = lock_stem(Path::new("/tmp/leindex/proj-alpha"));
        let b = lock_stem(Path::new("/tmp/leindex/proj-alpha"));
        assert_eq!(a, b);
        assert!(a.starts_with("leindex-mcp-"));
        // Different projects → different stems.
        let other = lock_stem(Path::new("/tmp/leindex/proj-beta"));
        assert_ne!(a, other);
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_acquire_writes_and_releases_sidecars() {
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-a");
        let (outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(outcome, LockOutcome::Acquired);
        let guard = guard.expect("guard");
        let lock_path = dir.path().join(format!("{}.lock", lock_stem(canonical)));
        assert!(lock_path.exists());
        // Dropping the guard removes the sidecars.
        drop(guard);
        assert!(!lock_path.exists());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_second_live_instance_reports_owned() {
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-b");
        let (_o1, guard1) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert!(guard1.is_some());

        // Second acquire from the same live process (this test binary) must
        // report AlreadyOwned with our own pid.
        let (outcome, guard2) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(
            outcome,
            LockOutcome::AlreadyOwned {
                pid: std::process::id()
            }
        );
        assert!(guard2.is_none());
        // After the first guard drops, a fresh acquire succeeds again.
        drop(guard1);
        let (outcome, _guard3) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(outcome, LockOutcome::Acquired);
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_unparseable_lock_file_is_not_stolen() {
        // Codex P2: an empty/mid-write lock file must never be removed+stolen —
        // that would let a concurrent acquirer lose its lock mid-write. The
        // acquirer must degrade to NotAvailable and leave the file intact.
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-empty-lock");
        let stem = lock_stem(canonical);
        std::fs::write(dir.path().join(format!("{stem}.lock")), b"").unwrap();

        let (outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(outcome, LockOutcome::NotAvailable);
        assert!(guard.is_none());
        assert!(
            dir.path().join(format!("{stem}.lock")).exists(),
            "unparseable lock file must be left intact"
        );
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_release_only_removes_owned_sidecars() {
        // Codex P2: a guard must not delete sidecars it no longer owns (a newer
        // process may have stolen the lock after this one exited).
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-stolen-lock");
        let (_outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        let guard = guard.expect("guard");
        let stem = lock_stem(canonical);
        // Simulate a stale-steal: overwrite the lock with a different owner
        // before dropping the guard.
        std::fs::write(dir.path().join(format!("{stem}.lock")), "12345\n").unwrap();
        drop(guard);
        // release() saw a foreign owner → left the sidecars alone.
        assert!(
            dir.path().join(format!("{stem}.lock")).exists(),
            "foreign-owned sidecars must survive a guard drop"
        );
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_stale_lock_with_dead_pid_is_stolen() {
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-c");
        let stem = lock_stem(canonical);
        // Dead PID sidecars (pid 2^22 will not exist as this process).
        let dead_pid = 1 << 22;
        std::fs::write(
            dir.path().join(format!("{stem}.lock")),
            format!("{dead_pid}\n"),
        )
        .unwrap();
        std::fs::write(dir.path().join(format!("{stem}.start")), "12345\n").unwrap();

        let (outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(outcome, LockOutcome::Acquired, "stale lock must be stolen");
        assert!(guard.is_some());
        // The stolen lock now records our own pid.
        let lock_contents =
            std::fs::read_to_string(dir.path().join(format!("{stem}.lock"))).unwrap();
        assert_eq!(
            lock_contents.trim().parse::<u32>().unwrap(),
            std::process::id()
        );
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_live_owner_without_start_sidecar_is_not_stolen() {
        // Codex P2 (publication TOCTOU, lock.rs:130): a loser must never unlink
        // a lock whose owner is a LIVE leindex process even when the `.start`
        // sidecar is missing or mismatched (the winner is between `write_pid`
        // and `write_start_time`). Unlinking would let both processes return
        // `Acquired` and defeat the duplicate-instance warning.
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-toctou");
        let stem = lock_stem(canonical);
        // Precondition: this test process must be classified as a live owner
        // (its cmdline path contains "leindex"). If the liveness gate ever
        // misclassifies us, the test must fail loudly, not silently.
        assert_eq!(
            crate::cli::cleanup::pid_is_alive(std::process::id()),
            Some(true),
            "test precondition: this process must be detected as a live leindex/mcp process"
        );
        // Simulate the winner mid-initialization: the lock file records a live
        // PID but no `.start` sidecar exists yet.
        std::fs::write(
            dir.path().join(format!("{stem}.lock")),
            format!("{}\n", std::process::id()),
        )
        .unwrap();

        let (outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(
            outcome,
            LockOutcome::NotAvailable,
            "live owner without .start must degrade to NotAvailable, never steal"
        );
        assert!(guard.is_none());
        // The lock file must be left intact with the original owner recorded.
        let lock_path = dir.path().join(format!("{stem}.lock"));
        assert!(lock_path.exists(), "live owner's lock must not be unlinked");
        assert_eq!(read_lock_owner(&lock_path), Some(std::process::id()));
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_different_projects_coexist() {
        let dir = temp_run_dir();
        let (o1, g1) = McpProjectLock::try_acquire_in_dir(Path::new("/tmp/proj-d1"), dir.path());
        let (o2, g2) = McpProjectLock::try_acquire_in_dir(Path::new("/tmp/proj-d2"), dir.path());
        assert_eq!(o1, LockOutcome::Acquired);
        assert_eq!(o2, LockOutcome::Acquired);
        assert!(g1.is_some());
        assert!(g2.is_some());
    }

    #[cfg(target_os = "linux")]
    #[test]
    fn test_pid_is_owned_self() {
        // Our own process + our own start-time must be reported as owned.
        let dir = temp_run_dir();
        let start_path = dir.path().join("self.start");
        write_start_time(&start_path, std::process::id()).unwrap();
        assert!(pid_is_owned(std::process::id(), &start_path));
        // A wrong start-time (bogus pid) must be reported as not owned.
        assert!(!pid_is_owned(
            std::process::id(),
            &dir.path().join("missing.start")
        ));
    }

    #[cfg(not(target_os = "linux"))]
    #[test]
    fn test_non_linux_acquire_is_documented_noop() {
        // Non-Linux: advisory no-op. Must return NotAvailable and write NO
        // sidecars (no half-written lock file), per the module doc contract.
        let dir = temp_run_dir();
        let canonical = Path::new("/tmp/proj-nonlinux");
        let (outcome, guard) = McpProjectLock::try_acquire_in_dir(canonical, dir.path());
        assert_eq!(outcome, LockOutcome::NotAvailable);
        assert!(guard.is_none());
        let stem = lock_stem(canonical);
        assert!(!dir.path().join(format!("{stem}.lock")).exists());
        assert!(!dir.path().join(format!("{stem}.start")).exists());
    }
}