Module: KairosMcp::Daemon::PidLock

Defined in:
lib/kairos_mcp/daemon/pid_lock.rb

Overview

PID lock using flock(LOCK_EX|LOCK_NB).

Design rationale (see design v0.2 §3.1):

  • flock is atomic and OS-managed; no stale PID file detection required.

  • If the daemon process dies, the OS releases the lock automatically.

  • On acquisition we write our PID for operational visibility, but the lock itself — not the file contents — is the source of truth.

Defined Under Namespace

Classes: AlreadyLocked

Class Method Summary collapse

Class Method Details

.acquire!(path) ⇒ File

Acquire an exclusive, non-blocking flock on ‘path`. Returns the opened File handle (caller must keep a reference for the duration of the lock — GC-closing the file releases the lock).

Parameters:

  • path (String)

    absolute path to the pid file

Returns:

  • (File)

    the locked file handle (pid already written)

Raises:



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/kairos_mcp/daemon/pid_lock.rb', line 33

def self.acquire!(path)
  FileUtils.mkdir_p(File.dirname(path))

  # Open rw + create. We deliberately do NOT truncate before the flock,
  # so a failing acquisition does not wipe the holder's pid.
  file = File.open(path, File::RDWR | File::CREAT, 0o644)

  unless file.flock(File::LOCK_EX | File::LOCK_NB)
    holder = read_pid(file)
    file.close
    raise AlreadyLocked.new(path, holder)
  end

  # We own the lock — rewrite pid file
  file.truncate(0)
  file.rewind
  file.write("#{Process.pid}\n")
  file.flush
  file
end

.read_pid(file) ⇒ Object

Read the pid written in the (already-opened) file, if any. Used only for diagnostics in AlreadyLocked.



83
84
85
86
87
88
89
90
# File 'lib/kairos_mcp/daemon/pid_lock.rb', line 83

def self.read_pid(file)
  file.rewind
  contents = file.read
  pid = contents.to_s.strip.to_i
  pid.positive? ? pid : nil
rescue StandardError
  nil
end

.read_pid_from(path) ⇒ Object

Read the pid directly from disk (used when we don’t hold the file).



93
94
95
96
97
98
99
100
# File 'lib/kairos_mcp/daemon/pid_lock.rb', line 93

def self.read_pid_from(path)
  return nil unless File.exist?(path)

  pid = File.read(path).to_s.strip.to_i
  pid.positive? ? pid : nil
rescue StandardError
  nil
end

.release(file, path) ⇒ Object

Release and remove the pid file. CF-1 fix: delete WHILE holding the lock to prevent unlink race. Safe to call with nil or an already-closed file.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kairos_mcp/daemon/pid_lock.rb', line 57

def self.release(file, path)
  return if file.nil?

  begin
    # Delete while still holding the lock — prevents a replacement
    # daemon from acquiring the same path before we unlink it.
    File.delete(path) if File.exist?(path)
  rescue Errno::ENOENT
    # Already gone — fine.
  end

  begin
    file.flock(File::LOCK_UN)
  rescue StandardError
    # Already unlocked or closed — ignore
  end

  begin
    file.close unless file.closed?
  rescue StandardError
    # ignore
  end
end