Skip to content

Commit 298fec1

Browse files
Tyriardeepak1556
andauthored
Update to conpty v1.22 (#759)
* Update to conpty v1.22 Fixes #490 * Add logs to show the problem happening * spec: increase exit delay * refactor: input and output handling with conpty * Close the input read and output write handles after creating the client process * Call ReleasePseudoConsole after creating the client process which will cause the output read handle to close when there is no more data from the session * For manual termination via Kill, we close the input write handle and call into ClosePseudoConsole, we should then drain the output handle NB: ideally draining the output handle should have been enough to cause the client process to close but it doesn't work, we call TerminateProcess to fix this case. * chore: restore legacy conpty path --------- Co-authored-by: deepak1556 <hop2deep@gmail.com>
1 parent 5a61096 commit 298fec1

File tree

14 files changed

+79
-89
lines changed

14 files changed

+79
-89
lines changed

publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@ extends:
6161
tag: beta
6262
publishRequiresApproval: false
6363

64-
apiScanExcludes: 'package/third_party/conpty/1.20.240626001/win10-arm64/*.*'
64+
apiScanExcludes: 'package/third_party/conpty/1.22.250204002/win10-arm64/*.*'
6565
apiScanSoftwareName: 'vscode-node-pty'
6666
apiScanSoftwareVersion: '1'

src/win/conpty.cc

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,17 @@ typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE
3434
typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize);
3535
typedef HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc);
3636
typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
37+
typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc);
3738

3839
#endif
3940

40-
typedef struct _PseudoConsole
41-
{
42-
HANDLE hSignal;
43-
HANDLE hPtyReference;
44-
HANDLE hConPtyProcess;
45-
} PseudoConsole;
46-
4741
struct pty_baton {
4842
int id;
4943
HANDLE hIn;
5044
HANDLE hOut;
5145
HPCON hpc;
5246

5347
HANDLE hShell;
54-
HANDLE pty_job_handle;
5548

5649
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
5750
};
@@ -80,27 +73,6 @@ static bool remove_pty_baton(int id) {
8073
return false;
8174
}
8275

83-
static BOOL InitPtyJobHandle(HANDLE* pty_job_handle) {
84-
SECURITY_ATTRIBUTES attr;
85-
memset(&attr, 0, sizeof(attr));
86-
attr.bInheritHandle = FALSE;
87-
*pty_job_handle = CreateJobObjectW(&attr, nullptr);
88-
if (*pty_job_handle == NULL) {
89-
return FALSE;
90-
}
91-
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
92-
limits.BasicLimitInformation.LimitFlags =
93-
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION |
94-
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
95-
if (!SetInformationJobObject(*pty_job_handle,
96-
JobObjectExtendedLimitInformation,
97-
&limits,
98-
sizeof(limits))) {
99-
return FALSE;
100-
}
101-
return TRUE;
102-
}
103-
10476
struct ExitEvent {
10577
int exit_code = 0;
10678
};
@@ -130,11 +102,6 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
130102
// Get process exit code.
131103
GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
132104
// Clean up handles
133-
// Calling DisconnectNamedPipes here or in PtyKill results in a crash,
134-
// ref https://github.com/microsoft/node-pty/issues/512,
135-
// so we only call CloseHandle for now.
136-
CloseHandle(baton->hIn);
137-
CloseHandle(baton->hOut);
138105
CloseHandle(baton->hShell);
139106
assert(remove_pty_baton(baton->id));
140107

@@ -459,14 +426,15 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
459426
throw errorWithCode(info, "Cannot create process");
460427
}
461428

