Showing preview only (215K chars total). Download the full file or copy to clipboard to get everything.
Repository: Tyriar/node-pty
Branch: main
Commit: 21b81aca1e25
Files: 71
Total size: 197.9 KB
Directory structure:
gitextract_g61cbamu/
├── .config/
│ └── tsaoptions.json
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── binding.gyp
├── eslint.config.js
├── examples/
│ ├── electron/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── npm_install.bat
│ │ ├── npm_install.sh
│ │ ├── package.json
│ │ ├── preload.js
│ │ └── renderer.js
│ ├── fork/
│ │ ├── index.js
│ │ └── package.json
│ └── killDeepTree/
│ ├── README.md
│ ├── entry.js
│ ├── index.js
│ ├── package.json
│ └── webpack.config.js
├── fixtures/
│ └── utf8-character.txt
├── package.json
├── pipelines/
│ ├── build.yml
│ └── prebuilds.yml
├── publish.yml
├── scripts/
│ ├── gen-compile-commands.js
│ ├── increment-version.js
│ ├── linux/
│ │ ├── checksums.txt
│ │ ├── install-sysroot.js
│ │ └── verify-glibc-requirements.sh
│ ├── post-install.js
│ └── prebuild.js
├── src/
│ ├── conpty_console_list_agent.ts
│ ├── eventEmitter2.test.ts
│ ├── eventEmitter2.ts
│ ├── index.ts
│ ├── interfaces.ts
│ ├── native.d.ts
│ ├── shared/
│ │ └── conout.ts
│ ├── terminal.test.ts
│ ├── terminal.ts
│ ├── testUtils.test.ts
│ ├── tsconfig.json
│ ├── types.ts
│ ├── unix/
│ │ ├── pty.cc
│ │ └── spawn-helper.cc
│ ├── unixTerminal.test.ts
│ ├── unixTerminal.ts
│ ├── utils.ts
│ ├── win/
│ │ ├── conpty.cc
│ │ ├── conpty.h
│ │ ├── conpty_console_list.cc
│ │ ├── path_util.cc
│ │ └── path_util.h
│ ├── windowsConoutConnection.ts
│ ├── windowsPtyAgent.test.ts
│ ├── windowsPtyAgent.ts
│ ├── windowsTerminal.test.ts
│ ├── windowsTerminal.ts
│ └── worker/
│ └── conoutSocketWorker.ts
├── test/
│ └── spam-close.js
└── typings/
└── node-pty.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .config/tsaoptions.json
================================================
{
"codebaseName": "devdiv_microsoft_vscode_node_pty",
"instanceUrl": "https://devdiv.visualstudio.com/defaultcollection",
"projectName": "DevDiv",
"areaPath": "DevDiv\\VS Code (compliance tracking only)\\Visual Studio Code NPM Packages",
"notificationAliases": [
"stbatt@microsoft.com",
"lszomoru@microsoft.com"
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.ts]
max_line_length = 100
================================================
FILE: .gitattributes
================================================
* text=auto
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
## Environment details
- OS:
- OS version:
- node-pty version:
## Issue description
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
pull_request:
jobs:
lint:
name: Lint
runs-on: ubuntu-slim
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies (skip build)
run: npm ci --ignore-scripts
- name: Lint
run: npm run lint
build-test:
name: Build & Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-14, windows-2022, macos-15-intel, ubuntu-22.04-arm, windows-11-arm]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Set architecture
id: arch
shell: bash
run: |
if [ "${{ runner.arch }}" = "ARM64" ]; then
echo "arch=arm64" >> $GITHUB_OUTPUT
else
echo "arch=x64" >> $GITHUB_OUTPUT
fi
- name: Install sysroot
if: runner.os == 'Linux'
run: |
set -eo pipefail
sudo apt-get update -qq
sudo apt-get install -y gcc-10 g++-10
echo "CC=gcc-10" >> $GITHUB_ENV
echo "CXX=g++-10" >> $GITHUB_ENV
SYSROOT_PATH=$(node scripts/linux/install-sysroot.js ${{ steps.arch.outputs.arch }} | grep "SYSROOT_PATH=" | cut -d= -f2)
echo "SYSROOT_PATH=$SYSROOT_PATH" >> $GITHUB_ENV
echo "Sysroot path set to: $SYSROOT_PATH"
env:
# Set github token for authenticated sysroot download
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies and build
run: npm ci
env:
ARCH: ${{ steps.arch.outputs.arch }}
npm_config_arch: ${{ steps.arch.outputs.arch }}
- name: Verify GLIBC requirements
if: runner.os == 'Linux'
run: |
EXPECTED_GLIBC_VERSION="2.28" \
EXPECTED_GLIBCXX_VERSION="3.4.25" \
SEARCH_PATH="build" \
./scripts/linux/verify-glibc-requirements.sh
- name: Test
run: npm test
================================================
FILE: .gitignore
================================================
build/
.lock-wscript
out/
Makefile.gyp
*.Makefile
*.target.gyp.mk
node_modules/
builderror.log
lib/
npm-debug.log
fixtures/space folder/
.vscode/settings.json
.vscode/ipch/
yarn.lock
prebuilds/
_codeql_detected_source_root
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Unit Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib/**/*.test.js"
],
"env": {
"NODE_PATH": "${workspaceRoot}/lib"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/lib/**/*.js"
],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "attach",
"name": "Attach to process ID",
"processId": "${command:PickProcess}"
}
]
}
================================================
FILE: .vscode/tasks.json
================================================
{
"version": "2.0.0",
"presentation": {
"echo": false,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true
},
"tasks": [
{
"label": "tsc",
"type": "npm",
"script": "watch",
"isBackground": true,
"problemMatcher": "$tsc-watch",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
================================================
FILE: CONTRIBUTING.md
================================================
## Testing in a real terminal
The recommended way to test node-pty during development is via the electron example:
```sh
cd examples/electron
npm ci
npm start
```
Alternatively, clone the xterm.js repository and link's node-pty module to this directory.
1. Clone xterm.js in a separate folder:
```sh
git clone https://github.com/xtermjs/xterm.js
npm ci
npm run setup
```
2. Link the node-pty repo:
```
rm -rf node_modules/node-pty # in xterm.js folder
ln -s <path_to_node-pty> <path to xtermjs>/node_modules/node-pty
```
3. Hit ctrl/cmd+shift+b in VS Code or run the build/demo scripts manually:
```sh
npm run tsc-watch # build ts
npm run esbuild-watch # bundle ts/js
npm run esbuild-demo # build demo/server
npm run start # run server
```
4. Open http://127.0.0.1:3000 and test
5. Kill and restart the `npm run start` command to apply any changes made in node-pty
================================================
FILE: LICENSE
================================================
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
The MIT License (MIT)
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
MIT License
Copyright (c) 2018 - present Microsoft Corporation
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# node-pty
[](https://dev.azure.com/vscode/node-pty/_build/latest?definitionId=11&branchName=main)
`forkpty(3)` bindings for node.js. This allows you to fork processes with pseudoterminal file descriptors. It returns a terminal object which allows reads and writes.
This is useful for:
- Writing a terminal emulator (eg. via [xterm.js](https://github.com/sourcelair/xterm.js)).
- Getting certain programs to *think* you're a terminal, such as when you need a program to send you control sequences.
`node-pty` supports Linux, macOS and Windows. Windows support is possible by utilizing the [Windows conpty API](https://blogs.msdn.microsoft.com/commandline/2018/08/02/windows-command-line-introducing-the-windows-pseudo-console-conpty/) on Windows 1809+.
> **Note:** Support for the `winpty` library has been removed. Windows 10 version 1809 (build 18309) or later is now required.
## API
The full API for node-pty is contained within the [TypeScript declaration file](https://github.com/microsoft/node-pty/blob/main/typings/node-pty.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
## Example Usage
```js
import * as os from 'node:os';
import * as pty from 'node-pty';
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.onData((data) => {
process.stdout.write(data);
});
ptyProcess.write('ls\r');
ptyProcess.resize(100, 40);
ptyProcess.write('ls\r');
```
## Real-world Uses
`node-pty` powers many different terminal emulators, including:
- [Microsoft Visual Studio Code](https://code.visualstudio.com)
- [Hyper](https://hyper.is/)
- [Upterm](https://github.com/railsware/upterm)
- [Script Runner](https://github.com/ioquatix/script-runner) for Atom.
- [Theia](https://github.com/theia-ide/theia)
- [FreeMAN](https://github.com/matthew-matvei/freeman) file manager
- [terminus](https://atom.io/packages/terminus) - An Atom plugin for providing terminals inside your Atom workspace.
- [x-terminal](https://atom.io/packages/x-terminal) - Also an Atom plugin that provides terminals inside your Atom workspace.
- [Termination](https://atom.io/packages/termination) - Also an Atom plugin that provides terminals inside your Atom workspace.
- [atom-xterm](https://atom.io/packages/atom-xterm) - Also an Atom plugin that provides terminals inside your Atom workspace.
- [electerm](https://github.com/electerm/electerm) Terminal/SSH/SFTP client(Linux, macOS, Windows).
- [Extraterm](http://extraterm.org/)
- [Wetty](https://github.com/krishnasrinivas/wetty) Browser based Terminal over HTTP and HTTPS
- [nomad](https://github.com/lukebarnard1/nomad-term)
- [DockerStacks](https://github.com/sfx101/docker-stacks) Local LAMP/LEMP stack using Docker
- [TeleType](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [mesos-term](https://github.com/criteo/mesos-term): A web terminal for Apache Mesos. It allows to execute commands within containers.
- [Commas](https://github.com/CyanSalt/commas): A hackable terminal and command runner.
- [ENiGMA½ BBS Software](https://github.com/NuSkooler/enigma-bbs): A modern BBS software with a nostalgic flair!
- [Tinkerun](https://github.com/tinkerun/tinkerun): A new way of running Tinker.
- [Tess](https://tessapp.dev): Hackable, simple and rapid terminal for the new era of technology 👍
- [NxShell](https://nxshell.github.io/): An easy to use new terminal for Windows/Linux/MacOS platform.
- [OpenSumi](https://github.com/opensumi/core): A framework helps you quickly build Cloud or Desktop IDE products.
- [Enjoy Git](https://github.com/huangcs427/enjoy-git-release): A modern Git client featuring an intuitive user interface, built with Electron, Vue 3, and TypeScript.
- [Logos](https://github.com/zixiao-labs/logos): A Modern, Lightweight Code Editor, built with Electron, Vue 3, and TypeScript.
Do you use node-pty in your application as well? Please open a [Pull Request](https://github.com/Tyriar/node-pty/pulls) to include it here. We would love to have it in our list.
## Building
```bash
# Install dependencies and build C++
npm install
# Compile TypeScript -> JavaScript
npm run build
```
## Dependencies
Node.JS 16 or Electron 19 is required to use `node-pty`. What version of node is supported is currently mostly bound to [whatever version Visual Studio Code is using](https://github.com/microsoft/node-pty/issues/557#issuecomment-1332193541).
### Linux (apt)
```sh
sudo apt install -y make python build-essential
```
### macOS
Xcode is needed to compile the sources, this can be installed from the App Store.
### Windows
`npm install` requires some tools to be present in the system like Python and C++ compiler. Windows users can easily install them by running the following command in PowerShell as administrator. For more information see https://github.com/felixrieseberg/windows-build-tools:
```sh
npm install --global --production windows-build-tools
```
The following are also needed:
- [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk) - only the "Desktop C++ Apps" components are needed to be installed
- Spectre-mitigated libraries - In order to avoid the build error "MSB8040: Spectre-mitigated libraries are required for this project", open the Visual Studio Installer, press the Modify button, navigate to the "Individual components" tab, search "Spectre", and install an option like "MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)" (the exact option to install will depend on your version of Visual Studio as well as your operating system architecture)
## Debugging
[The wiki](https://github.com/Microsoft/node-pty/wiki/Debugging) contains instructions for debugging node-pty.
## Security
All processes launched from node-pty will launch at the same permission level of the parent process. Take care particularly when using node-pty inside a server that's accessible on the internet. We recommend launching the pty inside a container to protect your host machine.
## Thread Safety
Note that node-pty is not thread safe so running it across multiple worker threads in node.js could cause issues.
## Flow Control
Automatic flow control can be enabled by either providing `handleFlowControl = true` in the constructor options or setting it later on:
```js
const PAUSE = '\x13'; // XOFF
const RESUME = '\x11'; // XON
const ptyProcess = pty.spawn(shell, [], {handleFlowControl: true});
// flow control in action
ptyProcess.write(PAUSE); // pty will block and pause the child program
...
ptyProcess.write(RESUME); // pty will enter flow mode and resume the child program
// temporarily disable/re-enable flow control
ptyProcess.handleFlowControl = false;
...
ptyProcess.handleFlowControl = true;
```
By default `PAUSE` and `RESUME` are XON/XOFF control codes (as shown above). To avoid conflicts in environments that use these control codes for different purposes the messages can be customized as `flowControlPause: string` and `flowControlResume: string` in the constructor options. `PAUSE` and `RESUME` are not passed to the underlying pseudoterminal if flow control is enabled.
## Troubleshooting
### Powershell gives error 8009001d
> Internal Windows PowerShell error. Loading managed Windows PowerShell failed with error 8009001d.
This happens when PowerShell is launched with no `SystemRoot` environment variable present.
## pty.js
This project is forked from [chjj/pty.js](https://github.com/chjj/pty.js) with the primary goals being to provide better support for later Node.js versions and Windows.
## License
Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).<br>
Copyright (c) 2016, Daniel Imms (MIT License).<br>
Copyright (c) 2018, Microsoft Corporation (MIT License).
================================================
FILE: SECURITY.md
================================================
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->
================================================
FILE: binding.gyp
================================================
{
'target_defaults': {
'dependencies': [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
'conditions': [
['OS=="win"', {
'msvs_configuration_attributes': {
'SpectreMitigation': 'Spectre'
},
'msvs_settings': {
'VCCLCompilerTool': {
'AdditionalOptions': [
'/guard:cf',
'/sdl',
'/W3',
'/we4146',
'/we4244',
'/we4267',
'/ZH:SHA_256'
]
},
'VCLinkerTool': {
'AdditionalOptions': [
'/DYNAMICBASE',
'/guard:cf'
]
}
},
}, {
'cflags': ['-O2', '-fstack-protector-strong'],
}],
],
},
'conditions': [
['OS=="win"', {
'targets': [
{
'target_name': 'conpty',
'sources' : [
'src/win/conpty.cc',
'src/win/path_util.cc'
],
'libraries': [
'-lshlwapi'
],
},
{
'target_name': 'conpty_console_list',
'sources' : [
'src/win/conpty_console_list.cc'
],
}
]
}, { # OS!="win"
'targets': [
{
'target_name': 'pty',
'sources': [
'src/unix/pty.cc',
],
'libraries': [
'-lutil'
],
'cflags': ['-Wall'],
'ldflags': [],
'conditions': [
# http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html
# One some systems (at least including Cygwin, Interix,
# OSF/1 4 and 5, and Mac OS X) linking with -lutil is not required.
['OS=="mac" or OS=="solaris"', {
'libraries!': [
'-lutil'
]
}],
['OS=="linux"', {
'variables': {
'sysroot%': '<!(node -p "process.env.SYSROOT_PATH || \'\'")',
'target_arch%': '<!(node -p "process.env.npm_config_arch || process.arch")',
},
'conditions': [
['sysroot!=""', {
'variables': {
'gcc_include%': '<!(${CXX:-g++} -print-file-name=include)',
},
'conditions': [
['target_arch=="x64"', {
'cflags': [
'--sysroot=<(sysroot)',
'-nostdinc',
'-isystem<(gcc_include)',
'-isystem<(sysroot)/usr/include',
'-isystem<(sysroot)/usr/include/x86_64-linux-gnu'
],
'cflags_cc': [
'-nostdinc++',
'-isystem<(sysroot)/../include/c++/10.5.0',
'-isystem<(sysroot)/../include/c++/10.5.0/x86_64-linux-gnu',
'-isystem<(sysroot)/../include/c++/10.5.0/backward'
],
'ldflags': [
'--sysroot=<(sysroot)',
'-L<(sysroot)/lib',
'-L<(sysroot)/usr/lib'
],
}],
['target_arch=="arm64"', {
'cflags': [
'--sysroot=<(sysroot)',
'-nostdinc',
'-isystem<(gcc_include)',
'-isystem<(sysroot)/usr/include',
'-isystem<(sysroot)/usr/include/aarch64-linux-gnu'
],
'cflags_cc': [
'-nostdinc++',
'-isystem<(sysroot)/../include/c++/10.5.0',
'-isystem<(sysroot)/../include/c++/10.5.0/aarch64-linux-gnu',
'-isystem<(sysroot)/../include/c++/10.5.0/backward'
],
'ldflags': [
'--sysroot=<(sysroot)',
'-L<(sysroot)/lib',
'-L<(sysroot)/usr/lib'
],
}]
]
}]
]
}]
]
}
]
}],
['OS=="mac"', {
'targets': [
{
'target_name': 'spawn-helper',
'type': 'executable',
'sources': [
'src/unix/spawn-helper.cc',
],
"xcode_settings": {
"MACOSX_DEPLOYMENT_TARGET":"10.7"
}
},
]
}]
]
}
================================================
FILE: eslint.config.js
================================================
// @ts-check
const tsParser = require('@typescript-eslint/parser');
const tsPlugin = require('@typescript-eslint/eslint-plugin');
const globals = require('globals');
/** @type {import('eslint').Linter.Config[]} */
module.exports = [
{
ignores: [
'**/typings/*.d.ts',
'scripts/**/*',
'examples/**/*',
],
},
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: 'src/tsconfig.json',
sourceType: 'module',
},
globals: {
...globals.browser,
...globals.es2015,
...globals.node,
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
'@typescript-eslint/array-type': [
'error',
{
default: 'array-simple',
readonly: 'generic',
},
],
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/explicit-function-return-type': [
'error',
{
'allowExpressions': true,
},
],
'@typescript-eslint/naming-convention': [
'error',
{ 'selector': 'default', 'format': ['camelCase'] },
// variableLike
{ 'selector': 'variable', 'format': ['camelCase', 'UPPER_CASE'] },
{ 'selector': 'variable', 'filter': '^I.+Service$', 'format': ['PascalCase'], 'prefix': ['I'] },
// memberLike
{ 'selector': 'memberLike', 'modifiers': ['private'], 'format': ['camelCase'], 'leadingUnderscore': 'require' },
{ 'selector': 'memberLike', 'modifiers': ['protected'], 'format': ['camelCase'], 'leadingUnderscore': 'require' },
{ 'selector': 'enumMember', 'format': ['UPPER_CASE'] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ 'selector': 'property', 'modifiers': ['public'], 'format': ['camelCase', 'UPPER_CASE'] },
{ 'selector': 'method', 'modifiers': ['public'], 'format': ['camelCase', 'UPPER_CASE'] },
// typeLike
{ 'selector': 'typeLike', 'format': ['PascalCase'] },
{ 'selector': 'interface', 'format': ['PascalCase'], 'prefix': ['I'] },
],
'@typescript-eslint/prefer-namespace-keyword': 'error',
'comma-dangle': [
'error',
{
'objects': 'never',
'arrays': 'never',
'functions': 'never',
},
],
'curly': [
'error',
'multi-line',
],
'eol-last': 'error',
'eqeqeq': [
'error',
'always',
],
'keyword-spacing': 'error',
'new-parens': 'error',
'no-duplicate-imports': 'error',
'no-else-return': [
'error',
{
allowElseIf: false,
},
],
'no-eval': 'error',
'no-irregular-whitespace': 'error',
'no-restricted-imports': [
'error',
{
'patterns': [
'.*\\/out\\/.*',
],
},
],
'no-trailing-spaces': 'error',
'no-unsafe-finally': 'error',
'no-var': 'error',
'one-var': [
'error',
'never',
],
'prefer-const': 'error',
'quotes': [
'error',
'single',
{ 'allowTemplateLiterals': true },
],
'semi': [
'error',
'always',
],
'spaced-comment': [
'error',
'always',
{
'markers': ['/'],
'exceptions': ['-'],
},
],
},
},
];
================================================
FILE: examples/electron/README.md
================================================
This is a minimal example of getting a terminal running in Electron using [node-pty](https://github.com/microsoft/node-pty) and [xterm.js](https://github.com/xtermjs/xterm.js).

It works by using xterm.js on the renderer process and node-pty on the main process with IPC to communicate back and forth.
## Usage
```bash
# Install dependencies (Windows)
./npm-install.bat
# Install dependencies (non-Windows)
./npm-install.sh
# Launch the app
npm start
```
================================================
FILE: examples/electron/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>node-pty Electron example</title>
<link rel="Stylesheet" href="./node_modules/@xterm/xterm/css/xterm.css">
</head>
<body>
<div id="xterm"></div>
<script src="./node_modules/@xterm/xterm/lib/xterm.js"></script>
<script type="module" src="./renderer.js"></script>
</body>
</html>
================================================
FILE: examples/electron/main.js
================================================
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const os = require('os');
const pty = require('../..');
let mainWindow;
let ptyProcess;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadFile('index.html');
mainWindow.on('closed', () => {
if (ptyProcess) {
ptyProcess.kill();
ptyProcess = null;
}
mainWindow = null;
});
}
// Handle pty spawn request from renderer
ipcMain.on('pty-spawn', () => {
if (ptyProcess) return;
const shell = process.env[os.platform() === 'win32' ? 'COMSPEC' : 'SHELL'];
ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 30,
cwd: process.env.HOME || process.env.USERPROFILE,
env: process.env
});
ptyProcess.onData(data => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('pty-data', data);
}
});
});
// Handle pty input from renderer
ipcMain.on('pty-write', (_, data) => {
if (ptyProcess) {
ptyProcess.write(data);
}
});
// Handle resize from renderer
ipcMain.on('pty-resize', (_, { cols, rows }) => {
if (ptyProcess) {
ptyProcess.resize(cols, rows);
}
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});
================================================
FILE: examples/electron/npm_install.bat
================================================
@echo off
setlocal
set npm_config_disturl="https://atom.io/download/electron"
set npm_config_target=9.1.0
set npm_config_runtime="electron"
set npm_config_cache=~\.npm-electron
npm i
endlocal
================================================
FILE: examples/electron/npm_install.sh
================================================
#!/usr/bin/env sh
# Electron's version.
export npm_config_target=9.1.0
# The architecture of Electron, can be ia32 or x64.
export npm_config_arch=x64
export npm_config_target_arch=x64
# Download headers for Electron.
export npm_config_disturl=https://atom.io/download/electron
# Tell node-pre-gyp that we are building for Electron.
export npm_config_runtime=electron
# Tell node-pre-gyp to build module from source code.
export npm_config_build_from_source=true
# Install all dependencies, and store cache to ~/.electron-gyp.
HOME=~/.electron-gyp npm install
================================================
FILE: examples/electron/package.json
================================================
{
"name": "node-pty-electron-example",
"version": "1.0.0",
"description": "A minimal node-pty Electron example",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"repository": "https://github.com/microsoft/node-pty",
"author": "Tyriar",
"dependencies": {
"@xterm/xterm": "^6.0.0",
"electron": "^39.8.5"
}
}
================================================
FILE: examples/electron/preload.js
================================================
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('pty', {
spawn: () => ipcRenderer.send('pty-spawn'),
write: (data) => ipcRenderer.send('pty-write', data),
onData: (callback) => ipcRenderer.on('pty-data', (_, data) => callback(data)),
resize: (cols, rows) => ipcRenderer.send('pty-resize', { cols, rows })
});
================================================
FILE: examples/electron/renderer.js
================================================
// Initialize xterm.js and attach it to the DOM
const xterm = new window.Terminal();
xterm.open(document.getElementById('xterm'));
// Setup communication between xterm.js and node-pty via IPC
xterm.onData(data => window.pty.write(data));
window.pty.onData(data => xterm.write(data));
// Initiate pty spawn as renderer is ready
window.pty.spawn();
================================================
FILE: examples/fork/index.js
================================================
import * as os from 'node:os';
import * as pty from '../../lib/index.js';
const isWindows = os.platform() === 'win32';
const shell = isWindows ? 'powershell.exe' : 'bash';
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-256color',
cols: 80,
rows: 26,
cwd: isWindows ? process.env.USERPROFILE : process.env.HOME,
env: Object.assign({ TEST: "Environment vars work" }, process.env),
useConptyDll: true
});
ptyProcess.onData(data => process.stdout.write(data));
ptyProcess.write(isWindows ? 'dir\r' : 'ls\r');
setTimeout(() => {
ptyProcess.resize(30, 19);
ptyProcess.write(isWindows ? '$Env:TEST\r' : 'echo $TEST\r');
}, 2000);
process.on('exit', () => ptyProcess.kill());
setTimeout(() => process.exit(), 4000);
================================================
FILE: examples/fork/package.json
================================================
{
"type": "module"
}
================================================
FILE: examples/killDeepTree/README.md
================================================
This is a manual test to verify deeply nested trees are getting killed correctly on Windows.
To run:
```bash
npm i
node index.js
```
It should launch a notepad window and a webpack dev server, after 10 seconds the webpack dev server should be killed and the notepad instance should not. To verify the server is kill correctly either run the test again or check using ProcessExplorer.
================================================
FILE: examples/killDeepTree/entry.js
================================================
const test = 0;
================================================
FILE: examples/killDeepTree/index.js
================================================
var os = require('os');
var pty = require('../..');
var shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
var ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: __dirname,
env: process.env
});
ptyProcess.onData((data) => process.stdout.write(data));
ptyProcess.write('start notepad\r');
ptyProcess.write('npm start\r');
// Kill the tree at the end
setTimeout(() => {
console.log('Killing pty');
ptyProcess.kill();
}, 10000);
================================================
FILE: examples/killDeepTree/package.json
================================================
{
"name": "vscode-process-leak",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server"
},
"private": true,
"devDependencies": {
"webpack": "^4.28.4",
"webpack-dev-server": "^3.1.14"
}
}
================================================
FILE: examples/killDeepTree/webpack.config.js
================================================
const path = require('path');
module.exports = {
entry: './entry.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
devServer: {
port: 8000
}
};
================================================
FILE: fixtures/utf8-character.txt
================================================
æ
================================================
FILE: package.json
================================================
{
"name": "node-pty",
"description": "Fork pseudoterminals in Node.JS",
"author": {
"name": "Microsoft Corporation"
},
"version": "1.1.0",
"license": "MIT",
"main": "./lib/index.js",
"types": "./typings/node-pty.d.ts",
"repository": {
"type": "git",
"url": "git://github.com/microsoft/node-pty.git"
},
"files": [
"binding.gyp",
"lib/**/*.js",
"!lib/**/*.test.js",
"scripts/post-install.js",
"scripts/prebuild.js",
"src/**/*.{cc,h}",
"prebuilds/",
"third_party/",
"typings/"
],
"homepage": "https://github.com/microsoft/node-pty",
"bugs": {
"url": "https://github.com/microsoft/node-pty/issues"
},
"keywords": [
"pty",
"tty",
"terminal",
"pseudoterminal",
"forkpty",
"openpty"
],
"scripts": {
"build": "tsc -b ./src/tsconfig.json",
"watch": "tsc -b -w ./src/tsconfig.json",
"lint": "eslint src/",
"install": "node scripts/prebuild.js || node-gyp rebuild",
"postinstall": "node scripts/post-install.js",
"compileCommands": "node scripts/gen-compile-commands.js",
"test": "cross-env NODE_ENV=test mocha -R spec --exit lib/*.test.js",
"posttest": "npm run lint",
"prepare": "npm run build",
"prepublishOnly": "npm run build"
},
"dependencies": {
"node-addon-api": "^7.1.0"
},
"devDependencies": {
"@types/mocha": "^7.0.2",
"@types/node": "12",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"cross-env": "^5.1.4",
"eslint": "^9.0.0",
"mocha": "10",
"node-gyp": "^11.4.2",
"ps-list": "^6.0.0",
"typescript": "^5.0.0"
}
}
================================================
FILE: pipelines/build.yml
================================================
parameters:
arch: 'x64'
steps:
- task: UseNode@1
inputs:
version: '22.x'
displayName: 'Install Node.js 22.x'
- task: UsePythonVersion@0
inputs:
versionSpec: '3.x'
addToPath: true
displayName: 'Use latest Python 3.x'
- bash: |
if [ "$(uname)" = "Linux" ]; then
sudo apt-get update -qq
if [ "${{ parameters.arch }}" = "arm64" ]; then
sudo apt-get install -y gcc-10-aarch64-linux-gnu g++-10-aarch64-linux-gnu
echo "##vso[task.setvariable variable=CC]aarch64-linux-gnu-gcc-10"
echo "##vso[task.setvariable variable=CXX]aarch64-linux-gnu-g++-10"
else
sudo apt-get install -y gcc-10 g++-10
echo "##vso[task.setvariable variable=CC]gcc-10"
echo "##vso[task.setvariable variable=CXX]g++-10"
fi
SYSROOT_PATH=$(node scripts/linux/install-sysroot.js ${{ parameters.arch }} | grep "SYSROOT_PATH=" | cut -d= -f2)
echo "##vso[task.setvariable variable=SYSROOT_PATH]$SYSROOT_PATH"
echo "Sysroot path set to: $SYSROOT_PATH"
elif [ "$(uname)" = "Darwin" ]; then
echo "##vso[task.setvariable variable=CC]clang"
echo "##vso[task.setvariable variable=CXX]clang++"
fi
displayName: 'Configure compiler'
- script: npm ci
displayName: 'Install dependencies'
env:
ARCH: ${{ parameters.arch }}
npm_config_arch: ${{ parameters.arch }}
SYSROOT_PATH: $(SYSROOT_PATH)
CC: $(CC)
CXX: $(CXX)
================================================
FILE: pipelines/prebuilds.yml
================================================
trigger:
branches:
include:
- main
pr: none
resources:
repositories:
- repository: 1esPipelines
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
parameters:
sdl:
sourceAnalysisPool: 1es-windows-2022-x64
tsa:
enabled: true
stages:
- stage: Build
jobs:
- job: win32_x64
pool:
name: 1es-windows-2022-x64
os: windows
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'win32-x64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: x64
- job: win32_arm64
pool:
name: 1es-windows-2022-x64
os: windows
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'win32-arm64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: arm64
- job: macOS_x64
pool:
name: Azure Pipelines
vmImage: macOS-latest
os: macOS
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'darwin-x64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: x64
- job: macOS_arm64
pool:
name: Azure Pipelines
vmImage: macOS-latest
os: macOS
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'darwin-arm64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: arm64
- job: linux_x64
pool:
name: 1es-ubuntu-22.04-x64
os: linux
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'linux-x64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: x64
- job: linux_arm64
pool:
name: 1es-ubuntu-22.04-x64
os: linux
templateContext:
outputs:
- output: pipelineArtifact
targetPath: $(Build.SourcesDirectory)/build/Release
artifactName: 'linux-arm64'
steps:
- template: pipelines/build.yml@self
parameters:
arch: arm64
- stage: Archive
dependsOn: Build
jobs:
- job: archive
pool:
name: 1es-ubuntu-22.04-x64
os: linux
templateContext:
inputs:
- input: pipelineArtifact
artifactName: win32-x64
targetPath: $(Build.ArtifactStagingDirectory)/win32-x64
- input: pipelineArtifact
artifactName: win32-arm64
targetPath: $(Build.ArtifactStagingDirectory)/win32-arm64
- input: pipelineArtifact
artifactName: darwin-x64
targetPath: $(Build.ArtifactStagingDirectory)/darwin-x64
- input: pipelineArtifact
artifactName: darwin-arm64
targetPath: $(Build.ArtifactStagingDirectory)/darwin-arm64
- input: pipelineArtifact
artifactName: linux-x64
targetPath: $(Build.ArtifactStagingDirectory)/linux-x64
- input: pipelineArtifact
artifactName: linux-arm64
targetPath: $(Build.ArtifactStagingDirectory)/linux-arm64
outputs:
- output: pipelineArtifact
targetPath: $(Build.ArtifactStagingDirectory)/prebuilds
artifactName: 'prebuilds-$(Build.SourceVersion)'
steps:
- script: |
mkdir -p $(Build.ArtifactStagingDirectory)/prebuilds
cp -r $(Build.ArtifactStagingDirectory)/win32-x64 $(Build.ArtifactStagingDirectory)/prebuilds/
cp -r $(Build.ArtifactStagingDirectory)/win32-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/
cp -r $(Build.ArtifactStagingDirectory)/darwin-x64 $(Build.ArtifactStagingDirectory)/prebuilds/
cp -r $(Build.ArtifactStagingDirectory)/darwin-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/
cp -r $(Build.ArtifactStagingDirectory)/linux-x64 $(Build.ArtifactStagingDirectory)/prebuilds/
cp -r $(Build.ArtifactStagingDirectory)/linux-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/
displayName: 'Create prebuilds archive'
================================================
FILE: publish.yml
================================================
name: $(Date:yyyyMMdd)$(Rev:.r)
trigger:
branches:
include:
- main
pr: none
resources:
repositories:
- repository: templates
type: github
name: microsoft/vscode-engineering
ref: main
endpoint: Monaco
parameters:
- name: publishPackage
displayName: 🚀 Publish node-pty
type: boolean
default: false
- name: releaseQuality
displayName: Quality
type: string
values:
- beta
- stable
default: beta
variables:
- name: releaseQuality
value: ${{ parameters.releaseQuality }}
extends:
template: azure-pipelines/npm-package/pipeline.yml@templates
parameters:
npmPackages:
- name: node-pty
buildSteps:
- task: DownloadPipelineArtifact@2
displayName: 'Download prebuilds'
inputs:
buildType: 'specific'
project: 'Monaco'
definition: '647'
buildVersionToDownload: 'latestFromBranch'
branchName: '$(Build.SourceBranch)'
artifactName: 'prebuilds-$(Build.SourceVersion)'
targetPath: 'prebuilds'
- pwsh: |
Get-ChildItem -Path . -Recurse -Directory -Name "_manifest" | Remove-Item -Recurse -Force
displayName: 'Delete _manifest folders'
- bash: chmod +x prebuilds/darwin-*/spawn-helper
displayName: 'Ensure spawn-helper is executable'
- script: npm ci
displayName: 'Install dependencies and build'
# The following script leaves the version unchanged for
# stable releases, but increments the version for beta releases.
- script: node scripts/increment-version.js
displayName: 'Increment version'
testSteps:
- task: DownloadPipelineArtifact@2
displayName: 'Download prebuilds'
inputs:
buildType: 'specific'
project: 'Monaco'
definition: '647'
buildVersionToDownload: 'latestFromBranch'
branchName: '$(Build.SourceBranch)'
artifactName: 'prebuilds-$(Build.SourceVersion)'
targetPath: 'prebuilds'
- bash: chmod +x prebuilds/darwin-*/spawn-helper
displayName: 'Ensure spawn-helper is executable'
- script: npm ci
displayName: 'Install dependencies and build'
- script: npm test
displayName: 'Test'
- script: npm run lint
displayName: 'Lint'
publishPackage: ${{ parameters.publishPackage }}
${{ if eq(variables['releaseQuality'], 'stable') }}:
tag: latest
publishRequiresApproval: true
${{ else }}:
tag: beta
publishRequiresApproval: false
apiScanExcludes: |
package/third_party/conpty/**/win10-arm64/*.*
package/prebuilds/win32-arm64/conpty/*.*
package/prebuilds/win32-arm64/*.*
apiScanSoftwareName: 'vscode-node-pty'
apiScanSoftwareVersion: '1'
================================================
FILE: scripts/gen-compile-commands.js
================================================
/**
* Copyright (c) 2025, Microsoft Corporation (MIT License).
*/
const { execSync } = require('child_process');
console.log(`\x1b[32m> Generating compile_commands.json...\x1b[0m`);
execSync('npx --offline node-gyp configure -- -f compile_commands_json');
================================================
FILE: scripts/increment-version.js
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
const cp = require('child_process');
const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
// Determine if this is a stable or beta release
const publishedVersions = getPublishedVersions();
const isStableRelease = !publishedVersions.includes(packageJson.version);
// Get the next version
const nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
console.log(`Setting version to ${nextVersion}`);
// Set the version in package.json
const packageJsonFile = path.resolve(__dirname, '..', 'package.json');
packageJson.version = nextVersion;
fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
function getNextBetaVersion() {
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
console.error('The package.json version must be of the form x.y.z');
process.exit(1);
}
const tag = 'beta';
const stableVersion = packageJson.version.split('.');
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
const publishedVersions = getPublishedVersions(nextStableVersion, tag);
if (publishedVersions.length === 0) {
return `${nextStableVersion}-${tag}.1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
const bVersion = parseInt(b.substr(b.search(/[0-9]+$/)));
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
}
function getPublishedVersions(version, tag) {
const isWin32 = process.platform === 'win32';
const versionsProcess = isWin32 ?
cp.spawnSync('npm.cmd', ['view', packageJson.name, 'versions', '--json'], { shell: true }) :
cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
const versionsJson = JSON.parse(versionsProcess.stdout);
if (tag) {
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}\.[0-9]+`)));
}
return versionsJson;
}
================================================
FILE: scripts/linux/checksums.txt
================================================
3122af49c493c5c767c2b0772a41119cbdc9803125a705683445b4066dc88b82 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz
3baac81a39b69e0929e4700f4f78f022adefc515010054ec393565657c4fff32 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz
================================================
FILE: scripts/linux/install-sysroot.js
================================================
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const { execSync } = require('child_process');
const { tmpdir } = require('os');
const fs = require('fs');
const path = require('path');
const { createHash } = require('crypto');
const REPO_ROOT = path.join(__dirname, '..', '..');
const ghApiHeaders = {
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'node-pty Build',
};
if (process.env.GITHUB_TOKEN) {
ghApiHeaders.Authorization = 'Basic ' + Buffer.from(process.env.GITHUB_TOKEN).toString('base64');
console.error('Using GITHUB_TOKEN for authenticated requests to GitHub API.');
}
const ghDownloadHeaders = {
...ghApiHeaders,
Accept: 'application/octet-stream',
};
function getSysrootChecksum(expectedName) {
const checksumPath = path.join(REPO_ROOT, 'scripts', 'linux', 'checksums.txt');
const checksums = fs.readFileSync(checksumPath, 'utf8');
for (const line of checksums.split('\n')) {
const [checksum, name] = line.split(/\s+/);
if (name === expectedName) {
return checksum;
}
}
return undefined;
}
async function fetchUrl(options, retries = 10, retryDelay = 1000) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30 * 1000);
const version = '20250407-330404';
try {
const response = await fetch(`https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`, {
headers: ghApiHeaders,
signal: controller.signal
});
if (response.ok && (response.status >= 200 && response.status < 300)) {
console.error(`Fetch completed: Status ${response.status}.`);
const contents = Buffer.from(await response.arrayBuffer());
const asset = JSON.parse(contents.toString()).assets.find((a) => a.name === options.assetName);
if (!asset) {
throw new Error(`Could not find asset in release of Microsoft/vscode-linux-build-agent @ ${version}`);
}
console.error(`Found asset ${options.assetName} @ ${asset.url}.`);
const assetResponse = await fetch(asset.url, {
headers: ghDownloadHeaders
});
if (assetResponse.ok && (assetResponse.status >= 200 && assetResponse.status < 300)) {
const assetContents = Buffer.from(await assetResponse.arrayBuffer());
console.error(`Fetched response body buffer: ${assetContents.byteLength} bytes`);
if (options.checksumSha256) {
const actualSHA256Checksum = createHash('sha256').update(assetContents).digest('hex');
if (actualSHA256Checksum !== options.checksumSha256) {
throw new Error(`Checksum mismatch for ${asset.url} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum})`);
}
}
console.error(`Verified SHA256 checksums match for ${asset.url}`);
const tarCommand = `tar -xz -C ${options.dest}`;
execSync(tarCommand, { input: assetContents });
console.error(`Fetch complete!`);
return;
}
throw new Error(`Request ${asset.url} failed with status code: ${assetResponse.status}`);
}
throw new Error(`Request https://api.github.com failed with status code: ${response.status}`);
} finally {
clearTimeout(timeout);
}
} catch (e) {
if (retries > 0) {
console.error(`Fetching failed: ${e}`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
return fetchUrl(options, retries - 1, retryDelay);
}
throw e;
}
}
async function getSysroot(arch) {
let expectedName;
let triple;
const prefix = '-glibc-2.28-gcc-10.5.0';
switch (arch) {
case 'x64':
expectedName = `x86_64-linux-gnu${prefix}.tar.gz`;
triple = 'x86_64-linux-gnu';
break;
case 'arm64':
expectedName = `aarch64-linux-gnu${prefix}.tar.gz`;
triple = 'aarch64-linux-gnu';
break;
default:
throw new Error(`Unsupported architecture: ${arch}`);
}
console.log(`Fetching ${expectedName} for ${triple}`);
const checksumSha256 = getSysrootChecksum(expectedName);
if (!checksumSha256) {
throw new Error(`Could not find checksum for ${expectedName}`);
}
const sysroot = path.join(tmpdir(), `vscode-${arch}-sysroot`);
const stamp = path.join(sysroot, '.stamp');
const result = `${sysroot}/${triple}/${triple}/sysroot`;
if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) {
console.log(`Sysroot already installed: ${result}`);
return result;
}
console.error(`Installing ${arch} root image: ${sysroot}`);
fs.rmSync(sysroot, { recursive: true, force: true });
fs.mkdirSync(sysroot, { recursive: true });
await fetchUrl({
checksumSha256,
assetName: expectedName,
dest: sysroot
});
fs.writeFileSync(stamp, expectedName);
console.log(`Sysroot installed: ${result}`);
return result;
}
async function main() {
const arch = process.argv[2] || process.env.ARCH || 'x64';
console.error(`Installing sysroot for architecture: ${arch}`);
try {
const sysrootPath = await getSysroot(arch);
console.log(`SYSROOT_PATH=${sysrootPath}`);
} catch (error) {
console.error('Error installing sysroot:', error);
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = { getSysroot };
================================================
FILE: scripts/linux/verify-glibc-requirements.sh
================================================
#!/usr/bin/env bash
set -e
# Get all files with .node extension from given folder
files=$(find $SEARCH_PATH -name "*.node")
echo "Verifying requirements for files: $files"
for file in $files; do
glibc_version="$EXPECTED_GLIBC_VERSION"
glibcxx_version="$EXPECTED_GLIBCXX_VERSION"
while IFS= read -r line; do
if [[ $line == *"GLIBC_"* ]]; then
version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()')
version=${version#*_}
if [[ $(printf "%s\n%s" "$version" "$glibc_version" | sort -V | tail -n1) == "$version" ]]; then
glibc_version=$version
fi
elif [[ $line == *"GLIBCXX_"* ]]; then
version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()')
version=${version#*_}
if [[ $(printf "%s\n%s" "$version" "$glibcxx_version" | sort -V | tail -n1) == "$version" ]]; then
glibcxx_version=$version
fi
fi
done < <(objdump -T "$file")
if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then
echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version"
exit 1
fi
if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then
echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version"
fi
done
================================================
FILE: scripts/post-install.js
================================================
//@ts-check
const fs = require('fs');
const os = require('os');
const path = require('path');
const RELEASE_DIR = path.join(__dirname, '../build/Release');
const BUILD_FILES = [
path.join(RELEASE_DIR, 'conpty.node'),
path.join(RELEASE_DIR, 'conpty.pdb'),
path.join(RELEASE_DIR, 'conpty_console_list.node'),
path.join(RELEASE_DIR, 'conpty_console_list.pdb'),
path.join(RELEASE_DIR, 'pty.node'),
path.join(RELEASE_DIR, 'pty.pdb'),
path.join(RELEASE_DIR, 'spawn-helper')
];
const CONPTY_DIR = path.join(__dirname, '../third_party/conpty');
const CONPTY_SUPPORTED_ARCH = ['x64', 'arm64'];
console.log('\x1b[32m> Cleaning release folder...\x1b[0m');
/** @param {string} folder */
function cleanFolderRecursive(folder) {
var files = [];
if (fs.existsSync(folder)) {
files = fs.readdirSync(folder);
files.forEach(function(file,index) {
var curPath = path.join(folder, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
cleanFolderRecursive(curPath);
fs.rmdirSync(curPath);
} else if (BUILD_FILES.indexOf(curPath) < 0){ // delete file
fs.unlinkSync(curPath);
}
});
}
};
try {
cleanFolderRecursive(RELEASE_DIR);
} catch(e) {
console.log(e);
process.exit(1);
}
console.log(`\x1b[32m> Moving conpty.dll...\x1b[0m`);
if (os.platform() !== 'win32') {
console.log(' SKIPPED (not Windows)');
} else {
let windowsArch;
if (process.env.npm_config_arch) {
windowsArch = process.env.npm_config_arch;
console.log(` Using $npm_config_arch: ${windowsArch}`);
} else {
windowsArch = os.arch();
console.log(` Using os.arch(): ${windowsArch}`);
}
if (!CONPTY_SUPPORTED_ARCH.includes(windowsArch)) {
console.log(` SKIPPED (unsupported architecture ${windowsArch})`);
} else {
const versionFolder = fs.readdirSync(CONPTY_DIR)[0];
console.log(` Found version ${versionFolder}`);
const sourceFolder = path.join(CONPTY_DIR, versionFolder, `win10-${windowsArch}`);
const destFolder = path.join(RELEASE_DIR, 'conpty');
fs.mkdirSync(destFolder, { recursive: true });
for (const file of ['conpty.dll', 'OpenConsole.exe']) {
const sourceFile = path.join(sourceFolder, file);
const destFile = path.join(destFolder, file);
console.log(` Copying ${sourceFile} -> ${destFile}`);
fs.copyFileSync(sourceFile, destFile);
}
}
}
process.exit(0);
================================================
FILE: scripts/prebuild.js
================================================
//@ts-check
const fs = require('fs');
const path = require('path');
/**
* This script checks for the prebuilt binaries for the current platform and
* architecture. It exits with 0 if prebuilds are found and 1 if not.
*
* If npm_config_build_from_source is set then it removes the prebuilds for the
* current platform so they are not loaded at runtime.
*
* Usage:
* node scripts/prebuild.js
*/
const PREBUILDS_ROOT = path.join(__dirname, '..', 'prebuilds');
const PREBUILD_DIR = path.join(__dirname, '..', 'prebuilds', `${process.platform}-${process.arch}`);
// Do not use prebuilds when npm_config_build_from_source is set
if (process.env.npm_config_build_from_source === 'true') {
console.log('\x1b[33m> Removing prebuilds and rebuilding because npm_config_build_from_source is set\x1b[0m');
fs.rmSync(PREBUILDS_ROOT, { recursive: true, force: true });
process.exit(1);
}
// Check whether the correct prebuilt files exist
console.log('\x1b[32m> Checking prebuilds...\x1b[0m');
if (!fs.existsSync(PREBUILD_DIR)) {
console.log(`\x1b[33m> Rebuilding because directory ${PREBUILD_DIR} does not exist\x1b[0m`);
process.exit(1);
}
process.exit(0);
================================================
FILE: src/conpty_console_list_agent.ts
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*
* This module fetches the console process list for a particular PID. It must be
* called from a different process (child_process.fork) as there can only be a
* single console attached to a process.
*/
import { loadNativeModule } from './utils';
const getConsoleProcessList = loadNativeModule('conpty_console_list').module.getConsoleProcessList;
const shellPid = parseInt(process.argv[2], 10);
let consoleProcessList: number[] = [];
if (shellPid > 0) {
try {
consoleProcessList = getConsoleProcessList(shellPid);
} catch {
// AttachConsole can fail if the process already exited or is invalid.
consoleProcessList = [];
}
}
process.send!({ consoleProcessList });
process.exit(0);
================================================
FILE: src/eventEmitter2.test.ts
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
import * as assert from 'assert';
import { EventEmitter2 } from './eventEmitter2';
describe('EventEmitter2', () => {
it('should fire listeners multiple times', () => {
const order: string[] = [];
const emitter = new EventEmitter2<number>();
emitter.event(data => order.push(data + 'a'));
emitter.event(data => order.push(data + 'b'));
emitter.fire(1);
emitter.fire(2);
assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]);
});
it('should not fire listeners once disposed', () => {
const order: string[] = [];
const emitter = new EventEmitter2<number>();
emitter.event(data => order.push(data + 'a'));
const disposeB = emitter.event(data => order.push(data + 'b'));
emitter.event(data => order.push(data + 'c'));
emitter.fire(1);
disposeB.dispose();
emitter.fire(2);
assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]);
});
});
================================================
FILE: src/eventEmitter2.ts
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
import { IDisposable } from './types';
interface IListener<T> {
(e: T): void;
}
export interface IEvent<T> {
(listener: (e: T) => any): IDisposable;
}
export class EventEmitter2<T> {
private _listeners: Array<IListener<T>> = [];
private _event?: IEvent<T>;
public get event(): IEvent<T> {
if (!this._event) {
this._event = (listener: (e: T) => any) => {
this._listeners.push(listener);
const disposable = {
dispose: () => {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return;
}
}
}
};
return disposable;
};
}
return this._event;
}
public fire(data: T): void {
const queue: Array<IListener<T>> = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, data);
}
}
}
================================================
FILE: src/index.ts
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import { ITerminal, IPtyOpenOptions, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces';
import { ArgvOrCommandLine } from './types';
import { loadNativeModule } from './utils';
let terminalCtor: any;
if (process.platform === 'win32') {
terminalCtor = require('./windowsTerminal').WindowsTerminal;
} else {
terminalCtor = require('./unixTerminal').UnixTerminal;
}
/**
* Forks a process as a pseudoterminal.
* @param file The file to launch.
* @param args The file's arguments as argv (string[]) or in a pre-escaped
* CommandLine format (string). Note that the CommandLine option is only
* available on Windows and is expected to be escaped properly.
* @param options The options of the terminal.
* @throws When the file passed to spawn with does not exists.
* @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx
* @see Parsing C++ Comamnd-Line Arguments https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
* @see GetCommandLine https://msdn.microsoft.com/en-us/library/windows/desktop/ms683156.aspx
*/
export function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal {
return new terminalCtor(file, args, opt);
}
/** @deprecated */
export function fork(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal {
return new terminalCtor(file, args, opt);
}
/** @deprecated */
export function createTerminal(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal {
return new terminalCtor(file, args, opt);
}
export function open(options: IPtyOpenOptions): ITerminal {
return terminalCtor.open(options);
}
/**
* Expose the native API when not Windows, note that this is not public API and
* could be removed at any time.
*/
export const native = (process.platform !== 'win32' ? loadNativeModule('pty').module : null);
================================================
FILE: src/interfaces.ts
================================================
/**
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
export interface IProcessEnv {
[key: string]: string | undefined;
}
export interface ITerminal {
/**
* Gets the name of the process.
*/
process: string;
/**
* Gets the process ID.
*/
pid: number;
/**
* Writes data to the socket.
* @param data The data to write.
*/
write(data: string | Buffer): void;
/**
* Resize the pty.
* @param cols The number of columns.
* @param rows The number of rows.
* @param pixelSize Optional pixel dimensions of the pty. On Unix, this sets the `ws_xpixel`
* and `ws_ypixel` fields of the `winsize` struct. Applications running in the pty can read
* these values via the `TIOCGWINSZ` ioctl. This parameter is ignored on Windows.
*/
resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void;
/**
* Clears the pty's internal representation of its buffer. This is a no-op
* unless on Windows/ConPTY.
*/
clear(): void;
/**
* Close, kill and destroy the socket.
*/
destroy(): void;
/**
* Kill the pty.
* @param signal The signal to send, by default this is SIGHUP. This is not
* supported on Windows.
*/
kill(signal?: string): void;
/**
* Set the pty socket encoding.
*/
setEncoding(encoding: string | null): void;
/**
* Resume the pty socket.
*/
resume(): void;
/**
* Pause the pty socket.
*/
pause(): void;
/**
* Alias for ITerminal.on(eventName, listener).
*/
addListener(eventName: string, listener: (...args: any[]) => any): void;
/**
* Adds the listener function to the end of the listeners array for the event
* named eventName.
* @param eventName The event name.
* @param listener The callback function
*/
on(eventName: string, listener: (...args: any[]) => any): void;
/**
* Returns a copy of the array of listeners for the event named eventName.
*/
listeners(eventName: string): Function[];
/**
* Removes the specified listener from the listener array for the event named
* eventName.
*/
removeListener(eventName: string, listener: (...args: any[]) => any): void;
/**
* Removes all listeners, or those of the specified eventName.
*/
removeAllListeners(eventName: string): void;
/**
* Adds a one time listener function for the event named eventName. The next
* time eventName is triggered, this listener is removed and then invoked.
*/
once(eventName: string, listener: (...args: any[]) => any): void;
}
interface IBasePtyForkOptions {
name?: string;
cols?: number;
rows?: number;
cwd?: string;
env?: IProcessEnv;
encoding?: string | null;
handleFlowControl?: boolean;
flowControlPause?: string;
flowControlResume?: string;
}
export interface IPtyForkOptions extends IBasePtyForkOptions {
uid?: number;
gid?: number;
}
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
/**
* Whether to use the ConPTY system on Windows. When this is not set, ConPTY will be used when
* the Windows build number is >= 18309 (instead of winpty). Note that ConPTY is available from
* build 17134 but is too unstable to enable by default.
*
* @deprecated This option is ignored and will be removed in a future version.
* https://github.com/microsoft/node-pty/issues/871
*/
useConpty?: boolean;
useConptyDll?: boolean;
conptyInheritCursor?: boolean;
}
export interface IPtyOpenOptions {
cols?: number;
rows?: number;
encoding?: string | null;
}
================================================
FILE: src/native.d.ts
================================================
/**
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
interface IConptyNative {
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number };
resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void;
clear(ptyId: number, useConptyDll: boolean): void;
kill(ptyId: number, useConptyDll: boolean): void;
}
interface IUnixNative {
fork(file: string, args: string[], parsedEnv: string[], cwd: string, cols: number, rows: number, uid: number, gid: number, useUtf8: boolean, helperPath: string, onExitCallback: (code: number, signal: number) => void): IUnixProcess;
open(cols: number, rows: number): IUnixOpenProcess;
process(fd: number, pty?: string): string;
resize(fd: number, cols: number, rows: number, pixelWidth: number, pixelHeight: number): void;
}
interface IConptyProcess {
pty: number;
fd: number;
conin: string;
conout: string;
}
interface IUnixProcess {
fd: number;
pid: number;
pty: string;
}
interface IUnixOpenProcess {
master: number;
slave: number;
pty: string;
}
================================================
FILE: src/shared/conout.ts
================================================
/**
* Copyright (c) 2020, Microsoft Corporation (MIT License).
*/
export interface IWorkerData {
conoutPipeName: string;
}
export const enum ConoutWorkerMessage {
READY = 1
}
export function getWorkerPipeName(conoutPipeName: string): string {
return `${conoutPipeName}-worker`;
}
================================================
FILE: src/terminal.test.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as assert from 'assert';
import { Terminal } from './terminal';
import { Socket } from 'net';
const terminalConstructor = (process.platform === 'win32')
? require('./windowsTerminal').WindowsTerminal
: require('./unixTerminal').UnixTerminal;
const SHELL = (process.platform === 'win32') ? 'cmd.exe' : '/bin/bash';
let terminalCtor: any; // Will be WindowsTerminal | UnixTerminal depending on conditional report
if (process.platform === 'win32') {
terminalCtor = require('./windowsTerminal');
} else {
terminalCtor = require('./unixTerminal');
}
class TestTerminal extends Terminal {
public checkType<T>(name: string, value: T, type: string, allowArray: boolean = false): void {
this._checkType(name, value, type, allowArray);
}
protected _write(data: string | Buffer): void {
throw new Error('Method not implemented.');
}
public resize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
public clear(): void {
throw new Error('Method not implemented.');
}
public destroy(): void {
throw new Error('Method not implemented.');
}
public kill(signal?: string): void {
throw new Error('Method not implemented.');
}
public get process(): string {
throw new Error('Method not implemented.');
}
public get master(): Socket {
throw new Error('Method not implemented.');
}
public get slave(): Socket {
throw new Error('Method not implemented.');
}
}
describe('Terminal', () => {
describe('constructor', () => {
it('should do basic type checks', () => {
assert.throws(
() => new (<any>terminalCtor)('a', 'b', { 'name': {} }),
'name must be a string (not a object)'
);
});
});
describe('checkType', () => {
it('should throw for the wrong type', () => {
const t = new TestTerminal();
assert.doesNotThrow(() => t.checkType('foo', 'test', 'string'));
assert.doesNotThrow(() => t.checkType('foo', 1, 'number'));
assert.doesNotThrow(() => t.checkType('foo', {}, 'object'));
assert.throws(() => t.checkType('foo', 'test', 'number'));
assert.throws(() => t.checkType('foo', 1, 'object'));
assert.throws(() => t.checkType('foo', {}, 'string'));
});
it('should throw for wrong types within arrays', () => {
const t = new TestTerminal();
assert.doesNotThrow(() => t.checkType('foo', ['test'], 'string', true));
assert.doesNotThrow(() => t.checkType('foo', [1], 'number', true));
assert.doesNotThrow(() => t.checkType('foo', [{}], 'object', true));
assert.throws(() => t.checkType('foo', ['test'], 'number', true));
assert.throws(() => t.checkType('foo', [1], 'object', true));
assert.throws(() => t.checkType('foo', [{}], 'string', true));
});
});
describe('automatic flow control', () => {
it('should respect ctor flow control options', () => {
const pty = new terminalConstructor(SHELL, [], {handleFlowControl: true, flowControlPause: 'abc', flowControlResume: '123'});
assert.equal(pty.handleFlowControl, true);
assert.equal((pty as any)._flowControlPause, 'abc');
assert.equal((pty as any)._flowControlResume, '123');
});
// TODO: I don't think this test ever worked due to pollUntil being used incorrectly
// it('should do flow control automatically', async function(): Promise<void> {
// // Flow control doesn't work on Windows
// if (process.platform === 'win32') {
// return;
// }
// this.timeout(10000);
// const pty = new terminalConstructor(SHELL, [], {handleFlowControl: true, flowControlPause: 'PAUSE', flowControlResume: 'RESUME'});
// let read: string = '';
// pty.on('data', data => read += data);
// pty.on('pause', () => read += 'paused');
// pty.on('resume', () => read += 'resumed');
// pty.write('1');
// pty.write('PAUSE');
// pty.write('2');
// pty.write('RESUME');
// pty.write('3');
// await pollUntil(() => {
// return stripEscapeSequences(read).endsWith('1pausedresumed23');
// }, 100, 10);
// });
});
});
function stripEscapeSequences(data: string): string {
return data.replace(/\u001b\[0K/, '');
}
================================================
FILE: src/terminal.ts
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import { Socket } from 'net';
import { EventEmitter } from 'events';
import { ITerminal, IPtyForkOptions, IProcessEnv } from './interfaces';
import { EventEmitter2, IEvent } from './eventEmitter2';
import { IExitEvent } from './types';
export const DEFAULT_COLS: number = 80;
export const DEFAULT_ROWS: number = 24;
/**
* Default messages to indicate PAUSE/RESUME for automatic flow control.
* To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
* the sequences can be customized in `IPtyForkOptions`.
*/
const FLOW_CONTROL_PAUSE = '\x13'; // defaults to XOFF
const FLOW_CONTROL_RESUME = '\x11'; // defaults to XON
export abstract class Terminal implements ITerminal {
protected _socket!: Socket; // HACK: This is unsafe
protected _pid: number = 0;
protected _fd: number = 0;
protected _pty: any;
protected _file!: string; // HACK: This is unsafe
protected _name!: string; // HACK: This is unsafe
protected _cols: number = 0;
protected _rows: number = 0;
protected _readable: boolean = false;
protected _writable: boolean = false;
protected _internalee: EventEmitter;
private _flowControlPause: string;
private _flowControlResume: string;
public handleFlowControl: boolean;
private _onData = new EventEmitter2<string>();
public get onData(): IEvent<string> { return this._onData.event; }
private _onExit = new EventEmitter2<IExitEvent>();
public get onExit(): IEvent<IExitEvent> { return this._onExit.event; }
public get pid(): number { return this._pid; }
public get cols(): number { return this._cols; }
public get rows(): number { return this._rows; }
constructor(opt?: IPtyForkOptions) {
// for 'close'
this._internalee = new EventEmitter();
// setup flow control handling
this.handleFlowControl = !!(opt?.handleFlowControl);
this._flowControlPause = opt?.flowControlPause || FLOW_CONTROL_PAUSE;
this._flowControlResume = opt?.flowControlResume || FLOW_CONTROL_RESUME;
if (!opt) {
return;
}
// Do basic type checks here in case node-pty is being used within JavaScript. If the wrong
// types go through to the C++ side it can lead to hard to diagnose exceptions.
this._checkType('name', opt.name ? opt.name : undefined, 'string');
this._checkType('cols', opt.cols ? opt.cols : undefined, 'number');
this._checkType('rows', opt.rows ? opt.rows : undefined, 'number');
this._checkType('cwd', opt.cwd ? opt.cwd : undefined, 'string');
this._checkType('env', opt.env ? opt.env : undefined, 'object');
this._checkType('uid', opt.uid ? opt.uid : undefined, 'number');
this._checkType('gid', opt.gid ? opt.gid : undefined, 'number');
this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string');
}
protected abstract _write(data: string | Buffer): void;
public write(data: string | Buffer): void {
if (this.handleFlowControl) {
// PAUSE/RESUME messages are not forwarded to the pty
if (data === this._flowControlPause) {
this.pause();
return;
}
if (data === this._flowControlResume) {
this.resume();
return;
}
}
// everything else goes to the real pty
this._write(data);
}
protected _forwardEvents(): void {
this.on('data', e => this._onData.fire(e));
this.on('exit', (exitCode, signal) => this._onExit.fire({ exitCode, signal }));
}
protected _checkType<T>(name: string, value: T | undefined, type: string, allowArray: boolean = false): void {
if (value === undefined) {
return;
}
if (allowArray) {
if (Array.isArray(value)) {
value.forEach((v, i) => {
if (typeof v !== type) {
throw new Error(`${name}[${i}] must be a ${type} (not a ${typeof v[i]})`);
}
});
return;
}
}
if (typeof value !== type) {
throw new Error(`${name} must be a ${type} (not a ${typeof value})`);
}
}
/** See net.Socket.end */
public end(data: string): void {
this._socket.end(data);
}
/** See stream.Readable.pipe */
public pipe(dest: any, options: any): any {
return this._socket.pipe(dest, options);
}
/** See net.Socket.pause */
public pause(): Socket {
return this._socket.pause();
}
/** See net.Socket.resume */
public resume(): Socket {
return this._socket.resume();
}
/** See net.Socket.setEncoding */
public setEncoding(encoding: string | null): void {
if ((this._socket as any)._decoder) {
delete (this._socket as any)._decoder;
}
if (encoding) {
this._socket.setEncoding(encoding);
}
}
public addListener(eventName: string, listener: (...args: any[]) => any): void { this.on(eventName, listener); }
public on(eventName: string, listener: (...args: any[]) => any): void {
if (eventName === 'close') {
this._internalee.on('close', listener);
return;
}
this._socket.on(eventName, listener);
}
public emit(eventName: string, ...args: any[]): any {
if (eventName === 'close') {
return this._internalee.emit.apply(this._internalee, arguments as any);
}
return this._socket.emit.apply(this._socket, arguments as any);
}
public listeners(eventName: string): Function[] {
return this._socket.listeners(eventName);
}
public removeListener(eventName: string, listener: (...args: any[]) => any): void {
this._socket.removeListener(eventName, listener);
}
public removeAllListeners(eventName: string): void {
this._socket.removeAllListeners(eventName);
}
public once(eventName: string, listener: (...args: any[]) => any): void {
this._socket.once(eventName, listener);
}
public abstract resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void;
public abstract clear(): void;
public abstract destroy(): void;
public abstract kill(signal?: string): void;
public abstract get process(): string;
public abstract get master(): Socket| undefined;
public abstract get slave(): Socket | undefined;
protected _close(): void {
this._socket.readable = false;
this.write = () => {};
this.end = () => {};
this._writable = false;
this._readable = false;
}
protected _parseEnv(env: IProcessEnv): string[] {
const keys = Object.keys(env || {});
const pairs = [];
for (let i = 0; i < keys.length; i++) {
if (keys[i] === undefined) {
continue;
}
pairs.push(keys[i] + '=' + env[keys[i]]);
}
return pairs;
}
}
================================================
FILE: src/testUtils.test.ts
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
export function pollUntil(cb: () => boolean, timeout: number, interval: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const intervalId = setInterval(() => {
if (cb()) {
clearInterval(intervalId);
clearTimeout(timeoutId);
resolve();
}
}, interval);
const timeoutId = setTimeout(() => {
clearInterval(intervalId);
if (cb()) {
resolve();
} else {
reject();
}
}, timeout);
});
}
================================================
FILE: src/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../lib",
"sourceMap": true,
"lib": [
"es2015"
],
"strict": true
},
"exclude": [
"node_modules",
"scripts",
"index.js",
"demo.js",
"lib",
"test",
"examples"
]
}
================================================
FILE: src/types.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
export type ArgvOrCommandLine = string[] | string;
export interface IExitEvent {
exitCode: number;
signal: number | undefined;
}
export interface IDisposable {
dispose(): void;
}
================================================
FILE: src/unix/pty.cc
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2017, Daniel Imms (MIT License)
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*
* See:
* man pty
* man tty_ioctl
* man termios
* man forkpty
*/
/**
* Includes
*/
#define NODE_ADDON_API_DISABLE_DEPRECATED
#include <napi.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <thread>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
/* forkpty */
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
#if defined(__linux__)
#include <pty.h>
#include <dirent.h>
#include <sys/syscall.h>
#elif defined(__APPLE__)
#include <util.h>
#elif defined(__FreeBSD__)
#include <libutil.h>
#include <termios.h>
#elif defined(__OpenBSD__)
#include <util.h>
#include <termios.h>
#endif
/* Some platforms name VWERASE and VDISCARD differently */
#if !defined(VWERASE) && defined(VWERSE)
#define VWERASE VWERSE
#endif
#if !defined(VDISCARD) && defined(VDISCRD)
#define VDISCARD VDISCRD
#endif
/* for pty_getproc */
#if defined(__linux__)
#include <stdio.h>
#include <stdint.h>
#elif defined(__APPLE__)
#include <libproc.h>
#include <os/availability.h>
#include <paths.h>
#include <spawn.h>
#include <sys/event.h>
#include <sys/sysctl.h>
#include <termios.h>
#endif
/* NSIG - macro for highest signal + 1, should be defined */
#ifndef NSIG
#define NSIG 32
#endif
/* macOS 10.14 back does not define this constant */
#ifndef POSIX_SPAWN_SETSID
#define POSIX_SPAWN_SETSID 1024
#endif
/* environ for execvpe */
/* node/src/node_child_process.cc */
#if !defined(__APPLE__)
extern char **environ;
#endif
#if defined(__APPLE__)
extern "C" {
// Changes the current thread's directory to a path or directory file
// descriptor. libpthread only exposes a syscall wrapper starting in
// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes,
// the syscall is issued directly.
int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12));
int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12));
}
#define HANDLE_EINTR(x) ({ \
int eintr_wrapper_counter = 0; \
decltype(x) eintr_wrapper_result; \
do { \
eintr_wrapper_result = (x); \
} while (eintr_wrapper_result == -1 && errno == EINTR && \
eintr_wrapper_counter++ < 100); \
eintr_wrapper_result; \
})
#endif
struct ExitEvent {
int exit_code = 0, signal_code = 0;
};
#if defined(__linux__)
static int
SetCloseOnExec(int fd) {
int flags = fcntl(fd, F_GETFD, 0);
if (flags == -1)
return flags;
if (flags & FD_CLOEXEC)
return 0;
return fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
/**
* Close all file descriptors >= 3 to prevent FD leakage to child processes.
* Uses close_range() syscall on Linux 5.9+, falls back to /proc/self/fd iteration.
*/
static void
pty_close_inherited_fds() {
// Try close_range() first (Linux 5.9+, glibc 2.34+)
#if defined(SYS_close_range) && defined(CLOSE_RANGE_CLOEXEC)
if (syscall(SYS_close_range, 3, ~0U, CLOSE_RANGE_CLOEXEC) == 0) {
return;
}
#endif
int fd;
// Set the CLOEXEC flag on all open descriptors. Unconditionally try the first
// 16 file descriptors. After that, bail out after the first error.
for (fd = 3; ; fd++)
if (SetCloseOnExec(fd) && fd > 15)
break;
}
#endif
void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
std::thread *th = new std::thread;
// Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE.
auto tsfn = Napi::ThreadSafeFunction::New(
env,
cb, // JavaScript function called asynchronously
"SetupExitCallback_resource", // Name
0, // Unlimited queue
1, // Only one thread will use this initially
[th](Napi::Env) { // Finalizer used to clean threads up
th->join();
delete th;
});
*th = std::thread([tsfn = std::move(tsfn), pid] {
auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
cb.Call({Napi::Number::New(env, exit_event->exit_code),
Napi::Number::New(env, exit_event->signal_code)});
delete exit_event;
};
int ret;
int stat_loc;
#if defined(__APPLE__)
// Based on
// https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69?
int kq = HANDLE_EINTR(kqueue());
struct kevent change = {0};
EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL));
if (ret == -1) {
if (errno == ESRCH) {
// At this point, one of the following has occurred:
// 1. The process has died but has not yet been reaped.
// 2. The process has died and has already been reaped.
// 3. The process is in the process of dying. It's no longer
// kqueueable, but it may not be waitable yet either. Mark calls
// this case the "zombie death race".
ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG));
if (ret == 0) {
ret = kill(pid, SIGKILL);
if (ret != -1) {
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
}
}
}
} else {
struct kevent event = {0};
ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL));
if (ret == 1) {
if ((event.fflags & NOTE_EXIT) &&
(event.ident == static_cast<uintptr_t>(pid))) {
// The process is dead or dying. This won't block for long, if at
// all.
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
}
}
}
#else
while (true) {
errno = 0;
if ((ret = waitpid(pid, &stat_loc, 0)) != pid) {
if (ret == -1 && errno == EINTR) {
continue;
}
if (ret == -1 && errno == ECHILD) {
// XXX node v0.8.x seems to have this problem.
// waitpid is already handled elsewhere.
;
} else {
assert(false);
}
}
break;
}
#endif
ExitEvent *exit_event = new ExitEvent;
if (WIFEXITED(stat_loc)) {
exit_event->exit_code = WEXITSTATUS(stat_loc); // errno?
}
if (WIFSIGNALED(stat_loc)) {
exit_event->signal_code = WTERMSIG(stat_loc);
}
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
switch (status) {
case napi_closing:
break;
case napi_queue_full:
Napi::Error::Fatal("SetupExitCallback", "Queue was full");
case napi_ok:
if (tsfn.Release() != napi_ok) {
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
}
break;
default:
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
}
});
}
/**
* Methods
*/
Napi::Value PtyFork(const Napi::CallbackInfo& info);
Napi::Value PtyOpen(const Napi::CallbackInfo& info);
Napi::Value PtyResize(const Napi::CallbackInfo& info);
Napi::Value PtyGetProc(const Napi::CallbackInfo& info);
/**
* Functions
*/
static int
pty_nonblock(int);
#if defined(__APPLE__)
static char *
pty_getproc(int);
#else
static char *
pty_getproc(int, char *);
#endif
#if defined(__APPLE__) || defined(__OpenBSD__)
static void
pty_posix_spawn(char** argv, char** env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
std::string* err);
#endif
struct DelBuf {
int len;
DelBuf(int len) : len(len) {}
void operator()(char **p) {
if (p == nullptr)
return;
for (int i = 0; i < len; i++)
free(p[i]);
delete[] p;
}
};
Napi::Value PtyFork(const Napi::CallbackInfo& info) {
Napi::Env napiEnv(info.Env());
Napi::HandleScope scope(napiEnv);
if (info.Length() != 11 ||
!info[0].IsString() ||
!info[1].IsArray() ||
!info[2].IsArray() ||
!info[3].IsString() ||
!info[4].IsNumber() ||
!info[5].IsNumber() ||
!info[6].IsNumber() ||
!info[7].IsNumber() ||
!info[8].IsBoolean() ||
!info[9].IsString() ||
!info[10].IsFunction()) {
throw Napi::Error::New(napiEnv, "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)");
}
// file
std::string file = info[0].As<Napi::String>();
// args
Napi::Array argv_ = info[1].As<Napi::Array>();
// env
Napi::Array env_ = info[2].As<Napi::Array>();
int envc = env_.Length();
std::unique_ptr<char *, DelBuf> env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1));
char **env = env_unique_ptr.get();
env[envc] = NULL;
for (int i = 0; i < envc; i++) {
std::string pair = env_.Get(i).As<Napi::String>();
env[i] = strdup(pair.c_str());
}
// cwd
std::string cwd_ = info[3].As<Napi::String>();
// size
struct winsize winp;
winp.ws_col = info[4].As<Napi::Number>().Int32Value();
winp.ws_row = info[5].As<Napi::Number>().Int32Value();
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
#if !defined(__APPLE__)
// uid / gid
int uid = info[6].As<Napi::Number>().Int32Value();
int gid = info[7].As<Napi::Number>().Int32Value();
#endif
// termios
struct termios t = termios();
struct termios *term = &t;
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
if (info[8].As<Napi::Boolean>().Value()) {
#if defined(IUTF8)
term->c_iflag |= IUTF8;
#endif
}
term->c_oflag = OPOST | ONLCR;
term->c_cflag = CREAD | CS8 | HUPCL;
term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL;
term->c_cc[VEOF] = 4;
term->c_cc[VEOL] = -1;
term->c_cc[VEOL2] = -1;
term->c_cc[VERASE] = 0x7f;
term->c_cc[VWERASE] = 23;
term->c_cc[VKILL] = 21;
term->c_cc[VREPRINT] = 18;
term->c_cc[VINTR] = 3;
term->c_cc[VQUIT] = 0x1c;
term->c_cc[VSUSP] = 26;
term->c_cc[VSTART] = 17;
term->c_cc[VSTOP] = 19;
term->c_cc[VLNEXT] = 22;
term->c_cc[VDISCARD] = 15;
term->c_cc[VMIN] = 1;
term->c_cc[VTIME] = 0;
#if (__APPLE__)
term->c_cc[VDSUSP] = 25;
term->c_cc[VSTATUS] = 20;
#endif
cfsetispeed(term, B38400);
cfsetospeed(term, B38400);
// helperPath
std::string helper_path = info[9].As<Napi::String>();
pid_t pid;
int master = -1;
#if defined(__APPLE__)
int argc = argv_.Length();
int argl = argc + 4;
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
char **argv = argv_unique_ptr.get();
argv[0] = strdup(helper_path.c_str());
argv[1] = strdup(cwd_.c_str());
argv[2] = strdup(file.c_str());
argv[argl - 1] = NULL;
for (int i = 0; i < argc; i++) {
std::string arg = argv_.Get(i).As<Napi::String>();
argv[i + 3] = strdup(arg.c_str());
}
std::string err;
pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
if (!err.empty()) {
if (master != -1) {
close(master);
}
throw Napi::Error::New(napiEnv, err);
}
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
}
#else
int argc = argv_.Length();
int argl = argc + 2;
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
char** argv = argv_unique_ptr.get();
argv[0] = strdup(file.c_str());
argv[argl - 1] = NULL;
for (int i = 0; i < argc; i++) {
std::string arg = argv_.Get(i).As<Napi::String>();
argv[i + 1] = strdup(arg.c_str());
}
sigset_t newmask, oldmask;
struct sigaction sig_action;
// temporarily block all signals
// this is needed due to a race condition in openpty
// and to avoid running signal handlers in the child
// before exec* happened
sigfillset(&newmask);
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
pid = forkpty(&master, nullptr, static_cast<termios*>(term), static_cast<winsize*>(&winp));
if (!pid) {
// remove all signal handler from child
sig_action.sa_handler = SIG_DFL;
sig_action.sa_flags = 0;
sigemptyset(&sig_action.sa_mask);
for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1
sigaction(i, &sig_action, NULL);
}
}
// reenable signals
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
switch (pid) {
case -1:
throw Napi::Error::New(napiEnv, "forkpty(3) failed.");
case 0:
if (strlen(cwd_.c_str())) {
if (chdir(cwd_.c_str()) == -1) {
perror("chdir(2) failed.");
_exit(1);
}
}
if (uid != -1 && gid != -1) {
if (setgid(gid) == -1) {
perror("setgid(2) failed.");
_exit(1);
}
if (setuid(uid) == -1) {
perror("setuid(2) failed.");
_exit(1);
}
}
// Close inherited FDs to prevent leaking pty master FDs to child
pty_close_inherited_fds();
{
char **old = environ;
environ = env;
execvp(argv[0], argv);
environ = old;
perror("execvp(3) failed.");
_exit(1);
}
default:
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
}
}
#endif
Napi::Object obj = Napi::Object::New(napiEnv);
obj.Set("fd", Napi::Number::New(napiEnv, master));
obj.Set("pid", Napi::Number::New(napiEnv, pid));
obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
// Set up process exit callback.
Napi::Function cb = info[10].As<Napi::Function>();
SetupExitCallback(napiEnv, cb, pid);
return obj;
}
Napi::Value PtyOpen(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
if (info.Length() != 2 ||
!info[0].IsNumber() ||
!info[1].IsNumber()) {
throw Napi::Error::New(env, "Usage: pty.open(cols, rows)");
}
// size
struct winsize winp;
winp.ws_col = info[0].As<Napi::Number>().Int32Value();
winp.ws_row = info[1].As<Napi::Number>().Int32Value();
winp.ws_xpixel = 0;
winp.ws_ypixel = 0;
// pty
int master, slave;
int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize*>(&winp));
if (ret == -1) {
throw Napi::Error::New(env, "openpty(3) failed.");
}
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(env, "Could not set master fd to nonblocking.");
}
if (pty_nonblock(slave) == -1) {
throw Napi::Error::New(env, "Could not set slave fd to nonblocking.");
}
Napi::Object obj = Napi::Object::New(env);
obj.Set("master", Napi::Number::New(env, master));
obj.Set("slave", Napi::Number::New(env, slave));
obj.Set("pty", Napi::String::New(env, ptsname(master)));
return obj;
}
Napi::Value PtyResize(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
if (info.Length() != 5 ||
!info[0].IsNumber() ||
!info[1].IsNumber() ||
!info[2].IsNumber() ||
!info[3].IsNumber() ||
!info[4].IsNumber()) {
throw Napi::Error::New(env, "Usage: pty.resize(fd, cols, rows, xPixel, yPixel)");
}
int fd = info[0].As<Napi::Number>().Int32Value();
struct winsize winp;
winp.ws_col = info[1].As<Napi::Number>().Int32Value();
winp.ws_row = info[2].As<Napi::Number>().Int32Value();
winp.ws_xpixel = info[3].As<Napi::Number>().Int32Value();
winp.ws_ypixel = info[4].As<Napi::Number>().Int32Value();
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
switch (errno) {
case EBADF:
throw Napi::Error::New(env, "ioctl(2) failed, EBADF");
case EFAULT:
throw Napi::Error::New(env, "ioctl(2) failed, EFAULT");
case EINVAL:
throw Napi::Error::New(env, "ioctl(2) failed, EINVAL");
case ENOTTY:
throw Napi::Error::New(env, "ioctl(2) failed, ENOTTY");
}
throw Napi::Error::New(env, "ioctl(2) failed");
}
return env.Undefined();
}
/**
* Foreground Process Name
*/
Napi::Value PtyGetProc(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
#if defined(__APPLE__)
if (info.Length() != 1 ||
!info[0].IsNumber()) {
throw Napi::Error::New(env, "Usage: pty.process(pid)");
}
int fd = info[0].As<Napi::Number>().Int32Value();
char *name = pty_getproc(fd);
#else
if (info.Length() != 2 ||
!info[0].IsNumber() ||
!info[1].IsString()) {
throw Napi::Error::New(env, "Usage: pty.process(fd, tty)");
}
int fd = info[0].As<Napi::Number>().Int32Value();
std::string tty_ = info[1].As<Napi::String>();
char *tty = strdup(tty_.c_str());
char *name = pty_getproc(fd, tty);
free(tty);
#endif
if (name == NULL) {
return env.Undefined();
}
Napi::String name_ = Napi::String::New(env, name);
free(name);
return name_;
}
/**
* Nonblocking FD
*/
static int
pty_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/**
* pty_getproc
* Taken from tmux.
*/
// Taken from: tmux (http://tmux.sourceforge.net/)
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#if defined(__linux__)
static char *
pty_getproc(int fd, char *tty) {
FILE *f;
char *path, *buf;
size_t len;
int ch;
pid_t pgrp;
int r;
if ((pgrp = tcgetpgrp(fd)) == -1) {
return NULL;
}
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
if (r == -1 || path == NULL) return NULL;
if ((f = fopen(path, "r")) == NULL) {
free(path);
return NULL;
}
free(path);
len = 0;
buf = NULL;
while ((ch = fgetc(f)) != EOF) {
if (ch == '\0') break;
buf = (char *)realloc(buf, len + 2);
if (buf == NULL) return NULL;
buf[len++] = ch;
}
if (buf != NULL) {
buf[len] = '\0';
}
fclose(f);
return buf;
}
#elif defined(__APPLE__)
static char *
pty_getproc(int fd) {
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 };
size_t size;
struct kinfo_proc kp;
if ((mib[3] = tcgetpgrp(fd)) == -1) {
return NULL;
}
size = sizeof kp;
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) {
return NULL;
}
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') {
return NULL;
}
return strdup(kp.kp_proc.p_comm);
}
#else
static char *
pty_getproc(int fd, char *tty) {
return NULL;
}
#endif
#if defined(__APPLE__)
static std::string format_error(const char* func, int err_code) {
char buf[256];
snprintf(buf, sizeof(buf), "%s: %s", func, strerror(err_code));
return buf;
}
static void
pty_posix_spawn(char** argv, char** env,
const struct termios *termp,
const struct winsize *winp,
int* master,
pid_t* pid,
std::string* err) {
int low_fds[3];
size_t count = 0;
int res = 0;
int slave = -1;
char slave_pty_name[128];
int spawn_err;
sigset_t signal_set;
for (; count < 3; count++) {
low_fds[count] = posix_openpt(O_RDWR);
if (low_fds[count] >= STDERR_FILENO)
break;
}
int flags = POSIX_SPAWN_CLOEXEC_DEFAULT |
POSIX_SPAWN_SETSIGDEF |
POSIX_SPAWN_SETSIGMASK |
POSIX_SPAWN_SETSID;
posix_spawn_file_actions_t acts;
posix_spawn_file_actions_init(&acts);
posix_spawnattr_t attrs;
posix_spawnattr_init(&attrs);
*master = posix_openpt(O_RDWR);
if (*master == -1) {
*err = format_error("posix_openpt failed", errno);
goto done;
}
res = grantpt(*master);
if (res == -1) {
*err = format_error("grantpt failed", errno);
goto done;
}
res = unlockpt(*master);
if (res == -1) {
*err = format_error("unlockpt failed", errno);
goto done;
}
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
if (res == -1) {
*err = format_error("ioctl(TIOCPTYGNAME) failed", errno);
goto done;
}
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
if (slave == -1) {
*err = format_error("open slave pty failed", errno);
goto done;
}
if (termp) {
res = tcsetattr(slave, TCSANOW, termp);
if (res == -1) {
*err = format_error("tcsetattr failed", errno);
goto done;
};
}
if (winp) {
res = ioctl(slave, TIOCSWINSZ, winp);
if (res == -1) {
*err = format_error("ioctl(TIOCSWINSZ) failed", errno);
goto done;
}
}
posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO);
posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO);
posix_spawn_file_actions_addclose(&acts, slave);
posix_spawn_file_actions_addclose(&acts, *master);
spawn_err = posix_spawnattr_setflags(&attrs, flags);
if (spawn_err != 0) {
*err = format_error("posix_spawnattr_setflags failed", spawn_err);
goto done;
}
/* Reset all signal the child to their default behavior */
sigfillset(&signal_set);
spawn_err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
if (spawn_err != 0) {
*err = format_error("posix_spawnattr_setsigdefault failed", spawn_err);
goto done;
}
/* Reset the signal mask for all signals */
sigemptyset(&signal_set);
spawn_err = posix_spawnattr_setsigmask(&attrs, &signal_set);
if (spawn_err != 0) {
*err = format_error("posix_spawnattr_setsigmask failed", spawn_err);
goto done;
}
do
spawn_err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
while (spawn_err == EINTR);
if (spawn_err != 0) {
*err = format_error("posix_spawn failed", spawn_err);
}
done:
posix_spawn_file_actions_destroy(&acts);
posix_spawnattr_destroy(&attrs);
if (slave != -1) {
close(slave);
}
for (size_t i = 0; i <= count; i++) {
close(low_fds[i]);
}
}
#endif
/**
* Init
*/
Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("fork", Napi::Function::New(env, PtyFork));
exports.Set("open", Napi::Function::New(env, PtyOpen));
exports.Set("resize", Napi::Function::New(env, PtyResize));
exports.Set("process", Napi::Function::New(env, PtyGetProc));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init)
================================================
FILE: src/unix/spawn-helper.cc
================================================
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main (int argc, char** argv) {
char *slave_path = ttyname(STDIN_FILENO);
// open implicit attaches a process to a terminal device if:
// - process has no controlling terminal yet
// - O_NOCTTY is not set
close(open(slave_path, O_RDWR));
char *cwd = argv[1];
char *file = argv[2];
argv = &argv[2];
if (strlen(cwd) && chdir(cwd) == -1) {
_exit(1);
}
execvp(file, argv);
return 1;
}
================================================
FILE: src/unixTerminal.test.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as assert from 'assert';
import * as cp from 'child_process';
import * as path from 'path';
import * as tty from 'tty';
import * as fs from 'fs';
import { constants } from 'os';
import { pollUntil } from './testUtils.test';
import { pid } from 'process';
import type { UnixTerminal as UnixTerminalType } from './unixTerminal';
const FIXTURES_PATH = path.normalize(path.join(__dirname, '..', 'fixtures', 'utf8-character.txt'));
if (process.platform !== 'win32') {
// Dynamic require to avoid loading pty.node on Windows
// eslint-disable-next-line @typescript-eslint/naming-convention
const { UnixTerminal } = require('./unixTerminal') as { UnixTerminal: typeof UnixTerminalType };
describe('UnixTerminal', () => {
describe('Constructor', () => {
it('should set a valid pts name', () => {
const term = new UnixTerminal('/bin/bash', [], {});
let regExp: RegExp | undefined;
if (process.platform === 'linux') {
// https://linux.die.net/man/4/pts
regExp = /^\/dev\/pts\/\d+$/;
}
if (process.platform === 'darwin') {
// https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man4/pty.4.html
regExp = /^\/dev\/tty[p-sP-S][a-z0-9]+$/;
}
if (regExp) {
assert.ok(regExp.test(term.ptsName), '"' + term.ptsName + '" should match ' + regExp.toString());
}
assert.ok(tty.isatty(term.fd));
});
});
describe('PtyForkEncodingOption', () => {
it('should default to utf8', (done) => {
const term = new UnixTerminal('/bin/bash', [ '-c', `cat "${FIXTURES_PATH}"` ]);
term.on('data', (data) => {
assert.strictEqual(typeof data, 'string');
assert.strictEqual(data, '\u00E6');
done();
});
});
it('should return a Buffer when encoding is null', (done) => {
const term = new UnixTerminal('/bin/bash', [ '-c', `cat "${FIXTURES_PATH}"` ], {
encoding: null
});
term.on('data', (data) => {
assert.strictEqual(typeof data, 'object');
assert.ok(data instanceof Buffer);
assert.strictEqual(0xC3, data[0]);
assert.strictEqual(0xA6, data[1]);
done();
});
});
it('should support other encodings', (done) => {
const text = 'test æ!';
const term = new UnixTerminal(undefined, ['-c', 'echo "' + text + '"'], {
encoding: 'base64'
});
let buffer = '';
term.onData((data) => {
assert.strictEqual(typeof data, 'string');
buffer += data;
});
term.onExit(() => {
assert.strictEqual(Buffer.alloc(8, buffer, 'base64').toString().replace('\r', '').replace('\n', ''), text);
done();
});
});
});
describe('open', () => {
let term: UnixTerminalType;
afterEach(() => {
if (term) {
term.slave!.destroy();
term.master!.destroy();
}
});
it('should open a pty with access to a master and slave socket', (done) => {
term = UnixTerminal.open({});
let slavebuf = '';
term.slave!.on('data', (data) => {
slavebuf += data;
});
let masterbuf = '';
term.master!.on('data', (data) => {
masterbuf += data;
});
pollUntil(() => {
if (masterbuf === 'slave\r\nmaster\r\n' && slavebuf === 'master\n') {
done();
return true;
}
return false;
}, 200, 10);
term.slave!.write('slave\n');
term.master!.write('master\n');
});
});
describe('close', () => {
const term = new UnixTerminal('node');
it('should exit when terminal is destroyed programmatically', (done) => {
term.on('exit', (code, signal) => {
assert.strictEqual(code, 0);
assert.strictEqual(signal, constants.signals.SIGHUP);
done();
});
term.destroy();
});
});
describe('signals in parent and child', () => {
it('SIGINT - custom in parent and child', done => {
// this test is cumbersome - we have to run it in a sub process to
// see behavior of SIGINT handlers
const data = `
var pty = require('./lib/index');
process.on('SIGINT', () => console.log('SIGINT in parent'));
var ptyProcess = pty.spawn('node', ['-e', 'process.on("SIGINT", ()=>console.log("SIGINT in child"));setTimeout(() => null, 300);'], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.on('data', function (data) {
console.log(data);
});
setTimeout(() => null, 500);
console.log('ready', ptyProcess.pid);
`;
const buffer: string[] = [];
const p = cp.spawn('node', ['-e', data]);
let sub = '';
p.stdout.on('data', (data) => {
if (!data.toString().indexOf('ready')) {
sub = data.toString().split(' ')[1].slice(0, -1);
setTimeout(() => {
process.kill(parseInt(sub), 'SIGINT'); // SIGINT to child
p.kill('SIGINT'); // SIGINT to parent
}, 200);
} else {
buffer.push(data.toString().replace(/^\s+|\s+$/g, ''));
}
});
p.on('close', () => {
// handlers in parent and child should have been triggered
assert.strictEqual(buffer.indexOf('SIGINT in child') !== -1, true);
assert.strictEqual(buffer.indexOf('SIGINT in parent') !== -1, true);
done();
});
});
it('SIGINT - custom in parent, default in child', done => {
// this tests the original idea of the signal(...) change in pty.cc:
// to make sure the SIGINT handler of a pty child is reset to default
// and does not interfere with the handler in the parent
const data = `
var pty = require('./lib/index');
process.on('SIGINT', () => console.log('SIGINT in parent'));
var ptyProcess = pty.spawn('node', ['-e', 'setTimeout(() => console.log("should not be printed"), 300);'], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.env.HOME,
env: process.env
});
ptyProcess.on('data', function (data) {
console.log(data);
});
setTimeout(() => null, 500);
console.log('ready', ptyProcess.pid);
`;
const buffer: string[] = [];
const p = cp.spawn('node', ['-e', data]);
let sub = '';
p.stdout.on('data', (data) => {
if (!data.toString().indexOf('ready')) {
sub = data.toString().split(' ')[1].slice(0, -1);
setTimeout(() => {
process.kill(parseInt(sub), 'SIGINT'); // SIGINT to child
p.kill('SIGINT'); // SIGINT to parent
}, 200);
} else {
buffer.push(data.toString().replace(/^\s+|\s+$/g, ''));
}
});
p.on('close', () => {
// handlers in parent and child should have been triggered
assert.strictEqual(buffer.indexOf('should not be printed') !== -1, false);
assert.strictEqual(buffer.indexOf('SIGINT in parent') !== -1, true);
done();
});
});
it('SIGHUP default (child only)', done => {
const term = new UnixTerminal('node', [ '-e', `
console.log('ready');
setTimeout(()=>console.log('timeout'), 200);`
]);
let buffer = '';
term.on('data', (data) => {
if (data === 'ready\r\n') {
term.kill();
} else {
buffer += data;
}
});
term.on('exit', () => {
// no timeout in buffer
assert.strictEqual(buffer, '');
done();
});
});
it('SIGUSR1 - custom in parent and child', done => {
let pHandlerCalled = 0;
const handleSigUsr = function(h: any): any {
return function(): void {
pHandlerCalled += 1;
process.removeListener('SIGUSR1', h);
};
};
process.on('SIGUSR1', handleSigUsr(handleSigUsr));
const term = new UnixTerminal('node', [ '-e', `
process.on('SIGUSR1', () => {
console.log('SIGUSR1 in child');
});
console.log('ready');
setTimeout(()=>null, 200);`
]);
let buffer = '';
term.on('data', (data) => {
if (data === 'ready\r\n') {
process.kill(process.pid, 'SIGUSR1');
term.kill('SIGUSR1');
} else {
buffer += data;
}
});
term.on('exit', () => {
// should have called both handlers and only once
assert.strictEqual(pHandlerCalled, 1);
assert.strictEqual(buffer, 'SIGUSR1 in child\r\n');
done();
});
});
});
describe('spawn', () => {
if (process.platform === 'linux') {
it('should not leak pty file descriptors to child processes', (done) => {
// Spawn 3 ptys - the 3rd should not see FDs from the first two
const ptys: UnixTerminalType[] = [];
for (let i = 0; i < 3; i++) {
ptys.push(new UnixTerminal('/bin/bash', [], {}));
}
let output = '';
ptys[2].onData((data) => {
output += data;
});
// Check for ptmx FDs in the 3rd terminal's shell
ptys[2].write('echo "PTMX_COUNT:$(file /proc/$$/fd/* 2>/dev/null | grep -c ptmx)"\n');
setTimeout(() => {
for (const pty of ptys) {
pty.kill();
}
// Extract the count from output - should be 0
const match = output.match(/PTMX_COUNT:(\d+)/);
assert.ok(match, `Could not find PTMX_COUNT in output: ${output}`);
assert.strictEqual(match![1], '0', `Expected 0 ptmx FDs but got ${match![1]}`);
done();
}, 1000);
});
}
if (process.platform === 'darwin') {
it('should return the name of the process', (done) => {
const term = new UnixTerminal('/bin/echo');
assert.strictEqual(term.process, '/bin/echo');
term.on('exit', () => done());
term.destroy();
});
it('should return the name of the sub process', (done) => {
const data = `
var pty = require('./lib/index');
var ptyProcess = pty.spawn('zsh', ['-c', 'python3'], {
env: process.env
});
ptyProcess.on('data', function (data) {
if (ptyProcess.process === 'Python') {
console.log('title', ptyProcess.process);
console.log('ready', ptyProcess.pid);
}
});
`;
const p = cp.spawn('node', ['-e', data]);
let sub = '';
let pid = '';
p.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.startsWith('title ')) {
sub = line.split(' ')[1];
} else if (line.startsWith('ready ')) {
pid = line.split(' ')[1];
process.kill(parseInt(pid), 'SIGINT');
p.kill('SIGINT');
}
}
});
p.on('exit', () => {
assert.notStrictEqual(pid, '');
assert.strictEqual(sub, 'Python');
done();
});
});
it('should close on exec', (done) => {
const data = `
var pty = require('./lib/index');
var ptyProcess = pty.spawn('node', ['-e', 'setTimeout(() => console.log("hello from terminal"), 300);']);
ptyProcess.on('data', function (data) {
console.log(data);
});
setTimeout(() => null, 500);
console.log('ready', ptyProcess.pid);
`;
const buffer: string[] = [];
const readFd = fs.openSync(FIXTURES_PATH, 'r');
const p = cp.spawn('node', ['-e', data], {
stdio: ['ignore', 'pipe', 'pipe', readFd]
});
let sub = '';
p.stdout!.on('data', (data) => {
if (!data.toString().indexOf('ready')) {
sub = data.toString().split(' ')[1].slice(0, -1);
try {
fs.statSync(`/proc/${sub}/fd/${readFd}`);
done('not reachable');
} catch (error) {
assert.notStrictEqual((error as NodeJS.ErrnoException).message.indexOf('ENOENT'), -1);
}
setTimeout(() => {
process.kill(parseInt(sub), 'SIGINT'); // SIGINT to child
p.kill('SIGINT'); // SIGINT to parent
}, 200);
} else {
buffer.push(data.toString().replace(/^\s+|\s+$/g, ''));
}
});
p.on('close', () => {
done();
});
});
it('should not leak /dev/ptmx file descriptors after pty exit', async function(): Promise<void> {
this.timeout(30000);
const getPtmxFDCount = (): number => {
try {
const output = cp.execSync(`lsof -p ${process.pid} 2>/dev/null`, { encoding: 'utf8' });
return output.split('\n').filter(line => line.includes('ptmx')).length;
} catch {
return 0;
}
};
const initialCount = getPtmxFDCount();
for (let i = 0; i < 20; i++) {
const term = new UnixTerminal('/bin/bash', ['-c', 'echo hello']);
await new Promise<void>(resolve => {
term.onExit(() => {
term.destroy();
resolve();
});
});
}
await new Promise(r => setTimeout(r, 500));
const finalCount = getPtmxFDCount();
assert.ok(
finalCount <= initialCount,
`Leaked ${finalCount - initialCount} /dev/ptmx FDs after spawning 20 PTYs (initial: ${initialCount}, final: ${finalCount})`
);
});
}
it('should handle exec() errors', (done) => {
const term = new UnixTerminal('/bin/bogus.exe', []);
term.on('exit', (code, signal) => {
assert.strictEqual(code, 1);
done();
});
});
it('should handle chdir() errors', (done) => {
const term = new UnixTerminal('/bin/echo', [], { cwd: '/nowhere' });
term.on('exit', (code, signal) => {
assert.strictEqual(code, 1);
done();
});
});
it('should not leak child process', (done) => {
const count = cp.execSync('ps -ax | grep node | wc -l');
const term = new UnixTerminal('node', [ '-e', `
console.log('ready');
setTimeout(()=>console.log('timeout'), 200);`
]);
term.on('data', async (data) => {
if (data === 'ready\r\n') {
process.kill(term.pid, 'SIGINT');
await setTimeout(() => null, 1000);
const newCount = cp.execSync('ps -ax | grep node | wc -l');
assert.strictEqual(count.toString(), newCount.toString());
done();
}
});
});
});
});
}
================================================
FILE: src/unixTerminal.ts
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as fs from 'fs';
import * as net from 'net';
import * as path from 'path';
import * as tty from 'tty';
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces';
import { ArgvOrCommandLine, IDisposable } from './types';
import { assign, loadNativeModule } from './utils';
const native = loadNativeModule('pty');
const pty: IUnixNative = native.module;
let helperPath = native.dir + '/spawn-helper';
helperPath = path.resolve(__dirname, helperPath);
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
const DEFAULT_FILE = 'sh';
const DEFAULT_NAME = 'xterm';
const DESTROY_SOCKET_TIMEOUT_MS = 200;
export class UnixTerminal extends Terminal {
protected _fd: number;
protected _pty: string;
protected _file: string;
protected _name: string;
protected _readable: boolean;
protected _writable: boolean;
private _boundClose: boolean = false;
private _emittedClose: boolean = false;
private _writeStream: CustomWriteStream;
private _master: net.Socket | undefined;
private _slave: net.Socket | undefined;
public get master(): net.Socket | undefined { return this._master; }
public get slave(): net.Socket | undefined { return this._slave; }
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
super(opt);
if (typeof args === 'string') {
throw new Error('args as a string is not supported on unix.');
}
// Initialize arguments
args = args || [];
file = file || DEFAULT_FILE;
opt = opt || {};
opt.env = opt.env || process.env;
this._cols = opt.cols || DEFAULT_COLS;
this._rows = opt.rows || DEFAULT_ROWS;
const uid = opt.uid ?? -1;
const gid = opt.gid ?? -1;
const env: IProcessEnv = assign({}, opt.env);
if (opt.env === process.env) {
this._sanitizeEnv(env);
}
const cwd = opt.cwd || process.cwd();
env.PWD = cwd;
const name = opt.name || env.TERM || DEFAULT_NAME;
env.TERM = name;
const parsedEnv = this._parseEnv(env);
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
const onexit = (code: number, signal: number): void => {
// XXX Sometimes a data event is emitted after exit. Wait til socket is
// destroyed.
if (!this._emittedClose) {
if (this._boundClose) {
return;
}
this._boundClose = true;
// From macOS High Sierra 10.13.2 sometimes the socket never gets
// closed. A timeout is applied here to avoid the terminal never being
// destroyed when this occurs.
let timeout: NodeJS.Timeout | null = setTimeout(() => {
timeout = null;
// Destroying the socket now will cause the close event to fire
this._socket.destroy();
}, DESTROY_SOCKET_TIMEOUT_MS);
this.once('close', () => {
if (timeout !== null) {
clearTimeout(timeout);
}
this.emit('exit', code, signal);
});
return;
}
this.emit('exit', code, signal);
};
// fork
const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit);
this._socket = new tty.ReadStream(term.fd);
if (encoding !== null) {
this._socket.setEncoding(encoding);
}
this._writeStream = new CustomWriteStream(term.fd, (encoding || undefined) as BufferEncoding);
// setup
this._socket.on('error', (err: any) => {
// NOTE: fs.ReadStream gets EAGAIN twice at first:
if (err.code) {
if (~err.code.indexOf('EAGAIN')) {
return;
}
}
// close
this._close();
// EIO on exit from fs.ReadStream:
if (!this._emittedClose) {
this._emittedClose = true;
this.emit('close');
}
// EIO, happens when someone closes our child process: the only process in
// the terminal.
// node < 0.6.14: errno 5
// node >= 0.6.14: read EIO
if (err.code) {
if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
return;
}
}
// throw anything else
if (this.listeners('error').length < 2) {
throw err;
}
});
this._pid = term.pid;
this._fd = term.fd;
this._pty = term.pty;
this._file = file;
this._name = name;
this._readable = true;
this._writable = true;
this._socket.on('close', () => {
if (this._emittedClose) {
return;
}
this._emittedClose = true;
this._close();
this.emit('close');
});
this._forwardEvents();
}
protected _write(data: string | Buffer): void {
this._writeStream.write(data);
}
/* Accessors */
get fd(): number { return this._fd; }
get ptsName(): string { return this._pty; }
/**
* openpty
*/
public static open(opt: IPtyOpenOptions): UnixTerminal {
const self: UnixTerminal = Object.create(UnixTerminal.prototype);
opt = opt || {};
if (arguments.length > 1) {
opt = {
cols: arguments[1],
rows: arguments[2]
};
}
const cols = opt.cols || DEFAULT_COLS;
const rows = opt.rows || DEFAULT_ROWS;
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
// open
const term: IUnixOpenProcess = pty.open(cols, rows);
self._master = new tty.ReadStream(term.master);
if (encoding !== null) {
self._master.setEncoding(encoding);
}
self._master.resume();
self._slave = new tty.ReadStream(term.slave);
if (encoding !== null) {
self._slave.setEncoding(encoding);
}
self._slave.resume();
self._socket = self._master;
self._pid = -1;
self._fd = term.master;
self._pty = term.pty;
self._file = process.argv[0] || 'node';
self._name = process.env.TERM || '';
self._readable = true;
self._writable = true;
self._socket.on('error', err => {
self._close();
if (self.listeners('error').length < 2) {
throw err;
}
});
self._socket.on('close', () => {
self._close();
});
return self;
}
public destroy(): void {
this._close();
// Need to close the read stream so node stops reading a dead file
// descriptor. Then we can safely SIGHUP the shell.
this._socket.once('close', () => {
this.kill('SIGHUP');
});
this._socket.destroy();
this._writeStream.dispose();
}
public kill(signal?: string): void {
try {
process.kill(this.pid, signal || 'SIGHUP');
} catch (e) { /* swallow */ }
}
/**
* Gets the name of the process.
*/
public get process(): string {
if (process.platform === 'darwin') {
const title = pty.process(this._fd);
return (title !== 'kernel_task' && title !== 'spawn_helper') ? title : this._file;
}
return pty.process(this._fd, this._pty) || this._file;
}
/**
* TTY
*/
public resize(cols: number, rows: number, pixelSize?: { width: number, height: number }): void {
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
throw new Error('resizing must be done using positive cols and rows');
}
const pixelWidth = pixelSize?.width ?? 0;
const pixelHeight = pixelSize?.height ?? 0;
pty.resize(this._fd, cols, rows, pixelWidth, pixelHeight);
this._cols = cols;
this._rows = rows;
}
public clear(): void {
}
private _sanitizeEnv(env: IProcessEnv): void {
// Make sure we didn't start our server from inside tmux.
delete env['TMUX'];
delete env['TMUX_PANE'];
// Make sure we didn't start our server from inside screen.
// http://web.mit.edu/gnu/doc/html/screen_20.html
delete env['STY'];
delete env['WINDOW'];
// Delete some variables that might confuse our terminal.
delete env['WINDOWID'];
delete env['TERMCAP'];
delete env['COLUMNS'];
delete env['LINES'];
}
}
interface IWriteTask {
/** The buffer being written. */
buffer: Buffer;
/** The current offset of not yet written data. */
offset: number;
}
/**
* A custom write stream that writes directly to a file descriptor with proper
* handling of backpressure and errors. This avoids some event loop exhaustion
* issues that can occur when using the standard APIs in Node.
*/
class CustomWriteStream implements IDisposable {
private readonly _writeQueue: IWriteTask[] = [];
private _writeImmediate: NodeJS.Immediate | undefined;
constructor(
private readonly _fd: number,
private readonly _encoding: BufferEncoding
) {
}
dispose(): void {
clearImmediate(this._writeImmediate);
this._writeImmediate = undefined;
}
write(data: string | Buffer): void {
// Writes are put in a queue and processed asynchronously in order to handle
// backpressure from the kernel buffer.
const buffer = typeof data === 'string'
? Buffer.from(data, this._encoding)
: Buffer.from(data);
if (buffer.byteLength !== 0) {
this._writeQueue.push({ buffer, offset: 0 });
if (this._writeQueue.length === 1) {
this._processWriteQueue();
}
}
}
private _processWriteQueue(): void {
this._writeImmediate = undefined;
if (this._writeQueue.length === 0) {
return;
}
const task = this._writeQueue[0];
// Write to the underlying file descriptor and handle it directly, rather
// than using the `net.Socket`/`tty.WriteStream` wrappers which swallow and
// mask errors like EAGAIN and can cause the thread to block indefinitely.
fs.write(this._fd, task.buffer, task.offset, (err, written) => {
if (err) {
if ('code' in err && err.code === 'EAGAIN') {
// `setImmediate` is used to yield to the event loop and re-attempt
// the write later.
this._writeImmediate = setImmediate(() => this._processWriteQueue());
} else {
// Stop processing immediately on unexpected error and log
this._writeQueue.length = 0;
console.error('Unhandled pty write error', err);
}
return;
}
task.offset += written;
if (task.offset >= task.buffer.byteLength) {
this._writeQueue.shift();
}
// Since there is more room in the kernel buffer, we can continue to write
// until we hit EAGAIN or exhaust the queue.
//
// Note that old versions of bash, like v3.2 which ships in macOS, appears
// to have a bug in its readline implementation that causes data
// corruption when writes to the pty happens too quickly. Instead of
// trying to workaround that we just accept it so that large pastes are as
// fast as possible.
// Context: https://github.com/microsoft/node-pty/issues/833
this._processWriteQueue();
});
}
}
================================================
FILE: src/utils.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
export function assign(target: any, ...sources: any[]): any {
sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key]));
return target;
}
export function loadNativeModule(name: string): {dir: string, module: any} {
// Check build, debug, and then prebuilds.
const dirs = ['build/Release', 'build/Debug', `prebuilds/${process.platform}-${process.arch}`];
// Check relative to the parent dir for unbundled and then the current dir for bundled
const relative = ['..', '.'];
let lastError: unknown;
for (const d of dirs) {
for (const r of relative) {
const dir = `${r}/${d}`;
try {
return { dir, module: require(`${dir}/${name}.node`) };
} catch (e) {
lastError = e;
}
}
}
throw new Error(`Failed to load native module: ${name}.node, checked: ${dirs.join(', ')}: ${lastError}`);
}
================================================
FILE: src/win/conpty.cc
================================================
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*
* pty.cc:
* This file is responsible for starting processes
* with pseudo-terminal file descriptors.
*/
#define _WIN32_WINNT 0x600
#define NODE_ADDON_API_DISABLE_DEPRECATED
#include <node_api.h>
#include <assert.h>
#include <Shlwapi.h> // PathCombine, PathIsRelative
#include <sstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <Windows.h>
#include <strsafe.h>
#include "path_util.h"
#include "conpty.h"
// Taken from the RS5 Windows SDK, but redefined here in case we're targeting <= 17134
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)
typedef VOID* HPCON;
typedef HRESULT (__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn, HANDLE hOut, DWORD dwFlags, HPCON* phpcon);
typedef HRESULT (__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize);
typedef HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc);
typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc);
#endif
struct pty_baton {
int id;
HANDLE hIn;
HANDLE hOut;
HPCON hpc;
HANDLE hShell;
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
};
static std::vector<std::unique_ptr<pty_baton>> ptyHandles;
static volatile LONG ptyCounter;
static pty_baton* get_pty_baton(int id) {
auto it = std::find_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
return ptyHandle->id == id;
});
if (it != ptyHandles.end()) {
return it->get();
}
return nullptr;
}
static bool remove_pty_baton(int id) {
auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
return ptyHandle->id == id;
});
if (it != ptyHandles.end()) {
ptyHandles.erase(it);
return true;
}
return false;
}
struct ExitEvent {
int exit_code = 0;
};
void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
std::thread *th = new std::thread;
// Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE.
auto tsfn = Napi::ThreadSafeFunction::New(
env,
cb, // JavaScript function called asynchronously
"SetupExitCallback_resource", // Name
0, // Unlimited queue
1, // Only one thread will use this initially
[th](Napi::Env) { // Finalizer used to clean threads up
th->join();
delete th;
});
*th = std::thread([tsfn = std::move(tsfn), baton] {
auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
cb.Call({Napi::Number::New(env, exit_event->exit_code)});
delete exit_event;
};
ExitEvent *exit_event = new ExitEvent;
// Wait for process to complete.
WaitForSingleObject(baton->hShell, INFINITE);
// Get process exit code.
GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
// Clean up handles
CloseHandle(baton->hShell);
assert(remove_pty_baton(baton->id));
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
switch (status) {
case napi_closing:
break;
case napi_queue_full:
Napi::Error::Fatal("SetupExitCallback", "Queue was full");
case napi_ok:
if (tsfn.Release() != napi_ok) {
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
}
break;
default:
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
}
});
}
Napi::Error errorWithCode(const Napi::CallbackInfo& info, const char* text) {
std::stringstream errorText;
errorText << text;
errorText << ", error code: " << GetLastError();
return Napi::Error::New(info.Env(), errorText.str());
}
// Returns a new server named pipe. It has not yet been connected.
bool createDataServerPipe(bool write,
std::wstring kind,
HANDLE* hServer,
std::wstring &name,
const std::wstring &pipeName)
{
*hServer = INVALID_HANDLE_VALUE;
name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind;
const DWORD winOpenMode = PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE/* | FILE_FLAG_OVERLAPPED */;
SECURITY_ATTRIBUTES sa = {};
sa.nLength = sizeof(sa);
*hServer = CreateNamedPipeW(
name.c_str(),
/*dwOpenMode=*/winOpenMode,
/*dwPipeMode=*/PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
/*nMaxInstances=*/1,
/*nOutBufferSize=*/128 * 1024,
/*nInBufferSize=*/128 * 1024,
/*nDefaultTimeOut=*/30000,
&sa);
return *hServer != INVALID_HANDLE_VALUE;
}
HANDLE LoadConptyDll(const Napi::CallbackInfo& info,
const bool useConptyDll)
{
if (!useConptyDll) {
return LoadLibraryExW(L"kernel32.dll", 0, 0);
}
wchar_t currentDir[MAX_PATH];
HMODULE hModule = GetModuleHandleA("conpty.node");
if (hModule == NULL) {
throw errorWithCode(info, "Failed to get conpty.node module handle");
}
DWORD result = GetModuleFileNameW(hModule, currentDir, MAX_PATH);
if (result == 0) {
throw errorWithCode(info, "Failed to get conpty.node module file name");
}
PathRemoveFileSpecW(currentDir);
wchar_t conptyDllPath[MAX_PATH];
PathCombineW(conptyDllPath, currentDir, L"conpty\\conpty.dll");
if (!path_util::file_exists(conptyDllPath)) {
std::wstring errorMessage = L"Cannot find conpty.dll at " + std::wstring(conptyDllPath);
std::string errorMessageStr = path_util::wstring_to_string(errorMessage);
throw errorWithCode(info, errorMessageStr.c_str());
}
return LoadLibraryW(conptyDllPath);
}
HRESULT CreateNamedPipesAndPseudoConsole(const Napi::CallbackInfo& info,
COORD size,
DWORD dwFlags,
HANDLE *phInput,
HANDLE *phOutput,
HPCON* phPC,
std::wstring& inName,
std::wstring& outName,
const std::wstring& pipeName,
const bool useConptyDll)
{
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
DWORD error = GetLastError();
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll)
{
PFNCREATEPSEUDOCONSOLE const pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress(
(HMODULE)hLibrary,
useConptyDll ? "ConptyCreatePseudoConsole" : "CreatePseudoConsole");
if (pfnCreate)
{
if (phPC == NULL || phInput == NULL || phOutput == NULL)
{
return E_INVALIDARG;
}
bool success = createDataServerPipe(true, L"in", phInput, inName, pipeName);
if (!success)
{
return HRESULT_FROM_WIN32(GetLastError());
}
success = createDataServerPipe(false, L"out", phOutput, outName, pipeName);
if (!success)
{
return HRESULT_FROM_WIN32(GetLastError());
}
return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC);
}
else
{
// Failed to find CreatePseudoConsole in kernel32. This is likely because
// the user is not running a build of Windows that supports that API.
return HRESULT_FROM_WIN32(GetLastError());
}
} else {
throw errorWithCode(info, "Failed to load conpty.dll");
}
// Failed to find kernel32. This is realy unlikely - honestly no idea how
// this is even possible to hit.
return HRESULT_FROM_WIN32(GetLastError());
}
static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
Napi::Object marshal;
std::wstring inName, outName;
BOOL fSuccess = FALSE;
std::unique_ptr<wchar_t[]> mutableCommandline;
PROCESS_INFORMATION _piClient{};
if (info.Length() != 7 ||
!info[0].IsString() ||
!info[1].IsNumber() ||
!info[2].IsNumber() ||
!info[3].IsBoolean() ||
!info[4].IsString() ||
!info[5].IsBoolean() ||
!info[6].IsBoolean()) {
throw Napi::Error::New(env, "Usage: pty.startProcess(file, cols, rows, debug, pipeName, inheritCursor, useConptyDll)");
}
const std::wstring filename(path_util::to_wstring(info[0].As<Napi::String>()));
const SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value());
const SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value());
const bool debug = info[3].As<Napi::Boolean>().Value();
const std::wstring pipeName(path_util::to_wstring(info[4].As<Napi::String>()));
const bool inheritCursor = info[5].As<Napi::Boolean>().Value();
const bool useConptyDll = info[6].As<Napi::Boolean>().Value();
// use environment 'Path' variable to determine location of
// the relative path that we have recieved (e.g cmd.exe)
std::wstring shellpath;
if (::PathIsRelativeW(filename.c_str())) {
shellpath = path_util::get_shell_path(filename.c_str());
} else {
shellpath = filename;
}
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
std::string why;
why += "File not found: ";
why += path_util::wstring_to_string(shellpath);
throw Napi::Error::New(env, why);
}
HANDLE hIn, hOut;
HPCON hpc;
HRESULT hr = CreateNamedPipesAndPseudoConsole(info, {cols, rows}, inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0, &hIn, &hOut, &hpc, inName, outName, pipeName, useConptyDll);
// Restore default handling of ctrl+c
SetConsoleCtrlHandler(NULL, FALSE);
// Set return values
marshal = Napi::Object::New(env);
if (SUCCEEDED(hr)) {
// We were able to instantiate a conpty
const int ptyId = InterlockedIncrement(&ptyCounter);
marshal.Set("pty", Napi::Number::New(env, ptyId));
ptyHandles.emplace_back(
std::make_unique<pty_baton>(ptyId, hIn, hOut, hpc));
} else {
throw Napi::Error::New(env, "Cannot launch conpty");
}
std::string inNameStr = path_util::wstring_to_string(inName);
if (inNameStr.empty()) {
throw Napi::Error::New(env, "Failed to initialize conpty conin");
}
std::string outNameStr = path_util::wstring_to_string(outName);
if (outNameStr.empty()) {
throw Napi::Error::New(env, "Failed to initialize conpty conout");
}
marshal.Set("fd", Napi::Number::New(env, -1));
marshal.Set("conin", Napi::String::New(env, inNameStr));
marshal.Set("conout", Napi::String::New(env, outNameStr));
return marshal;
}
static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
// If we're working with conpty's we need to call ConnectNamedPipe here AFTER
// the Socket has attempted to connect to the other end, then actually
// spawn the process here.
std::stringstream errorText;
BOOL fSuccess = FALSE;
if (info.Length() != 6 ||
!info[0].IsNumber() ||
!info[1].IsString() ||
!info[2].IsString() ||
!info[3].IsArray() ||
!info[4].IsBoolean() ||
!info[5].IsFunction()) {
throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, useConptyDll, exitCallback)");
}
const int id = info[0].As<Napi::Number>().Int32Value();
const std::wstring cmdline(path_util::to_wstring(info[1].As<Napi::String>()));
const std::wstring cwd(path_util::to_wstring(info[2].As<Napi::String>()));
const Napi::Array envValues = info[3].As<Napi::Array>();
const bool useConptyDll = info[4].As<Napi::Boolean>().Value();
Napi::Function exitCallback = info[5].As<Napi::Function>();
// Fetch pty handle from ID and start process
pty_baton* handle = get_pty_baton(id);
if (!handle) {
throw Napi::Error::New(env, "Invalid pty handle");
}
// Prepare command line
std::unique_ptr<wchar_t[]> mutableCommandline = std::make_unique<wchar_t[]>(cmdline.length() + 1);
HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1, cmdline.c_str());
// Prepare cwd
std::unique_ptr<wchar_t[]> mutableCwd = std::make_unique<wchar_t[]>(cwd.length() + 1);
hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str());
// Prepare environment
std::wstring envStr;
if (!envValues.IsEmpty()) {
std::wstring envBlock;
for(uint32_t i = 0; i < envValues.Length(); i++) {
envBlock += path_util::to_wstring(envValues.Get(i).As<Napi::String>());
envBlock += L'\0';
}
envBlock += L'\0';
envStr = std::move(envBlock);
}
std::vector<wchar_t> envV(envStr.cbegin(), envStr.cend());
LPWSTR envArg = envV.empty() ? nullptr : envV.data();
ConnectNamedPipe(handle->hIn, nullptr);
ConnectNamedPipe(handle->hOut, nullptr);
// Attach the pseudoconsole to the client application we're creating
STARTUPINFOEXW siEx{0};
siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
siEx.StartupInfo.hStdError = nullptr;
siEx.StartupInfo.hStdInput = nullptr;
siEx.StartupInfo.hStdOutput = nullptr;
SIZE_T size = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
BYTE *attrList = new BYTE[size];
siEx.lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
fSuccess = InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
if (!fSuccess) {
throw errorWithCode(info, "InitializeProcThreadAttributeList failed");
}
fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
handle->hpc,
sizeof(HPCON),
NULL,
NULL);
if (!fSuccess) {
throw errorWithCode(info, "UpdateProcThreadAttribute failed");
}
PROCESS_INFORMATION piClient{};
fSuccess = !!CreateProcessW(
nullptr,
mutableCommandline.get(),
nullptr, // lpProcessAttributes
nullptr, // lpThreadAttributes
false, // bInheritHandles VERY IMPORTANT that this is false
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
envArg, // lpEnvironment
mutableCwd.get(), // lpCurrentDirectory
&siEx.StartupInfo, // lpStartupInfo
&piClient // lpProcessInformation
);
if (!fSuccess) {
throw errorWithCode(info, "Cannot create process");
}
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
bool fLoadedDll = hLibrary != nullptr;
if (useConptyDll && fLoadedDll)
{
PFNRELEASEPSEUDOCONSOLE const pfnReleasePseudoConsole = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress(
(HMODULE)hLibrary, "ConptyReleasePseudoConsole");
if (pfnReleasePseudoConsole)
{
pfnReleasePseudoConsole(handle->hpc);
}
}
// Update handle
handle->hShell = piClient.hProcess;
// Close the thread handle to avoid resource leak
CloseHandle(piClient.hThread);
// Close the input read and output write handle of the pseudoconsole
CloseHandle(handle->hIn);
CloseHandle(handle->hOut);
SetupExitCallback(env, exitCallback, handle);
// Return
auto marshal = Napi::Object::New(env);
marshal.Set("pid", Napi::Number::New(env, piClient.dwProcessId));
return marshal;
}
static Napi::Value PtyResize(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
if (info.Length() != 4 ||
!info[0].IsNumber() ||
!info[1].IsNumber() ||
!info[2].IsNumber() ||
!info[3].IsBoolean()) {
throw Napi::Error::New(env, "Usage: pty.resize(id, cols, rows, useConptyDll)");
}
int id = info[0].As<Napi::Number>().Int32Value();
SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value());
SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value());
const bool useConptyDll = info[3].As<Napi::Boolean>().Value();
const pty_baton* handle = get_pty_baton(id);
if (handle != nullptr) {
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll)
{
PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress(
(HMODULE)hLibrary,
useConptyDll ? "ConptyResizePseudoConsole" : "ResizePseudoConsole");
if (pfnResizePseudoConsole)
{
COORD size = {cols, rows};
pfnResizePseudoConsole(handle->hpc, size);
}
}
}
return env.Undefined();
}
static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
if (info.Length() != 2 ||
!info[0].IsNumber() ||
!info[1].IsBoolean()) {
throw Napi::Error::New(env, "Usage: pty.clear(id, useConptyDll)");
}
int id = info[0].As<Napi::Number>().Int32Value();
const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
// This API is only supported for conpty.dll as it was introduced in a later version of Windows.
// We could hook it up to point at >= a version of Windows only, but the future is conpty.dll
// anyway.
if (!useConptyDll) {
return env.Undefined();
}
const pty_baton* handle = get_pty_baton(id);
if (handle != nullptr) {
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll)
{
PFNCLEARPSEUDOCONSOLE const pfnClearPseudoConsole = (PFNCLEARPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ConptyClearPseudoConsole");
if (pfnClearPseudoConsole)
{
pfnClearPseudoConsole(handle->hpc);
}
}
}
return env.Undefined();
}
static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
Napi::HandleScope scope(env);
if (info.Length() != 2 ||
!info[0].IsNumber() ||
!info[1].IsBoolean()) {
throw Napi::Error::New(env, "Usage: pty.kill(id, useConptyDll)");
}
int id = info[0].As<Napi::Number>().Int32Value();
const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
const pty_baton* handle = get_pty_baton(id);
if (handle != nullptr) {
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
bool fLoadedDll = hLibrary != nullptr;
if (fLoadedDll)
{
PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress(
(HMODULE)hLibrary,
useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole");
if (pfnClosePseudoConsole)
{
pfnClosePseudoConsole(handle->hpc);
}
}
if (useConptyDll) {
TerminateProcess(handle->hShell, 1);
}
}
return env.Undefined();
}
/**
* Init
*/
Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess));
exports.Set("connect", Napi::Function::New(env, PtyConnect));
exports.Set("resize", Napi::Function::New(env, PtyResize));
exports.Set("clear", Napi::Function::New(env, PtyClear));
exports.Set("kill", Napi::Function::New(env, PtyKill));
return exports;
};
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init);
================================================
FILE: src/win/conpty.h
================================================
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// This header prototypes the Pseudoconsole symbols from conpty.lib with their original names.
// This is required because we cannot import __imp_CreatePseudoConsole from a static library
// as it doesn't produce an import lib.
// We can't use an /ALTERNATENAME trick because it seems that that name is only resolved when the
// linker cannot otherwise find the symbol.
#pragma once
#include <consoleapi.h>
#ifndef CONPTY_IMPEXP
#define CONPTY_IMPEXP __declspec(dllimport)
#endif
#ifndef CONPTY_EXPORT
#ifdef __cplusplus
#define CONPTY_EXPORT extern "C" CONPTY_IMPEXP
#else
#define CONPTY_EXPORT extern CONPTY_IMPEXP
#endif
#endif
#define PSEUDOCONSOLE_RESIZE_QUIRK (2u)
#define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u)
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC);
CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show);
CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC);
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC);
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleTimeout(HPCON hPC, DWORD dwMilliseconds);
CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC);
================================================
FILE: src/win/conpty_console_list.cc
================================================
/**
* Copyright (c) 2019, Microsoft Corporation (MIT License).
*/
#define NODE_ADDON_API_DISABLE_DEPRECATED
#include <napi.h>
#include <windows.h>
static Napi::Value ApiConsoleProcessList(const Napi::CallbackInfo& info) {
Napi::Env env(info.Env());
if (info.Length() != 1 ||
!info[0].IsNumber()) {
throw Napi::Error::New(env, "Usage: getConsoleProcessList(shellPid)");
}
const DWORD pid = info[0].As<Napi::Number>().Uint32Value();
if (!FreeConsole()) {
throw Napi::Error::New(env, "FreeConsole failed");
}
if (!AttachConsole(pid)) {
throw Napi::Error::New(env, "AttachConsole failed");
}
auto processList = std::vector<DWORD>(64);
auto processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size()));
if (processList.size() < processCount) {
processList.resize(processCount);
processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size()));
}
FreeConsole();
Napi::Array result = Napi::Array::New(env);
for (DWORD i = 0; i < processCount; i++) {
result.Set(i, Napi::Number::New(env, processList[i]));
}
return result;
}
Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set("getConsoleProcessList", Napi::Function::New(env, ApiConsoleProcessList));
return exports;
};
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init);
================================================
FILE: src/win/path_util.cc
================================================
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
#include <stdexcept>
#include <Shlwapi.h> // PathCombine
#include <Windows.h>
#include "path_util.h"
namespace path_util {
std::wstring to_wstring(const Napi::String& str) {
const std::u16string & u16 = str.Utf16Value();
return std::wstring(u16.begin(), u16.end());
}
std::string wstring_to_string(const std::wstring &wide_string) {
if (wide_string.empty()) {
return "";
}
const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), nullptr, 0, nullptr, nullptr);
if (size_needed <= 0) {
return "";
}
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), &result.at(0), size_needed, nullptr, nullptr);
return result;
}
const char* from_wstring(const wchar_t* wstr) {
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL);
if (bufferSize <= 0) {
return "";
}
char *output = new char[bufferSize];
int status = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, output, bufferSize, NULL, NULL);
if (status == 0) {
return "";
}
return output;
}
bool file_exists(std::wstring filename) {
DWORD attr = ::GetFileAttributesW(filename.c_str());
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
return false;
}
return true;
}
// cmd.exe -> C:\Windows\system32\cmd.exe
std::wstring get_shell_path(std::wstring filename) {
std::wstring shellpath;
if (file_exists(filename)) {
return shellpath;
}
wchar_t* buffer_ = new wchar_t[MAX_ENV];
int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV);
if (read) {
std::wstring delimiter = L";";
size_t pos = 0;
std::vector<std::wstring> paths;
std::wstring buffer(buffer_);
while ((pos = buffer.find(delimiter)) != std::wstring::npos) {
paths.push_back(buffer.substr(0, pos));
buffer.erase(0, pos + delimiter.length());
}
const wchar_t *filename_ = filename.c_str();
for (size_t i = 0; i < paths.size(); ++i) {
std::wstring path = paths[i];
wchar_t searchPath[MAX_PATH];
::PathCombineW(searchPath, const_cast<wchar_t*>(path.c_str()), filename_);
if (searchPath == NULL) {
continue;
}
if (file_exists(searchPath)) {
shellpath = searchPath;
break;
}
}
}
delete[] buffer_;
return shellpath;
}
} // namespace path_util
================================================
FILE: src/win/path_util.h
================================================
/**
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
#ifndef NODE_PTY_PATH_UTIL_H_
#define NODE_PTY_PATH_UTIL_H_
#define NODE_ADDON_API_DISABLE_DEPRECATED
#include <napi.h>
#include <string>
#define MAX_ENV 65536
namespace path_util {
std::wstring to_wstring(const Napi::String& str);
std::string wstring_to_string(const std::wstring &wide_string);
const char* from_wstring(const wchar_t* wstr);
bool file_exists(std::wstring filename);
std::wstring get_shell_path(std::wstring filename);
} // namespace path_util
#endif // NODE_PTY_PATH_UTIL_H_
================================================
FILE: src/windowsConoutConnection.ts
================================================
/**
* Copyright (c) 2020, Microsoft Corporation (MIT License).
*/
import { Worker } from 'worker_threads';
import { Socket } from 'net';
import { IDisposable } from './types';
import { IWorkerData, ConoutWorkerMessage, getWorkerPipeName } from './shared/conout';
import { join } from 'path';
import { IEvent, EventEmitter2 } from './eventEmitter2';
/**
* The amount of time to wait for additional data after the conpty shell process has exited before
* shutting down the worker and sockets. The timer will be reset if a new data event comes in after
* the timer has started.
*/
const FLUSH_DATA_INTERVAL = 1000;
/**
* Connects to and manages the lifecycle of the conout socket. This socket must be drained on
* another thread in order to avoid deadlocks where Conpty waits for the out socket to drain
* when `ClosePseudoConsole` is called. This happens when data is being written to the terminal when
* the pty is closed.
*
* See also:
* - https://github.com/microsoft/node-pty/issues/375
* - https://github.com/microsoft/vscode/issues/76548
* - https://github.com/microsoft/terminal/issues/1810
* - https://docs.microsoft.com/en-us/windows/console/closepseudoconsole
*/
export class ConoutConnection implements IDisposable {
private _worker: Worker;
private _drainTimeout: NodeJS.Timeout | undefined;
private _isDisposed: boolean = false;
private _onReady = new EventEmitter2<void>();
public get onReady(): IEvent<void> { return this._onReady.event; }
constructor(
private _conoutPipeName: string,
private _useConptyDll: boolean
) {
const workerData: IWorkerData = {
conoutPipeName: _conoutPipeName
};
const scriptPath = __dirname.replace('node_modules.asar', 'node_modules.asar.unpacked');
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });
this._worker.on('message', (message: ConoutWorkerMessage) => {
switch (message) {
case ConoutWorkerMessage.READY:
this._onReady.fire();
return;
default:
console.warn('Unexpected ConoutWorkerMessage', message);
}
});
}
dispose(): void {
if (!this._useConptyDll && this._isDisposed) {
return;
}
this._isDisposed = true;
// Drain all data from the socket before closing
this._drainDataAndClose();
}
connectSocket(socket: Socket): void {
socket.connect(getWorkerPipeName(this._conoutPipeName));
}
private _drainDataAndClose(): void {
if (this._drainTimeout) {
clearTimeout(this._drainTimeout);
}
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
}
private async _destroySocket(): Promise<void> {
await this._worker.terminate();
}
}
================================================
FILE: src/windowsPtyAgent.test.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as assert from 'assert';
import { argsToCommandLine, WindowsPtyAgent } from './windowsPtyAgent';
function check(file: string, args: string | string[], expected: string): void {
assert.equal(argsToCommandLine(file, args), expected);
}
if (process.platform === 'win32') {
describe('argsToCommandLine', () => {
describe('Plain strings', () => {
it('doesn\'t quote plain string', () => {
check('asdf', [], 'asdf');
});
it('doesn\'t escape backslashes', () => {
check('\\asdf\\qwer\\', [], '\\asdf\\qwer\\');
});
it('doesn\'t escape multiple backslashes', () => {
check('asdf\\\\qwer', [], 'asdf\\\\qwer');
});
it('adds backslashes before quotes', () => {
check('"asdf"qwer"', [], '\\"asdf\\"qwer\\"');
});
it('escapes backslashes before quotes', () => {
check('asdf\\"qwer', [], 'asdf\\\\\\"qwer');
});
});
describe('Quoted strings', () => {
it('quotes string with spaces', () => {
check('asdf qwer', [], '"asdf qwer"');
});
it('quotes empty string', () => {
check('', [], '""');
});
it('quotes string with tabs', () => {
check('asdf\tqwer', [], '"asdf\tqwer"');
});
it('escapes only the last backslash', () => {
check('\\asdf \\qwer\\', [], '"\\asdf \\qwer\\\\"');
});
it('doesn\'t escape multiple backslashes', () => {
check('asdf \\\\qwer', [], '"asdf \\\\qwer"');
});
it('escapes backslashes before quotes', () => {
check('asdf \\"qwer', [], '"asdf \\\\\\"qwer"');
});
it('escapes multiple backslashes at the end', () => {
check('asdf qwer\\\\', [], '"asdf qwer\\\\\\\\"');
});
});
describe('Multiple arguments', () => {
it('joins arguments with spaces', () => {
check('asdf', ['qwer zxcv', '', '"'], 'asdf "qwer zxcv" "" \\"');
});
it('array argument all in quotes', () => {
check('asdf', ['"surounded by quotes"'], 'asdf \\"surounded by quotes\\"');
});
it('array argument quotes in the middle', () => {
check('asdf', ['quotes "in the" middle'], 'asdf "quotes \\"in the\\" middle"');
});
it('array argument quotes near start', () => {
check('asdf', ['"quotes" near start'], 'asdf "\\"quotes\\" near start"');
});
it('array argument quotes near end', () => {
check('asdf', ['quotes "near end"'], 'asdf "quotes \\"near end\\""');
});
});
describe('Args as CommandLine', () => {
it('should handle empty string', () => {
check('file', '', 'file');
});
it('should not change args', () => {
check('file', 'foo bar baz', 'file foo bar baz');
check('file', 'foo \\ba"r \baz', 'file foo \\ba"r \baz');
});
});
describe('Real-world cases', () => {
it('quotes within quotes', () => {
check('cmd.exe', ['/c', 'powershell -noexit -command \'Set-location \"C:\\user\"\''], 'cmd.exe /c "powershell -noexit -command \'Set-location \\\"C:\\user\\"\'"');
});
it('space within quotes', () => {
check('cmd.exe', ['/k', '"C:\\Users\\alros\\Desktop\\test script.bat"'], 'cmd.exe /k \\"C:\\Users\\alros\\Desktop\\test script.bat\\"');
});
});
});
describe('WindowsPtyAgent', () => {
describe('connection timing (issue #763)', () => {
it('should defer conptyNative.connect() until worker is ready', function (done) {
this.timeout(10000);
const term = new WindowsPtyAgent(
'cmd.exe',
'/c echo test',
Object.keys(process.env).map(k => `${k}=${process.env[k]}`),
process.cwd(),
80,
30,
false,
false,
false
);
// The innerPid should be 0 initially since connect() is deferred
// until the worker signals ready. This verifies the fix for #763.
const initialPid = term.innerPid;
// Wait for the connection to complete via ready_datapipe event
term.outSocket.on('ready_datapipe', () => {
// After worker is ready and connect() is called, innerPid should be set
// Use a small delay to ensure _completePtyConnection has run
setTimeout(() => {
assert.notStrictEqual(term.innerPid, 0, 'innerPid should be set after worker is ready');
assert.strictEqual(initialPid, 0, 'innerPid should have been 0 before worker was ready');
term.kill();
done();
}, 100);
});
});
it('should successfully spawn a process after deferred connection', function (done) {
this.timeout(10000);
const term = new WindowsPtyAgent(
'cmd.exe',
'/c echo hello',
Object.keys(process.env).map(k => `${k}=${process.env[k]}`),
process.cwd(),
80,
30,
false,
false,
false
);
let output = '';
term.outSocket.on('data', (data: string) => {
output += data;
});
// Wait for process to complete and verify output
setTimeout(() => {
assert.ok(output.includes('hello'), `Expected output to contain "hello", got: ${output}`);
term.kill();
done();
}, 2000);
});
it('should allow async work between construction and connection (non-blocking)', function (done) {
this.timeout(10000);
// Track the sequence of events to verify non-blocking behavior
const events: string[] = [];
const term = new WindowsPtyAgent(
'cmd.exe',
'/c echo test',
Object.keys(process.env).map(k => `${k}=${process.env[k]}`),
process.cwd(),
80,
30,
false,
false,
false
);
events.push('constructor_returned');
assert.strictEqual(term.innerPid, 0, 'innerPid should be 0 immediately after construction');
// Schedule async work - this MUST run before ready_datapipe if constructor is non-blocking
setImmediate(() => {
events.push('setImmediate_ran');
// innerPid might still be 0 or might be set by now, depending on timing
// The key is that setImmediate ran, proving the event loop wasn't blocked
});
term.outSocket.on('ready_datapipe', () => {
events.push('ready_datapipe');
setTimeout(() => {
events.push('final_check');
// Verify the sequence: constructor returned, then async work could run
assert.ok(events.includes('constructor_returned'), 'constructor should have returned');
assert.ok(events.includes('setImmediate_ran'), 'setImmediate should have run (event loop not blocked)');
assert.ok(events.indexOf('constructor_returned') < events.indexOf('setImmediate_ran'),
'constructor should return before setImmediate runs');
// Most importantly: innerPid should now be set
assert.notStrictEqual(term.innerPid, 0, 'innerPid should be set after connection');
term.kill();
done();
}, 100);
});
});
});
});
}
================================================
FILE: src/windowsPtyAgent.ts
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as fs from 'fs';
import * as path from 'path';
import { fork } from 'child_process';
import { Socket } from 'net';
import { ArgvOrCommandLine } from './types';
import { ConoutConnection } from './windowsConoutConnection';
import { loadNativeModule } from './utils';
let conptyNative: IConptyNative;
/**
* The amount of time to wait for additional data after the conpty shell process has exited before
* shutting down the socket. The timer will be reset if a new data event comes in after the timer
* has started.
*/
const FLUSH_DATA_INTERVAL = 1000;
/**
* This agent sits between the WindowsTerminal class and provides an interface for conpty.
*/
export class WindowsPtyAgent {
private _inSocket: Socket;
private _outSocket: Socket;
private _innerPid: number = 0;
private _closeTimeout: NodeJS.Timer | undefined;
private _exitCode: number | undefined;
private _conoutSocketWorker: ConoutConnection;
private _fd: any;
private _pty: number;
private _ptyNative: IConptyNative;
public get inSocket(): Socket { return this._inSocket; }
public get outSocket(): Socket { return this._outSocket; }
public get fd(): any { return this._fd; }
public get innerPid(): number { return this._innerPid; }
public get pty(): number { return this._pty; }
private _pendingPtyInfo: { pty: number, commandLine: string, cwd: string, env: string[] } | undefined;
constructor(
file: string,
args: ArgvOrCommandLine,
env: string[],
cwd: string,
cols: number,
rows: number,
debug: boolean,
private _useConptyDll: boolean = false,
conptyInheritCursor: boolean = false
) {
if (!conptyNative) {
conptyNative = loadNativeModule('conpty').module;
}
this._ptyNative = conptyNative;
// Sanitize input variable.
cwd = path.resolve(cwd);
// Compose command line
const commandLine = argsToCommandLine(file, args);
// Open pty session.
const term: IConptyProcess = conptyNative.startProcess(file, cols, rows, debug, this._generatePipeName(), conptyInheritCursor, this._useConptyDll);
// Not available on windows.
this._fd = term.fd;
// Generated incremental number that has no real purpose besides using it
// as a terminal id.
this._pty = term.pty;
// Create terminal pipe IPC channel and forward to a local unix socket.
this._outSocket = new Socket();
this._outSocket.setEncoding('utf8');
// The conout socket must be ready out on another thread to avoid deadlocks
// We must wait for the worker to connect before calling conptyNative.connect()
// to avoid blocking the Node.js event loop in ConnectNamedPipe.
// See https://github.com/microsoft/node-pty/issues/763
this._conoutSocketWorker = new ConoutConnection(term.conout, this._useConptyDll);
// Store pending connection info - we'll complete the connection when worker is ready
this._pendingPtyInfo = { pty: this._pty, commandLine, cwd, env };
// Timeout to ensure connection completes even if worker fails to signal ready
const connectionTimeout = setTimeout(() => {
if (this._pendingPtyInfo) {
// Worker never signaled ready - complete connection anyway to avoid zombie state
this._completePtyConnection();
}
}, 5000);
this._conoutSocketWorker.onReady(() => {
clearTimeout(connectionTimeout);
this._conoutSocketWorker.connectSocket(this._outSocket);
// Now that the worker has connected to the output pipe, we can safely call
// conptyNative.connect() which calls ConnectNamedPipe - it won't block because
// the client (worker) is already connected
this._completePtyConnection();
});
this._outSocket.on('connect', () => {
this._outSocket.emit('ready_datapipe');
});
const inSocketFD = fs.openSync(term.conin, 'w');
this._inSocket = new Socket({
fd: inSocketFD,
readable: false,
writable: true
});
this._inSocket.setEncoding('utf8');
}
private _completePtyConnection(): void {
if (!this._pendingPtyInfo) {
return;
}
const { pty, commandLine, cwd, env } = this._pendingPtyInfo;
this._pendingPtyInfo = undefined;
const connect = conptyNative.connect(pty, commandLine, cwd, env, this._useConptyDll, c => this._$onProcessExit(c));
this._innerPid = connect.pid;
}
public resize(cols: number, rows: number): void {
if (this._exitCode !== undefined) {
throw new Error('Cannot resize a pty that has already exited');
}
this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll);
}
public clear(): void {
this._ptyNative.clear(this._pty, this._useConptyDll);
}
public kill(): void {
// Prevent deferred connection from completing after kill
this._pendingPtyInfo = undefined;
// Tell the agent to kill the pty, this releases handles to the process
if (!this._useConptyDll) {
this._inSocket.readable = false;
this._outSocket.readable = false;
this._getConsoleProcessList().then(consoleProcessList => {
consoleProcessList.forEach((pid: number) => {
try {
process.kill(pid);
} catch (e) {
// Ignore if process cannot be found (kill ESRCH error)
}
});
});
this._ptyNative.kill(this._pty, this._useConptyDll);
this._conoutSocketWorker.dispose();
} else {
// Close the input write handle to signal the end of session.
this._inSocket.destroy();
this._ptyNative.kill(this._pty, this._useConptyDll);
this._outSocket.on('data', () => {
this._conoutSocketWorker.dispose();
});
}
}
private _getConsoleProcessList(): Promise<number[]> {
if (this._innerPid <= 0) {
return Promise.resolve([]);
}
return new Promise<number[]>(resolve => {
const agent = fork(path.join(__dirname, 'conpty_console_list_agent'), [ this._innerPid.toString() ]);
agent.on('message', message => {
clearTimeout(timeout);
resolve(message.consoleProcessList);
});
const timeout = setTimeout(() => {
// Something went wrong, just send back the shell PID
agent.kill();
resolve([ this._innerPid ]);
}, 5000);
});
}
public get exitCode(): number | undefined {
return this._exitCode;
}
private _generatePipeName(): string {
return `conpty-${Math.random() * 10000000}`;
}
/**
* Triggered from the native side when a contpy process exits.
*/
private _$onProcessExit(exitCode: number): void {
this._exitCode = exitCode;
if (!this._useConptyDll) {
this._flushDataAndCleanUp();
this._outSocket.on('data', () => this._flushDataAndCleanUp());
}
}
private _flushDataAndCleanUp(): void {
if (this._useConptyDll) {
return;
}
if (this._closeTimeout) {
clearTimeout(this._closeTimeout);
}
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
}
private _cleanUpProcess(): void {
if (this._useConptyDll) {
return;
}
this._inSocket.readable = false;
this._outSocket.readable = false;
this._outSocket.destroy();
}
}
// Convert argc/argv into a Win32 command-line following the escaping convention
// documented on MSDN (e.g. see CommandLineToArgvW documentation). Copied from
// winpty project.
export function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
if (isCommandLine(args)) {
if (args.length === 0) {
return file;
}
return `${argsToCommandLine(file, [])} ${args}`;
}
const argv = [file];
Array.prototype.push.apply(argv, args);
let result = '';
for (let argIndex = 0; argIndex < argv.length; argIndex++) {
if (argIndex > 0) {
result += ' ';
}
const arg = argv[argIndex];
// if it is empty or it contains whitespace and is not already quoted
const hasLopsidedEnclosingQuote = xOr((arg[0] !== '"'), (arg[arg.length - 1] !== '"'));
const hasNoEnclosingQuotes = ((arg[0] !== '"') && (arg[arg.length - 1] !== '"'));
const quote =
arg === '' ||
(arg.indexOf(' ') !== -1 ||
arg.indexOf('\t') !== -1) &&
((arg.length > 1) &&
(hasLopsidedEnclosingQuote || hasNoEnclosingQuotes));
if (quote) {
result += '\"';
}
let bsCount = 0;
for (let i = 0; i < arg.length; i++) {
const p = arg[i];
if (p === '\\') {
bsCount++;
} else if (p === '"') {
result += repeatText('\\', bsCount * 2 + 1);
result += '"';
bsCount = 0;
} else {
result += repeatText('\\', bsCount);
bsCount = 0;
result += p;
}
}
if (quote) {
result += repeatText('\\', bsCount * 2);
result += '\"';
} else {
result += repeatText('\\', bsCount);
}
}
return result;
}
function isCommandLine(args: ArgvOrCommandLine): args is string {
return typeof args === 'string';
}
function repeatText(text: string, count: number): string {
let result = '';
for (let i = 0; i < count; i++) {
result += text;
}
return result;
}
function xOr(arg1: boolean, arg2: boolean): boolean {
return ((arg1 && !arg2) || (!arg1 && arg2));
}
================================================
FILE: src/windowsTerminal.test.ts
================================================
/**
* Copyright (c) 2017, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import * as fs from 'fs';
import * as assert from 'assert';
import { WindowsTerminal } from './windowsTerminal';
import * as path from 'path';
import * as psList from 'ps-list';
interface IProcessState {
// Whether the PID must exist or must not exist
[pid: number]: boolean;
}
interface IWindowsProcessTreeResult {
name: string;
pid: number;
}
function pollForProcessState(desiredState: IProcessState, intervalMs: number = 100, timeoutMs: number = 2000): Promise<void> {
return new Promise<void>(resolve => {
let tries = 0;
const interval = setInterval(() => {
psList({ all: true }).then(ps => {
let success = true;
const pids = Object.keys(desiredState).map(k => parseInt(k, 10));
console.log('expected pids', JSON.stringify(pids));
pids.forEach(pid => {
if (desiredState[pid]) {
if (!ps.some(p => p.pid === pid)) {
console.log(`pid ${pid} does not exist`);
success = false;
}
} else {
if (ps.some(p => p.pid === pid)) {
console.log(`pid ${pid} still exists`);
success = false;
}
}
});
if (success) {
clearInterval(interval);
resolve();
return;
}
tries++;
if (tries * intervalMs >= timeoutMs) {
clearInterval(interval);
const processListing = pids.map(k => `${k}: ${desiredState[k]}`).join('\n');
assert.fail(`Bad process state, expected:\n${processListing}`);
resolve();
}
});
}, intervalMs);
});
}
function pollForProcessTreeSize(pid: number, size: number, intervalMs: number = 100, timeoutMs: number = 2000): Promise<IWindowsProcessTreeResult[]> {
return new Promise<IWindowsProcessTreeResult[]>(resolve => {
let tries = 0;
const interval = setInterval(() => {
psList({ all: true }).then(ps => {
const openList: IWindowsProcessTreeResult[] = [];
openList.push(ps.filter(p => p.pid === pid).map(p => {
return { name: p.name, pid: p.pid };
})[0]);
const list: IWindowsProcessTreeResult[] = [];
while (openList.length) {
const current = openList.shift()!;
ps.filter(p => p.ppid === current.pid).map(p => {
return { name: p.name, pid: p.pid };
}).forEach(p => openList.push(p));
list.push(current);
}
console.log('list', JSON.stringify(list));
const success = list.length === size;
if (success) {
clearInterval(interval);
resolve(list);
return;
}
tries++;
if (tries * intervalMs >= timeoutMs) {
clearInterval(interval);
assert.fail(`Bad process state, expected: ${size}, actual: ${list.length}`);
}
});
}, intervalMs);
});
}
if (process.platform === 'win32') {
[false, true].forEach((useConptyDll) => {
describe(`WindowsTerminal (useConptyDll = ${useConptyDll})`, () => {
describe('kill', () => {
it('should not crash parent process', function (done) {
this.timeout(20000);
const term = new WindowsTerminal('cmd.exe', [], { useConptyDll });
term.on('exit', () => done());
term.kill();
});
it('should kill the process tree', function (done: Mocha.Done): void {
this.timeout(20000);
const term = new WindowsTerminal('cmd.exe', [], { useConptyDll });
const socket = (term as any)._socket;
let started = false;
const startPolling = (): void => {
if (started) {
return;
}
if (term.pid === 0) {
setTimeout(startPolling, 50);
return;
}
started = true;
// Start sub-processes
term.write('powershell.exe\r');
term.write('node.exe\r');
console.log('start poll for tree size');
pollForProcessTreeSize(term.pid, 3, 500, 5000).then(list => {
assert.strictEqual(list[0].name.toLowerCase(), 'cmd.exe');
assert.strictEqual(list[1].name.toLowerCase(), 'powershell.exe');
assert.strictEqual(list[2].name.toLowerCase(), 'node.exe');
term.kill();
const desiredState: IProcessState = {};
desiredState[list[0].pid] = false;
desiredState[list[1].pid] = false;
desiredState[list[2].pid] = false;
term.on('exit', () => {
pollForProcessState(desiredState, 1000, 5000).then(() => {
done();
}).catch(done);
});
}).catch(done);
};
if (term.pid > 0) {
startPolling();
} else {
socket.once('ready_datapipe', () => setTimeout(startPolling, 50));
}
});
});
describe('pid', () => {
it('should be 0 before ready and set after ready_datapipe (issue #763)', function (done) {
this.timeout(10000);
const term = new WindowsTerminal('cmd.exe', '/c echo test', { useConptyDll });
// pid may be 0 immediately after construction due to deferred connection
const initialPid = term.pid;
// Access internal socket to listen for ready_datapipe
const socket = (term as any)._socket;
socket.on('ready_datapipe', () => {
// After ready_datapipe, pid should be set to a valid non-zero value
setTimeout(() => {
assert.notStrictEqual(term.pid, 0, 'pid should be set after ready_datapipe');
assert.strictEqual(typeof term.pid, 'number', 'pid should be a number');
// If initial was 0, it should now be different (proves the fix works)
if (initialPid === 0) {
assert.notStrictEqual(term.pid, initialPid, 'pid should be updated from initial value');
}
term.on('exit', () => done());
term.kill();
}, 100);
});
});
});
describe('resize', () => {
it('should throw a non-native exception when resizing an invalid value', function(done) {
this.timeout(20000);
const term = new WindowsTerminal('cmd.exe', [], { useConptyDll });
assert.throws(() => term.resize(-1, -1));
assert.throws(() => term.resize(0, 0));
assert.doesNotThrow(() => term.resize(1, 1));
term.on('exit', () => {
done();
});
term.kill();
});
it('should throw a non-native exception when resizing a killed terminal', function(done) {
this.timeout(20000);
const term = new WindowsTerminal('cmd.exe', [], { useConptyDll });
(<any>term)._defer(() => {
term.once('exit', () => {
assert.throws(() => term.resize(1, 1));
done();
});
term.destroy();
});
});
});
describe('Args as CommandLine', () => {
it('should not fail running a file containing a space in the path', function (done) {
this.timeout(10000);
const spaceFolder = path.resolve(__dirname, '..', 'fixtures', 'space folder');
if (!fs.existsSync(spaceFolder)) {
fs.mkdirSync(spaceFolder);
}
const cmdCopiedPath = path.resolve(spaceFolder, 'cmd.exe');
const data = fs.readFileSync(`${process.env.windir}\\System32\\cmd.exe`);
fs.writeFileSync(cmdCopiedPath, data);
if (!fs.existsSync(cmdCopiedPath)) {
// Skip test if git bash isn't installed
return;
}
const term = new WindowsTerminal(cmdCopiedPath, '/c echo "hello world"', { useConptyDll });
let result = '';
term.on('data', (data) => {
result += data;
});
term.on('exit', () => {
assert.ok(result.indexOf('hello world') >= 1);
done();
});
});
});
describe('env', () => {
it('should set environment variables of the shell', function (done) {
this.timeout(10000);
const term = new WindowsTerminal('cmd.exe', '/C echo %FOO%', { useConptyDll, env: { FOO: 'BAR' }});
let result = '';
term.on('data', (data) => {
result += data;
});
term.on('exit', () => {
assert.ok(result.indexOf('BAR') >= 0);
done();
});
});
});
describe('On close', () => {
it('should return process zero exit codes', function (done) {
this.timeout(10000);
const term = new WindowsTerminal('cmd.exe', '/C exit', { useConptyDll });
term.on('exit', (code) => {
assert.strictEqual(code, 0);
done();
});
});
it('should return process non-zero exit codes', function (done) {
this.timeout(10000);
const term = new WindowsTerminal('cmd.exe', '/C exit 2', { useConptyDll });
term.on('exit', (code) => {
assert.strictEqual(code, 2);
done();
});
});
});
describe('Write', () => {
it('should accept input', function (done) {
this.timeout(10000);
const term = new WindowsTerminal('cmd.exe', '', { useConptyDll });
term.write('exit\r');
term.on('exit', () => {
done();
});
});
});
});
});
}
================================================
FILE: src/windowsTerminal.ts
================================================
/**
* Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)
* Copyright (c) 2016, Daniel Imms (MIT License).
* Copyright (c) 2018, Microsoft Corporation (MIT License).
*/
import { Socket } from 'net';
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
import { WindowsPtyAgent } from './windowsPtyAgent';
import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces';
import { ArgvOrCommandLine } from './types';
import { assign } from './utils';
const DEFAULT_FILE = 'cmd.exe';
const DEFAULT_NAME = 'Windows Shell';
export class WindowsTerminal extends Terminal {
private _isReady: boolean;
private _deferreds: Array<{ run: () => void }>;
private _agent: WindowsPtyAgent;
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions) {
super(opt);
this._checkType('args', args, 'string', true);
// Initialize arguments
args = args || [];
file = file || DEFAULT_FILE;
opt = opt || {};
opt.env = opt.env || process.env;
if (opt.encoding) {
console.warn('Setting encoding on Windows is not supported');
}
const env = assign({}, opt.env);
this._cols = opt.cols || DEFAULT_COLS;
this._rows = opt.rows || DEFAULT_ROWS;
const cwd = opt.cwd || process.cwd();
const name = opt.name || env.TERM || DEFAULT_NAME;
const parsedEnv = this._parseEnv(env);
// If the terminal is ready
this._isReady = false;
// Functions that need to run after `ready` event is emitted.
this._deferreds = [];
// Create new termal.
this._agent = new WindowsPtyAgent(file, args, parsedEnv, cwd, this._cols, this._rows, false, opt.useConptyDll, opt.conptyInheritCursor);
this._socket = this._agent.outSocket;
// Not available until `ready` event emitted.
this._pid = this._agent.innerPid;
this._fd = this._agent.fd;
this._pty = this._agent.pty;
// The forked windows terminal is not available until `ready` event is
// emitted.
this._socket.on('ready_datapipe', () => {
// Update pid now that the agent has connected
this._pid = this._agent.innerPid;
// Run deferreds and set ready state once the first data event is received.
this._socket.once('data', () => {
// Wait until the first data event is fired then we can run deferreds.
if (!this._isReady) {
// Terminal is now ready and we can avoid having to defer method
// calls.
this._isReady = true;
// Execute all deferred methods
this._deferreds.forEach(fn => {
// NB! In order to ensure that `this` has all its references
// updated any variable that need to be available in `this` before
// the deferred is run has to be declared above this forEach
// statement.
fn.run();
});
// Reset
this._deferreds = [];
gitextract_g61cbamu/
├── .config/
│ └── tsaoptions.json
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── binding.gyp
├── eslint.config.js
├── examples/
│ ├── electron/
│ │ ├── README.md
│ │ ├── index.html
│ │ ├── main.js
│ │ ├── npm_install.bat
│ │ ├── npm_install.sh
│ │ ├── package.json
│ │ ├── preload.js
│ │ └── renderer.js
│ ├── fork/
│ │ ├── index.js
│ │ └── package.json
│ └── killDeepTree/
│ ├── README.md
│ ├── entry.js
│ ├── index.js
│ ├── package.json
│ └── webpack.config.js
├── fixtures/
│ └── utf8-character.txt
├── package.json
├── pipelines/
│ ├── build.yml
│ └── prebuilds.yml
├── publish.yml
├── scripts/
│ ├── gen-compile-commands.js
│ ├── increment-version.js
│ ├── linux/
│ │ ├── checksums.txt
│ │ ├── install-sysroot.js
│ │ └── verify-glibc-requirements.sh
│ ├── post-install.js
│ └── prebuild.js
├── src/
│ ├── conpty_console_list_agent.ts
│ ├── eventEmitter2.test.ts
│ ├── eventEmitter2.ts
│ ├── index.ts
│ ├── interfaces.ts
│ ├── native.d.ts
│ ├── shared/
│ │ └── conout.ts
│ ├── terminal.test.ts
│ ├── terminal.ts
│ ├── testUtils.test.ts
│ ├── tsconfig.json
│ ├── types.ts
│ ├── unix/
│ │ ├── pty.cc
│ │ └── spawn-helper.cc
│ ├── unixTerminal.test.ts
│ ├── unixTerminal.ts
│ ├── utils.ts
│ ├── win/
│ │ ├── conpty.cc
│ │ ├── conpty.h
│ │ ├── conpty_console_list.cc
│ │ ├── path_util.cc
│ │ └── path_util.h
│ ├── windowsConoutConnection.ts
│ ├── windowsPtyAgent.test.ts
│ ├── windowsPtyAgent.ts
│ ├── windowsTerminal.test.ts
│ ├── windowsTerminal.ts
│ └── worker/
│ └── conoutSocketWorker.ts
├── test/
│ └── spam-close.js
└── typings/
└── node-pty.d.ts
SYMBOL INDEX (206 symbols across 29 files)
FILE: examples/electron/main.js
function createWindow (line 9) | function createWindow() {
FILE: scripts/increment-version.js
function getNextBetaVersion (line 23) | function getNextBetaVersion() {
function getPublishedVersions (line 44) | function getPublishedVersions(version, tag) {
FILE: scripts/linux/install-sysroot.js
constant REPO_ROOT (line 12) | const REPO_ROOT = path.join(__dirname, '..', '..');
function getSysrootChecksum (line 29) | function getSysrootChecksum(expectedName) {
function fetchUrl (line 41) | async function fetchUrl(options, retries = 10, retryDelay = 1000) {
function getSysroot (line 93) | async function getSysroot(arch) {
function main (line 141) | async function main() {
FILE: scripts/post-install.js
constant RELEASE_DIR (line 7) | const RELEASE_DIR = path.join(__dirname, '../build/Release');
constant BUILD_FILES (line 8) | const BUILD_FILES = [
constant CONPTY_DIR (line 17) | const CONPTY_DIR = path.join(__dirname, '../third_party/conpty');
constant CONPTY_SUPPORTED_ARCH (line 18) | const CONPTY_SUPPORTED_ARCH = ['x64', 'arm64'];
function cleanFolderRecursive (line 23) | function cleanFolderRecursive(folder) {
FILE: scripts/prebuild.js
constant PREBUILDS_ROOT (line 17) | const PREBUILDS_ROOT = path.join(__dirname, '..', 'prebuilds');
constant PREBUILD_DIR (line 18) | const PREBUILD_DIR = path.join(__dirname, '..', 'prebuilds', `${process....
FILE: src/eventEmitter2.ts
type IListener (line 7) | interface IListener<T> {
type IEvent (line 11) | interface IEvent<T> {
class EventEmitter2 (line 15) | class EventEmitter2<T> {
method event (line 19) | public get event(): IEvent<T> {
method fire (line 39) | public fire(data: T): void {
FILE: src/index.ts
function spawn (line 30) | function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOp...
function fork (line 35) | function fork(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOpt...
function createTerminal (line 40) | function createTerminal(file?: string, args?: ArgvOrCommandLine, opt?: I...
function open (line 44) | function open(options: IPtyOpenOptions): ITerminal {
FILE: src/interfaces.ts
type IProcessEnv (line 6) | interface IProcessEnv {
type ITerminal (line 10) | interface ITerminal {
type IBasePtyForkOptions (line 106) | interface IBasePtyForkOptions {
type IPtyForkOptions (line 118) | interface IPtyForkOptions extends IBasePtyForkOptions {
type IWindowsPtyForkOptions (line 123) | interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
type IPtyOpenOptions (line 137) | interface IPtyOpenOptions {
FILE: src/native.d.ts
type IConptyNative (line 5) | interface IConptyNative {
type IUnixNative (line 13) | interface IUnixNative {
type IConptyProcess (line 20) | interface IConptyProcess {
type IUnixProcess (line 27) | interface IUnixProcess {
type IUnixOpenProcess (line 33) | interface IUnixOpenProcess {
FILE: src/shared/conout.ts
type IWorkerData (line 5) | interface IWorkerData {
type ConoutWorkerMessage (line 9) | const enum ConoutWorkerMessage {
function getWorkerPipeName (line 13) | function getWorkerPipeName(conoutPipeName: string): string {
FILE: src/terminal.test.ts
constant SHELL (line 13) | const SHELL = (process.platform === 'win32') ? 'cmd.exe' : '/bin/bash';
class TestTerminal (line 22) | class TestTerminal extends Terminal {
method checkType (line 23) | public checkType<T>(name: string, value: T, type: string, allowArray: ...
method _write (line 26) | protected _write(data: string | Buffer): void {
method resize (line 29) | public resize(cols: number, rows: number): void {
method clear (line 32) | public clear(): void {
method destroy (line 35) | public destroy(): void {
method kill (line 38) | public kill(signal?: string): void {
method process (line 41) | public get process(): string {
method master (line 44) | public get master(): Socket {
method slave (line 47) | public get slave(): Socket {
function stripEscapeSequences (line 117) | function stripEscapeSequences(data: string): string {
FILE: src/terminal.ts
constant DEFAULT_COLS (line 13) | const DEFAULT_COLS: number = 80;
constant DEFAULT_ROWS (line 14) | const DEFAULT_ROWS: number = 24;
constant FLOW_CONTROL_PAUSE (line 21) | const FLOW_CONTROL_PAUSE = '\x13';
constant FLOW_CONTROL_RESUME (line 22) | const FLOW_CONTROL_RESUME = '\x11';
method onData (line 44) | public get onData(): IEvent<string> { return this._onData.event; }
method onExit (line 46) | public get onExit(): IEvent<IExitEvent> { return this._onExit.event; }
method pid (line 48) | public get pid(): number { return this._pid; }
method cols (line 49) | public get cols(): number { return this._cols; }
method rows (line 50) | public get rows(): number { return this._rows; }
method constructor (line 52) | constructor(opt?: IPtyForkOptions) {
method write (line 79) | public write(data: string | Buffer): void {
method _forwardEvents (line 95) | protected _forwardEvents(): void {
method _checkType (line 100) | protected _checkType<T>(name: string, value: T | undefined, type: string...
method end (line 120) | public end(data: string): void {
method pipe (line 125) | public pipe(dest: any, options: any): any {
method pause (line 130) | public pause(): Socket {
method resume (line 135) | public resume(): Socket {
method setEncoding (line 140) | public setEncoding(encoding: string | null): void {
method addListener (line 149) | public addListener(eventName: string, listener: (...args: any[]) => any)...
method on (line 150) | public on(eventName: string, listener: (...args: any[]) => any): void {
method emit (line 158) | public emit(eventName: string, ...args: any[]): any {
method listeners (line 165) | public listeners(eventName: string): Function[] {
method removeListener (line 169) | public removeListener(eventName: string, listener: (...args: any[]) => a...
method removeAllListeners (line 173) | public removeAllListeners(eventName: string): void {
method once (line 177) | public once(eventName: string, listener: (...args: any[]) => any): void {
method _close (line 190) | protected _close(): void {
method _parseEnv (line 198) | protected _parseEnv(env: IProcessEnv): string[] {
FILE: src/testUtils.test.ts
function pollUntil (line 5) | function pollUntil(cb: () => boolean, timeout: number, interval: number)...
FILE: src/types.ts
type ArgvOrCommandLine (line 6) | type ArgvOrCommandLine = string[] | string;
type IExitEvent (line 8) | interface IExitEvent {
type IDisposable (line 13) | interface IDisposable {
FILE: src/unix/pty.cc
type ExitEvent (line 111) | struct ExitEvent {
function SetCloseOnExec (line 117) | static int
function pty_close_inherited_fds (line 131) | static void
function SetupExitCallback (line 149) | void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
type termios (line 278) | struct termios
type winsize (line 279) | struct winsize
type DelBuf (line 285) | struct DelBuf {
method DelBuf (line 287) | DelBuf(int len) : len(len) {}
function PtyFork (line 297) | Napi::Value PtyFork(const Napi::CallbackInfo& info) {
function PtyOpen (line 504) | Napi::Value PtyOpen(const Napi::CallbackInfo& info) {
function PtyResize (line 545) | Napi::Value PtyResize(const Napi::CallbackInfo& info) {
function PtyGetProc (line 586) | Napi::Value PtyGetProc(const Napi::CallbackInfo& info) {
function pty_nonblock (line 626) | static int
type kinfo_proc (line 703) | struct kinfo_proc
function format_error (line 731) | static std::string format_error(const char* func, int err_code) {
function pty_posix_spawn (line 737) | static void
function init (line 867) | Napi::Object init(Napi::Env env, Napi::Object exports) {
FILE: src/unix/spawn-helper.cc
function main (line 6) | int main (int argc, char** argv) {
FILE: src/unixTerminal.test.ts
constant FIXTURES_PATH (line 16) | const FIXTURES_PATH = path.normalize(path.join(__dirname, '..', 'fixture...
FILE: src/unixTerminal.ts
constant DEFAULT_FILE (line 22) | const DEFAULT_FILE = 'sh';
constant DEFAULT_NAME (line 23) | const DEFAULT_NAME = 'xterm';
constant DESTROY_SOCKET_TIMEOUT_MS (line 24) | const DESTROY_SOCKET_TIMEOUT_MS = 200;
class UnixTerminal (line 26) | class UnixTerminal extends Terminal {
method master (line 44) | public get master(): net.Socket | undefined { return this._master; }
method slave (line 45) | public get slave(): net.Socket | undefined { return this._slave; }
method constructor (line 47) | constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOpt...
method _write (line 169) | protected _write(data: string | Buffer): void {
method fd (line 174) | get fd(): number { return this._fd; }
method ptsName (line 175) | get ptsName(): string { return this._pty; }
method open (line 181) | public static open(opt: IPtyOpenOptions): UnixTerminal {
method destroy (line 236) | public destroy(): void {
method kill (line 249) | public kill(signal?: string): void {
method process (line 258) | public get process(): string {
method resize (line 271) | public resize(cols: number, rows: number, pixelSize?: { width: number,...
method clear (line 282) | public clear(): void {
method _sanitizeEnv (line 286) | private _sanitizeEnv(env: IProcessEnv): void {
type IWriteTask (line 304) | interface IWriteTask {
class CustomWriteStream (line 316) | class CustomWriteStream implements IDisposable {
method constructor (line 321) | constructor(
method dispose (line 327) | dispose(): void {
method write (line 332) | write(data: string | Buffer): void {
method _processWriteQueue (line 347) | private _processWriteQueue(): void {
FILE: src/utils.ts
function assign (line 6) | function assign(target: any, ...sources: any[]): any {
function loadNativeModule (line 12) | function loadNativeModule(name: string): {dir: string, module: any} {
FILE: src/win/conpty.cc
type pty_baton (line 41) | struct pty_baton {
method pty_baton (line 49) | pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), h...
function pty_baton (line 55) | static pty_baton* get_pty_baton(int id) {
method pty_baton (line 49) | pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), h...
function remove_pty_baton (line 65) | static bool remove_pty_baton(int id) {
type ExitEvent (line 76) | struct ExitEvent {
function SetupExitCallback (line 80) | void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* bato...
function errorWithCode (line 128) | Napi::Error errorWithCode(const Napi::CallbackInfo& info, const char* te...
function createDataServerPipe (line 136) | bool createDataServerPipe(bool write,
function HANDLE (line 164) | HANDLE LoadConptyDll(const Napi::CallbackInfo& info,
function HRESULT (line 191) | HRESULT CreateNamedPipesAndPseudoConsole(const Napi::CallbackInfo& info,
function PtyStartProcess (line 244) | static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) {
function PtyConnect (line 324) | static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
function PtyResize (line 457) | static Napi::Value PtyResize(const Napi::CallbackInfo& info) {
function PtyClear (line 495) | static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
function PtyKill (line 533) | static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
function init (line 573) | Napi::Object init(Napi::Env env, Napi::Object exports) {
FILE: src/win/conpty_console_list.cc
function ApiConsoleProcessList (line 9) | static Napi::Value ApiConsoleProcessList(const Napi::CallbackInfo& info) {
function init (line 39) | Napi::Object init(Napi::Env env, Napi::Object exports) {
FILE: src/win/path_util.cc
type path_util (line 12) | namespace path_util {
function to_wstring (line 14) | std::wstring to_wstring(const Napi::String& str) {
function wstring_to_string (line 19) | std::string wstring_to_string(const std::wstring &wide_string) {
function file_exists (line 45) | bool file_exists(std::wstring filename) {
function get_shell_path (line 54) | std::wstring get_shell_path(std::wstring filename) {
FILE: src/win/path_util.h
function namespace (line 16) | namespace path_util {
FILE: src/windowsConoutConnection.ts
constant FLUSH_DATA_INTERVAL (line 17) | const FLUSH_DATA_INTERVAL = 1000;
class ConoutConnection (line 31) | class ConoutConnection implements IDisposable {
method onReady (line 37) | public get onReady(): IEvent<void> { return this._onReady.event; }
method constructor (line 39) | constructor(
method dispose (line 59) | dispose(): void {
method connectSocket (line 68) | connectSocket(socket: Socket): void {
method _drainDataAndClose (line 72) | private _drainDataAndClose(): void {
method _destroySocket (line 79) | private async _destroySocket(): Promise<void> {
FILE: src/windowsPtyAgent.test.ts
function check (line 9) | function check(file: string, args: string | string[], expected: string):...
FILE: src/windowsPtyAgent.ts
constant FLUSH_DATA_INTERVAL (line 22) | const FLUSH_DATA_INTERVAL = 1000;
class WindowsPtyAgent (line 27) | class WindowsPtyAgent {
method inSocket (line 39) | public get inSocket(): Socket { return this._inSocket; }
method outSocket (line 40) | public get outSocket(): Socket { return this._outSocket; }
method fd (line 41) | public get fd(): any { return this._fd; }
method innerPid (line 42) | public get innerPid(): number { return this._innerPid; }
method pty (line 43) | public get pty(): number { return this._pty; }
method constructor (line 47) | constructor(
method _completePtyConnection (line 120) | private _completePtyConnection(): void {
method resize (line 131) | public resize(cols: number, rows: number): void {
method clear (line 138) | public clear(): void {
method kill (line 142) | public kill(): void {
method _getConsoleProcessList (line 171) | private _getConsoleProcessList(): Promise<number[]> {
method exitCode (line 189) | public get exitCode(): number | undefined {
method _generatePipeName (line 193) | private _generatePipeName(): string {
method _$onProcessExit (line 200) | private _$onProcessExit(exitCode: number): void {
method _flushDataAndCleanUp (line 208) | private _flushDataAndCleanUp(): void {
method _cleanUpProcess (line 218) | private _cleanUpProcess(): void {
function argsToCommandLine (line 231) | function argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
function isCommandLine (line 283) | function isCommandLine(args: ArgvOrCommandLine): args is string {
function repeatText (line 287) | function repeatText(text: string, count: number): string {
function xOr (line 295) | function xOr(arg1: boolean, arg2: boolean): boolean {
FILE: src/windowsTerminal.test.ts
type IProcessState (line 12) | interface IProcessState {
type IWindowsProcessTreeResult (line 17) | interface IWindowsProcessTreeResult {
function pollForProcessState (line 22) | function pollForProcessState(desiredState: IProcessState, intervalMs: nu...
function pollForProcessTreeSize (line 60) | function pollForProcessTreeSize(pid: number, size: number, intervalMs: n...
FILE: src/windowsTerminal.ts
constant DEFAULT_FILE (line 14) | const DEFAULT_FILE = 'cmd.exe';
constant DEFAULT_NAME (line 15) | const DEFAULT_NAME = 'Windows Shell';
class WindowsTerminal (line 17) | class WindowsTerminal extends Terminal {
method constructor (line 22) | constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPty...
method _write (line 123) | protected _write(data: string | Buffer): void {
method _doWrite (line 127) | private _doWrite(data: string | Buffer): void {
method open (line 135) | public static open(options?: IPtyOpenOptions): void {
method resize (line 143) | public resize(cols: number, rows: number, pixelSize?: { width: number,...
method clear (line 154) | public clear(): void {
method destroy (line 160) | public destroy(): void {
method kill (line 166) | public kill(signal?: string): void {
method _deferNoArgs (line 176) | private _deferNoArgs<A>(deferredFn: () => void): void {
method _defer (line 189) | private _defer<A>(deferredFn: (arg: A) => void, arg: A): void {
method process (line 202) | public get process(): string { return this._name; }
method master (line 203) | public get master(): Socket { throw new Error('master is not supported...
method slave (line 204) | public get slave(): Socket { throw new Error('slave is not supported o...
FILE: typings/node-pty.d.ts
type IBasePtyForkOptions (line 20) | interface IBasePtyForkOptions {
type IPtyForkOptions (line 79) | interface IPtyForkOptions extends IBasePtyForkOptions {
type IWindowsPtyForkOptions (line 88) | interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
type IPty (line 117) | interface IPty {
type IDisposable (line 204) | interface IDisposable {
type IEvent (line 212) | interface IEvent<T> {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (214K chars).
[
{
"path": ".config/tsaoptions.json",
"chars": 336,
"preview": "{\n \"codebaseName\": \"devdiv_microsoft_vscode_node_pty\",\n \"instanceUrl\": \"https://devdiv.visualstudio.com/defaultcollect"
},
{
"path": ".editorconfig",
"chars": 161,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of"
},
{
"path": ".gitattributes",
"chars": 12,
"preview": "* text=auto\n"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 87,
"preview": "## Environment details\n\n- OS:\n- OS version:\n- node-pty version:\n\n## Issue description\n\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2315,
"preview": "name: CI\n\non:\n pull_request:\n\njobs:\n lint:\n name: Lint\n runs-on: ubuntu-slim\n steps:\n - name: Checkout\n "
},
{
"path": ".gitignore",
"chars": 223,
"preview": "build/\n.lock-wscript\nout/\nMakefile.gyp\n*.Makefile\n*.target.gyp.mk\nnode_modules/\nbuilderror.log\nlib/\nnpm-debug.log\nfixtur"
},
{
"path": ".vscode/launch.json",
"chars": 1022,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n //"
},
{
"path": ".vscode/tasks.json",
"chars": 408,
"preview": "{\n \"version\": \"2.0.0\",\n \"presentation\": {\n \"echo\": false,\n \"reveal\": \"always\",\n \"focus\": false,\n \"panel\": "
},
{
"path": "CONTRIBUTING.md",
"chars": 929,
"preview": "## Testing in a real terminal\n\nThe recommended way to test node-pty during development is via the electron example:\n\n```"
},
{
"path": "LICENSE",
"chars": 3326,
"preview": "Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)\n\nPermission is hereby granted, free of charge, t"
},
{
"path": "README.md",
"chars": 8100,
"preview": "# node-pty\n\n[.targets\\\"):node_addon_api_e"
},
{
"path": "eslint.config.js",
"chars": 3476,
"preview": "// @ts-check\nconst tsParser = require('@typescript-eslint/parser');\nconst tsPlugin = require('@typescript-eslint/eslint-"
},
{
"path": "examples/electron/README.md",
"chars": 486,
"preview": "This is a minimal example of getting a terminal running in Electron using [node-pty](https://github.com/microsoft/node-p"
},
{
"path": "examples/electron/index.html",
"chars": 370,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <title>node-pty Electron example</title>\n <link rel=\"S"
},
{
"path": "examples/electron/main.js",
"chars": 1604,
"preview": "const { app, BrowserWindow, ipcMain } = require('electron');\nconst path = require('path');\nconst os = require('os');\ncon"
},
{
"path": "examples/electron/npm_install.bat",
"chars": 192,
"preview": "@echo off\nsetlocal\nset npm_config_disturl=\"https://atom.io/download/electron\"\nset npm_config_target=9.1.0\nset npm_config"
},
{
"path": "examples/electron/npm_install.sh",
"chars": 560,
"preview": "#!/usr/bin/env sh\n\n# Electron's version.\nexport npm_config_target=9.1.0\n# The architecture of Electron, can be ia32 or x"
},
{
"path": "examples/electron/package.json",
"chars": 347,
"preview": "{\n \"name\": \"node-pty-electron-example\",\n \"version\": \"1.0.0\",\n \"description\": \"A minimal node-pty Electron example\",\n "
},
{
"path": "examples/electron/preload.js",
"chars": 362,
"preview": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('pty', {\n spawn: () => ipc"
},
{
"path": "examples/electron/renderer.js",
"chars": 349,
"preview": "// Initialize xterm.js and attach it to the DOM\nconst xterm = new window.Terminal();\nxterm.open(document.getElementById("
},
{
"path": "examples/fork/index.js",
"chars": 739,
"preview": "import * as os from 'node:os';\nimport * as pty from '../../lib/index.js';\n\nconst isWindows = os.platform() === 'win32';\n"
},
{
"path": "examples/fork/package.json",
"chars": 23,
"preview": "{\n \"type\": \"module\"\n}\n"
},
{
"path": "examples/killDeepTree/README.md",
"chars": 387,
"preview": "This is a manual test to verify deeply nested trees are getting killed correctly on Windows.\n\nTo run:\n\n```bash\nnpm i\nnod"
},
{
"path": "examples/killDeepTree/entry.js",
"chars": 16,
"preview": "const test = 0;\n"
},
{
"path": "examples/killDeepTree/index.js",
"chars": 488,
"preview": "var os = require('os');\nvar pty = require('../..');\n\nvar shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';\n"
},
{
"path": "examples/killDeepTree/package.json",
"chars": 249,
"preview": "{\n\t\"name\": \"vscode-process-leak\",\n\t\"version\": \"1.0.0\",\n\t\"description\": \"\",\n\t\"main\": \"index.js\",\n\t\"scripts\": {\n\t\t\"start\":"
},
{
"path": "examples/killDeepTree/webpack.config.js",
"chars": 185,
"preview": "const path = require('path');\n\nmodule.exports = {\n\tentry: './entry.js',\n\toutput: {\n\t\tfilename: 'bundle.js',\n\t\tpath: path"
},
{
"path": "fixtures/utf8-character.txt",
"chars": 1,
"preview": "æ"
},
{
"path": "package.json",
"chars": 1660,
"preview": "{\n \"name\": \"node-pty\",\n \"description\": \"Fork pseudoterminals in Node.JS\",\n \"author\": {\n \"name\": \"Microsoft Corpora"
},
{
"path": "pipelines/build.yml",
"chars": 1426,
"preview": "parameters:\n arch: 'x64'\n\nsteps:\n- task: UseNode@1\n inputs:\n version: '22.x'\n displayName: 'Install Node.js 22.x'\n"
},
{
"path": "pipelines/prebuilds.yml",
"chars": 5407,
"preview": "trigger:\n branches:\n include:\n - main\npr: none\n\nresources:\n repositories:\n - repository: 1esPipelines\n "
},
{
"path": "publish.yml",
"chars": 3045,
"preview": "name: $(Date:yyyyMMdd)$(Rev:.r)\n\ntrigger:\n branches:\n include:\n - main\n\npr: none\n\nresources:\n repositories:\n "
},
{
"path": "scripts/gen-compile-commands.js",
"chars": 260,
"preview": "/**\n * Copyright (c) 2025, Microsoft Corporation (MIT License).\n */\n\nconst { execSync } = require('child_process');\n\ncon"
},
{
"path": "scripts/increment-version.js",
"chars": 2192,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n */\n\nconst cp = require('child_process');\nconst fs = req"
},
{
"path": "scripts/linux/checksums.txt",
"chars": 225,
"preview": "3122af49c493c5c767c2b0772a41119cbdc9803125a705683445b4066dc88b82 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz\n3baac81a"
},
{
"path": "scripts/linux/install-sysroot.js",
"chars": 5636,
"preview": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Micros"
},
{
"path": "scripts/linux/verify-glibc-requirements.sh",
"chars": 1356,
"preview": "#!/usr/bin/env bash\n\nset -e\n\n# Get all files with .node extension from given folder\nfiles=$(find $SEARCH_PATH -name \"*.n"
},
{
"path": "scripts/post-install.js",
"chars": 2401,
"preview": "//@ts-check\n\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\n\nconst RELEASE_DIR = path"
},
{
"path": "scripts/prebuild.js",
"chars": 1172,
"preview": "//@ts-check\n\nconst fs = require('fs');\nconst path = require('path');\n\n/**\n * This script checks for the prebuilt binarie"
},
{
"path": "src/conpty_console_list_agent.ts",
"chars": 769,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n *\n * This module fetches the console process list for a"
},
{
"path": "src/eventEmitter2.test.ts",
"chars": 975,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n */\n\nimport * as assert from 'assert';\nimport { EventEmi"
},
{
"path": "src/eventEmitter2.ts",
"chars": 1121,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n */\n\nimport { IDisposable } from './types';\n\ninterface I"
},
{
"path": "src/index.ts",
"chars": 2153,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/interfaces.ts",
"chars": 3606,
"preview": "/**\n * Copyright (c) 2016, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ne"
},
{
"path": "src/native.d.ts",
"chars": 1309,
"preview": "/**\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ninterface IConptyNative {\n startProcess(file: stri"
},
{
"path": "src/shared/conout.ts",
"chars": 291,
"preview": "/**\n * Copyright (c) 2020, Microsoft Corporation (MIT License).\n */\n\nexport interface IWorkerData {\n conoutPipeName: st"
},
{
"path": "src/terminal.test.ts",
"chars": 4361,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ni"
},
{
"path": "src/terminal.ts",
"chars": 6730,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT License).\n * C"
},
{
"path": "src/testUtils.test.ts",
"chars": 567,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n */\n\nexport function pollUntil(cb: () => boolean, timeou"
},
{
"path": "src/tsconfig.json",
"chars": 323,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"target\": \"es5\",\n \"rootDir\": \".\",\n \"outDir\": \"../lib\",\n "
},
{
"path": "src/types.ts",
"chars": 306,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ne"
},
{
"path": "src/unix/pty.cc",
"chars": 23075,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)\n * Copyright (c) 2017, Daniel Imms (MIT License)\n *\n *"
},
{
"path": "src/unix/spawn-helper.cc",
"chars": 494,
"preview": "#include <errno.h>\n#include <fcntl.h>\n#include <string.h>\n#include <unistd.h>\n\nint main (int argc, char** argv) {\n char"
},
{
"path": "src/unixTerminal.test.ts",
"chars": 15680,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ni"
},
{
"path": "src/unixTerminal.ts",
"chars": 11193,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT License).\n * C"
},
{
"path": "src/utils.ts",
"chars": 997,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ne"
},
{
"path": "src/win/conpty.cc",
"chars": 19646,
"preview": "/**\n * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/win/conpty.h",
"chars": 1727,
"preview": "// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\n// This header prototypes the Pseudoconsole "
},
{
"path": "src/win/conpty_console_list.cc",
"chars": 1368,
"preview": "/**\n * Copyright (c) 2019, Microsoft Corporation (MIT License).\n */\n\n#define NODE_ADDON_API_DISABLE_DEPRECATED\n#include "
},
{
"path": "src/win/path_util.cc",
"chars": 2607,
"preview": "/**\n * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/win/path_util.h",
"chars": 695,
"preview": "/**\n * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/windowsConoutConnection.ts",
"chars": 2752,
"preview": "/**\n * Copyright (c) 2020, Microsoft Corporation (MIT License).\n */\n\nimport { Worker } from 'worker_threads';\nimport { S"
},
{
"path": "src/windowsPtyAgent.test.ts",
"chars": 7396,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ni"
},
{
"path": "src/windowsPtyAgent.ts",
"chars": 9433,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/windowsTerminal.test.ts",
"chars": 9773,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\ni"
},
{
"path": "src/windowsTerminal.ts",
"chars": 5865,
"preview": "/**\n * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License)\n * Copyright (c) 2016, Daniel Imms (MIT L"
},
{
"path": "src/worker/conoutSocketWorker.ts",
"chars": 713,
"preview": "/**\n * Copyright (c) 2020, Microsoft Corporation (MIT License).\n */\n\nimport { parentPort, workerData } from 'worker_thre"
},
{
"path": "test/spam-close.js",
"chars": 1002,
"preview": "// This test creates a pty periodically, spamming it with echo calls and killing it shortly after.\n// It's a test case f"
},
{
"path": "typings/node-pty.d.ts",
"chars": 6811,
"preview": "/**\n * Copyright (c) 2017, Daniel Imms (MIT License).\n * Copyright (c) 2018, Microsoft Corporation (MIT License).\n */\n\nd"
}
]
About this extraction
This page contains the full source code of the Tyriar/node-pty GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (197.9 KB), approximately 54.1k tokens, and a symbol index with 206 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.