node-pty 1.1.0-beta9 → 1.2.0-beta.1

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.
Files changed (81) hide show
  1. package/README.md +2 -0
  2. package/binding.gyp +7 -4
  3. package/deps/winpty/src/agent/Agent.cc +11 -3
  4. package/deps/winpty/src/winpty.gyp +29 -1
  5. package/lib/conpty_console_list_agent.js +3 -7
  6. package/lib/conpty_console_list_agent.js.map +1 -1
  7. package/lib/index.js +2 -1
  8. package/lib/index.js.map +1 -1
  9. package/lib/terminal.js.map +1 -1
  10. package/lib/terminal.test.js.map +1 -1
  11. package/lib/unixTerminal.js +78 -37
  12. package/lib/unixTerminal.js.map +1 -1
  13. package/lib/unixTerminal.test.js +3 -5
  14. package/lib/unixTerminal.test.js.map +1 -1
  15. package/lib/utils.js +23 -1
  16. package/lib/utils.js.map +1 -1
  17. package/lib/windowsConoutConnection.js +6 -3
  18. package/lib/windowsConoutConnection.js.map +1 -1
  19. package/lib/windowsPtyAgent.js +45 -48
  20. package/lib/windowsPtyAgent.js.map +1 -1
  21. package/lib/windowsTerminal.js +19 -21
  22. package/lib/windowsTerminal.js.map +1 -1
  23. package/lib/windowsTerminal.test.js +40 -26
  24. package/lib/windowsTerminal.test.js.map +1 -1
  25. package/lib/worker/conoutSocketWorker.js.map +1 -1
  26. package/package.json +6 -2
  27. package/prebuilds/darwin-arm64/pty.node +0 -0
  28. package/prebuilds/darwin-arm64/spawn-helper +0 -0
  29. package/prebuilds/darwin-x64/pty.node +0 -0
  30. package/prebuilds/darwin-x64/spawn-helper +0 -0
  31. package/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  32. package/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  33. package/prebuilds/win32-arm64/conpty.node +0 -0
  34. package/prebuilds/win32-arm64/conpty.pdb +0 -0
  35. package/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  36. package/prebuilds/win32-arm64/conpty_console_list.pdb +0 -0
  37. package/prebuilds/win32-arm64/pty.node +0 -0
  38. package/prebuilds/win32-arm64/pty.pdb +0 -0
  39. package/prebuilds/win32-arm64/winpty-agent.exe +0 -0
  40. package/prebuilds/win32-arm64/winpty-agent.pdb +0 -0
  41. package/prebuilds/win32-arm64/winpty.dll +0 -0
  42. package/prebuilds/win32-arm64/winpty.pdb +0 -0
  43. package/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  44. package/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  45. package/prebuilds/win32-x64/conpty.node +0 -0
  46. package/prebuilds/win32-x64/conpty.pdb +0 -0
  47. package/prebuilds/win32-x64/conpty_console_list.node +0 -0
  48. package/prebuilds/win32-x64/conpty_console_list.pdb +0 -0
  49. package/prebuilds/win32-x64/pty.node +0 -0
  50. package/prebuilds/win32-x64/pty.pdb +0 -0
  51. package/prebuilds/win32-x64/winpty-agent.exe +0 -0
  52. package/prebuilds/win32-x64/winpty-agent.pdb +0 -0
  53. package/prebuilds/win32-x64/winpty.dll +0 -0
  54. package/prebuilds/win32-x64/winpty.pdb +0 -0
  55. package/scripts/gen-compile-commands.js +8 -0
  56. package/scripts/{publish.js → increment-version.js} +10 -18
  57. package/scripts/post-install.js +48 -10
  58. package/scripts/prebuild.js +34 -0
  59. package/src/conpty_console_list_agent.ts +2 -6
  60. package/src/index.ts +2 -1
  61. package/src/interfaces.ts +2 -1
  62. package/src/native.d.ts +5 -5
  63. package/src/terminal.test.ts +1 -1
  64. package/src/terminal.ts +2 -2
  65. package/src/unix/pty.cc +28 -11
  66. package/src/unixTerminal.test.ts +3 -6
  67. package/src/unixTerminal.ts +101 -34
  68. package/src/utils.ts +20 -0
  69. package/src/win/conpty.cc +141 -55
  70. package/src/win/conpty.h +41 -0
  71. package/src/win/path_util.cc +23 -24
  72. package/src/windowsConoutConnection.ts +6 -3
  73. package/src/windowsPtyAgent.ts +42 -43
  74. package/src/windowsTerminal.test.ts +46 -33
  75. package/src/windowsTerminal.ts +24 -29
  76. package/src/worker/conoutSocketWorker.ts +2 -2
  77. package/third_party/conpty/1.23.251008001/win10-arm64/OpenConsole.exe +0 -0
  78. package/third_party/conpty/1.23.251008001/win10-arm64/conpty.dll +0 -0
  79. package/third_party/conpty/1.23.251008001/win10-x64/OpenConsole.exe +0 -0
  80. package/third_party/conpty/1.23.251008001/win10-x64/conpty.dll +0 -0
  81. package/typings/node-pty.d.ts +9 -1
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Copyright (c) 2025, Microsoft Corporation (MIT License).
3
+ */
4
+
5
+ const { execSync } = require('child_process');
6
+
7
+ console.log(`\x1b[32m> Generating compile_commands.json...\x1b[0m`);
8
+ execSync('npx --offline node-gyp configure -- -f compile_commands_json');
@@ -7,30 +7,19 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const packageJson = require('../package.json');
9
9
 
