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 /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 [![Build Status](https://dev.azure.com/vscode/node-pty/_apis/build/status/Microsoft.node-pty?branchName=main)](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).
Copyright (c) 2016, Daniel Imms (MIT License).
Copyright (c) 2018, Microsoft Corporation (MIT License). ================================================ FILE: SECURITY.md ================================================ ## 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). ================================================ FILE: binding.gyp ================================================ { 'target_defaults': { 'dependencies': [ " node-pty Electron example
================================================ 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(); 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(); 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 { (e: T): void; } export interface IEvent { (listener: (e: T) => any): IDisposable; } export class EventEmitter2 { private _listeners: Array> = []; private _event?: IEvent; public get event(): IEvent { 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> = []; 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(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 (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 { // // 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(); public get onData(): IEvent { return this._onData.event; } private _onExit = new EventEmitter2(); public get onExit(): IEvent { 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(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 { return new Promise((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 #include #include #include #include #include #include #include #include #include #include #include #include /* forkpty */ /* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */ #if defined(__linux__) #include #include #include #elif defined(__APPLE__) #include #elif defined(__FreeBSD__) #include #include #elif defined(__OpenBSD__) #include #include #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 #include #elif defined(__APPLE__) #include #include #include #include #include #include #include #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(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(); // args Napi::Array argv_ = info[1].As(); // env Napi::Array env_ = info[2].As(); int envc = env_.Length(); std::unique_ptr 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(); env[i] = strdup(pair.c_str()); } // cwd std::string cwd_ = info[3].As(); // size struct winsize winp; winp.ws_col = info[4].As().Int32Value(); winp.ws_row = info[5].As().Int32Value(); winp.ws_xpixel = 0; winp.ws_ypixel = 0; #if !defined(__APPLE__) // uid / gid int uid = info[6].As().Int32Value(); int gid = info[7].As().Int32Value(); #endif // termios struct termios t = termios(); struct termios *term = &t; term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; if (info[8].As().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(); pid_t pid; int master = -1; #if defined(__APPLE__) int argc = argv_.Length(); int argl = argc + 4; std::unique_ptr 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(); 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 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(); 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(term), static_cast(&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(); 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().Int32Value(); winp.ws_row = info[1].As().Int32Value(); winp.ws_xpixel = 0; winp.ws_ypixel = 0; // pty int master, slave; int ret = openpty(&master, &slave, nullptr, NULL, static_cast(&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().Int32Value(); struct winsize winp; winp.ws_col = info[1].As().Int32Value(); winp.ws_row = info[2].As().Int32Value(); winp.ws_xpixel = info[3].As().Int32Value(); winp.ws_ypixel = info[4].As().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().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().Int32Value(); std::string tty_ = info[1].As(); 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 // Copyright (c) 2009 Joshua Elsasser // Copyright (c) 2009 Todd Carson // // 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 #include #include #include 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 { 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(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 #include #include // PathCombine, PathIsRelative #include #include #include #include #include #include #include #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> 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 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())); const SHORT cols = static_cast(info[1].As().Uint32Value()); const SHORT rows = static_cast(info[2].As().Uint32Value()); const bool debug = info[3].As().Value(); const std::wstring pipeName(path_util::to_wstring(info[4].As())); const bool inheritCursor = info[5].As().Value(); const bool useConptyDll = info[6].As().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(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().Int32Value(); const std::wstring cmdline(path_util::to_wstring(info[1].As())); const std::wstring cwd(path_util::to_wstring(info[2].As())); const Napi::Array envValues = info[3].As(); const bool useConptyDll = info[4].As().Value(); Napi::Function exitCallback = info[5].As(); // 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 mutableCommandline = std::make_unique(cmdline.length() + 1); HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1, cmdline.c_str()); // Prepare cwd std::unique_ptr mutableCwd = std::make_unique(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()); envBlock += L'\0'; } envBlock += L'\0'; envStr = std::move(envBlock); } std::vector 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(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().Int32Value(); SHORT cols = static_cast(info[1].As().Uint32Value()); SHORT rows = static_cast(info[2].As().Uint32Value()); const bool useConptyDll = info[3].As().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().Int32Value(); const bool useConptyDll = info[1].As().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().Int32Value(); const bool useConptyDll = info[1].As().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 #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 #include 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().Uint32Value(); if (!FreeConsole()) { throw Napi::Error::New(env, "FreeConsole failed"); } if (!AttachConsole(pid)) { throw Napi::Error::New(env, "AttachConsole failed"); } auto processList = std::vector(64); auto processCount = GetConsoleProcessList(&processList[0], static_cast(processList.size())); if (processList.size() < processCount) { processList.resize(processCount); processCount = GetConsoleProcessList(&processList[0], static_cast(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 #include // PathCombine #include #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 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(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 #include #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(); public get onReady(): IEvent { 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 { 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 { if (this._innerPid <= 0) { return Promise.resolve([]); } return new Promise(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 { return new Promise(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 { return new Promise(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 }); (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 = []; } }); // Shutdown if `error` event is emitted. this._socket.on('error', err => { // Close terminal session. this._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; } }); // Cleanup after the socket is closed. this._socket.on('close', () => { this.emit('exit', this._agent.exitCode); this._close(); }); }); this._file = file; this._name = name; this._readable = true; this._writable = true; this._forwardEvents(); } protected _write(data: string | Buffer): void { this._defer(this._doWrite, data); } private _doWrite(data: string | Buffer): void { this._agent.inSocket.write(data); } /** * openpty */ public static open(options?: IPtyOpenOptions): void { throw new Error('open() not supported on windows, use Fork() instead.'); } /** * 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'); } this._deferNoArgs(() => { this._agent.resize(cols, rows); this._cols = cols; this._rows = rows; }); } public clear(): void { this._deferNoArgs(() => { this._agent.clear(); }); } public destroy(): void { this._deferNoArgs(() => { this.kill(); }); } public kill(signal?: string): void { this._deferNoArgs(() => { if (signal) { throw new Error('Signals not supported on windows.'); } this._close(); this._agent.kill(); }); } private _deferNoArgs(deferredFn: () => void): void { // If the terminal is ready, execute. if (this._isReady) { deferredFn.call(this); return; } // Queue until terminal is ready. this._deferreds.push({ run: () => deferredFn.call(this) }); } private _defer(deferredFn: (arg: A) => void, arg: A): void { // If the terminal is ready, execute. if (this._isReady) { deferredFn.call(this, arg); return; } // Queue until terminal is ready. this._deferreds.push({ run: () => deferredFn.call(this, arg) }); } public get process(): string { return this._name; } public get master(): Socket { throw new Error('master is not supported on Windows'); } public get slave(): Socket { throw new Error('slave is not supported on Windows'); } } ================================================ FILE: src/worker/conoutSocketWorker.ts ================================================ /** * Copyright (c) 2020, Microsoft Corporation (MIT License). */ import { parentPort, workerData } from 'worker_threads'; import { Socket, createServer } from 'net'; import { ConoutWorkerMessage, IWorkerData, getWorkerPipeName } from '../shared/conout'; const { conoutPipeName } = (workerData as IWorkerData); const conoutSocket = new Socket(); conoutSocket.setEncoding('utf8'); conoutSocket.connect(conoutPipeName, () => { const server = createServer(workerSocket => { conoutSocket.pipe(workerSocket); }); server.listen(getWorkerPipeName(conoutPipeName)); if (!parentPort) { throw new Error('worker_threads parentPort is null'); } parentPort.postMessage(ConoutWorkerMessage.READY); }); ================================================ FILE: test/spam-close.js ================================================ // This test creates a pty periodically, spamming it with echo calls and killing it shortly after. // It's a test case for https://github.com/microsoft/node-pty/issues/375, the script will hang // when it show this bug instead of continuing to create more processes. var os = require('os'); var pty = require('..'); var isWindows = os.platform() === 'win32'; var shell = isWindows ? 'cmd.exe' : 'bash'; let i = 0; setInterval(() => { console.log(`creating pty ${++i}`); var 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) }); ptyProcess.onData(data => console.log(` data: ${data.replace(/\x1b|\n|\r/g, '_')}`)); setInterval(() => { ptyProcess.write('echo foo\r'.repeat(50)); }, 10); setTimeout(() => { console.log(` killing ${ptyProcess.pid}...`); ptyProcess.kill(); }, 100); }, 1200); ================================================ FILE: typings/node-pty.d.ts ================================================ /** * Copyright (c) 2017, Daniel Imms (MIT License). * Copyright (c) 2018, Microsoft Corporation (MIT License). */ declare module 'node-pty' { /** * 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. * @see CommandLineToArgvW https://msdn.microsoft.com/en-us/library/windows/desktop/bb776391(v=vs.85).aspx * @see Parsing C++ Command-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: string[] | string, options: IPtyForkOptions | IWindowsPtyForkOptions): IPty; export interface IBasePtyForkOptions { /** * Name of the terminal to be set in environment ($TERM variable). */ name?: string; /** * Number of intial cols of the pty. */ cols?: number; /** * Number of initial rows of the pty. */ rows?: number; /** * Working directory to be set for the child program. */ cwd?: string; /** * Environment to be set for the child program. */ env?: { [key: string]: string | undefined }; /** * String encoding of the underlying pty. * If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding. * If unset, incoming data will be delivered as raw bytes (Buffer type). * By default 'utf8' is assumed, to unset it explicitly set it to `null`. */ encoding?: string | null; /** * (EXPERIMENTAL) * Whether to enable flow control handling (false by default). If enabled a message of `flowControlPause` * will pause the socket and thus blocking the child program execution due to buffer back pressure. * A message of `flowControlResume` will resume the socket into flow mode. * For performance reasons only a single message as a whole will match (no message part matching). * If flow control is enabled the `flowControlPause` and `flowControlResume` messages are not forwarded to * the underlying pseudoterminal. */ handleFlowControl?: boolean; /** * (EXPERIMENTAL) * The string that should pause the pty when `handleFlowControl` is true. Default is XOFF ('\x13'). */ flowControlPause?: string; /** * (EXPERIMENTAL) * The string that should resume the pty when `handleFlowControl` is true. Default is XON ('\x11'). */ flowControlResume?: string; } export interface IPtyForkOptions extends IBasePtyForkOptions { /** * Security warning: use this option with great caution, * as opened file descriptors with higher privileges might leak to the child program. */ 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; /** * (EXPERIMENTAL) * * Whether to use the conpty.dll shipped with the node-pty package instead of the one built into * Windows. Defaults to false. */ useConptyDll?: boolean; /** * Whether to use PSEUDOCONSOLE_INHERIT_CURSOR in conpty. * @see https://docs.microsoft.com/en-us/windows/console/createpseudoconsole */ conptyInheritCursor?: boolean; } /** * An interface representing a pseudoterminal. */ export interface IPty { /** * The process ID of the outer process. */ readonly pid: number; /** * The column size in characters. */ readonly cols: number; /** * The row size in characters. */ readonly rows: number; /** * The title of the active process. */ readonly process: string; /** * (EXPERIMENTAL) * Whether to handle flow control. Useful to disable/re-enable flow control during runtime. * Use this for binary data that is likely to contain the `flowControlPause` string by accident. */ handleFlowControl: boolean; /** * Adds an event listener for when a data event fires. This happens when data is returned from * the pty. * @returns an `IDisposable` to stop listening. */ readonly onData: IEvent; /** * Adds an event listener for when an exit event fires. This happens when the pty exits. * @returns an `IDisposable` to stop listening. */ readonly onExit: IEvent<{ exitCode: number, signal?: number }>; /** * Resizes the dimensions of the pty. * @param columns The number of columns to use. * @param rows The number of rows to use. * @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(columns: 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. This is useful if the buffer is cleared on the * frontend in order to synchronize state with the backend to avoid ConPTY * possibly reprinting the screen. */ clear(): void; /** * Writes data to the pty. * @param data The data to write. */ write(data: string | Buffer): void; /** * Kills the pty. * @param signal The signal to use, defaults to SIGHUP. This parameter is not supported on * Windows. * @throws Will throw when signal is used on Windows. */ kill(signal?: string): void; /** * Pauses the pty for customizable flow control. */ pause(): void; /** * Resumes the pty for customizable flow control. */ resume(): void; } /** * An object that can be disposed via a dispose function. */ export interface IDisposable { dispose(): void; } /** * An event that can be listened to. * @returns an `IDisposable` to stop listening. */ export interface IEvent { (listener: (e: T) => any): IDisposable; } }