From c14b686782fb2797694f42d616c2c314b0192c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 27 Aug 2026 07:52:46 +0000 Subject: [PATCH] pm2 save when the process list changes, and a check that does not lie about symlinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the owner asked whether anything runs `pm2 save` when a sidecar is added or removed. nothing did. PM2 resurrects from ~/.pm2/dump.pm2, written only by `pm2 save`, so the dump was whatever a human last happened to save. today it was FIVE DAYS old: 13 processes saved against 26 running. a reboot would have silently lost every sidecar installed since — bitcoin-wallet, desktop, gitea, invoices, iris, jellyfin, minesweeper, music, photos, soulseek, transcribe, transmission, vault. thirteen features gone, machine healthy, nothing saying why. saved after start AND after delete. removal matters as much: a dump that keeps resurrecting a deleted sidecar brings it back after a reboot as a crash loop for code no longer on the machine. a RESTART does not save — the same processes are running and the dump is already right. best effort and never fatal: a failed save leaves the estate correct and only the dump stale, which is where things already were. failing an install over bookkeeping would be the worse trade. separately, plugins:check filtered directories on the DIRENT's isDirectory(), which answers false for a symlink to a directory. platform/plugins is symlinked now, so the check would have reported "0 plugin repositories, all well-formed" — a green check over nothing, which is worse than a red one. it stats the path instead, the way discover.ts already does. dump saved once by hand as well, since five days of drift was already there: 26 saved, 26 running, no difference. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- scripts/plugins-check.ts | 16 ++++++++++++++-- src/servers/app-store/pm2.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/scripts/plugins-check.ts b/scripts/plugins-check.ts index 4ea24a82..54728c19 100644 --- a/scripts/plugins-check.ts +++ b/scripts/plugins-check.ts @@ -25,7 +25,7 @@ that running the check can never be the thing that changes your tree. */ -import { readdirSync, existsSync } from 'node:fs'; +import { readdirSync, existsSync, statSync } from 'node:fs'; import { join } from 'node:path'; /* @@ -52,7 +52,19 @@ type Problem = { plugin: string; what: string; fix?: () => void }; const problems: Problem[] = []; const dirs = readdirSync(PLUGINS_DIR, { withFileTypes: true }) - .filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') + // `statSync().isDirectory()`, not the dirent's — a Dirent for a SYMLINK to a directory answers false to + // `isDirectory()` and true to `isSymbolicLink()`, so filtering on the dirent silently skips every + // symlinked plugin. `platform/plugins/` is symlinks into the workspace clones since 2026-08-27, which + // would have made this report "0 plugin repositories, all well-formed" — a green check over nothing. + // `discover.ts` already does it this way, at line 239, for the same reason. + .filter((e) => !e.name.startsWith('.') && e.name !== 'node_modules') + .filter((e) => { + try { + return statSync(join(PLUGINS_DIR, e.name)).isDirectory(); + } catch { + return false; // a broken symlink, which the checks below have nothing useful to say about + } + }) .map((e) => e.name) .filter((name) => existsSync(join(PLUGINS_DIR, name, '.git'))) .sort(); diff --git a/src/servers/app-store/pm2.ts b/src/servers/app-store/pm2.ts index c8f8136f..8e6a7d67 100644 --- a/src/servers/app-store/pm2.ts +++ b/src/servers/app-store/pm2.ts @@ -63,6 +63,8 @@ export async function startProcess(name: string, cwd: string): Promise { + const { code, out } = await pm2(['save'], cwd); + if (code !== 0) console.error(`[pm2] could not save the process list — a reboot may not restore it: ${out}`); +} + /** 'online' | 'stopped' | 'errored' | … , or null when PM2 has never heard of it. */ export async function processStatus(name: string, cwd: string): Promise { const { code, out } = await pm2(['jlist'], cwd);