151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import * as net from 'net';
|
|
import { spawn } from 'child_process';
|
|
import { fileURLToPath } from 'url';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import { getSocketPath, isDaemonRunning, setSession, getSession } from './daemon.js';
|
|
import type { Response } from './types.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
let DEBUG = false;
|
|
|
|
export function setDebug(enabled: boolean): void {
|
|
DEBUG = enabled;
|
|
}
|
|
|
|
export { setSession, getSession };
|
|
|
|
function debug(...args: unknown[]): void {
|
|
if (DEBUG) {
|
|
console.error('[debug]', ...args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for socket to exist
|
|
*/
|
|
async function waitForSocket(maxAttempts = 30): Promise<boolean> {
|
|
const socketPath = getSocketPath();
|
|
debug('Waiting for socket at', socketPath);
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
if (fs.existsSync(socketPath)) {
|
|
debug('Socket found after', i * 100, 'ms');
|
|
return true;
|
|
}
|
|
await new Promise(r => setTimeout(r, 100));
|
|
}
|
|
debug('Socket not found after', maxAttempts * 100, 'ms');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Ensure daemon is running, start if not
|
|
*/
|
|
export async function ensureDaemon(): Promise<void> {
|
|
const session = getSession();
|
|
debug(`Checking if daemon is running for session "${session}"...`);
|
|
if (isDaemonRunning()) {
|
|
debug('Daemon already running');
|
|
return;
|
|
}
|
|
|
|
debug('Starting daemon...');
|
|
const daemonPath = path.join(__dirname, 'daemon.js');
|
|
const child = spawn(process.execPath, [daemonPath], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
env: { ...process.env, VEB_DAEMON: '1', VEB_SESSION: session },
|
|
});
|
|
child.unref();
|
|
|
|
// Wait for socket to be created
|
|
const ready = await waitForSocket();
|
|
if (!ready) {
|
|
throw new Error('Failed to start daemon');
|
|
}
|
|
|
|
debug(`Daemon started for session "${session}"`);
|
|
}
|
|
|
|
/**
|
|
* Send a command to the daemon
|
|
*/
|
|
export async function sendCommand(command: Record<string, unknown>): Promise<Response> {
|
|
const socketPath = getSocketPath();
|
|
debug('Sending command:', JSON.stringify(command));
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let resolved = false;
|
|
let buffer = '';
|
|
const startTime = Date.now();
|
|
|
|
const socket = net.createConnection(socketPath);
|
|
|
|
socket.on('connect', () => {
|
|
debug('Connected to daemon, sending command...');
|
|
socket.write(JSON.stringify(command) + '\n');
|
|
});
|
|
|
|
socket.on('data', (data) => {
|
|
buffer += data.toString();
|
|
debug('Received data:', buffer.length, 'bytes');
|
|
|
|
// Try to parse complete JSON from buffer
|
|
const newlineIdx = buffer.indexOf('\n');
|
|
if (newlineIdx !== -1) {
|
|
const jsonStr = buffer.substring(0, newlineIdx);
|
|
try {
|
|
const response = JSON.parse(jsonStr) as Response;
|
|
debug('Response received in', Date.now() - startTime, 'ms');
|
|
resolved = true;
|
|
socket.end();
|
|
resolve(response);
|
|
} catch (e) {
|
|
debug('JSON parse error:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
debug('Socket error:', err.message);
|
|
if (!resolved) {
|
|
reject(new Error(`Connection error: ${err.message}`));
|
|
}
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length);
|
|
if (!resolved && buffer.trim()) {
|
|
try {
|
|
const response = JSON.parse(buffer.trim()) as Response;
|
|
resolve(response);
|
|
} catch {
|
|
reject(new Error('Invalid response from daemon'));
|
|
}
|
|
} else if (!resolved) {
|
|
reject(new Error('Connection closed without response'));
|
|
}
|
|
});
|
|
|
|
// Timeout after 60 seconds
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
debug('Command timeout after 60s');
|
|
socket.destroy();
|
|
reject(new Error('Command timeout'));
|
|
}
|
|
}, 60000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a command, ensuring daemon is running first
|
|
*/
|
|
export async function send(command: Record<string, unknown>): Promise<Response> {
|
|
const startTime = Date.now();
|
|
await ensureDaemon();
|
|
debug('ensureDaemon took', Date.now() - startTime, 'ms');
|
|
return sendCommand(command);
|
|
}
|