feat(daemon): Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs

This commit is contained in:
2025-08-26 15:00:15 +00:00
parent 4e0944034b
commit 50c5fdb0ea
12 changed files with 233 additions and 236 deletions

View File

@@ -44,13 +44,33 @@ export class TspmIpcClient {
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
id: 'tspm-cli',
socketPath: this.socketPath,
heartbeat: false, // Disable heartbeat for now
clientId: `cli-${process.pid}`,
connectRetry: {
enabled: true,
initialDelay: 100,
maxDelay: 2000,
maxAttempts: 30,
totalTimeout: 15000,
},
registerTimeoutMs: 8000,
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000,
heartbeatThrowOnTimeout: false // Don't throw, emit events instead
});
// Connect to the daemon
try {
await this.ipcClient.connect();
await this.ipcClient.connect({ waitForReady: true });
this.isConnected = true;
// Handle heartbeat timeouts gracefully
this.ipcClient.on('heartbeatTimeout', () => {
console.warn('Heartbeat timeout detected, connection may be degraded');
this.isConnected = false;
});
console.log('Connected to TSPM daemon');
} catch (error) {
console.error('Failed to connect to daemon:', error);
@@ -109,6 +129,30 @@ export class TspmIpcClient {
throw error;
}
}
/**
* Subscribe to log updates for a specific process
*/
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.subscribe(`topic:${topic}`, handler);
}
/**
* Unsubscribe from log updates for a specific process
*/
public async unsubscribe(processId: string): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.unsubscribe(`topic:${topic}`);
}
/**
* Check if the daemon is running
@@ -176,18 +220,15 @@ export class TspmIpcClient {
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready (check for socket file)
const maxWaitTime = 10000; // 10 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (await this.isDaemonRunning()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 500));
// Wait for daemon to be ready using SmartIPC's helper
try {
await plugins.smartipc.SmartIpc.waitForServer({
socketPath: this.socketPath,
timeoutMs: 15000,
});
} catch (error) {
throw new Error(`Daemon failed to start: ${error.message}`);
}
throw new Error('Daemon failed to start within timeout period');
}
/**