-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathclient.ts
More file actions
124 lines (106 loc) · 3.02 KB
/
client.ts
File metadata and controls
124 lines (106 loc) · 3.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {spawn} from 'node:child_process';
import fs from 'node:fs';
import net from 'node:net';
import {logger} from '../logger.js';
import {PipeTransport} from '../third_party/index.js';
import type {DaemonMessage} from './types.js';
import {
DAEMON_SCRIPT_PATH,
getSocketPath,
getPidFilePath,
isDaemonRunning,
} from './utils.js';
/**
* Waits for a file to be created and populated.
*/
function waitForFile(filePath: string, timeout = 5000) {
return new Promise<void>((resolve, reject) => {
if (fs.existsSync(filePath) && fs.statSync(filePath).size > 0) {
resolve();
return;
}
const timer = setTimeout(() => {
fs.unwatchFile(filePath);
reject(
new Error(`Timeout: file ${filePath} not found within ${timeout}ms`),
);
}, timeout);
fs.watchFile(filePath, {interval: 500}, curr => {
if (curr.size > 0) {
clearTimeout(timer);
fs.unwatchFile(filePath); // Always clean up your listeners!
resolve();
}
});
});
}
export async function startDaemon(mcpArgs: string[] = []) {
if (isDaemonRunning()) {
logger('Daemon is already running');
return;
}
logger('Starting daemon...');
const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
detached: true,
stdio: 'ignore',
cwd: process.cwd(),
});
await new Promise<void>((resolve, reject) => {
child.on('error', err => {
reject(err);
});
child.on('exit', code => {
logger(`Child exited with code ${code}`);
reject(new Error(`Daemon process exited prematurely with code ${code}`));
});
waitForFile(getPidFilePath()).then(resolve).catch(reject);
});
child.unref();
logger(`Pid file found ${getPidFilePath()}`);
}
const SEND_COMMAND_TIMEOUT = 60_000; // ms
/**
* `sendCommand` opens a socket connection sends a single command and disconnects.
*/
async function sendCommand(command: DaemonMessage) {
const socketPath = getSocketPath();
const socket = net.createConnection({
path: socketPath,
});
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
socket.destroy();
reject(new Error('Timeout waiting for daemon response'));
}, SEND_COMMAND_TIMEOUT);
const transport = new PipeTransport(socket, socket);
transport.onmessage = async (message: string) => {
clearTimeout(timer);
logger('onmessage', message);
resolve(JSON.parse(message));
};
socket.on('error', error => {
clearTimeout(timer);
logger('Socket error:', error);
reject(error);
});
socket.on('close', () => {
clearTimeout(timer);
logger('Socket closed:');
reject(new Error('Socket closed'));
});
logger('Sending message', command);
transport.send(JSON.stringify(command));
});
}
export async function stopDaemon() {
if (!isDaemonRunning()) {
logger('Daemon is not running');
return;
}
await sendCommand({method: 'stop'});
}