462-
if (useConptyDll) {
463-
if (!InitPtyJobHandle(&handle->pty_job_handle)) {
464-
throw errorWithCode(info, "Initialize pty job handle failed");
465-
}
466-
467-
PseudoConsole* server = reinterpret_cast<PseudoConsole*>(handle->hpc);
468-
if (!AssignProcessToJobObject(handle->pty_job_handle, server->hConPtyProcess)) {
469-
throw errorWithCode(info, "AssignProcessToJobObject for server failed");
429+
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
430+
bool fLoadedDll = hLibrary != nullptr;
431+
if (fLoadedDll)
432+
{
433+
PFNRELEASEPSEUDOCONSOLE const pfnReleasePseudoConsole = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress(
434+
(HMODULE)hLibrary, "ConptyReleasePseudoConsole");
435+
if (pfnReleasePseudoConsole)
436+
{
437+
pfnReleasePseudoConsole(handle->hpc);
470438
}
471439
}
472440

@@ -475,6 +443,9 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
475443

476444
// Close the thread handle to avoid resource leak
477445
CloseHandle(piClient.hThread);
446+
// Close the input read and output write handle of the pseudoconsole
447+
CloseHandle(handle->hIn);
448+
CloseHandle(handle->hOut);
478449

479450
SetupExitCallback(env, exitCallback, handle);
480451

@@ -588,11 +559,7 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
588559
pfnClosePseudoConsole(handle->hpc);
589560
}
590561
}
591-
592562
TerminateProcess(handle->hShell, 1);
593-
if (useConptyDll) {
594-
CloseHandle(handle->pty_job_handle);
595-
}
596563
}
597564

598565
return env.Undefined();

src/windowsConoutConnection.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ export class ConoutConnection implements IDisposable {
5454
}
5555

5656
dispose(): void {
57-
if (this._isDisposed) {
58-
return;
59-
}
60-
this._isDisposed = true;
6157
// Drain all data from the socket before closing
6258
this._drainDataAndClose();
6359
}

src/windowsPtyAgent.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ export class WindowsPtyAgent {
158158
}
159159

160160
public kill(): void {
161-
this._inSocket.readable = false;
162-
this._outSocket.readable = false;
163161
// Tell the agent to kill the pty, this releases handles to the process
164162
if (this._useConpty) {
165163
if (!this._useConptyDll) {
164+
this._inSocket.readable = false;
165+
this._outSocket.readable = false;
166166
this._getConsoleProcessList().then(consoleProcessList => {
167167
consoleProcessList.forEach((pid: number) => {
168168
try {
@@ -172,8 +172,16 @@ export class WindowsPtyAgent {
172172
}
173173
});
174174
});
175+
(this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll);
176+
this._conoutSocketWorker.dispose();
177+
} else {
178+
// Close the input write handle to signal the end of session.
179+
this._inSocket.destroy();
180+
(this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll);
181+
this._outSocket.on('data', () => {
182+
this._conoutSocketWorker.dispose();
183+
});
175184
}
176-
(this._ptyNative as IConptyNative).kill(this._pty, this._useConptyDll);
177185
} else {
178186
// Because pty.kill closes the handle, it will kill most processes by itself.
179187
// Process IDs can be reused as soon as all handles to them are
@@ -191,7 +199,6 @@ export class WindowsPtyAgent {
191199
}
192200
});
193201
}
194-
this._conoutSocketWorker.dispose();
195202
}
196203