10
- // Setup auth
11
- fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
12
-
13
10
  // Determine if this is a stable or beta release
14
11
  const publishedVersions = getPublishedVersions();
15
- const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
12
+ const isStableRelease = !publishedVersions.includes(packageJson.version);
16
13
 
17
14
  // Get the next version
18
- let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
19
- console.log(`Publishing version: ${nextVersion}`);
15
+ const nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
16
+ console.log(`Setting version to ${nextVersion}`);
20
17
 
21
18
  // Set the version in package.json
22
19
  const packageJsonFile = path.resolve(__dirname, '..', 'package.json');
23
20
  packageJson.version = nextVersion;
24
21
  fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
25
22
 
26
- // Publish
27
- const args = ['publish'];
28
- if (!isStableRelease) {
29
- args.push('--tag', 'beta');
30
- }
31
- const result = cp.spawn('npm', args, { stdio: 'inherit' });
32
- result.on('exit', code => process.exit(code));
33
-
34
23
  function getNextBetaVersion() {
35
24
  if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
36
25
  console.error('The package.json version must be of the form x.y.z');
@@ -41,7 +30,7 @@ function getNextBetaVersion() {
41
30
  const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
42
31
  const publishedVersions = getPublishedVersions(nextStableVersion, tag);
43
32
  if (publishedVersions.length === 0) {
44
- return `${nextStableVersion}-${tag}1`;
33
+ return `${nextStableVersion}-${tag}.1`;
45
34
  }
46
35
  const latestPublishedVersion = publishedVersions.sort((a, b) => {
47
36
  const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
@@ -49,14 +38,17 @@ function getNextBetaVersion() {
49
38
  return aVersion > bVersion ? -1 : 1;
50
39
  })[0];
51
40
  const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
52
- return `${nextStableVersion}-${tag}${latestTagVersion + 1}`;
41
+ return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
53
42
  }
54
43
 
55
44
  function getPublishedVersions(version, tag) {
56
- const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
45
+ const isWin32 = process.platform === 'win32';
46
+ const versionsProcess = isWin32 ?
47
+ cp.spawnSync('npm.cmd', ['view', packageJson.name, 'versions', '--json'], { shell: true }) :
48
+ cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
57
49
  const versionsJson = JSON.parse(versionsProcess.stdout);
58
50
  if (tag) {
59
- return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
51
+ return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}\.[0-9]+`)));
60
52
  }
61
53
  return versionsJson;
62
54
  }
@@ -1,8 +1,11 @@
1
- var fs = require('fs');
2
- var path = require('path');
1
+ //@ts-check
3
2
 
4
- var RELEASE_DIR = path.join(__dirname, '..', 'build', 'Release');
5
- var BUILD_FILES = [
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const RELEASE_DIR = path.join(__dirname, '../build/Release');
8
+ const BUILD_FILES = [
6
9
  path.join(RELEASE_DIR, 'conpty.node'),
7
10
  path.join(RELEASE_DIR, 'conpty.pdb'),
8
11
  path.join(RELEASE_DIR, 'conpty_console_list.node'),
@@ -15,14 +18,19 @@ var BUILD_FILES = [
15
18
  path.join(RELEASE_DIR, 'winpty.dll'),
16
19
  path.join(RELEASE_DIR, 'winpty.pdb')
17
20
  ];
21
+ const CONPTY_DIR = path.join(__dirname, '../third_party/conpty');
22
+ const CONPTY_SUPPORTED_ARCH = ['x64', 'arm64'];
23
+
24
+ console.log('\x1b[32m> Cleaning release folder...\x1b[0m');
18
25
 
19
- cleanFolderRecursive = function(folder) {
26
+ /** @param {string} folder */
27
+ function cleanFolderRecursive(folder) {
20
28
  var files = [];
21
- if( fs.existsSync(folder) ) {
29
+ if (fs.existsSync(folder)) {
22
30
  files = fs.readdirSync(folder);
23
31
  files.forEach(function(file,index) {
24
32
  var curPath = path.join(folder, file);
25
- if(fs.lstatSync(curPath).isDirectory()) { // recurse
33
+ if (fs.lstatSync(curPath).isDirectory()) { // recurse
26
34
  cleanFolderRecursive(curPath);
27
35
  fs.rmdirSync(curPath);
28
36
  } else if (BUILD_FILES.indexOf(curPath) < 0){ // delete file
@@ -36,7 +44,37 @@ try {
36
44
  cleanFolderRecursive(RELEASE_DIR);
37
45
  } catch(e) {
38
46
  console.log(e);
39
- //process.exit(1);
40
- } finally {
41
- process.exit(0);
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(`\x1b[32m> Moving conpty.dll...\x1b[0m`);
51
+ if (os.platform() !== 'win32') {
52
+ console.log(' SKIPPED (not Windows)');
53
+ } else {
54
+ let windowsArch;
55
+ if (process.env.npm_config_arch) {
56
+ windowsArch = process.env.npm_config_arch;
57
+ console.log(` Using $npm_config_arch: ${windowsArch}`);
58
+ } else {
59
+ windowsArch = os.arch();
60
+ console.log(` Using os.arch(): ${windowsArch}`);
61
+ }
62
+
63
+ if (!CONPTY_SUPPORTED_ARCH.includes(windowsArch)) {
64
+ console.log(` SKIPPED (unsupported architecture ${windowsArch})`);
65
+ } else {
66
+ const versionFolder = fs.readdirSync(CONPTY_DIR)[0];
67
+ console.log(` Found version ${versionFolder}`);
68
+ const sourceFolder = path.join(CONPTY_DIR, versionFolder, `win10-${windowsArch}`);
69
+ const destFolder = path.join(RELEASE_DIR, 'conpty');
70
+ fs.mkdirSync(destFolder, { recursive: true });
71
+ for (const file of ['conpty.dll', 'OpenConsole.exe']) {
72
+ const sourceFile = path.join(sourceFolder, file);
73
+ const destFile = path.join(destFolder, file);
74
+ console.log(` Copying ${sourceFile} -> ${destFile}`);
75
+ fs.copyFileSync(sourceFile, destFile);
76
+ }
77
+ }
42
78
  }
79
+
80
+ process.exit(0);
@@ -0,0 +1,34 @@
1
+ //@ts-check
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * This script checks for the prebuilt binaries for the current platform and
8
+ * architecture. It exits with 0 if prebuilds are found and 1 if not.
9
+ *
10
+ * If npm_config_build_from_source is set then it removes the prebuilds for the
11
+ * current platform so they are not loaded at runtime.
12
+ *
13
+ * Usage:
14
+ * node scripts/prebuild.js
15
+ */
16
+
17
+ const PREBUILDS_ROOT = path.join(__dirname, '..', 'prebuilds');
18
+ const PREBUILD_DIR = path.join(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`);
19
+
20
+ // Do not use prebuilds when npm_config_build_from_source is set
21
+ if (process.env.npm_config_build_from_source === 'true') {
22
+ console.log('\x1b[33m> Removing prebuilds and rebuilding because npm_config_build_from_source is set\x1b[0m');
23
+ fs.rmSync(PREBUILDS_ROOT, { recursive: true, force: true });
24
+ process.exit(1);
25
+ }
26
+
27
+ // Check whether the correct prebuilt files exist
28
+ console.log('\x1b[32m> Checking prebuilds...\x1b[0m');
29
+ if (!fs.existsSync(PREBUILD_DIR)) {
30
+ console.log(`\x1b[33m> Rebuilding because directory ${PREBUILD_DIR} does not exist\x1b[0m`);
31
+ process.exit(1);
32
+ }
33
+
34
+ process.exit(0);
@@ -6,13 +6,9 @@
6
6
  * single console attached to a process.
7
7
  */
8
8
 
9
- let getConsoleProcessList: any;
10
- try {
11
- getConsoleProcessList = require('../build/Release/conpty_console_list.node').getConsoleProcessList;
12
- } catch (err) {
13
- getConsoleProcessList = require('../build/Debug/conpty_console_list.node').getConsoleProcessList;
14
- }
9
+ import { loadNativeModule } from './utils';
15
10
 
11
+ const getConsoleProcessList = loadNativeModule('conpty_console_list').module.getConsoleProcessList;
16
12
  const shellPid = parseInt(process.argv[2], 10);
17
13
  const consoleProcessList = getConsoleProcessList(shellPid);
18
14
  process.send!({ consoleProcessList });
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { ITerminal, IPtyOpenOptions, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces';
8
8
  import { ArgvOrCommandLine } from './types';
9
+ import { loadNativeModule } from './utils';
9
10
 
10
11
  let terminalCtor: any;
11
12
  if (process.platform === 'win32') {
@@ -48,4 +49,4 @@ export function open(options: IPtyOpenOptions): ITerminal {
48
49
  * Expose the native API when not Windows, note that this is not public API and
49
50
  * could be removed at any time.
50
51
  */
51
- export const native = (process.platform !== 'win32' ? require('../build/Release/pty.node') : null);
52
+ export const native = (process.platform !== 'win32' ? loadNativeModule('pty').module : null);
package/src/interfaces.ts CHANGED
@@ -22,7 +22,7 @@ export interface ITerminal {
22
22
  * Writes data to the socket.
23
23
  * @param data The data to write.
24
24
  */
25
- write(data: string): void;
25
+ write(data: string | Buffer): void;
26
26
 
27
27
  /**
28
28
  * Resize the pty.
@@ -119,6 +119,7 @@ export interface IPtyForkOptions extends IBasePtyForkOptions {
119
119
 
120
120
  export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
121
121
  useConpty?: boolean;
122
+ useConptyDll?: boolean;
122
123
  conptyInheritCursor?: boolean;
123
124
  }
124
125
 
package/src/native.d.ts CHANGED
@@ -3,11 +3,11 @@
3
3
  */
4
4
 
5
5
  interface IConptyNative {
6
- startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean): IConptyProcess;
7
- connect(ptyId: number, commandLine: string, cwd: string, env: string[], onExitCallback: (exitCode: number) => void): { pid: number };
8
- resize(ptyId: number, cols: number, rows: number): void;
9
- clear(ptyId: number): void;
10
- kill(ptyId: number): void;
6
+ startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
7
+ connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number };
8
+ resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void;
9
+ clear(ptyId: number, useConptyDll: boolean): void;
10
+ kill(ptyId: number, useConptyDll: boolean): void;
11
11
  }
12
12
 
13
13
  interface IWinptyNative {
@@ -23,7 +23,7 @@ class TestTerminal extends Terminal {
23
23
  public checkType<T>(name: string, value: T, type: string, allowArray: boolean = false): void {
24
24
  this._checkType(name, value, type, allowArray);
25
25
  }
26
- protected _write(data: string): void {
26
+ protected _write(data: string | Buffer): void {
27
27
  throw new Error('Method not implemented.');
28
28
  }
29
29
  public resize(cols: number, rows: number): void {
package/src/terminal.ts CHANGED
@@ -74,9 +74,9 @@ export abstract class Terminal implements ITerminal {
74
74
  this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string');
75
75
  }
76
76
 
77
- protected abstract _write(data: string): void;
77
+ protected abstract _write(data: string | Buffer): void;
78
78
 
79
- public write(data: string): void {
79
+ public write(data: string | Buffer): void {
80
80
  if (this.handleFlowControl) {
81
81
  // PAUSE/RESUME messages are not forwarded to the pty
82
82
  if (data === this._flowControlPause) {
package/src/unix/pty.cc CHANGED
@@ -41,6 +41,10 @@
41
41
  #include <util.h>
42
42
  #elif defined(__FreeBSD__)
43
43
  #include <libutil.h>
44
+ #include <termios.h>
45
+ #elif defined(__OpenBSD__)
46
+ #include <util.h>
47
+ #include <termios.h>
44
48
  #endif
45
49
 
46
50
  /* Some platforms name VWERASE and VDISCARD differently */
@@ -189,9 +193,22 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
189
193
  exit_event->signal_code = WTERMSIG(stat_loc);
190
194
  }
191
195
  auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
192
- assert(status == napi_ok);
196
+ switch (status) {
197
+ case napi_closing:
198
+ break;
199
+
200
+ case napi_queue_full:
201
+ Napi::Error::Fatal("SetupExitCallback", "Queue was full");
193
202
 
194
- tsfn.Release();
203
+ case napi_ok:
204
+ if (tsfn.Release() != napi_ok) {
205
+ Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
206
+ }
207
+ break;
208
+
209
+ default:
210
+ Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
211
+ }
195
212
  });
196
213
  }
197
214
 
@@ -353,7 +370,7 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
353
370
  int err = -1;
354
371
  pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
355
372
  if (err != 0) {
356
- throw Napi::Error::New(napiEnv, "posix_spawnp failed.");
373
+ throw Napi::Error::New(napiEnv, std::string("pty_posix_spawn failed with error: ") + std::to_string(err) + " (" + strerror(err) + ")");
357
374
  }
358
375
  if (pty_nonblock(master) == -1) {
359
376
  throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
@@ -431,10 +448,10 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
431
448
  }
432
449
  #endif
433
450
 
434
- Napi::Object obj = Napi::Object::New(napiEnv);
435
- obj.Set("fd", Napi::Number::New(napiEnv, master));
436
- obj.Set("pid", Napi::Number::New(napiEnv, pid));
437
- obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
451
+ Napi::Object obj = Napi::Object::New(napiEnv);
452
+ obj.Set("fd", Napi::Number::New(napiEnv, master));
453
+ obj.Set("pid", Napi::Number::New(napiEnv, pid));
454
+ obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
438
455
 
439
456
  // Set up process exit callback.
440
457
  Napi::Function cb = info[10].As<Napi::Function>();
@@ -475,10 +492,10 @@ Napi::Value PtyOpen(const Napi::CallbackInfo& info) {
475
492
  throw Napi::Error::New(env, "Could not set slave fd to nonblocking.");
476
493
  }
477
494
 
478
- Napi::Object obj = Napi::Object::New(env);
479
- obj.Set("master", Napi::Number::New(env, master));
480
- obj.Set("slave", Napi::Number::New(env, slave));
481
- obj.Set("pty", Napi::String::New(env, ptsname(master)));
495
+ Napi::Object obj = Napi::Object::New(env);
496
+ obj.Set("master", Napi::Number::New(env, master));
497
+ obj.Set("slave", Napi::Number::New(env, slave));
498
+ obj.Set("pty", Napi::String::New(env, ptsname(master)));
482
499
 
483
500
  return obj;
484
501
  }
@@ -272,10 +272,9 @@ if (process.platform !== 'win32') {
272
272
  ptyProcess.on('data', function (data) {
273
273
  if (ptyProcess.process === 'Python') {
274
274
  console.log('title', ptyProcess.process);
275
+ console.log('ready', ptyProcess.pid);
275
276
  }
276
277
  });
277
- setTimeout(() => null, 500);
278
- console.log('ready', ptyProcess.pid);
279
278
  `;
280
279
  const p = cp.spawn('node', ['-e', data]);
281
280
  let sub = '';
@@ -283,12 +282,10 @@ if (process.platform !== 'win32') {
283
282
  p.stdout.on('data', (data) => {
284
283
  if (!data.toString().indexOf('title')) {
285
284
  sub = data.toString().split(' ')[1].slice(0, -1);
286
- setTimeout(() => {
287
- process.kill(parseInt(pid), 'SIGINT');
288
- p.kill('SIGINT');
289
- }, 200);
290
285
  } else if (!data.toString().indexOf('ready')) {
291
286
  pid = data.toString().split(' ')[1].slice(0, -1);
287
+ process.kill(parseInt(pid), 'SIGINT');
288
+ p.kill('SIGINT');
292
289
  }
293
290
  });
294
291
  p.on('exit', () => {
@@ -3,29 +3,18 @@
3
3
  * Copyright (c) 2016, Daniel Imms (MIT License).
4
4
  * Copyright (c) 2018, Microsoft Corporation (MIT License).
5
5
  */
6
+ import * as fs from 'fs';
6
7
  import * as net from 'net';
7
8
  import * as path from 'path';
9
+ import * as tty from 'tty';
8
10
  import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
9
11
  import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces';
10
- import { ArgvOrCommandLine } from './types';
11
- import { assign } from './utils';
12
-
13
- let pty: IUnixNative;
14
- let helperPath: string;
15
- try {
16
- pty = require('../build/Release/pty.node');
17
- helperPath = '../build/Release/spawn-helper';
18
- } catch (outerError) {
19
- try {
20
- pty = require('../build/Debug/pty.node');
21
- helperPath = '../build/Debug/spawn-helper';
22
- } catch (innerError) {
23
- console.error('innerError', innerError);
24
- // Re-throw the exception from the Release require if the Debug require fails as well
25
- throw outerError;
26
- }
27
- }
12
+ import { ArgvOrCommandLine, IDisposable } from './types';
13
+ import { assign, loadNativeModule } from './utils';
28
14
 
15
+ const native = loadNativeModule('pty');
16
+ const pty: IUnixNative = native.module;
17
+ let helperPath = native.dir + '/spawn-helper';
29
18
  helperPath = path.resolve(__dirname, helperPath);
30
19
  helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
31
20
  helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
@@ -46,6 +35,9 @@ export class UnixTerminal extends Terminal {
46
35
 
47
36
  private _boundClose: boolean = false;
48
37
  private _emittedClose: boolean = false;
38
+
39
+ private _writeStream: CustomWriteStream;
40
+
49
41
  private _master: net.Socket | undefined;
50
42
  private _slave: net.Socket | undefined;
51
43
 
@@ -113,10 +105,11 @@ export class UnixTerminal extends Terminal {
113
105
  // fork
114
106
  const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit);
115
107
 
116
- this._socket = new PipeSocket(term.fd);
108
+ this._socket = new tty.ReadStream(term.fd);
117
109
  if (encoding !== null) {
118
110
  this._socket.setEncoding(encoding);
119
111
  }
112
+ this._writeStream = new CustomWriteStream(term.fd, (encoding || undefined) as BufferEncoding);
120
113
 
121
114
  // setup
122
115
  this._socket.on('error', (err: any) => {
@@ -173,8 +166,8 @@ export class UnixTerminal extends Terminal {
173
166
  this._forwardEvents();
174
167
  }
175
168
 
176
- protected _write(data: string): void {
177
- this._socket.write(data);
169
+ protected _write(data: string | Buffer): void {
170
+ this._writeStream.write(data);
178
171
  }
179
172
 
180
173
  /* Accessors */
@@ -203,13 +196,13 @@ export class UnixTerminal extends Terminal {
203
196
  // open
204
197
  const term: IUnixOpenProcess = pty.open(cols, rows);
205
198
 
206
- self._master = new PipeSocket(<number>term.master);
199
+ self._master = new tty.ReadStream(term.master);
207
200
  if (encoding !== null) {
208
201
  self._master.setEncoding(encoding);
209
202
  }
210
203
  self._master.resume();
211
204
 
212
- self._slave = new PipeSocket(term.slave);
205
+ self._slave = new tty.ReadStream(term.slave);
213
206
  if (encoding !== null) {
214
207
  self._slave.setEncoding(encoding);
215
208
  }
@@ -250,6 +243,7 @@ export class UnixTerminal extends Terminal {
250
243
  });
251
244
 
252
245
  this._socket.destroy();
246
+ this._writeStream.dispose();
253
247
  }
254
248
 
255
249
  public kill(signal?: string): void {
@@ -264,7 +258,7 @@ export class UnixTerminal extends Terminal {
264
258
  public get process(): string {
265
259
  if (process.platform === 'darwin') {
266
260
  const title = pty.process(this._fd);
267
- return (title !== 'kernel_task' ) ? title : this._file;
261
+ return (title !== 'kernel_task') ? title : this._file;
268
262
  }
269
263
 
270
264
  return pty.process(this._fd, this._pty) || this._file;
@@ -305,17 +299,90 @@ export class UnixTerminal extends Terminal {
305
299
  }
306
300
  }
307
301
 
302
+ interface IWriteTask {
303
+ /** The buffer being written. */
304
+ buffer: Buffer;
305
+ /** The current offset of not yet written data. */
306
+ offset: number;
307
+ }
308
+
308
309
  /**
309
- * Wraps net.Socket to force the handle type "PIPE" by temporarily overwriting
310
- * tty_wrap.guessHandleType.
311
- * See: https://github.com/chjj/pty.js/issues/103
310
+ * A custom write stream that writes directly to a file descriptor with proper
311
+ * handling of backpressure and errors. This avoids some event loop exhaustion
312
+ * issues that can occur when using the standard APIs in Node.
312
313
  */
313
- class PipeSocket extends net.Socket {
314
- constructor(fd: number) {
315
- const pipeWrap = (<any>process).binding('pipe_wrap'); // tslint:disable-line
316
- // @types/node has fd as string? https://github.com/DefinitelyTyped/DefinitelyTyped/pull/18275
317
- const handle = new pipeWrap.Pipe(pipeWrap.constants.SOCKET);
318
- handle.open(fd);
319
- super(<any>{ handle });
314
+ class CustomWriteStream implements IDisposable {
315
+
316
+ private readonly _writeQueue: IWriteTask[] = [];
317
+ private _writeImmediate: NodeJS.Immediate | undefined;
318
+
319
+ constructor(
320
+ private readonly _fd: number,
321
+ private readonly _encoding: BufferEncoding
322
+ ) {
323
+ }
324
+
325
+ dispose(): void {
326
+ clearImmediate(this._writeImmediate);
327
+ this._writeImmediate = undefined;
328
+ }
329
+
330
+ write(data: string | Buffer): void {
331
+ // Writes are put in a queue and processed asynchronously in order to handle
332
+ // backpressure from the kernel buffer.
333
+ const buffer = typeof data === 'string'
334
+ ? Buffer.from(data, this._encoding)
335
+ : Buffer.from(data);
336
+
337
+ if (buffer.byteLength !== 0) {
338
+ this._writeQueue.push({ buffer, offset: 0 });
339
+ if (this._writeQueue.length === 1) {
340
+ this._processWriteQueue();
341
+ }
342
+ }
343
+ }
344
+
345
+ private _processWriteQueue(): void {
346
+ this._writeImmediate = undefined;
347
+
348
+ if (this._writeQueue.length === 0) {
349
+ return;
350
+ }
351
+
352
+ const task = this._writeQueue[0];
353
+
354
+ // Write to the underlying file descriptor and handle it directly, rather
355
+ // than using the `net.Socket`/`tty.WriteStream` wrappers which swallow and
356
+ // mask errors like EAGAIN and can cause the thread to block indefinitely.
357
+ fs.write(this._fd, task.buffer, task.offset, (err, written) => {
358
+ if (err) {
359
+ if ('code' in err && err.code === 'EAGAIN') {
360
+ // `setImmediate` is used to yield to the event loop and re-attempt
361
+ // the write later.
362
+ this._writeImmediate = setImmediate(() => this._processWriteQueue());
363
+ } else {
364
+ // Stop processing immediately on unexpected error and log
365
+ this._writeQueue.length = 0;
366
+ console.error('Unhandled pty write error', err);
367
+ }
368
+ return;
369
+ }
370
+
371
+ task.offset += written;
372
+ if (task.offset >= task.buffer.byteLength) {
373
+ this._writeQueue.shift();
374
+ }
375
+
376
+ // Since there is more room in the kernel buffer, we can continue to write
377
+ // until we hit EAGAIN or exhaust the queue.
378
+ //
379
+ // Note that old versions of bash, like v3.2 which ships in macOS, appears
380
+ // to have a bug in its readline implementation that causes data
381
+ // corruption when writes to the pty happens too quickly. Instead of
382
+ // trying to workaround that we just accept it so that large pastes are as
383
+ // fast as possible.
384
+ // Context: https://github.com/microsoft/node-pty/issues/833
385
+ this._processWriteQueue();
386
+ });
320
387
  }
321
388
  }
package/src/utils.ts CHANGED
@@ -7,3 +7,23 @@ export function assign(target: any, ...sources: any[]): any {
7
7
  sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key]));
8
8
  return target;
9
9
  }
10
+
11
+
12
+ export function loadNativeModule(name: string): {dir: string, module: any} {
13
+ // Check build, debug, and then prebuilds.
14
+ const dirs = ['build/Release', 'build/Debug', `prebuilds/${process.platform}-${process.arch}`];
15
+ // Check relative to the parent dir for unbundled and then the current dir for bundled
16
+ const relative = ['..', '.'];
17
+ let lastError: unknown;
18
+ for (const d of dirs) {
19
+ for (const r of relative) {
20
+ const dir = `${r}/${d}/`;
21
+ try {
22
+ return { dir, module: require(`${dir}/${name}.node`) };
23
+ } catch (e) {
24
+ lastError = e;
25
+ }
26
+ }
27
+ }
28
+ throw new Error(`Failed to load native module: ${name}.node, checked: ${dirs.join(', ')}: ${lastError}`);
29
+ }