Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/sshRemoteAgentHostHelpers.test.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
8
import { NullLogService } from '../../../log/common/log.js';
9
import {
10
buildCLIDownloadUrl,
11
cleanupRemoteAgentHost,
12
findRunningAgentHost,
13
getAgentHostStateFile,
14
getRemoteCLIBin,
15
getRemoteCLIDir,
16
redactToken,
17
resolveRemotePlatform,
18
shellEscape,
19
validateShellToken,
20
writeAgentHostState,
21
type ISshExec,
22
} from '../../node/sshRemoteAgentHostHelpers.js';
23
24
suite('SSH Remote Agent Host Helpers', () => {
25
26
ensureNoDisposablesAreLeakedInTestSuite();
27
28
const logService = new NullLogService();
29
30
suite('validateShellToken', () => {
31
test('accepts alphanumeric strings', () => {
32
assert.strictEqual(validateShellToken('insider', 'quality'), 'insider');
33
assert.strictEqual(validateShellToken('stable', 'quality'), 'stable');
34
assert.strictEqual(validateShellToken('exploration', 'quality'), 'exploration');
35
});
36
37
test('accepts dots, dashes, and underscores', () => {
38
assert.strictEqual(validateShellToken('my-build_1.0', 'quality'), 'my-build_1.0');
39
});
40
41
test('rejects strings with spaces', () => {
42
assert.throws(() => validateShellToken('foo bar', 'quality'), /Unsafe quality/);
43
});
44
45
test('rejects strings with shell metacharacters', () => {
46
assert.throws(() => validateShellToken('foo;rm -rf /', 'quality'), /Unsafe quality/);
47
assert.throws(() => validateShellToken('$(whoami)', 'quality'), /Unsafe quality/);
48
assert.throws(() => validateShellToken('foo\'bar', 'quality'), /Unsafe quality/);
49
});
50
51
test('rejects empty string', () => {
52
assert.throws(() => validateShellToken('', 'quality'), /Unsafe quality/);
53
});
54
});
55
56
suite('getRemoteCLIDir', () => {
57
test('returns standard path for stable', () => {
58
assert.strictEqual(getRemoteCLIDir('stable'), '~/.vscode-cli');
59
});
60
61
test('returns quality-suffixed path for insider', () => {
62
assert.strictEqual(getRemoteCLIDir('insider'), '~/.vscode-cli-insider');
63
});
64
65
test('returns quality-suffixed path for exploration', () => {
66
assert.strictEqual(getRemoteCLIDir('exploration'), '~/.vscode-cli-exploration');
67
});
68
});
69
70
suite('getRemoteCLIBin', () => {
71
test('returns code for stable', () => {
72
assert.strictEqual(getRemoteCLIBin('stable'), '~/.vscode-cli/code');
73
});
74
75
test('returns code-insiders for insider', () => {
76
assert.strictEqual(getRemoteCLIBin('insider'), '~/.vscode-cli-insider/code-insiders');
77
});
78
});
79
80
suite('shellEscape', () => {
81
test('wraps simple string in single quotes', () => {
82
assert.strictEqual(shellEscape('hello'), '\'hello\'');
83
});
84
85
test('escapes embedded single quotes', () => {
86
assert.strictEqual(shellEscape('it\'s'), '\'it\'\\\'\'s\'');
87
});
88
89
test('handles empty string', () => {
90
assert.strictEqual(shellEscape(''), '\'\'');
91
});
92
93
test('passes through special chars safely wrapped', () => {
94
assert.strictEqual(shellEscape('$(rm -rf /)'), '\'$(rm -rf /)\'');
95
});
96
});
97
98
suite('resolveRemotePlatform', () => {
99
test('detects Linux x64', () => {
100
assert.deepStrictEqual(resolveRemotePlatform('Linux', 'x86_64'), { os: 'linux', arch: 'x64' });
101
});
102
103
test('detects Linux amd64', () => {
104
assert.deepStrictEqual(resolveRemotePlatform('Linux', 'amd64'), { os: 'linux', arch: 'x64' });
105
});
106
107
test('detects Linux arm64 (aarch64)', () => {
108
assert.deepStrictEqual(resolveRemotePlatform('Linux', 'aarch64'), { os: 'linux', arch: 'arm64' });
109
});
110
111
test('detects Linux arm64', () => {
112
assert.deepStrictEqual(resolveRemotePlatform('Linux', 'arm64'), { os: 'linux', arch: 'arm64' });
113
});
114
115
test('detects Linux armhf', () => {
116
assert.deepStrictEqual(resolveRemotePlatform('Linux', 'armv7l'), { os: 'linux', arch: 'armhf' });
117
});
118
119
test('detects Darwin x64', () => {
120
assert.deepStrictEqual(resolveRemotePlatform('Darwin', 'x86_64'), { os: 'darwin', arch: 'x64' });
121
});
122
123
test('detects Darwin arm64', () => {
124
assert.deepStrictEqual(resolveRemotePlatform('Darwin', 'arm64'), { os: 'darwin', arch: 'arm64' });
125
});
126
127
test('handles whitespace in uname output', () => {
128
assert.deepStrictEqual(resolveRemotePlatform(' Linux\n', ' x86_64\n'), { os: 'linux', arch: 'x64' });
129
});
130
131
test('returns undefined for Windows', () => {
132
assert.strictEqual(resolveRemotePlatform('MINGW64_NT-10.0-19041', 'x86_64'), undefined);
133
});
134
135
test('returns undefined for unknown OS', () => {
136
assert.strictEqual(resolveRemotePlatform('FreeBSD', 'amd64'), undefined);
137
});
138
139
test('returns undefined for unknown arch', () => {
140
assert.strictEqual(resolveRemotePlatform('Linux', 'ppc64le'), undefined);
141
});
142
});
143
144
suite('buildCLIDownloadUrl', () => {
145
test('constructs correct URL', () => {
146
assert.strictEqual(
147
buildCLIDownloadUrl('linux', 'x64', 'insider'),
148
'https://update.code.visualstudio.com/latest/cli-linux-x64/insider'
149
);
150
});
151
152
test('works for darwin arm64 stable', () => {
153
assert.strictEqual(
154
buildCLIDownloadUrl('darwin', 'arm64', 'stable'),
155
'https://update.code.visualstudio.com/latest/cli-darwin-arm64/stable'
156
);
157
});
158
});
159
160
suite('redactToken', () => {
161
test('redacts token in WebSocket URL', () => {
162
assert.strictEqual(
163
redactToken('ws://127.0.0.1:12345?tkn=secret123'),
164
'ws://127.0.0.1:12345?tkn=***'
165
);
166
});
167
168
test('redacts token with following whitespace', () => {
169
assert.strictEqual(
170
redactToken('ws://127.0.0.1:12345?tkn=abc123 done'),
171
'ws://127.0.0.1:12345?tkn=*** done'
172
);
173
});
174
175
test('preserves text without tokens', () => {
176
assert.strictEqual(redactToken('no token here'), 'no token here');
177
});
178
179
test('redacts multiple tokens', () => {
180
assert.strictEqual(
181
redactToken('?tkn=one and ?tkn=two'),
182
'?tkn=*** and ?tkn=***'
183
);
184
});
185
});
186
187
suite('getAgentHostStateFile', () => {
188
test('returns path under CLI dir', () => {
189
assert.strictEqual(
190
getAgentHostStateFile('insider'),
191
'~/.vscode-cli-insider/.agent-host-state'
192
);
193
});
194
195
test('returns path for stable', () => {
196
assert.strictEqual(
197
getAgentHostStateFile('stable'),
198
'~/.vscode-cli/.agent-host-state'
199
);
200
});
201
});
202
203
suite('findRunningAgentHost', () => {
204
205
function createMockExec(responses: Map<string, { stdout: string; stderr: string; code: number }>): ISshExec {
206
return async (command: string, _opts?: { ignoreExitCode?: boolean }) => {
207
for (const [pattern, response] of responses) {
208
if (command.includes(pattern)) {
209
return response;
210
}
211
}
212
return { stdout: '', stderr: '', code: 1 };
213
};
214
}
215
216
test('returns undefined when no state file exists', async () => {
217
const exec = createMockExec(new Map([
218
['cat', { stdout: '', stderr: '', code: 1 }],
219
]));
220
const result = await findRunningAgentHost(exec, logService, 'insider');
221
assert.strictEqual(result, undefined);
222
});
223
224
test('returns undefined when state file is empty', async () => {
225
const exec = createMockExec(new Map([
226
['cat', { stdout: ' \n', stderr: '', code: 0 }],
227
]));
228
const result = await findRunningAgentHost(exec, logService, 'insider');
229
assert.strictEqual(result, undefined);
230
});
231
232
test('cleans up corrupt state file', async () => {
233
const commands: string[] = [];
234
const exec: ISshExec = async (command: string) => {
235
commands.push(command);
236
if (command.includes('cat')) {
237
return { stdout: 'not json at all', stderr: '', code: 0 };
238
}
239
return { stdout: '', stderr: '', code: 0 };
240
};
241
const result = await findRunningAgentHost(exec, logService, 'insider');
242
assert.strictEqual(result, undefined);
243
assert.ok(commands.some(c => c.includes('rm -f')));
244
});
245
246
test('cleans up state file with missing pid', async () => {
247
const commands: string[] = [];
248
const exec: ISshExec = async (command: string) => {
249
commands.push(command);
250
if (command.includes('cat')) {
251
return { stdout: JSON.stringify({ pid: 0, port: 8080, connectionToken: null }), stderr: '', code: 0 };
252
}
253
return { stdout: '', stderr: '', code: 0 };
254
};
255
const result = await findRunningAgentHost(exec, logService, 'insider');
256
assert.strictEqual(result, undefined);
257
assert.ok(commands.some(c => c.includes('rm -f')));
258
});
259
260
test('cleans up state file with missing port', async () => {
261
const commands: string[] = [];
262
const exec: ISshExec = async (command: string) => {
263
commands.push(command);
264
if (command.includes('cat')) {
265
return { stdout: JSON.stringify({ pid: 1234, port: 0, connectionToken: null }), stderr: '', code: 0 };
266
}
267
return { stdout: '', stderr: '', code: 0 };
268
};
269
const result = await findRunningAgentHost(exec, logService, 'insider');
270
assert.strictEqual(result, undefined);
271
assert.ok(commands.some(c => c.includes('rm -f')));
272
});
273
274
test('rejects state file with string pid', async () => {
275
const exec = createMockExec(new Map([
276
['cat', { stdout: JSON.stringify({ pid: '1234', port: 8080, connectionToken: null }), stderr: '', code: 0 }],
277
]));
278
const result = await findRunningAgentHost(exec, logService, 'insider');
279
assert.strictEqual(result, undefined);
280
});
281
282
test('rejects state file with negative pid', async () => {
283
const exec = createMockExec(new Map([
284
['cat', { stdout: JSON.stringify({ pid: -1, port: 8080, connectionToken: null }), stderr: '', code: 0 }],
285
]));
286
const result = await findRunningAgentHost(exec, logService, 'insider');
287
assert.strictEqual(result, undefined);
288
});
289
290
test('rejects state file with non-integer port', async () => {
291
const exec = createMockExec(new Map([
292
['cat', { stdout: JSON.stringify({ pid: 1234, port: 8080.5, connectionToken: null }), stderr: '', code: 0 }],
293
]));
294
const result = await findRunningAgentHost(exec, logService, 'insider');
295
assert.strictEqual(result, undefined);
296
});
297
298
test('rejects state file with numeric connectionToken', async () => {
299
const exec = createMockExec(new Map([
300
['cat', { stdout: JSON.stringify({ pid: 1234, port: 8080, connectionToken: 42 }), stderr: '', code: 0 }],
301
]));
302
const result = await findRunningAgentHost(exec, logService, 'insider');
303
assert.strictEqual(result, undefined);
304
});
305
306
test('rejects state file with port above 65535', async () => {
307
const exec = createMockExec(new Map([
308
['cat', { stdout: JSON.stringify({ pid: 1234, port: 70000, connectionToken: null }), stderr: '', code: 0 }],
309
]));
310
const result = await findRunningAgentHost(exec, logService, 'insider');
311
assert.strictEqual(result, undefined);
312
});
313
314
test('cleans up stale state when PID is not running', async () => {
315
const state = { pid: 9999, port: 8080, connectionToken: 'tok123' };
316
const commands: string[] = [];
317
const exec: ISshExec = async (command: string) => {
318
commands.push(command);
319
if (command.includes('cat')) {
320
return { stdout: JSON.stringify(state), stderr: '', code: 0 };
321
}
322
if (command.includes('kill -0')) {
323
return { stdout: '', stderr: '', code: 1 }; // PID not running
324
}
325
return { stdout: '', stderr: '', code: 0 };
326
};
327
const result = await findRunningAgentHost(exec, logService, 'insider');
328
assert.strictEqual(result, undefined);
329
assert.ok(commands.some(c => c.includes('rm -f')));
330
});
331
332
test('returns port and token when PID is alive', async () => {
333
const state = { pid: 1234, port: 8080, connectionToken: 'mytoken' };
334
const exec = createMockExec(new Map([
335
['cat', { stdout: JSON.stringify(state), stderr: '', code: 0 }],
336
['kill -0', { stdout: '', stderr: '', code: 0 }],
337
]));
338
const result = await findRunningAgentHost(exec, logService, 'insider');
339
assert.deepStrictEqual(result, { port: 8080, connectionToken: 'mytoken' });
340
});
341
342
test('returns undefined connectionToken when state has null token', async () => {
343
const state = { pid: 1234, port: 8080, connectionToken: null };
344
const exec = createMockExec(new Map([
345
['cat', { stdout: JSON.stringify(state), stderr: '', code: 0 }],
346
['kill -0', { stdout: '', stderr: '', code: 0 }],
347
]));
348
const result = await findRunningAgentHost(exec, logService, 'insider');
349
assert.deepStrictEqual(result, { port: 8080, connectionToken: undefined });
350
});
351
});
352
353
suite('writeAgentHostState', () => {
354
355
test('does not write when pid is undefined', async () => {
356
const commands: string[] = [];
357
const exec: ISshExec = async (command: string) => {
358
commands.push(command);
359
return { stdout: '', stderr: '', code: 0 };
360
};
361
await writeAgentHostState(exec, logService, 'insider', undefined, 8080, 'token');
362
assert.strictEqual(commands.length, 0);
363
});
364
365
test('does not write when pid is 0', async () => {
366
const commands: string[] = [];
367
const exec: ISshExec = async (command: string) => {
368
commands.push(command);
369
return { stdout: '', stderr: '', code: 0 };
370
};
371
await writeAgentHostState(exec, logService, 'insider', 0, 8080, 'token');
372
assert.strictEqual(commands.length, 0);
373
});
374
375
test('writes state file with correct JSON', async () => {
376
const commands: string[] = [];
377
const exec: ISshExec = async (command: string) => {
378
commands.push(command);
379
return { stdout: '', stderr: '', code: 0 };
380
};
381
await writeAgentHostState(exec, logService, 'insider', 1234, 8080, 'mytoken');
382
assert.strictEqual(commands.length, 1);
383
assert.ok(commands[0].includes('.agent-host-state'));
384
// Verify the JSON content is present in the echo command
385
assert.ok(commands[0].includes('"pid":1234'));
386
assert.ok(commands[0].includes('"port":8080'));
387
assert.ok(commands[0].includes('"connectionToken":"mytoken"'));
388
// Verify restrictive umask in a subshell is used to protect the connection token
389
assert.ok(commands[0].includes('rm -f'));
390
assert.ok(commands[0].includes('(umask 077'));
391
});
392
393
test('writes null connectionToken when undefined', async () => {
394
const commands: string[] = [];
395
const exec: ISshExec = async (command: string) => {
396
commands.push(command);
397
return { stdout: '', stderr: '', code: 0 };
398
};
399
await writeAgentHostState(exec, logService, 'insider', 1234, 8080, undefined);
400
assert.strictEqual(commands.length, 1);
401
assert.ok(commands[0].includes('"connectionToken":null'));
402
});
403
404
test('logs warning when write command fails', async () => {
405
const exec: ISshExec = async () => {
406
return { stdout: '', stderr: 'Permission denied', code: 1 };
407
};
408
const warnings: string[] = [];
409
const capturingLog = new NullLogService();
410
capturingLog.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); };
411
await writeAgentHostState(exec, capturingLog, 'insider', 1234, 8080, 'tok');
412
// Should log a warning with exit code and stderr
413
assert.strictEqual(warnings.length, 1);
414
assert.ok(warnings[0].includes('Failed to write'));
415
assert.ok(warnings[0].includes('exit code 1'));
416
assert.ok(warnings[0].includes('Permission denied'));
417
});
418
});
419
420
suite('cleanupRemoteAgentHost', () => {
421
422
test('removes state file even when no state exists', async () => {
423
const commands: string[] = [];
424
const exec: ISshExec = async (command: string) => {
425
commands.push(command);
426
if (command.includes('cat')) {
427
return { stdout: '', stderr: '', code: 1 };
428
}
429
return { stdout: '', stderr: '', code: 0 };
430
};
431
await cleanupRemoteAgentHost(exec, logService, 'insider');
432
assert.ok(commands.some(c => c.includes('rm -f')));
433
});
434
435
test('kills process and removes state file', async () => {
436
const state = { pid: 5678, port: 9090, connectionToken: null };
437
const commands: string[] = [];
438
const exec: ISshExec = async (command: string) => {
439
commands.push(command);
440
if (command.includes('cat')) {
441
return { stdout: JSON.stringify(state), stderr: '', code: 0 };
442
}
443
return { stdout: '', stderr: '', code: 0 };
444
};
445
await cleanupRemoteAgentHost(exec, logService, 'insider');
446
assert.ok(commands.some(c => c.includes('kill 5678')));
447
assert.ok(commands.some(c => c.includes('rm -f')));
448
});
449
450
test('handles corrupt state file gracefully', async () => {
451
const commands: string[] = [];
452
const exec: ISshExec = async (command: string) => {
453
commands.push(command);
454
if (command.includes('cat')) {
455
return { stdout: '{invalid json', stderr: '', code: 0 };
456
}
457
return { stdout: '', stderr: '', code: 0 };
458
};
459
// Should not throw
460
await cleanupRemoteAgentHost(exec, logService, 'insider');
461
// Should still clean up the file
462
assert.ok(commands.some(c => c.includes('rm -f')));
463
// Should not have attempted to kill anything
464
assert.ok(!commands.some(c => c.startsWith('kill')));
465
});
466
467
test('does not kill when pid is 0', async () => {
468
const state = { pid: 0, port: 9090, connectionToken: null };
469
const commands: string[] = [];
470
const exec: ISshExec = async (command: string) => {
471
commands.push(command);
472
if (command.includes('cat')) {
473
return { stdout: JSON.stringify(state), stderr: '', code: 0 };
474
}
475
return { stdout: '', stderr: '', code: 0 };
476
};
477
await cleanupRemoteAgentHost(exec, logService, 'insider');
478
assert.ok(!commands.some(c => c.match(/^kill \d/)));
479
assert.ok(commands.some(c => c.includes('rm -f')));
480
});
481
482
test('kills process for stable quality', async () => {
483
const state = { pid: 1234, port: 8080, connectionToken: null };
484
const commands: string[] = [];
485
const exec: ISshExec = async (command: string) => {
486
commands.push(command);
487
if (command.includes('cat')) {
488
return { stdout: JSON.stringify(state), stderr: '', code: 0 };
489
}
490
return { stdout: '', stderr: '', code: 0 };
491
};
492
await cleanupRemoteAgentHost(exec, logService, 'stable');
493
assert.ok(commands.some(c => c.includes('kill 1234')));
494
});
495
});
496
});
497
498