197204
private _getConsoleProcessList(): Promise<number[]> {
@@ -235,18 +242,26 @@ export class WindowsPtyAgent {
235242
*/
236243
private _$onProcessExit(exitCode: number): void {
237244
this._exitCode = exitCode;
238-
this._flushDataAndCleanUp();
239-
this._outSocket.on('data', () => this._flushDataAndCleanUp());
245+
if (!this._useConptyDll) {
246+
this._flushDataAndCleanUp();
247+
this._outSocket.on('data', () => this._flushDataAndCleanUp());
248+
}
240249
}
241250

242251
private _flushDataAndCleanUp(): void {
252+
if (this._useConptyDll) {
253+
return;
254+
}
243255
if (this._closeTimeout) {
244256
clearTimeout(this._closeTimeout);
245257
}
246258
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
247259
}
248260

249261
private _cleanUpProcess(): void {
262+
if (this._useConptyDll) {
263+
return;
264+
}
250265
this._inSocket.readable = false;
251266
this._outSocket.readable = false;
252267
this._outSocket.destroy();

src/windowsTerminal.test.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ function pollForProcessState(desiredState: IProcessState, intervalMs: number = 1
2626
psList({ all: true }).then(ps => {
2727
let success = true;
2828
const pids = Object.keys(desiredState).map(k => parseInt(k, 10));
29+
console.log('expected pids', JSON.stringify(pids));
2930
pids.forEach(pid => {
3031
if (desiredState[pid]) {
3132
if (!ps.some(p => p.pid === pid)) {
33+
console.log(`pid ${pid} does not exist`);
3234
success = false;
3335
}
3436
} else {
3537
if (ps.some(p => p.pid === pid)) {
38+
console.log(`pid ${pid} still exists`);
3639
success = false;
3740
}
3841
}
@@ -71,6 +74,7 @@ function pollForProcessTreeSize(pid: number, size: number, intervalMs: number =
7174
}).forEach(p => openList.push(p));
7275
list.push(current);
7376
}
77+
console.log('list', JSON.stringify(list));
7478
const success = list.length === size;
7579
if (success) {
7680
clearInterval(interval);
@@ -88,37 +92,33 @@ function pollForProcessTreeSize(pid: number, size: number, intervalMs: number =
8892
}
8993

9094
if (process.platform === 'win32') {
91-
[[true, false], [true, true], [false, false]].forEach(([useConpty, useConptyDll]) => {
95+
[[false, false], [true, false], [true, true]].forEach(([useConpty, useConptyDll]) => {
9296
describe(`WindowsTerminal (useConpty = ${useConpty}, useConptyDll = ${useConptyDll})`, () => {
9397
describe('kill', () => {
94-
it('should not crash parent process', (done) => {
98+
it('should not crash parent process', function (done) {
99+
this.timeout(20000);
95100
const term = new WindowsTerminal('cmd.exe', [], { useConpty, useConptyDll });
101+
term.on('exit', () => done());
96102
term.kill();
97-
// Add done call to deferred function queue to ensure the kill call has completed
98-
(<any>term)._defer(done);
99103
});
100104
it('should kill the process tree', function (done: Mocha.Done): void {
101-
this.timeout(10000);
105+
this.timeout(20000);
102106
const term = new WindowsTerminal('cmd.exe', [], { useConpty, useConptyDll });
103107
// Start sub-processes
104108
term.write('powershell.exe\r');
105-
term.write('notepad.exe\r');
106109
term.write('node.exe\r');
107-
pollForProcessTreeSize(term.pid, 4, 500, 5000).then(list => {
110+
console.log('start poll for tree size');
111+
pollForProcessTreeSize(term.pid, 3, 500, 5000).then(list => {
108112
assert.strictEqual(list[0].name.toLowerCase(), 'cmd.exe');
109113
assert.strictEqual(list[1].name.toLowerCase(), 'powershell.exe');
110-
assert.strictEqual(list[2].name.toLowerCase(), 'notepad.exe');
111-
assert.strictEqual(list[3].name.toLowerCase(), 'node.exe');
114+
assert.strictEqual(list[2].name.toLowerCase(), 'node.exe');
112115
term.kill();
113116
const desiredState: IProcessState = {};
114117
desiredState[list[0].pid] = false;
115118
desiredState[list[1].pid] = false;
116-
desiredState[list[2].pid] = true;
117-
desiredState[list[3].pid] = false;
119+
desiredState[list[2].pid] = false;
118120
term.on('exit', () => {
119-
pollForProcessState(desiredState).then(() => {
120-
// Kill notepad before done
121-
process.kill(list[2].pid);
121+
pollForProcessState(desiredState, 1000, 5000).then(() => {
122122
done();
123123
});
124124
});
@@ -127,11 +127,15 @@ if (process.platform === 'win32') {
127127
});
128128

129129
describe('resize', () => {
130-
it('should throw a non-native exception when resizing an invalid value', () => {
130+
it('should throw a non-native exception when resizing an invalid value', (done) => {
131131
const term = new WindowsTerminal('cmd.exe', [], { useConpty, useConptyDll });
132132
assert.throws(() => term.resize(-1, -1));
133133
assert.throws(() => term.resize(0, 0));
134134
assert.doesNotThrow(() => term.resize(1, 1));
135+
term.on('exit', () => {
136+
done();
137+
});
138+
term.kill();
135139
});
136140
it('should throw a non-native exception when resizing a killed terminal', (done) => {
137141
const term = new WindowsTerminal('cmd.exe', [], { useConpty, useConptyDll });
@@ -146,7 +150,8 @@ if (process.platform === 'win32') {
146150
});
147151

148152
describe('Args as CommandLine', () => {
149-
it('should not fail running a file containing a space in the path', (done) => {
153+
it('should not fail running a file containing a space in the path', function (done) {
154+
this.timeout(10000);
150155
const spaceFolder = path.resolve(__dirname, '..', 'fixtures', 'space folder');
151156
if (!fs.existsSync(spaceFolder)) {
152157
fs.mkdirSync(spaceFolder);
@@ -173,8 +178,9 @@ if (process.platform === 'win32') {
173178
});
174179

175180
describe('env', () => {
176-
it('should set environment variables of the shell', (done) => {
177-
const term = new WindowsTerminal('cmd.exe', '/C echo %FOO%', { env: { FOO: 'BAR' }});
181+
it('should set environment variables of the shell', function (done) {
182+
this.timeout(10000);
183+
const term = new WindowsTerminal('cmd.exe', '/C echo %FOO%', { useConpty, useConptyDll, env: { FOO: 'BAR' }});
178184
let result = '';
179185
term.on('data', (data) => {
180186
result += data;
@@ -187,15 +193,17 @@ if (process.platform === 'win32') {
187193
});
188194

189195
describe('On close', () => {
190-
it('should return process zero exit codes', (done) => {
196+
it('should return process zero exit codes', function (done) {
197+
this.timeout(10000);
191198
const term = new WindowsTerminal('cmd.exe', '/C exit', { useConpty, useConptyDll });
192199
term.on('exit', (code) => {
193200
assert.strictEqual(code, 0);
194201
done();
195202
});
196203
});
197204

198-
it('should return process non-zero exit codes', (done) => {
205+
it('should return process non-zero exit codes', function (done) {
206+
this.timeout(10000);
199207
const term = new WindowsTerminal('cmd.exe', '/C exit 2', { useConpty, useConptyDll });
200208
term.on('exit', (code) => {
201209
assert.strictEqual(code, 2);
@@ -205,7 +213,8 @@ if (process.platform === 'win32') {
205213
});
206214

207215
describe('Write', () => {
208-
it('should accept input', (done) => {
216+
it('should accept input', function (done) {
217+
this.timeout(10000);
209218
const term = new WindowsTerminal('cmd.exe', '', { useConpty, useConptyDll });
210219
term.write('exit\r');
211220
term.on('exit', () => {

src/worker/conoutSocketWorker.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44

55
import { parentPort, workerData } from 'worker_threads';
66
import { Socket, createServer } from 'net';
7+
import * as fs from 'fs';
78
import { ConoutWorkerMessage, IWorkerData, getWorkerPipeName } from '../shared/conout';
89

910
const conoutPipeName = (workerData as IWorkerData).conoutPipeName;
10-
const conoutSocket = new Socket();
11+
const outSocketFD = fs.openSync(conoutPipeName, 'r');
12+
const conoutSocket = new Socket({
13+
fd: outSocketFD,
14+
readable: true
15+
});
1116
conoutSocket.setEncoding('utf8');
12-
conoutSocket.connect(conoutPipeName, () => {
13-
const server = createServer(workerSocket => {
14-
conoutSocket.pipe(workerSocket);
15-
});
16-
server.listen(getWorkerPipeName(conoutPipeName));
17-
18-
if (!parentPort) {
19-
throw new Error('worker_threads parentPort is null');
20-
}
21-
parentPort.postMessage(ConoutWorkerMessage.READY);
17+
const server = createServer(workerSocket => {
18+
conoutSocket.pipe(workerSocket);
2219
});
20+
server.listen(getWorkerPipeName(conoutPipeName));
21+
22+
if (!parentPort) {
23+
throw new Error('worker_threads parentPort is null');
24+
}
25+
parentPort.postMessage(ConoutWorkerMessage.READY);
-1.31 MB
Binary file not shown.
-103 KB
Binary file not shown.
-1.22 MB
Binary file not shown.
-111 KB
Binary file not shown.

0 commit comments

Comments
 (0)