← back to Vendor Agents Viewer
harden va-* launcher: EADDRINUSE backoff binder + --kill-timeout 4000 (memo-approved)
52c61e16a6e19d470aead3eafe6b0a2f52576e45 · 2026-06-26 10:47:46 -0700 · Steve
Files touched
M agent-host.jsM server.js
Diff
commit 52c61e16a6e19d470aead3eafe6b0a2f52576e45
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 10:47:46 2026 -0700
harden va-* launcher: EADDRINUSE backoff binder + --kill-timeout 4000 (memo-approved)
---
agent-host.js | 13 ++++++++++++-
server.js | 5 ++++-
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/agent-host.js b/agent-host.js
index ee09469..d6c9e64 100644
--- a/agent-host.js
+++ b/agent-host.js
@@ -78,4 +78,15 @@ app.post('/api/scan', async (req, res) => {
res.json({ agent: AGENT, vendor: VENDOR, ...r });
});
-app.listen(PORT, '0.0.0.0', () => console.log(`[agent] ${AGENT} (${VENDOR}) on :${PORT} scraper=${SCRAPER || 'none'}`));
+// Bind with EADDRINUSE backoff: on a pm2 restart the prior instance may not have
+// released the listen socket yet. Without this, the new instance hot-loops on
+// EADDRINUSE and spams the error log (241MB observed for va-muralsource). Throttle
+// to one retry / 2s so the old socket has time to free instead of millions of lines.
+const server = app.listen(PORT, '0.0.0.0', () =>
+ console.log(`[agent] ${AGENT} (${VENDOR}) on :${PORT} scraper=${SCRAPER || 'none'}`));
+server.on('error', (err) => {
+ if (err.code === 'EADDRINUSE') {
+ console.error(`[agent] :${PORT} busy (prior instance releasing), retry in 2s`);
+ setTimeout(() => { try { server.close(); } catch {} app.listen(PORT, '0.0.0.0'); }, 2000);
+ } else { throw err; }
+});
diff --git a/server.js b/server.js
index cdd1141..42be2d8 100644
--- a/server.js
+++ b/server.js
@@ -168,7 +168,10 @@ app.post('/api/launch', async (req, res) => {
portCache.at = 0;
const env = `AGENT_PORT=${v.agent_port} VENDOR_NAME=${sh(v.vendor_name)} VENDOR_CODE=${sh(code)} ` +
`AGENT_NAME=${sh(v.agent_name || 'Agent')} SCRAPER_ID=${sh(scraper)}`;
- const cmd = `pm2 delete ${name} >/dev/null 2>&1; ${env} pm2 start ${sh(AGENT_HOST)} --name ${name} --update-env 2>&1 | tail -2; pm2 save >/dev/null 2>&1`;
+ // --kill-timeout 4000: give the old instance 4s to release the listen socket on a
+ // restart before SIGKILL, so the new bind doesn't race into EADDRINUSE (pairs with
+ // the agent-host.js backoff binder).
+ const cmd = `pm2 delete ${name} >/dev/null 2>&1; ${env} pm2 start ${sh(AGENT_HOST)} --name ${name} --kill-timeout 4000 --update-env 2>&1 | tail -2; pm2 save >/dev/null 2>&1`;
const r = await localExec(cmd);
res.json({ ...r, name, port: v.agent_port, scraper: scraper || '(none — health only)' });
});
← 0b82d9d security: strip hardcoded secret -> env-first/passwordless.
·
back to Vendor Agents Viewer
·
chore: v1.0.1 (session close — lint ✓ / refactor no-op) ae7d49a →