agent-tank 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-tank.js +21 -5
- package/package.json +1 -1
- package/src/agents/base.js +110 -2
- package/src/agents/codex.js +3 -0
- package/src/index.js +50 -4
- package/src/json-rpc-client.js +3 -0
- package/src/shutdown-handler.js +32 -0
package/bin/agent-tank.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { parseArgs } = require('node:util');
|
|
5
5
|
const { AgentTank } = require('../src/index.js');
|
|
6
|
+
const { installShutdownHandlers } = require('../src/shutdown-handler.js');
|
|
7
|
+
const pkg = require('../package.json');
|
|
6
8
|
|
|
7
9
|
const options = {
|
|
8
10
|
claude: { type: 'boolean', default: false },
|
|
@@ -17,6 +19,7 @@ const options = {
|
|
|
17
19
|
'fresh-process': { type: 'boolean', default: false },
|
|
18
20
|
config: { type: 'string', short: 'c' },
|
|
19
21
|
help: { type: 'boolean', short: 'h', default: false },
|
|
22
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
20
23
|
'auto-discover': { type: 'boolean', default: true },
|
|
21
24
|
'auto-refresh': { type: 'boolean', default: true },
|
|
22
25
|
'auto-refresh-interval': { type: 'string', default: '60' },
|
|
@@ -56,6 +59,7 @@ Options:
|
|
|
56
59
|
--auth-token <token> API key for Bearer token auth
|
|
57
60
|
--fresh-process Spawn a new process per refresh (default: false)
|
|
58
61
|
--config, -c Path to config file (JSON)
|
|
62
|
+
--version, -v Show version
|
|
59
63
|
--auto-discover Auto-discover available agents (default: true)
|
|
60
64
|
--auto-refresh Enable/disable background auto-refresh (default: true)
|
|
61
65
|
--auto-refresh-mode <mode> Refresh mode: none, interval, activity (default: activity)
|
|
@@ -127,6 +131,12 @@ async function main() {
|
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
if (values.version) {
|
|
135
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
136
|
+
process.exitCode = 0;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
// Load config file if specified
|
|
131
141
|
let config = {};
|
|
132
142
|
if (values.config) {
|
|
@@ -255,15 +265,19 @@ async function main() {
|
|
|
255
265
|
});
|
|
256
266
|
|
|
257
267
|
// Graceful shutdown
|
|
258
|
-
|
|
268
|
+
let shuttingDown = false;
|
|
269
|
+
let cleanupShutdownHandlers = () => {};
|
|
270
|
+
const shutdown = (signal) => {
|
|
271
|
+
if (shuttingDown) return;
|
|
272
|
+
shuttingDown = true;
|
|
273
|
+
cleanupShutdownHandlers();
|
|
259
274
|
if (!jsonMode) {
|
|
260
|
-
originalLog(
|
|
275
|
+
originalLog(`\nShutting down${signal ? ` (${signal})` : ''}...`);
|
|
261
276
|
}
|
|
262
277
|
watcher.stop();
|
|
263
|
-
process.
|
|
278
|
+
process.exit(0);
|
|
264
279
|
};
|
|
265
|
-
|
|
266
|
-
process.on('SIGTERM', shutdown);
|
|
280
|
+
cleanupShutdownHandlers = installShutdownHandlers({ shutdown });
|
|
267
281
|
|
|
268
282
|
try {
|
|
269
283
|
if (onceMode) {
|
|
@@ -281,6 +295,7 @@ async function main() {
|
|
|
281
295
|
originalLog(JSON.stringify(agentStatus, null, 2));
|
|
282
296
|
}
|
|
283
297
|
}
|
|
298
|
+
cleanupShutdownHandlers();
|
|
284
299
|
watcher.stop();
|
|
285
300
|
process.exitCode = 0;
|
|
286
301
|
return;
|
|
@@ -288,6 +303,7 @@ async function main() {
|
|
|
288
303
|
|
|
289
304
|
await watcher.start();
|
|
290
305
|
} catch (err) {
|
|
306
|
+
cleanupShutdownHandlers();
|
|
291
307
|
if (jsonMode) {
|
|
292
308
|
console.log = originalLog;
|
|
293
309
|
console.log(JSON.stringify({ error: err.message }, null, 2));
|
package/package.json
CHANGED
package/src/agents/base.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const pty = require('node-pty');
|
|
2
|
+
const process = require('node:process');
|
|
2
3
|
const logger = require('../logger.js');
|
|
3
4
|
|
|
4
5
|
class BaseAgent {
|
|
@@ -24,6 +25,22 @@ class BaseAgent {
|
|
|
24
25
|
this._onDataCallback = null;
|
|
25
26
|
this._commandInFlight = false;
|
|
26
27
|
this._disposables = [];
|
|
28
|
+
this._stopRequested = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_createStopError() {
|
|
32
|
+
return new Error('Agent stopping');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isStopping() {
|
|
36
|
+
return this._stopRequested;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
requestStop() {
|
|
40
|
+
this._stopRequested = true;
|
|
41
|
+
this.isRefreshing = false;
|
|
42
|
+
this._commandInFlight = false;
|
|
43
|
+
this._onDataCallback = null;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
getStatus() {
|
|
@@ -39,6 +56,11 @@ class BaseAgent {
|
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
async refresh() {
|
|
59
|
+
if (this.isStopping()) {
|
|
60
|
+
logger.agent(this.name, 'Skipping refresh while stopping');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
if (this.isRefreshing) {
|
|
43
65
|
logger.agent(this.name, 'Already refreshing, skipping...');
|
|
44
66
|
return;
|
|
@@ -68,6 +90,10 @@ class BaseAgent {
|
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
|
|
93
|
+
if (this.isStopping()) {
|
|
94
|
+
throw this._createStopError();
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
const output = await this.runCommand();
|
|
72
98
|
logger.agent(this.name, 'Got output, length:', logger.dim(`${output.length} chars`));
|
|
73
99
|
|
|
@@ -108,6 +134,10 @@ class BaseAgent {
|
|
|
108
134
|
if (hasData) { this.usage = parsed; this.lastUpdated = new Date().toISOString(); } else if (this.usage) { logger.agent(this.name, 'Parse returned no data, preserving last known usage'); this.error = 'Failed to parse — using cached data'; } else { this.usage = parsed; this.lastUpdated = new Date().toISOString(); }
|
|
109
135
|
logger.agent(this.name, 'Parsed usage:', logger.json(this.usage));
|
|
110
136
|
} catch (err) {
|
|
137
|
+
if (this.isStopping() && err.message === 'Agent stopping') {
|
|
138
|
+
logger.agent(this.name, 'Refresh cancelled during shutdown');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
111
141
|
logger.error(`[${this.name}] Error during refresh:`, err.message);
|
|
112
142
|
this.error = err.message;
|
|
113
143
|
} finally {
|
|
@@ -117,6 +147,10 @@ class BaseAgent {
|
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
async runCommand() {
|
|
150
|
+
if (this.isStopping()) {
|
|
151
|
+
throw this._createStopError();
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
if (this.freshProcess) {
|
|
121
155
|
return this._runCommandFresh();
|
|
122
156
|
}
|
|
@@ -130,6 +164,10 @@ class BaseAgent {
|
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
async spawnProcess() {
|
|
167
|
+
if (this.isStopping()) {
|
|
168
|
+
throw this._createStopError();
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
return new Promise((resolve, reject) => {
|
|
134
172
|
let trustHandled = false;
|
|
135
173
|
let spawnOutput = '';
|
|
@@ -145,6 +183,9 @@ class BaseAgent {
|
|
|
145
183
|
cwd: '/tmp',
|
|
146
184
|
env,
|
|
147
185
|
});
|
|
186
|
+
if (typeof this.shell.pid === 'number') {
|
|
187
|
+
logger.agent(this.name, 'Persistent process pid:', logger.dim(String(this.shell.pid)));
|
|
188
|
+
}
|
|
148
189
|
} catch (spawnErr) {
|
|
149
190
|
logger.error(`[${this.name}] Failed to spawn:`, spawnErr.message);
|
|
150
191
|
reject(new Error(`Failed to spawn ${this.command}: ${spawnErr.message}`));
|
|
@@ -152,6 +193,10 @@ class BaseAgent {
|
|
|
152
193
|
}
|
|
153
194
|
|
|
154
195
|
const timer = setTimeout(() => {
|
|
196
|
+
if (this.isStopping()) {
|
|
197
|
+
reject(this._createStopError());
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
155
200
|
logger.error(`[${this.name}] Spawn timeout after ${this.getTimeout()}ms`);
|
|
156
201
|
this.killProcess();
|
|
157
202
|
reject(new Error('Timeout waiting for process to become ready'));
|
|
@@ -159,6 +204,15 @@ class BaseAgent {
|
|
|
159
204
|
|
|
160
205
|
let trustCooldownUntil = 0;
|
|
161
206
|
const spawnDataHandler = this.shell.onData((data) => {
|
|
207
|
+
if (this.isStopping()) {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
spawnDataHandler.dispose();
|
|
210
|
+
spawnExitHandler.dispose();
|
|
211
|
+
this.killProcess();
|
|
212
|
+
reject(this._createStopError());
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
162
216
|
spawnOutput += data;
|
|
163
217
|
|
|
164
218
|
// Handle trust/update prompts during spawn (only once each)
|
|
@@ -202,12 +256,20 @@ class BaseAgent {
|
|
|
202
256
|
spawnExitHandler.dispose();
|
|
203
257
|
this.shell = null;
|
|
204
258
|
this.processReady = false;
|
|
259
|
+
if (this.isStopping()) {
|
|
260
|
+
reject(this._createStopError());
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
205
263
|
reject(new Error(`Process exited during spawn with code ${exitCode}`));
|
|
206
264
|
});
|
|
207
265
|
});
|
|
208
266
|
}
|
|
209
267
|
|
|
210
268
|
async sendCommandAndWait() {
|
|
269
|
+
if (this.isStopping()) {
|
|
270
|
+
throw this._createStopError();
|
|
271
|
+
}
|
|
272
|
+
|
|
211
273
|
return new Promise((resolve, reject) => {
|
|
212
274
|
const existingOutput = this.output;
|
|
213
275
|
const existingAuthState = this.detectAuthenticationState ? this.detectAuthenticationState(existingOutput) : null;
|
|
@@ -228,6 +290,12 @@ class BaseAgent {
|
|
|
228
290
|
};
|
|
229
291
|
|
|
230
292
|
const timer = setTimeout(() => {
|
|
293
|
+
if (this.isStopping()) {
|
|
294
|
+
this._commandInFlight = false;
|
|
295
|
+
this._onDataCallback = null;
|
|
296
|
+
reject(this._createStopError());
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
231
299
|
logger.agent(this.name, 'Command timeout after', logger.dim(`${this.getTimeout()}ms`), ', output length:', logger.dim(`${this.output.length}`));
|
|
232
300
|
if (this.output.length > 0) {
|
|
233
301
|
logger.agent(this.name, 'Partial output:', logger.dim(this.stripAnsi(this.output).substring(0, 500)));
|
|
@@ -245,6 +313,13 @@ class BaseAgent {
|
|
|
245
313
|
}, this.getTimeout());
|
|
246
314
|
|
|
247
315
|
this._onDataCallback = () => {
|
|
316
|
+
if (this.isStopping()) {
|
|
317
|
+
this._commandInFlight = false;
|
|
318
|
+
this._onDataCallback = null;
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
reject(this._createStopError());
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
248
323
|
if (this.hasCompleteOutput(this.output)) {
|
|
249
324
|
logger.agent(this.name, 'Complete output detected');
|
|
250
325
|
// Small delay to capture any remaining output
|
|
@@ -298,15 +373,27 @@ class BaseAgent {
|
|
|
298
373
|
_handleAdditionalPrompts(_s, _d, _o) { } // Hook for subclasses; killProcess: see below
|
|
299
374
|
killProcess() { // Terminates the persistent PTY process
|
|
300
375
|
if (this.shell) {
|
|
301
|
-
|
|
376
|
+
const pidText = typeof this.shell.pid === 'number' ? ` (pid ${this.shell.pid})` : '';
|
|
377
|
+
logger.agent(this.name, `Killing persistent process${pidText}`);
|
|
378
|
+
const shell = this.shell;
|
|
302
379
|
for (const d of this._disposables) { d.dispose(); }
|
|
303
380
|
this._disposables = []; this._onDataCallback = null; this.processReady = false;
|
|
304
|
-
try {
|
|
381
|
+
try {
|
|
382
|
+
// PTY-backed CLIs may spawn descendants; kill the whole process group when available.
|
|
383
|
+
if (typeof shell.pid === 'number' && shell.pid > 0) {
|
|
384
|
+
try { process.kill(-shell.pid, 'SIGTERM'); } catch (_e) { /* Group may not exist */ }
|
|
385
|
+
}
|
|
386
|
+
shell.kill();
|
|
387
|
+
} catch (_e) { /* Process may already be dead */ }
|
|
305
388
|
this.shell = null;
|
|
306
389
|
}
|
|
307
390
|
}
|
|
308
391
|
|
|
309
392
|
async _runCommandFresh() {
|
|
393
|
+
if (this.isStopping()) {
|
|
394
|
+
throw this._createStopError();
|
|
395
|
+
}
|
|
396
|
+
|
|
310
397
|
return new Promise((resolve, reject) => {
|
|
311
398
|
const timeout = this.getTimeout();
|
|
312
399
|
let output = '';
|
|
@@ -334,6 +421,12 @@ class BaseAgent {
|
|
|
334
421
|
|
|
335
422
|
const timer = setTimeout(() => {
|
|
336
423
|
if (!completed) {
|
|
424
|
+
if (this.isStopping()) {
|
|
425
|
+
completed = true;
|
|
426
|
+
shell.kill();
|
|
427
|
+
reject(this._createStopError());
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
337
430
|
completed = true;
|
|
338
431
|
logger.agent(this.name, 'Timeout after', logger.dim(`${timeout}ms`), ', output length:', logger.dim(`${output.length}`));
|
|
339
432
|
if (output.length > 0) {
|
|
@@ -352,6 +445,16 @@ class BaseAgent {
|
|
|
352
445
|
|
|
353
446
|
let trustCooldownUntil = 0;
|
|
354
447
|
shell.onData((data) => {
|
|
448
|
+
if (this.isStopping()) {
|
|
449
|
+
if (!completed) {
|
|
450
|
+
completed = true;
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
shell.kill();
|
|
453
|
+
reject(this._createStopError());
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
355
458
|
output += data;
|
|
356
459
|
|
|
357
460
|
if (output.length <= data.length) {
|
|
@@ -395,6 +498,10 @@ class BaseAgent {
|
|
|
395
498
|
if (!completed) {
|
|
396
499
|
completed = true;
|
|
397
500
|
clearTimeout(timer);
|
|
501
|
+
if (this.isStopping()) {
|
|
502
|
+
reject(this._createStopError());
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
398
505
|
logger.agent(this.name, 'Process exited with code', logger.dim(exitCode), ', output length:', logger.dim(`${output.length}`));
|
|
399
506
|
if (output) {
|
|
400
507
|
resolve(output);
|
|
@@ -414,6 +521,7 @@ class BaseAgent {
|
|
|
414
521
|
|
|
415
522
|
/** Lightweight keepalive method to prevent session expiration. Subclasses can override. @returns {Promise<boolean>} True if keepalive succeeded */
|
|
416
523
|
async keepalive() {
|
|
524
|
+
if (this.isStopping()) { return false; }
|
|
417
525
|
if (this.freshProcess) { console.log(`[${this.name}] Keepalive skipped (fresh process mode)`); return true; }
|
|
418
526
|
if (!this.shell || !this.processReady) { console.log(`[${this.name}] Keepalive: spawning process...`); await this.spawnProcess(); }
|
|
419
527
|
if (this.shell) { console.log(`[${this.name}] Keepalive: sending ping...`); this.shell.write('\x1b'); return true; }
|
package/src/agents/codex.js
CHANGED
|
@@ -156,6 +156,9 @@ class CodexAgent extends BaseAgent {
|
|
|
156
156
|
cwd: '/tmp',
|
|
157
157
|
env: { ...process.env, TERM: 'xterm-256color' },
|
|
158
158
|
});
|
|
159
|
+
if (typeof this.shell.pid === 'number') {
|
|
160
|
+
logger.agent(this.name, 'Persistent process pid:', logger.dim(String(this.shell.pid)));
|
|
161
|
+
}
|
|
159
162
|
} catch (spawnErr) {
|
|
160
163
|
logger.error(`[${this.name}] Failed to spawn:`, spawnErr.message);
|
|
161
164
|
reject(new Error(`Failed to spawn ${this.command}: ${spawnErr.message}`));
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,24 @@ const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
|
23
23
|
*/
|
|
24
24
|
const AUTO_REFRESH_MODES = ['none', 'interval', 'activity'];
|
|
25
25
|
|
|
26
|
+
function describeAgentShutdown(agent) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
|
|
29
|
+
if (agent.shell && typeof agent.shell.pid === 'number') {
|
|
30
|
+
parts.push(`pty pid ${agent.shell.pid}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (agent._rpcClient) {
|
|
34
|
+
parts.push('json-rpc client');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (parts.length === 0) {
|
|
38
|
+
parts.push('no active child process');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return parts.join(', ');
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
class AgentTank {
|
|
27
45
|
constructor(options = {}) {
|
|
28
46
|
this.port = options.port || 3456;
|
|
@@ -37,6 +55,7 @@ class AgentTank {
|
|
|
37
55
|
this.publicStatus = {}; // Public API status from upstream providers
|
|
38
56
|
this.skipServer = options.skipServer || false; // Skip HTTP server in one-shot mode
|
|
39
57
|
this.lastRefreshedAt = null;
|
|
58
|
+
this.stopping = false;
|
|
40
59
|
|
|
41
60
|
// Auto-refresh configuration (backend periodic refresh)
|
|
42
61
|
// Determine the mode: 'none', 'interval', or 'activity' (default: 'activity')
|
|
@@ -121,6 +140,10 @@ class AgentTank {
|
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
async start() {
|
|
143
|
+
if (this.stopping) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
124
147
|
// Display startup banner with website URL, version, and copyright
|
|
125
148
|
this.displayStartupBanner();
|
|
126
149
|
|
|
@@ -169,18 +192,24 @@ class AgentTank {
|
|
|
169
192
|
await Promise.all(
|
|
170
193
|
Array.from(this.agents.values()).map(agent =>
|
|
171
194
|
agent.spawnProcess().catch(err =>
|
|
172
|
-
|
|
195
|
+
this.stopping || err.message === 'Agent stopping'
|
|
196
|
+
? null
|
|
197
|
+
: logger.error(`Error spawning ${agent.name}:`, err.message)
|
|
173
198
|
)
|
|
174
199
|
)
|
|
175
200
|
);
|
|
176
201
|
}
|
|
177
202
|
|
|
203
|
+
if (this.stopping) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
178
207
|
// Initial fetch
|
|
179
208
|
logger.info('📊 Fetching initial usage data...');
|
|
180
209
|
await this.refreshAll();
|
|
181
210
|
|
|
182
211
|
// Start backend auto-refresh if enabled (skip in one-shot mode)
|
|
183
|
-
if (!this.skipServer) {
|
|
212
|
+
if (!this.skipServer && !this.stopping) {
|
|
184
213
|
this.startAutoRefresh();
|
|
185
214
|
this.startKeepalive();
|
|
186
215
|
}
|
|
@@ -231,6 +260,10 @@ class AgentTank {
|
|
|
231
260
|
}
|
|
232
261
|
|
|
233
262
|
async refreshAll() {
|
|
263
|
+
if (this.stopping) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
234
267
|
logger.info('🔄 Refreshing all agents...');
|
|
235
268
|
const agentNames = Array.from(this.agents.keys());
|
|
236
269
|
|
|
@@ -240,7 +273,9 @@ class AgentTank {
|
|
|
240
273
|
Promise.all(
|
|
241
274
|
Array.from(this.agents.values()).map(agent =>
|
|
242
275
|
agent.refresh().catch(err => {
|
|
243
|
-
|
|
276
|
+
if (!this.stopping && err.message !== 'Agent stopping') {
|
|
277
|
+
logger.error(`Error refreshing ${agent.name}:`, err.message);
|
|
278
|
+
}
|
|
244
279
|
})
|
|
245
280
|
)
|
|
246
281
|
),
|
|
@@ -276,6 +311,10 @@ class AgentTank {
|
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
async refreshAgent(name) {
|
|
314
|
+
if (this.stopping) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
279
318
|
const agent = this.agents.get(name);
|
|
280
319
|
if (!agent) {
|
|
281
320
|
throw new Error(`Agent not found: ${name}`);
|
|
@@ -345,12 +384,19 @@ class AgentTank {
|
|
|
345
384
|
}
|
|
346
385
|
|
|
347
386
|
stop() {
|
|
387
|
+
this.stopping = true;
|
|
388
|
+
console.log('[Shutdown] Stopping Agent Tank...');
|
|
348
389
|
this.stopAutoRefresh();
|
|
349
390
|
this.stopKeepalive();
|
|
350
|
-
for (const agent of this.agents
|
|
391
|
+
for (const [name, agent] of this.agents) {
|
|
392
|
+
console.log(`[Shutdown] ${name}: ${describeAgentShutdown(agent)}`);
|
|
393
|
+
if (typeof agent.requestStop === 'function') {
|
|
394
|
+
agent.requestStop();
|
|
395
|
+
}
|
|
351
396
|
agent.killProcess();
|
|
352
397
|
}
|
|
353
398
|
if (this.server) {
|
|
399
|
+
console.log('[Shutdown] Closing HTTP server');
|
|
354
400
|
this.server.close();
|
|
355
401
|
}
|
|
356
402
|
}
|
package/src/json-rpc-client.js
CHANGED
|
@@ -44,6 +44,9 @@ class JsonRpcClient extends EventEmitter {
|
|
|
44
44
|
env: this.options.env,
|
|
45
45
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
46
|
});
|
|
47
|
+
if (typeof this.process.pid === 'number') {
|
|
48
|
+
console.log(`[JsonRpcClient] Started ${this.command} (pid ${this.process.pid})`);
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
this.process.on('error', (err) => {
|
|
49
52
|
this.isConnected = false;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function installShutdownHandlers({ shutdown, processObj = process, stdin = process.stdin }) {
|
|
2
|
+
const onSigterm = () => shutdown('SIGTERM');
|
|
3
|
+
const onStdinData = (chunk) => {
|
|
4
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk || ''));
|
|
5
|
+
if (data.includes(0x03)) {
|
|
6
|
+
shutdown('CTRL+C');
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
processObj.on('SIGTERM', onSigterm);
|
|
11
|
+
|
|
12
|
+
const shouldWatchStdin = stdin && stdin.isTTY && typeof stdin.on === 'function';
|
|
13
|
+
if (shouldWatchStdin) {
|
|
14
|
+
stdin.on('data', onStdinData);
|
|
15
|
+
if (typeof stdin.resume === 'function') {
|
|
16
|
+
stdin.resume();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return () => {
|
|
21
|
+
processObj.off('SIGTERM', onSigterm);
|
|
22
|
+
|
|
23
|
+
if (shouldWatchStdin) {
|
|
24
|
+
stdin.off('data', onStdinData);
|
|
25
|
+
if (typeof stdin.pause === 'function') {
|
|
26
|
+
stdin.pause();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { installShutdownHandlers };
|