From 6734d0d0fe101e179debe65fd91d344b9dabedc2 Mon Sep 17 00:00:00 2001 From: Cannon07 Date: Sun, 10 May 2026 16:13:36 +0530 Subject: [PATCH] feat: pidfile-based socket discovery (#47, phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each nvim instance running setup() now self-registers its RPC socket and cwd to ~/.local/state/code-preview/sockets/. nvim-socket.sh checks this directory first, falling back to the existing /var/folders, /tmp, and XDG_RUNTIME_DIR globs for nvim instances that haven't been restarted. Why: the glob-based discovery is OS-specific (relies on lsof, compgen, kill -0, /proc) and is the main blocker for Windows support tracked in \#46. The pidfile is computable from any platform without those tools and contains the cwd directly, eliminating the lsof lookup that today gives the startup cwd rather than the user's current :cd. Behavior preserved: - Multi-instance routing via the same match-or-parent-cwd rule - NVIM_LISTEN_ADDRESS override - Single-instance / fallback case (legacy globs still run when no pidfile matches) Behavior improved: - Liveness check is socket-connect, not kill -0 — catches hung nvim - cwd is read live from nvim, refreshed on DirChanged This is phase 1 of a four-phase migration to Lua-based orchestration. The pidfile itself is permanent; nvim-socket.sh will be removed in a later phase when orchestration moves entirely into Lua. Co-Authored-By: Claude Opus 4.7 --- bin/nvim-socket.sh | 35 ++++++++++++++++++ lua/code-preview/health.lua | 17 +++++++++ lua/code-preview/init.lua | 11 ++++++ lua/code-preview/pidfile.lua | 69 ++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 lua/code-preview/pidfile.lua diff --git a/bin/nvim-socket.sh b/bin/nvim-socket.sh index 95e501f..53a138b 100755 --- a/bin/nvim-socket.sh +++ b/bin/nvim-socket.sh @@ -48,6 +48,41 @@ find_nvim_socket() { local live_sockets=() local socket pid + # 2a. Pidfile discovery — each running nvim that called code-preview.setup() + # registers itself at ${XDG_STATE_HOME:-$HOME/.local/state}/code-preview/sockets/. + # File contents: line 1 = socket path, line 2 = cwd. Cheaper and OS-portable + # vs the glob-based discovery below, which we keep as a fallback for nvim + # instances that haven't been restarted since the upgrade. + local _state_home="${XDG_STATE_HOME:-$HOME/.local/state}" + local _pidfile_dir="$_state_home/code-preview/sockets" + if [[ -d "$_pidfile_dir" ]]; then + local pidfile pf_socket pf_cwd + for pidfile in "$_pidfile_dir"/*; do + [[ -f "$pidfile" ]] || continue + pid="$(basename "$pidfile")" + [[ "$pid" =~ ^[0-9]+$ ]] || continue + { IFS= read -r pf_socket; IFS= read -r pf_cwd; } < "$pidfile" || continue + [[ -n "$pf_socket" && -S "$pf_socket" ]] || continue + + # Validate socket is responsive — self-heals stale pidfiles from crashed + # nvim where the PID may even have been recycled. + if ! nvim --server "$pf_socket" --remote-expr "1" >/dev/null 2>&1; then + continue + fi + + # If project_cwd given, check match-or-parent rule using the cwd the + # nvim itself reported (more accurate than lsof, which gives startup cwd). + if [[ -n "$project_cwd" && -n "$pf_cwd" ]]; then + if [[ "$project_cwd" == "$pf_cwd" || "$project_cwd" == "$pf_cwd/"* ]]; then + eval "$_oldopts" + echo "$pf_socket" + return 0 + fi + fi + live_sockets+=("$pid:$pf_socket") + done + fi + # 2. Scan macOS /var/folders paths local _glob_out _glob_out="$(compgen -G '/var/folders/*/*/T/nvim.*/*/nvim.*.0' 2>/dev/null)" || true diff --git a/lua/code-preview/health.lua b/lua/code-preview/health.lua index 20d97d5..1d7a28d 100644 --- a/lua/code-preview/health.lua +++ b/lua/code-preview/health.lua @@ -25,6 +25,23 @@ function M.check() local layout = (cfg.diff and cfg.diff.layout) or "unknown" ok("Diff layout: " .. layout) + -- Pidfile registration — used by hook scripts to find this nvim's socket + -- without OS-specific socket-glob discovery. + local pidfile = require("code-preview.pidfile").path() + local pf = io.open(pidfile, "r") + if not pf then + warn("Pidfile not found at " .. pidfile .. " (hook scripts will fall back to socket discovery)") + else + local pf_socket = pf:read("*l") or "" + local pf_cwd = pf:read("*l") or "" + pf:close() + if pf_socket == "" or pf_cwd == "" then + warn("Pidfile " .. pidfile .. " is malformed (expected socket+cwd on two lines)") + else + ok("Pidfile registered: " .. pidfile) + end + end + -- ── Claude Code backend ─────────────────────────────────────── start("Claude Code backend") diff --git a/lua/code-preview/init.lua b/lua/code-preview/init.lua index 5d47309..00e4419 100644 --- a/lua/code-preview/init.lua +++ b/lua/code-preview/init.lua @@ -91,6 +91,9 @@ function M.setup(user_config) -- Initialise logging require("code-preview.log").init({ debug = M.config.debug }) + -- Self-register socket + cwd for hook-script discovery + require("code-preview.pidfile").setup() + -- ── New commands ────────────────────────────────────────────── vim.api.nvim_create_user_command("CodePreviewInstallClaudeCodeHooks", function() @@ -226,6 +229,14 @@ function M.status() table.insert(lines, "Neovim socket : not found") end + -- Pidfile (used by hook scripts for socket discovery) + local pidfile = require("code-preview.pidfile").path() + if vim.fn.filereadable(pidfile) == 1 then + table.insert(lines, "Pidfile : " .. pidfile) + else + table.insert(lines, "Pidfile : not registered") + end + -- jq dependency local jq_ok = vim.fn.executable("jq") == 1 table.insert(lines, "jq : " .. (jq_ok and "found" or "MISSING")) diff --git a/lua/code-preview/pidfile.lua b/lua/code-preview/pidfile.lua new file mode 100644 index 0000000..2769c22 --- /dev/null +++ b/lua/code-preview/pidfile.lua @@ -0,0 +1,69 @@ +-- pidfile.lua — Self-registers this nvim's RPC socket + cwd so the hook +-- scripts can find it without OS-specific socket-glob discovery. +-- +-- File format (two lines): +-- +-- +-- +-- Location: ${XDG_STATE_HOME:-$HOME/.local/state}/code-preview/sockets/ +-- The same path is computed by bin/nvim-socket.sh. + +local M = {} + +function M.dir() + local state = vim.env.XDG_STATE_HOME + if not state or state == "" then + state = (vim.env.HOME or "") .. "/.local/state" + end + return state .. "/code-preview/sockets" +end + +function M.path() + return M.dir() .. "/" .. tostring(vim.fn.getpid()) +end + +local function write() + local socket = vim.v.servername or "" + if socket == "" then return end + + vim.fn.mkdir(M.dir(), "p") + + local f, err = io.open(M.path(), "w") + if not f then + require("code-preview.log").warn("pidfile: open failed: " .. tostring(err)) + return + end + f:write(socket, "\n", vim.fn.getcwd(), "\n") + f:close() +end + +local function remove() + pcall(os.remove, M.path()) +end + +function M.setup() + -- Initial write + pcall(write) + + local group = vim.api.nvim_create_augroup("CodePreviewPidfile", { clear = true }) + + -- Refresh cwd line when the user :cd's so socket discovery stays accurate. + vim.api.nvim_create_autocmd("DirChanged", { + group = group, + callback = function() pcall(write) end, + }) + + -- Re-write if servername changes (rare, but possible via :let v:servername). + vim.api.nvim_create_autocmd("VimEnter", { + group = group, + callback = function() pcall(write) end, + }) + + -- Cleanup on exit + vim.api.nvim_create_autocmd("VimLeavePre", { + group = group, + callback = remove, + }) +end + +return M