mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-10 11:47:54 +02:00
fix: restart stale plan canvas servers
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
@@ -36,8 +37,33 @@ const DEFAULT_THINKING_STALE_MS = 90 * 1000;
|
||||
const DEFAULT_TYPING_EXPIRY_MS = 30 * 1000;
|
||||
// Presence is push-based, so expiring states need a tick to re-broadcast on.
|
||||
const DEFAULT_PRESENCE_SWEEP_MS = 5 * 1000;
|
||||
const PLAN_CANVAS_PROTOCOL_VERSION = 2;
|
||||
const TYPING_STATES = new Set(['thinking', 'typing', 'idle']);
|
||||
|
||||
// Package versions do not distinguish two worktrees on the same release.
|
||||
// Fingerprint every module loaded into the detached server so a current CLI
|
||||
// never reuses stale browser or protocol code from an older checkout.
|
||||
function computeRuntimeId() {
|
||||
const sources = [
|
||||
['loopback-guard.js', path.join(__dirname, '..', 'loopback-guard.js')],
|
||||
['markdown.js', path.join(__dirname, 'markdown.js')],
|
||||
['sdk.js', path.join(__dirname, 'sdk.js')],
|
||||
['server.js', __filename],
|
||||
['sessions.js', path.join(__dirname, 'sessions.js')],
|
||||
['ui.js', path.join(__dirname, 'ui.js')]
|
||||
];
|
||||
const digest = crypto.createHash('sha256');
|
||||
for (const [name, sourcePath] of sources) {
|
||||
digest.update(name);
|
||||
digest.update('\0');
|
||||
digest.update(fs.readFileSync(sourcePath));
|
||||
digest.update('\0');
|
||||
}
|
||||
return digest.digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
const PLAN_CANVAS_RUNTIME_ID = computeRuntimeId();
|
||||
|
||||
const CONTENT_TYPES = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.gif': 'image/gif',
|
||||
@@ -541,7 +567,13 @@ function createPlanCanvasServer({
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (req.method === 'GET' && pathname === '/health') {
|
||||
return sendJson(res, 200, { ok: true, app: 'ecc-plan-canvas', version });
|
||||
return sendJson(res, 200, {
|
||||
ok: true,
|
||||
app: 'ecc-plan-canvas',
|
||||
version,
|
||||
protocolVersion: PLAN_CANVAS_PROTOCOL_VERSION,
|
||||
runtimeId: PLAN_CANVAS_RUNTIME_ID
|
||||
});
|
||||
}
|
||||
if (req.method === 'POST' && pathname === '/shutdown') {
|
||||
sendJson(res, 200, { status: 'stopping' });
|
||||
@@ -628,6 +660,8 @@ module.exports = {
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_THINKING_STALE_MS,
|
||||
DEFAULT_TYPING_EXPIRY_MS,
|
||||
PLAN_CANVAS_PROTOCOL_VERSION,
|
||||
PLAN_CANVAS_RUNTIME_ID,
|
||||
createPlanCanvasServer,
|
||||
resolveIdleTimeoutMs,
|
||||
resolvePort
|
||||
|
||||
+32
-8
@@ -30,6 +30,8 @@ const {
|
||||
} = require('./lib/plan-canvas/sessions');
|
||||
const {
|
||||
DEFAULT_HOST,
|
||||
PLAN_CANVAS_PROTOCOL_VERSION,
|
||||
PLAN_CANVAS_RUNTIME_ID,
|
||||
createPlanCanvasServer,
|
||||
resolveIdleTimeoutMs,
|
||||
resolvePort
|
||||
@@ -168,12 +170,21 @@ function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Start (or reuse) the detached canvas server and return its port. A version
|
||||
// mismatch after an ECC update restarts the server so browser and CLI never
|
||||
// disagree about the protocol.
|
||||
function serverIsCompatible(health) {
|
||||
return Boolean(
|
||||
health &&
|
||||
health.version === VERSION &&
|
||||
health.protocolVersion === PLAN_CANVAS_PROTOCOL_VERSION &&
|
||||
health.runtimeId === PLAN_CANVAS_RUNTIME_ID
|
||||
);
|
||||
}
|
||||
|
||||
// Start (or reuse) the detached canvas server and return its port. Worktrees
|
||||
// can share a package version while carrying different Canvas code, so the
|
||||
// health handshake binds reuse to the exact server runtime as well.
|
||||
async function ensureServer({ stateDir, port }) {
|
||||
const health = await healthCheck(port);
|
||||
if (health && health.version === VERSION) return port;
|
||||
if (serverIsCompatible(health)) return port;
|
||||
if (health) {
|
||||
await request(port, 'POST', '/shutdown').catch(() => {});
|
||||
for (let i = 0; i < 20 && (await healthCheck(port)); i++) await sleep(100);
|
||||
@@ -189,9 +200,9 @@ async function ensureServer({ stateDir, port }) {
|
||||
fs.closeSync(logFd);
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await sleep(100);
|
||||
if (await healthCheck(port)) return port;
|
||||
if (serverIsCompatible(await healthCheck(port))) return port;
|
||||
}
|
||||
throw new Error(`plan-canvas server did not become healthy on port ${port}; check ${path.join(stateDir, 'server.log')}`);
|
||||
throw new Error(`plan-canvas server did not become compatible on port ${port}; check ${path.join(stateDir, 'server.log')}`);
|
||||
}
|
||||
|
||||
function openBrowser(url) {
|
||||
@@ -218,7 +229,13 @@ async function cmdStatus({ stateDir, port }) {
|
||||
return { server: 'not running', hint: 'open an artifact to start one', stateDir };
|
||||
}
|
||||
const sessions = await request(port, 'GET', '/api/sessions');
|
||||
return { server: `http://${DEFAULT_HOST}:${port}`, version: health.version, sessions: sessions.body.sessions };
|
||||
return {
|
||||
server: `http://${DEFAULT_HOST}:${port}`,
|
||||
version: health.version,
|
||||
protocolVersion: health.protocolVersion,
|
||||
runtimeId: health.runtimeId,
|
||||
sessions: sessions.body.sessions
|
||||
};
|
||||
}
|
||||
|
||||
async function cmdOpen(file, args, { stateDir, port }) {
|
||||
@@ -364,7 +381,14 @@ async function cmdServer(args, { stateDir, port }) {
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
serverInfoPath(stateDir),
|
||||
JSON.stringify({ pid: process.pid, port: bound.port, version: VERSION, startedAt: new Date().toISOString() }, null, 2)
|
||||
JSON.stringify({
|
||||
pid: process.pid,
|
||||
port: bound.port,
|
||||
version: VERSION,
|
||||
protocolVersion: PLAN_CANVAS_PROTOCOL_VERSION,
|
||||
runtimeId: PLAN_CANVAS_RUNTIME_ID,
|
||||
startedAt: new Date().toISOString()
|
||||
}, null, 2)
|
||||
);
|
||||
// Sessions restored from disk resume their file watchers.
|
||||
for (const session of store.list()) {
|
||||
|
||||
@@ -14,7 +14,10 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { createSessionStore } = require('../../scripts/lib/plan-canvas/sessions');
|
||||
const { createPlanCanvasServer } = require('../../scripts/lib/plan-canvas/server');
|
||||
const {
|
||||
PLAN_CANVAS_RUNTIME_ID,
|
||||
createPlanCanvasServer
|
||||
} = require('../../scripts/lib/plan-canvas/server');
|
||||
|
||||
async function test(name, fn) {
|
||||
try {
|
||||
@@ -143,7 +146,8 @@ async function main() {
|
||||
ok: true,
|
||||
app: 'ecc-plan-canvas',
|
||||
version: '9.9.9-test',
|
||||
protocolVersion: 2
|
||||
protocolVersion: 2,
|
||||
runtimeId: PLAN_CANVAS_RUNTIME_ID
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user