server: relocate .server.pid to $XDG_RUNTIME_DIR#413
Conversation
Addresses the PR #413 review pass: B1 (BLOCKER) — the new server acquired its flock only at the new location, so an upgrade with a pre-#412 server still running at ~/.memtomem/.server.pid could put two writers on the same DB. main() now holds a lifetime flock on the legacy path too: detect + exit 1 if an old server holds it, and keep our own hold so a future old server also detects us. Subprocess test pins the refusal. M1/M2/M3 — runtime dir now validated for symlink / owner / mode in both the XDG-gate (fall through silently) and the ensure path (raise PermissionError with a rm -rf hint). The previous "never re-chmod existing dir" docstring was at odds with the test that locked in 0o755 silently; that test is flipped to pytest.raises, and a new test covers each refusal case (symlink, loose mode, wrong owner, non-directory). XDG=/tmp or a user-symlinked XDG now transparently falls through to the per-user tempdir form instead of placing the pid file in a world-readable location. MINOR — umask belt-and-suspenders explicit chmod after mkdir; pin test for the rmdir non-empty guard (unrelated files must survive, runtime subdir must not prune); subprocess coverage for the tempdir fallback path (previously only XDG was exercised); the ~/.memtomem/-untouched headline assertion now lives directly in the sigterm integration test instead of being implied. NIT — module-level runtime_dir import in uninstall_cmd (no more inline shadowing); unused _state_dir param dropped from _check_server_liveness. Tests: 2201 pass, 0 regressions. Lint + mypy clean.
|
Thanks for the thorough pass — pushed B1 (BLOCKER) — legacy flock in
|
Pre-#412 the MCP server wrote ~/.memtomem/.server.pid at startup, which forced ~/.memtomem/ into existence on every handshake (claude mcp list, Cursor / Windsurf probes) even for clients that connected but never called a tool. Combined with the lazy DB initialization shipped in #399 Phase 3, this was the last write keeping that directory from being truly on-demand. Runtime files (pid / flock) now live under $XDG_RUNTIME_DIR/memtomem/ when the platform provides it (Linux + systemd), or a per-user temp subdir otherwise ($TMPDIR/memtomem-$UID/ on macOS / BSD, which tempfile.gettempdir() already resolves to a per-user path on macOS). Runtime state and persistent user data are now cleanly separated: a handshake that never reaches the first tool call leaves ~/.memtomem/ alone. mm uninstall probes both the new and legacy locations during the transition window so a mixed-version upgrade (pre-#412 server still running + new CLI) still refuses correctly. The new runtime subdir is rmdir'd after cleanup if empty; the XDG parent is kernel-managed and never touched. Non-goals: - Does not extend the liveness probe to other DB writers (mm web / mm watchdog), tracked separately in #384. Placing runtime files in a dedicated per-user dir is the right substrate for that expansion, but registering per-process locks is its own PR. - .current_session stays in ~/.memtomem/ — that is persistent session state, not runtime ephemera. Closes #412. Co-Authored-By: Claude <[email protected]>
Addresses the PR #413 review pass: B1 (BLOCKER) — the new server acquired its flock only at the new location, so an upgrade with a pre-#412 server still running at ~/.memtomem/.server.pid could put two writers on the same DB. main() now holds a lifetime flock on the legacy path too: detect + exit 1 if an old server holds it, and keep our own hold so a future old server also detects us. Subprocess test pins the refusal. M1/M2/M3 — runtime dir now validated for symlink / owner / mode in both the XDG-gate (fall through silently) and the ensure path (raise PermissionError with a rm -rf hint). The previous "never re-chmod existing dir" docstring was at odds with the test that locked in 0o755 silently; that test is flipped to pytest.raises, and a new test covers each refusal case (symlink, loose mode, wrong owner, non-directory). XDG=/tmp or a user-symlinked XDG now transparently falls through to the per-user tempdir form instead of placing the pid file in a world-readable location. MINOR — umask belt-and-suspenders explicit chmod after mkdir; pin test for the rmdir non-empty guard (unrelated files must survive, runtime subdir must not prune); subprocess coverage for the tempdir fallback path (previously only XDG was exercised); the ~/.memtomem/-untouched headline assertion now lives directly in the sigterm integration test instead of being implied. NIT — module-level runtime_dir import in uninstall_cmd (no more inline shadowing); unused _state_dir param dropped from _check_server_liveness. Tests: 2201 pass, 0 regressions. Lint + mypy clean.
cdb1423 to
c6fb0a0
Compare
Summary
memtomem-server's pid / flock file from~/.memtomem/.server.pidto$XDG_RUNTIME_DIR/memtomem/server.pid(with per-user
$TMPDIR/memtomem-$UID/fallback on macOS / BSD /non-systemd Linux).
against a fresh machine now leaves
~/.memtomem/untouched entirely— the last write that forced the dir into existence is gone.
mm uninstallprobes both the new and legacy locations during thetransition window so mixed-version upgrades (pre-server: relocate
.server.pidto $XDG_RUNTIME_DIR so~/.memtomem/stays lazy #412 server stillrunning + new CLI) still refuse correctly.
Details
New helper
memtomem._runtime_paths:runtime_dir()— pure path resolver (no mkdir). Checks$XDG_RUNTIME_DIR; falls back totempfile.gettempdir()/memtomem-$UID.ensure_runtime_dir()— explicit opt-in that creates the dir withmode=0o700. Never re-chmods an existing dir (aroot-ownedleftover surfaces via the caller's
open()rather than silentlyadjusted).
server_pid_path()/legacy_server_pid_path()— typed handles forthe two probe targets.
server/__init__.py:mainnow callsensure_runtime_dir() / "server.pid"instead of
Path("~/.memtomem").expanduser().mkdir(...) / ".server.pid".cli/uninstall_cmd.py:_check_server_livenesswalks(server_pid_path(), legacy_server_pid_path())and returns
alive=Truefor the first flock holder.so it is cleaned alongside
.current_session+ legacy.server.pid.rmdir'd (we ownthe subdir; the XDG parent is kernel-managed and never touched).
Non-goals
mm web,mm watchdog) — that ismm uninstallliveness check only sees MCP server pid — silently ignoresmm weband other DB writers #384 and needs per-process lockregistration of its own. The new runtime dir is the right place
for those future lock files; adding them is a separate PR.
.current_sessionstays in~/.memtomem/. That is persistentsession state across reboots, not runtime ephemera.
Test plan
uv run pytest packages/memtomem/tests -m "not ollama"— 2191passed, 0 regressions.
test_runtime_paths.pycovers XDG set / unset / stale,0o700 creation, idempotency, uid suffix on fallback.
test_uninstall_cmd.pynow covers refusal at both the new andlegacy flock locations, PID recycling at both locations, and
runtime-subdir cleanup.
test_server_sigterm.pyintegration test spawns a realmemtomem-serverwith isolated$HOME+$XDG_RUNTIME_DIRandverifies SIGTERM cleanup at the new location.
uv run ruff check+uv run ruff format --check— clean.uv run mypyadvisory on the three changed source files — clean.memtomem-serverwrites and cleans upserver.pidat the new location on macOS;~/.memtomem/is notcreated.
Closes #412.
🤖 Generated with Claude Code