├── .gitmodules ├── index.js ├── test ├── children │ ├── uidgid.js │ ├── resize.js │ ├── void.js │ └── stdin.js └── index.js ├── .npmignore ├── .gitignore ├── Makefile ├── wscript ├── package.json ├── LICENSE ├── binding.gyp ├── README.md ├── lib ├── pty_win.js └── pty.js └── src ├── win └── pty.cc └── unix └── pty.cc /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/winpty"] 2 | path = deps/winpty 3 | url = https://github.com/peters/winpty.git 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var os = process.platform === 'win32' ? '_win' : ''; 2 | module.exports = require('./lib/pty'+ os +'.js'); 3 | -------------------------------------------------------------------------------- /test/children/uidgid.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | assert.equal(process.getuid(), 777); 4 | assert.equal(process.getgid(), 777); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | build/ 3 | .lock-wscript 4 | out/ 5 | Makefile.gyp 6 | *.Makefile 7 | *.target.gyp.mk 8 | node_modules/ 9 | test/ 10 | *.node 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deps/winpty/Default/* 2 | Default/* 3 | node_modules/* 4 | out/* 5 | build/* 6 | .lock-wscript 7 | Makefile.gyp 8 | *.swp 9 | *.Makefile 10 | *.target.gyp.mk 11 | *.node 12 | *.sln 13 | *.sdf 14 | *.vcxproj 15 | *.suo 16 | *.opensdf 17 | *.filters 18 | *.user 19 | *.project 20 | .idea 21 | test.js 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @type node-gyp > /dev/null 2>&1 && make gyp || make waf 3 | 4 | waf: 5 | node-waf configure build 6 | 7 | gyp: 8 | node-gyp configure 9 | node-gyp build 10 | 11 | clean: 12 | @type node-gyp > /dev/null 2>&1 && make clean-gyp || make clean-waf 13 | 14 | clean-waf: 15 | @rm -rf ./build .lock-wscript 16 | 17 | clean-gyp: 18 | @node-gyp clean 2>/dev/null 19 | 20 | .PHONY: all waf gyp clean clean-waf clean-gyp 21 | -------------------------------------------------------------------------------- /test/children/resize.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // the test runner will call "term.resize(100, 100)" after 1 second 4 | process.on('SIGWINCH', function () { 5 | var size = process.stdout.getWindowSize(); 6 | assert.equal(size[0], 100); 7 | assert.equal(size[1], 100); 8 | clearTimeout(timeout); 9 | }); 10 | 11 | var timeout = setTimeout(function () { 12 | console.error('TIMEOUT!'); 13 | process.exit(7); 14 | }, 5000); 15 | -------------------------------------------------------------------------------- /wscript: -------------------------------------------------------------------------------- 1 | srcdir = "." 2 | blddir = "build" 3 | VERSION = "0.0.1" 4 | 5 | def set_options(opt): 6 | opt.tool_options("compiler_cxx") 7 | 8 | def configure(conf): 9 | conf.check_tool("compiler_cxx") 10 | conf.check_tool("node_addon") 11 | 12 | def build(bld): 13 | obj = bld.new_task_gen("cxx", "shlib", "node_addon") 14 | obj.cxxflags = ["-Wall"] 15 | obj.linkflags = ["-lutil"] 16 | obj.target = "pty" 17 | obj.source = "src/unix/pty.cc" 18 | -------------------------------------------------------------------------------- /test/children/void.js: -------------------------------------------------------------------------------- 1 | var tty = require('tty'); 2 | var assert = require('assert'); 3 | 4 | // these two are huge. with vanilla node, it's impossible to spawn 5 | // a child process that has the stdin and stdout fd's be isatty, except 6 | // when passing through its own stdin and stdout which isn't always desirable 7 | assert.ok(tty.isatty(0)); 8 | assert.ok(tty.isatty(1)); 9 | 10 | var size = process.stdout.getWindowSize(); 11 | assert.equal(size[0], 80); 12 | assert.equal(size[1], 24); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pty.js", 3 | "description": "Pseudo terminals for node.", 4 | "author": "Christopher Jeffrey", 5 | "version": "0.3.2", 6 | "license": "MIT", 7 | "main": "./index.js", 8 | "repository": "git://github.com/chjj/pty.js.git", 9 | "homepage": "https://github.com/chjj/pty.js", 10 | "bugs": { 11 | "url": "https://github.com/chjj/pty.js/issues" 12 | }, 13 | "keywords": [ 14 | "pty", 15 | "tty", 16 | "terminal" 17 | ], 18 | "scripts": { 19 | "test": "NODE_ENV=test mocha -R spec" 20 | }, 21 | "tags": [ 22 | "pty", 23 | "tty", 24 | "terminal" 25 | ], 26 | "dependencies": { 27 | "extend": "~1.2.1", 28 | "nan": "2.3.5" 29 | }, 30 | "devDependencies": { 31 | "mocha": "~1.17.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/children/stdin.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // testing reading data from stdin, since that's a crucial feature 4 | if (process.stdin.setRawMode) { 5 | process.stdin.setRawMode(true); 6 | } else { 7 | tty.setRawMode(true); 8 | } 9 | process.stdin.resume(); 10 | process.stdin.setEncoding('utf8'); 11 | 12 | // the child script expects 1 data event, with the text "☃" 13 | var dataCount = 0; 14 | process.stdin.on('data', function (data) { 15 | dataCount++; 16 | assert.equal(data, '☃'); 17 | 18 | // done! 19 | process.stdin.pause(); 20 | clearTimeout(timeout); 21 | }); 22 | 23 | var timeout = setTimeout(function () { 24 | console.error('TIMEOUT!'); 25 | process.exit(7); 26 | }, 5000); 27 | 28 | process.on('exit', function (code) { 29 | if (code === 7) return; // timeout 30 | assert.equal(dataCount, 1); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [{ 3 | 'target_name': 'pty', 4 | 'include_dirs' : [ 5 | '` 48 | 49 | ## License 50 | 51 | Copyright (c) 2012-2015, Christopher Jeffrey (MIT License). 52 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var pty = require('../'); 3 | var mocha = require('mocha'); 4 | 5 | var tests = [ 6 | { 7 | name: 'should be correctly setup', 8 | command: [ 'children/void.js' ], 9 | options: { cwd: __dirname }, 10 | test: function () { 11 | assert.equal(this.file, process.execPath); 12 | } 13 | }, { 14 | name: 'should support stdin', 15 | command: [ 'children/stdin.js' ], 16 | options: { cwd: __dirname }, 17 | test: function () { 18 | this.write('☃'); 19 | } 20 | }, { 21 | name: 'should support resize', 22 | command: [ 'children/resize.js' ], 23 | options: { cwd: __dirname }, 24 | test: function () { 25 | this.resize(100, 100); 26 | } 27 | }, { 28 | name: 'should change uid/gid', 29 | command: [ 'children/uidgid.js' ], 30 | options: { cwd: __dirname, uid: 777, gid: 777 }, 31 | test: function () {} 32 | } 33 | ]; 34 | 35 | describe('Pty', function() { 36 | tests.forEach(function (testCase) { 37 | if (testCase.options.uid && testCase.options.gid && (process.platform == 'win32' || process.getgid() !== 0)) { 38 | // Skip tests that contains user impersonation if we are not able to do so. 39 | return it.skip(testCase.name); 40 | } 41 | it(testCase.name, function (done) { 42 | var term = pty.fork(process.execPath, testCase.command, testCase.options); 43 | term.pipe(process.stderr); 44 | 45 | // any output is considered failure. this is only a workaround 46 | // until the actual error code is passed through 47 | var count = 0; 48 | term.on('data', function (data) { 49 | count++; 50 | }); 51 | term.on('exit', function () { 52 | // XXX Temporary until we find out why this gets emitted twice: 53 | if (done.done) return; 54 | done.done = true; 55 | 56 | assert.equal(count, 0); 57 | done(); 58 | }); 59 | 60 | // Wait for pty to be ready 61 | setTimeout(testCase.test.bind(term), 1000); 62 | }); 63 | }); 64 | 65 | it('should support starting with a paused socket', function (done) { 66 | var term = pty.spawn('echo', ['Immediate output'], { resume: false }); 67 | setTimeout(function () { 68 | term.stdout.on('data', function (chunk) { 69 | assert.equal(chunk.trim(), 'Immediate output'); 70 | done(); 71 | }); 72 | }, 1); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /lib/pty_win.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pty_win.js 3 | * Copyright (c) 2012-2015, Christopher Jeffrey, Peter Sunde (MIT License) 4 | */ 5 | 6 | var net = require('net'); 7 | var path = require('path'); 8 | var extend = require('extend'); 9 | var inherits = require('util').inherits; 10 | var BaseTerminal = require('./pty').Terminal; 11 | var pty = require('../build/Release/pty.node'); 12 | 13 | // Counter of number of "pipes" created so far. 14 | var pipeIncr = 0; 15 | 16 | /** 17 | * Agent. Internal class. 18 | * 19 | * Everytime a new pseudo terminal is created it is contained 20 | * within agent.exe. When this process is started there are two 21 | * available named pipes (control and data socket). 22 | */ 23 | 24 | function Agent(file, args, env, cwd, cols, rows, debug) { 25 | var self = this; 26 | 27 | // Increment the number of pipes created. 28 | pipeIncr++; 29 | 30 | // Unique identifier per pipe created. 31 | var timestamp = Date.now(); 32 | 33 | // The data pipe is the direct connection to the forked terminal. 34 | this.dataPipe = '\\\\.\\pipe\\winpty-data-' + pipeIncr + '' + timestamp; 35 | 36 | // Dummy socket for awaiting `ready` event. 37 | this.ptySocket = new net.Socket(); 38 | 39 | // Create terminal pipe IPC channel and forward 40 | // to a local unix socket. 41 | this.ptyDataPipe = net.createServer(function (socket) { 42 | 43 | // Default socket encoding. 44 | socket.setEncoding('utf8'); 45 | 46 | // Pause until `ready` event is emitted. 47 | socket.pause(); 48 | 49 | // Sanitize input variable. 50 | file = file; 51 | args = args.join(' '); 52 | cwd = path.resolve(cwd); 53 | 54 | // Start terminal session. 55 | pty.startProcess(self.pid, file, args, env, cwd); 56 | 57 | // Emit ready event. 58 | self.ptySocket.emit('ready_datapipe', socket); 59 | 60 | }).listen(this.dataPipe); 61 | 62 | // Open pty session. 63 | var term = pty.open(self.dataPipe, cols, rows, debug); 64 | 65 | // Terminal pid. 66 | this.pid = term.pid; 67 | 68 | // Not available on windows. 69 | this.fd = term.fd; 70 | 71 | // Generated incremental number that has no real purpose besides 72 | // using it as a terminal id. 73 | this.pty = term.pty; 74 | } 75 | 76 | /** 77 | * Terminal 78 | */ 79 | 80 | /* 81 | var pty = require('./'); 82 | 83 | var term = pty.fork('cmd.exe', [], { 84 | name: 'Windows Shell', 85 | cols: 80, 86 | rows: 30, 87 | cwd: process.env.HOME, 88 | env: process.env, 89 | debug: true 90 | }); 91 | 92 | term.on('data', function(data) { 93 | console.log(data); 94 | }); 95 | */ 96 | 97 | function Terminal(file, args, opt) { 98 | 99 | var self = this, 100 | env, cwd, name, cols, rows, term, agent, debug; 101 | 102 | // Backward compatibility. 103 | if (typeof args === 'string') { 104 | opt = { 105 | name: arguments[1], 106 | cols: arguments[2], 107 | rows: arguments[3], 108 | cwd: process.env.HOME 109 | }; 110 | args = []; 111 | } 112 | 113 | // Arguments. 114 | args = args || []; 115 | file = file || 'cmd.exe'; 116 | opt = opt || {}; 117 | 118 | env = extend({}, opt.env); 119 | 120 | cols = opt.cols || 80; 121 | rows = opt.rows || 30; 122 | cwd = opt.cwd || process.cwd(); 123 | name = opt.name || env.TERM || 'Windows Shell'; 124 | debug = opt.debug || false; 125 | 126 | env.TERM = name; 127 | 128 | // Initialize environment variables. 129 | env = environ(env); 130 | 131 | // If the terminal is ready 132 | this.isReady = false; 133 | 134 | // Functions that need to run after `ready` event is emitted. 135 | this.deferreds = []; 136 | 137 | // Create new termal. 138 | this.agent = new Agent(file, args, env, cwd, cols, rows, debug); 139 | 140 | // The dummy socket is used so that we can defer everything 141 | // until its available. 142 | this.socket = this.agent.ptySocket; 143 | 144 | // The terminal socket when its available 145 | this.dataPipe = null; 146 | 147 | // Not available until `ready` event emitted. 148 | this.pid = this.agent.pid; 149 | this.fd = this.agent.fd; 150 | this.pty = this.agent.pty; 151 | 152 | // The forked windows terminal is not available 153 | // until `ready` event is emitted. 154 | this.socket.on('ready_datapipe', function (socket) { 155 | 156 | // Set terminal socket 157 | self.dataPipe = socket; 158 | 159 | // These events needs to be forwarded. 160 | ['connect', 'data', 'end', 'timeout', 'drain'].forEach(function(event) { 161 | self.dataPipe.on(event, function(data) { 162 | 163 | // Wait until the first data event is fired 164 | // then we can run deferreds. 165 | if(!self.isReady && event == 'data') { 166 | 167 | // Terminal is now ready and we can 168 | // avoid having to defer method calls. 169 | self.isReady = true; 170 | 171 | // Execute all deferred methods 172 | self.deferreds.forEach(function(fn) { 173 | // NB! In order to ensure that `this` has all 174 | // its references updated any variable that 175 | // need to be available in `this` before 176 | // the deferred is run has to be declared 177 | // above this forEach statement. 178 | fn.run(); 179 | }); 180 | 181 | // Reset 182 | self.deferreds = []; 183 | 184 | } 185 | 186 | // Emit to dummy socket 187 | self.socket.emit(event, data); 188 | 189 | }); 190 | }); 191 | 192 | // Resume socket. 193 | self.dataPipe.resume(); 194 | 195 | // Shutdown if `error` event is emitted. 196 | self.dataPipe.on('error', function (err) { 197 | 198 | // Close terminal session. 199 | self._close(); 200 | 201 | // EIO, happens when someone closes our child 202 | // process: the only process in the terminal. 203 | // node < 0.6.14: errno 5 204 | // node >= 0.6.14: read EIO 205 | if (err.code) { 206 | if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) return; 207 | } 208 | 209 | // Throw anything else. 210 | if (self.listeners('error').length < 2) { 211 | throw err; 212 | } 213 | 214 | }); 215 | 216 | // Cleanup after the socket is closed. 217 | self.dataPipe.on('close', function () { 218 | Terminal.total--; 219 | self.emit('exit', null); 220 | self._close(); 221 | }); 222 | 223 | }); 224 | 225 | this.file = file; 226 | this.name = name; 227 | this.cols = cols; 228 | this.rows = rows; 229 | 230 | this.readable = true; 231 | this.writable = true; 232 | 233 | Terminal.total++; 234 | } 235 | 236 | Terminal.fork = 237 | Terminal.spawn = 238 | Terminal.createTerminal = function (file, args, opt) { 239 | return new Terminal(file, args, opt); 240 | }; 241 | 242 | // Inherit from pty.js 243 | inherits(Terminal, BaseTerminal); 244 | 245 | // Keep track of the total 246 | // number of terminals for 247 | // the process. 248 | Terminal.total = 0; 249 | 250 | /** 251 | * Events 252 | */ 253 | 254 | /** 255 | * openpty 256 | */ 257 | 258 | Terminal.open = function () { 259 | throw new Error("open() not supported on windows, use Fork() instead."); 260 | }; 261 | 262 | /** 263 | * Events 264 | */ 265 | 266 | Terminal.prototype.write = function(data) { 267 | defer(this, function() { 268 | this.dataPipe.write(data); 269 | }); 270 | }; 271 | 272 | /** 273 | * TTY 274 | */ 275 | 276 | Terminal.prototype.resize = function (cols, rows) { 277 | defer(this, function() { 278 | 279 | cols = cols || 80; 280 | rows = rows || 24; 281 | 282 | this.cols = cols; 283 | this.rows = rows; 284 | 285 | pty.resize(this.pid, cols, rows); 286 | }); 287 | }; 288 | 289 | Terminal.prototype.destroy = function () { 290 | defer(this, function() { 291 | this.kill(); 292 | }); 293 | }; 294 | 295 | Terminal.prototype.kill = function (sig) { 296 | defer(this, function() { 297 | if (sig !== undefined) { 298 | throw new Error("Signals not supported on windows."); 299 | } 300 | this._close(); 301 | pty.kill(this.pid); 302 | }); 303 | }; 304 | 305 | Terminal.prototype.__defineGetter__('process', function () { 306 | return this.name; 307 | }); 308 | 309 | /** 310 | * Helpers 311 | */ 312 | 313 | function defer(terminal, deferredFn) { 314 | 315 | // Ensure that this method is only used within Terminal class. 316 | if (!(terminal instanceof Terminal)) { 317 | throw new Error("Must be instanceof Terminal"); 318 | } 319 | 320 | // If the terminal is ready, execute. 321 | if (terminal.isReady) { 322 | deferredFn.apply(terminal, null); 323 | return; 324 | } 325 | 326 | // Queue until terminal is ready. 327 | terminal.deferreds.push({ 328 | run: function() { 329 | // Run deffered. 330 | deferredFn.apply(terminal, null); 331 | } 332 | }); 333 | } 334 | 335 | function environ(env) { 336 | var keys = Object.keys(env || {}) 337 | , l = keys.length 338 | , i = 0 339 | , pairs = []; 340 | 341 | for (; i < l; i++) { 342 | pairs.push(keys[i] + '=' + env[keys[i]]); 343 | } 344 | 345 | return pairs; 346 | } 347 | 348 | /** 349 | * Expose 350 | */ 351 | 352 | module.exports = exports = Terminal; 353 | exports.Terminal = Terminal; 354 | exports.native = pty; 355 | -------------------------------------------------------------------------------- /src/win/pty.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * pty.js 3 | * Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License) 4 | * 5 | * pty.cc: 6 | * This file is responsible for starting processes 7 | * with pseudo-terminal file descriptors. 8 | */ 9 | 10 | #include "nan.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include // PathCombine 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | using namespace v8; 22 | using namespace std; 23 | using namespace node; 24 | 25 | /** 26 | * Misc 27 | */ 28 | extern "C" void init(Handle); 29 | 30 | #define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG") 31 | #define MAX_ENV 65536 32 | 33 | /** 34 | * winpty 35 | */ 36 | static std::vector ptyHandles; 37 | static volatile LONG ptyCounter; 38 | 39 | struct winpty_s { 40 | winpty_s(); 41 | HANDLE controlPipe; 42 | HANDLE dataPipe; 43 | }; 44 | 45 | winpty_s::winpty_s() : 46 | controlPipe(nullptr), 47 | dataPipe(nullptr) 48 | { 49 | } 50 | 51 | /** 52 | * Helpers 53 | */ 54 | 55 | const wchar_t* to_wstring(const String::Utf8Value& str) 56 | { 57 | const char *bytes = *str; 58 | unsigned int sizeOfStr = MultiByteToWideChar(CP_ACP, 0, bytes, -1, NULL, 0); 59 | wchar_t *output = new wchar_t[sizeOfStr]; 60 | MultiByteToWideChar(CP_ACP, 0, bytes, -1, output, sizeOfStr); 61 | return output; 62 | } 63 | 64 | static winpty_t *get_pipe_handle(int handle) { 65 | for(size_t i = 0; i < ptyHandles.size(); ++i) { 66 | winpty_t *ptyHandle = ptyHandles[i]; 67 | int current = (int)ptyHandle->controlPipe; 68 | if(current == handle) { 69 | return ptyHandle; 70 | } 71 | } 72 | return nullptr; 73 | } 74 | 75 | static bool remove_pipe_handle(int handle) { 76 | for(size_t i = 0; i < ptyHandles.size(); ++i) { 77 | winpty_t *ptyHandle = ptyHandles[i]; 78 | if((int)ptyHandle->controlPipe == handle) { 79 | delete ptyHandle; 80 | ptyHandle = nullptr; 81 | return true; 82 | } 83 | } 84 | return false; 85 | } 86 | 87 | static bool file_exists(std::wstring filename) { 88 | DWORD attr = ::GetFileAttributesW(filename.c_str()); 89 | if(attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) { 90 | return false; 91 | } 92 | return true; 93 | } 94 | 95 | // cmd.exe -> C:\Windows\system32\cmd.exe 96 | static std::wstring get_shell_path(std::wstring filename) { 97 | 98 | std::wstring shellpath; 99 | 100 | if(file_exists(filename)) { 101 | return shellpath; 102 | } 103 | 104 | wchar_t buffer_[MAX_ENV]; 105 | int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV); 106 | if(!read) { 107 | return shellpath; 108 | } 109 | 110 | std::wstring delimiter = L";"; 111 | size_t pos = 0; 112 | vector paths; 113 | std::wstring buffer(buffer_); 114 | while ((pos = buffer.find(delimiter)) != std::wstring::npos) { 115 | paths.push_back(buffer.substr(0, pos)); 116 | buffer.erase(0, pos + delimiter.length()); 117 | } 118 | 119 | const wchar_t *filename_ = filename.c_str(); 120 | 121 | for (int i = 0; i < paths.size(); ++i) { 122 | wstring path = paths[i]; 123 | wchar_t searchPath[MAX_PATH]; 124 | ::PathCombineW(searchPath, const_cast(path.c_str()), filename_); 125 | 126 | if(searchPath == NULL) { 127 | continue; 128 | } 129 | 130 | if(file_exists(searchPath)) { 131 | shellpath = searchPath; 132 | break; 133 | } 134 | 135 | } 136 | 137 | return shellpath; 138 | } 139 | 140 | /* 141 | * PtyOpen 142 | * pty.open(dataPipe, cols, rows) 143 | * 144 | * If you need to debug winpty-agent.exe do the following: 145 | * ====================================================== 146 | * 147 | * 1) Install python 2.7 148 | * 2) Install win32pipe 149 | x86) http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win32-py2.7.exe/download 150 | x64) http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download 151 | * 3) Start deps/winpty/misc/DebugServer.py (Before you start node) 152 | * 153 | * Then you'll see output from winpty-agent.exe. 154 | * 155 | * Important part: 156 | * =============== 157 | * CreateProcess: success 8896 0 (Windows error code) 158 | * 159 | * Create test.js: 160 | * =============== 161 | * 162 | * var pty = require('./'); 163 | * 164 | * var term = pty.fork('cmd.exe', [], { 165 | * name: 'Windows Shell', 166 | * cols: 80, 167 | * rows: 30, 168 | * cwd: process.env.HOME, 169 | * env: process.env, 170 | * debug: true 171 | * }); 172 | * 173 | * term.on('data', function(data) { 174 | * console.log(data); 175 | * }); 176 | * 177 | */ 178 | 179 | static NAN_METHOD(PtyOpen) { 180 | Nan::HandleScope scope; 181 | 182 | if (info.Length() != 4 183 | || !info[0]->IsString() // dataPipe 184 | || !info[1]->IsNumber() // cols 185 | || !info[2]->IsNumber() // rows 186 | || !info[3]->IsBoolean()) // debug 187 | { 188 | return Nan::ThrowError("Usage: pty.open(dataPipe, cols, rows, debug)"); 189 | } 190 | 191 | std::wstring pipeName = to_wstring(String::Utf8Value(info[0]->ToString())); 192 | int cols = info[1]->Int32Value(); 193 | int rows = info[2]->Int32Value(); 194 | bool debug = info[3]->ToBoolean()->IsTrue(); 195 | 196 | // Enable/disable debugging 197 | SetEnvironmentVariable(WINPTY_DBG_VARIABLE, debug ? "1" : NULL); // NULL = deletes variable 198 | 199 | // Open a new pty session. 200 | winpty_t *pc = winpty_open_use_own_datapipe(pipeName.c_str(), cols, rows); 201 | 202 | // Error occured during startup of agent process. 203 | assert(pc != nullptr); 204 | 205 | // Save pty struct fpr later use. 206 | ptyHandles.insert(ptyHandles.end(), pc); 207 | 208 | // Pty object values. 209 | Local marshal = Nan::New(); 210 | marshal->Set(Nan::New("pid").ToLocalChecked(), Nan::New((int)pc->controlPipe)); 211 | marshal->Set(Nan::New("pty").ToLocalChecked(), Nan::New(InterlockedIncrement(&ptyCounter))); 212 | marshal->Set(Nan::New("fd").ToLocalChecked(), Nan::New(-1)); 213 | 214 | return info.GetReturnValue().Set(marshal); 215 | } 216 | 217 | /* 218 | * PtyStartProcess 219 | * pty.startProcess(pid, file, env, cwd); 220 | */ 221 | 222 | static NAN_METHOD(PtyStartProcess) { 223 | Nan::HandleScope scope; 224 | 225 | if (info.Length() != 5 226 | || !info[0]->IsNumber() // pid 227 | || !info[1]->IsString() // file 228 | || !info[2]->IsString() // cmdline 229 | || !info[3]->IsArray() // env 230 | || !info[4]->IsString()) // cwd 231 | { 232 | return Nan::ThrowError( 233 | "Usage: pty.startProcess(pid, file, cmdline, env, cwd)"); 234 | } 235 | 236 | std::stringstream why; 237 | 238 | // Get winpty_t by control pipe handle 239 | int pid = info[0]->Int32Value(); 240 | winpty_t *pc = get_pipe_handle(pid); 241 | assert(pc != nullptr); 242 | 243 | const wchar_t *filename = to_wstring(String::Utf8Value(info[1]->ToString())); 244 | const wchar_t *cmdline = to_wstring(String::Utf8Value(info[2]->ToString())); 245 | const wchar_t *cwd = to_wstring(String::Utf8Value(info[4]->ToString())); 246 | 247 | // create environment block 248 | wchar_t *env = NULL; 249 | const Handle envValues = Handle::Cast(info[3]); 250 | if(!envValues.IsEmpty()) { 251 | 252 | std::wstringstream envBlock; 253 | 254 | for(uint32_t i = 0; i < envValues->Length(); i++) { 255 | std::wstring envValue(to_wstring(String::Utf8Value(envValues->Get(i)->ToString()))); 256 | envBlock << envValue << L' '; 257 | } 258 | 259 | std::wstring output = envBlock.str(); 260 | 261 | size_t count = output.size(); 262 | env = new wchar_t[count + 2]; 263 | wcsncpy(env, output.c_str(), count); 264 | 265 | wcscat(env, L"\0"); 266 | } 267 | 268 | // use environment 'Path' variable to determine location of 269 | // the relative path that we have recieved (e.g cmd.exe) 270 | std::wstring shellpath; 271 | if(::PathIsRelativeW(filename)) { 272 | shellpath = get_shell_path(filename); 273 | } else { 274 | shellpath = filename; 275 | } 276 | 277 | std::string shellpath_(shellpath.begin(), shellpath.end()); 278 | 279 | if(shellpath.empty() || !file_exists(shellpath)) { 280 | goto invalid_filename; 281 | } 282 | 283 | goto open; 284 | 285 | open: 286 | int result = winpty_start_process(pc, shellpath.c_str(), cmdline, cwd, env); 287 | if(result != 0) { 288 | why << "Unable to start terminal process. Win32 error code: " << result; 289 | Nan::ThrowError(why.str().c_str()); 290 | } 291 | goto cleanup; 292 | 293 | invalid_filename: 294 | why << "File not found: " << shellpath_; 295 | Nan::ThrowError(why.str().c_str()); 296 | goto cleanup; 297 | 298 | cleanup: 299 | delete filename; 300 | delete cmdline; 301 | delete cwd; 302 | delete env; 303 | 304 | return info.GetReturnValue().SetUndefined(); 305 | } 306 | 307 | /* 308 | * PtyResize 309 | * pty.resize(pid, cols, rows); 310 | */ 311 | static NAN_METHOD(PtyResize) { 312 | Nan::HandleScope scope; 313 | 314 | if (info.Length() != 3 315 | || !info[0]->IsNumber() // pid 316 | || !info[1]->IsNumber() // cols 317 | || !info[2]->IsNumber()) // rows 318 | { 319 | return Nan::ThrowError("Usage: pty.resize(pid, cols, rows)"); 320 | } 321 | 322 | int handle = info[0]->Int32Value(); 323 | int cols = info[1]->Int32Value(); 324 | int rows = info[2]->Int32Value(); 325 | 326 | winpty_t *pc = get_pipe_handle(handle); 327 | 328 | assert(pc != nullptr); 329 | assert(0 == winpty_set_size(pc, cols, rows)); 330 | 331 | return info.GetReturnValue().SetUndefined(); 332 | } 333 | 334 | /* 335 | * PtyKill 336 | * pty.kill(pid); 337 | */ 338 | static NAN_METHOD(PtyKill) { 339 | Nan::HandleScope scope; 340 | 341 | if (info.Length() != 1 342 | || !info[0]->IsNumber()) // pid 343 | { 344 | return Nan::ThrowError("Usage: pty.kill(pid)"); 345 | } 346 | 347 | int handle = info[0]->Int32Value(); 348 | 349 | winpty_t *pc = get_pipe_handle(handle); 350 | 351 | assert(pc != nullptr); 352 | winpty_exit(pc); 353 | assert(true == remove_pipe_handle(handle)); 354 | 355 | return info.GetReturnValue().SetUndefined(); 356 | } 357 | 358 | /** 359 | * Init 360 | */ 361 | 362 | extern "C" void init(Handle target) { 363 | Nan::HandleScope scope; 364 | Nan::SetMethod(target, "open", PtyOpen); 365 | Nan::SetMethod(target, "startProcess", PtyStartProcess); 366 | Nan::SetMethod(target, "resize", PtyResize); 367 | Nan::SetMethod(target, "kill", PtyKill); 368 | }; 369 | 370 | NODE_MODULE(pty, init); 371 | -------------------------------------------------------------------------------- /lib/pty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * pty.js 3 | * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) 4 | * Binding to the pseudo terminals. 5 | */ 6 | 7 | var extend = require('extend'); 8 | var EventEmitter = require('events').EventEmitter; 9 | var pty = require('../build/Release/pty.node'); 10 | var net = require('net'); 11 | var tty = require('tty'); 12 | var nextTick = global.setImmediate || process.nextTick; 13 | 14 | var version = process.versions.node.split('.').map(function(n) { 15 | return +(n + '').split('-')[0]; 16 | }); 17 | 18 | /** 19 | * Terminal 20 | */ 21 | 22 | // Example: 23 | // var term = new Terminal('bash', [], { 24 | // name: 'xterm-color', 25 | // cols: 80, 26 | // rows: 24, 27 | // cwd: process.env.HOME, 28 | // env: process.env 29 | // }); 30 | 31 | function Terminal(file, args, opt) { 32 | if (!(this instanceof Terminal)) { 33 | return new Terminal(file, args, opt); 34 | } 35 | 36 | var self = this 37 | , env 38 | , cwd 39 | , name 40 | , cols 41 | , rows 42 | , uid 43 | , gid 44 | , term; 45 | 46 | // backward compatibility 47 | if (typeof args === 'string') { 48 | opt = { 49 | name: arguments[1], 50 | cols: arguments[2], 51 | rows: arguments[3], 52 | cwd: process.env.HOME 53 | }; 54 | args = []; 55 | } 56 | 57 | // for 'close' 58 | this._internalee = new EventEmitter; 59 | 60 | // arguments 61 | args = args || []; 62 | file = file || 'sh'; 63 | opt = opt || {}; 64 | 65 | cols = opt.cols || 80; 66 | rows = opt.rows || 24; 67 | 68 | uid = opt.uid != null ? opt.uid : -1; 69 | gid = opt.gid != null ? opt.gid : -1; 70 | 71 | if (opt.env) { 72 | opt.env = extend(process.env, opt.env); 73 | } else { 74 | opt.env = process.env; 75 | } 76 | env = extend({}, opt.env); 77 | 78 | if (opt.env === process.env) { 79 | // Make sure we didn't start our 80 | // server from inside tmux. 81 | delete env.TMUX; 82 | delete env.TMUX_PANE; 83 | 84 | // Make sure we didn't start 85 | // our server from inside screen. 86 | // http://web.mit.edu/gnu/doc/html/screen_20.html 87 | delete env.STY; 88 | delete env.WINDOW; 89 | 90 | // Delete some variables that 91 | // might confuse our terminal. 92 | delete env.WINDOWID; 93 | delete env.TERMCAP; 94 | delete env.COLUMNS; 95 | delete env.LINES; 96 | } 97 | 98 | // Could set some basic env vars 99 | // here, if they do not exist: 100 | // USER, SHELL, HOME, LOGNAME, WINDOWID 101 | 102 | cwd = opt.cwd || process.cwd(); 103 | name = opt.name || env.TERM || 'xterm'; 104 | env.TERM = name; 105 | // XXX Shouldn't be necessary: 106 | // env.LINES = rows + ''; 107 | // env.COLUMNS = cols + ''; 108 | 109 | env = environ(env); 110 | 111 | function onexit(code, signal) { 112 | // XXX Sometimes a data event is emitted 113 | // after exit. Wait til socket is destroyed. 114 | if (!self._emittedClose) { 115 | if (self._boundClose) return; 116 | self._boundClose = true; 117 | self.once('close', function() { 118 | self.emit('exit', code, signal); 119 | }); 120 | return; 121 | } 122 | self.emit('exit', code, signal); 123 | } 124 | 125 | // fork 126 | term = pty.fork(file, args, env, cwd, cols, rows, uid, gid, onexit); 127 | 128 | this.socket = TTYStream(term.fd); 129 | this.socket.setEncoding('utf8'); 130 | 131 | if (opt.resume !== false) { 132 | this.socket.resume(); 133 | } 134 | 135 | // setup 136 | this.socket.on('error', function(err) { 137 | // NOTE: fs.ReadStream gets EAGAIN twice at first: 138 | if (err.code) { 139 | if (~err.code.indexOf('EAGAIN')) return; 140 | } 141 | 142 | // close 143 | self._close(); 144 | // EIO on exit from fs.ReadStream: 145 | if (!self._emittedClose) { 146 | self._emittedClose = true; 147 | Terminal.total--; 148 | self.emit('close'); 149 | } 150 | 151 | // EIO, happens when someone closes our child 152 | // process: the only process in the terminal. 153 | // node < 0.6.14: errno 5 154 | // node >= 0.6.14: read EIO 155 | if (err.code) { 156 | if (~err.code.indexOf('errno 5') 157 | || ~err.code.indexOf('EIO')) return; 158 | } 159 | 160 | // throw anything else 161 | if (self.listeners('error').length < 2) { 162 | throw err; 163 | } 164 | }); 165 | 166 | this.pid = term.pid; 167 | this.fd = term.fd; 168 | this.pty = term.pty; 169 | 170 | this.file = file; 171 | this.name = name; 172 | this.cols = cols; 173 | this.rows = rows; 174 | 175 | this.readable = true; 176 | this.writable = true; 177 | 178 | Terminal.total++; 179 | 180 | this.socket.on('close', function() { 181 | if (self._emittedClose) return; 182 | self._emittedClose = true; 183 | Terminal.total--; 184 | self._close(); 185 | self.emit('close'); 186 | }); 187 | 188 | env = null; 189 | } 190 | 191 | Terminal.fork = 192 | Terminal.spawn = 193 | Terminal.createTerminal = function(file, args, opt) { 194 | return new Terminal(file, args, opt); 195 | }; 196 | 197 | /** 198 | * openpty 199 | */ 200 | 201 | Terminal.open = function(opt) { 202 | var self = Object.create(Terminal.prototype) 203 | , opt = opt || {}; 204 | 205 | if (arguments.length > 1) { 206 | opt = { 207 | cols: arguments[1], 208 | rows: arguments[2] 209 | }; 210 | } 211 | 212 | var cols = opt.cols || 80 213 | , rows = opt.rows || 24 214 | , term; 215 | 216 | // open 217 | term = pty.open(cols, rows); 218 | 219 | self.master = TTYStream(term.master); 220 | self.master.setEncoding('utf8'); 221 | self.master.resume(); 222 | 223 | self.slave = TTYStream(term.slave); 224 | self.slave.setEncoding('utf8'); 225 | self.slave.resume(); 226 | 227 | self.socket = self.master; 228 | self.pid = null; 229 | self.fd = term.master; 230 | self.pty = term.pty; 231 | 232 | self.file = process.argv[0] || 'node'; 233 | self.name = process.env.TERM || ''; 234 | self.cols = cols; 235 | self.rows = rows; 236 | 237 | self.readable = true; 238 | self.writable = true; 239 | 240 | self.socket.on('error', function(err) { 241 | Terminal.total--; 242 | self._close(); 243 | if (self.listeners('error').length < 2) { 244 | throw err; 245 | } 246 | }); 247 | 248 | Terminal.total++; 249 | self.socket.on('close', function() { 250 | Terminal.total--; 251 | self._close(); 252 | }); 253 | 254 | return self; 255 | }; 256 | 257 | /** 258 | * Total 259 | */ 260 | 261 | // Keep track of the total 262 | // number of terminals for 263 | // the process. 264 | Terminal.total = 0; 265 | 266 | /** 267 | * Events 268 | */ 269 | 270 | Terminal.prototype.write = function(data) { 271 | return this.socket.write(data); 272 | }; 273 | 274 | Terminal.prototype.end = function(data) { 275 | return this.socket.end(data); 276 | }; 277 | 278 | Terminal.prototype.pipe = function(dest, options) { 279 | return this.socket.pipe(dest, options); 280 | }; 281 | 282 | Terminal.prototype.pause = function() { 283 | return this.socket.pause(); 284 | }; 285 | 286 | Terminal.prototype.resume = function() { 287 | return this.socket.resume(); 288 | }; 289 | 290 | Terminal.prototype.setEncoding = function(enc) { 291 | if (this.socket._decoder) { 292 | delete this.socket._decoder; 293 | } 294 | if (enc) { 295 | this.socket.setEncoding(enc); 296 | } 297 | }; 298 | 299 | Terminal.prototype.addListener = 300 | Terminal.prototype.on = function(type, func) { 301 | if (type === 'close') { 302 | this._internalee.on('close', func); 303 | return this; 304 | } 305 | this.socket.on(type, func); 306 | return this; 307 | }; 308 | 309 | Terminal.prototype.emit = function(evt) { 310 | if (evt === 'close') { 311 | return this._internalee.emit.apply(this._internalee, arguments); 312 | } 313 | return this.socket.emit.apply(this.socket, arguments); 314 | }; 315 | 316 | Terminal.prototype.listeners = function(type) { 317 | return this.socket.listeners(type); 318 | }; 319 | 320 | Terminal.prototype.removeListener = function(type, func) { 321 | this.socket.removeListener(type, func); 322 | return this; 323 | }; 324 | 325 | Terminal.prototype.removeAllListeners = function(type) { 326 | this.socket.removeAllListeners(type); 327 | return this; 328 | }; 329 | 330 | Terminal.prototype.once = function(type, func) { 331 | this.socket.once(type, func); 332 | return this; 333 | }; 334 | 335 | Terminal.prototype.__defineGetter__('stdin', function() { 336 | return this; 337 | }); 338 | 339 | Terminal.prototype.__defineGetter__('stdout', function() { 340 | return this; 341 | }); 342 | 343 | Terminal.prototype.__defineGetter__('stderr', function() { 344 | throw new Error('No stderr.'); 345 | }); 346 | 347 | /** 348 | * TTY 349 | */ 350 | 351 | Terminal.prototype.resize = function(cols, rows) { 352 | cols = cols || 80; 353 | rows = rows || 24; 354 | 355 | this.cols = cols; 356 | this.rows = rows; 357 | 358 | pty.resize(this.fd, cols, rows); 359 | }; 360 | 361 | Terminal.prototype.destroy = function() { 362 | var self = this; 363 | 364 | // close 365 | this._close(); 366 | 367 | // Need to close the read stream so 368 | // node stops reading a dead file descriptor. 369 | // Then we can safely SIGHUP the shell. 370 | this.socket.once('close', function() { 371 | self.kill('SIGHUP'); 372 | }); 373 | 374 | this.socket.destroy(); 375 | }; 376 | 377 | Terminal.prototype.kill = function(sig) { 378 | try { 379 | process.kill(this.pid, sig || 'SIGHUP'); 380 | } catch(e) { 381 | ; 382 | } 383 | }; 384 | 385 | Terminal.prototype.redraw = function() { 386 | var self = this 387 | , cols = this.cols 388 | , rows = this.rows; 389 | 390 | // We could just send SIGWINCH, but most programs will 391 | // ignore it if the size hasn't actually changed. 392 | 393 | this.resize(cols + 1, rows + 1); 394 | 395 | setTimeout(function() { 396 | self.resize(cols, rows); 397 | }, 30); 398 | }; 399 | 400 | Terminal.prototype.__defineGetter__('process', function() { 401 | return pty.process(this.fd, this.pty) || this.file; 402 | }); 403 | 404 | Terminal.prototype._close = function() { 405 | this.socket.writable = false; 406 | this.socket.readable = false; 407 | this.write = function() {}; 408 | this.end = function() {}; 409 | this.writable = false; 410 | this.readable = false; 411 | }; 412 | 413 | /** 414 | * TTY Stream 415 | */ 416 | 417 | function TTYStream(fd) { 418 | // Could use: if (!require('tty').ReadStream) 419 | if (version[0] === 0 && version[1] < 7) { 420 | return new net.Socket(fd); 421 | } 422 | 423 | if (version[0] === 0 && version[1] < 12) { 424 | return new tty.ReadStream(fd); 425 | } 426 | 427 | return new Socket(fd); 428 | } 429 | 430 | /** 431 | * Wrap net.Socket for a workaround 432 | */ 433 | 434 | function Socket(options) { 435 | if (!(this instanceof Socket)) { 436 | return new Socket(options); 437 | } 438 | var tty = process.binding('tty_wrap'); 439 | var guessHandleType = tty.guessHandleType; 440 | tty.guessHandleType = function() { 441 | return 'PIPE'; 442 | }; 443 | net.Socket.call(this, options); 444 | tty.guessHandleType = guessHandleType; 445 | } 446 | 447 | Socket.prototype.__proto__ = net.Socket.prototype; 448 | 449 | /** 450 | * Helpers 451 | */ 452 | 453 | function environ(env) { 454 | var keys = Object.keys(env || {}) 455 | , l = keys.length 456 | , i = 0 457 | , pairs = []; 458 | 459 | for (; i < l; i++) { 460 | pairs.push(keys[i] + '=' + env[keys[i]]); 461 | } 462 | 463 | return pairs; 464 | } 465 | 466 | /** 467 | * Expose 468 | */ 469 | 470 | module.exports = exports = Terminal; 471 | exports.Terminal = Terminal; 472 | exports.native = pty; 473 | -------------------------------------------------------------------------------- /src/unix/pty.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * pty.js 3 | * Copyright (c) 2012-2015, Christopher Jeffrey (MIT License) 4 | * 5 | * pty.cc: 6 | * This file is responsible for starting processes 7 | * with pseudo-terminal file descriptors. 8 | * 9 | * See: 10 | * man pty 11 | * man tty_ioctl 12 | * man termios 13 | * man forkpty 14 | */ 15 | 16 | /** 17 | * Includes 18 | */ 19 | 20 | #include "nan.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | /* forkpty */ 34 | /* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */ 35 | #if defined(__GLIBC__) || defined(__CYGWIN__) 36 | #include 37 | #elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) 38 | /** 39 | * From node v0.10.28 (at least?) there is also a "util.h" in node/src, which 40 | * would confuse the compiler when looking for "util.h". 41 | */ 42 | #if NODE_VERSION_AT_LEAST(0, 10, 28) 43 | #include <../include/util.h> 44 | #else 45 | #include 46 | #endif 47 | #elif defined(__FreeBSD__) 48 | #include 49 | #elif defined(__sun) 50 | #include /* for I_PUSH */ 51 | #else 52 | #include 53 | #endif 54 | 55 | #include /* tcgetattr, tty_ioctl */ 56 | 57 | /* environ for execvpe */ 58 | /* node/src/node_child_process.cc */ 59 | #if defined(__APPLE__) && !TARGET_OS_IPHONE 60 | #include 61 | #define environ (*_NSGetEnviron()) 62 | #else 63 | extern char **environ; 64 | #endif 65 | 66 | /* for pty_getproc */ 67 | #if defined(__linux__) 68 | #include 69 | #include 70 | #elif defined(__APPLE__) 71 | #include 72 | #include 73 | #endif 74 | 75 | /** 76 | * Namespace 77 | */ 78 | 79 | using namespace node; 80 | using namespace v8; 81 | 82 | /** 83 | * Structs 84 | */ 85 | 86 | struct pty_baton { 87 | Nan::Persistent cb; 88 | int exit_code; 89 | int signal_code; 90 | pid_t pid; 91 | uv_async_t async; 92 | uv_thread_t tid; 93 | }; 94 | 95 | /** 96 | * Methods 97 | */ 98 | 99 | NAN_METHOD(PtyFork); 100 | NAN_METHOD(PtyOpen); 101 | NAN_METHOD(PtyResize); 102 | NAN_METHOD(PtyGetProc); 103 | 104 | /** 105 | * Functions 106 | */ 107 | 108 | static int 109 | pty_execvpe(const char *, char **, char **); 110 | 111 | static int 112 | pty_nonblock(int); 113 | 114 | static char * 115 | pty_getproc(int, char *); 116 | 117 | static int 118 | pty_openpty(int *, int *, char *, 119 | const struct termios *, 120 | const struct winsize *); 121 | 122 | static pid_t 123 | pty_forkpty(int *, char *, 124 | const struct termios *, 125 | const struct winsize *); 126 | 127 | static void 128 | pty_waitpid(void *); 129 | 130 | static void 131 | #if NODE_VERSION_AT_LEAST(0, 11, 0) 132 | pty_after_waitpid(uv_async_t *); 133 | #else 134 | pty_after_waitpid(uv_async_t *, int); 135 | #endif 136 | 137 | static void 138 | pty_after_close(uv_handle_t *); 139 | 140 | /** 141 | * PtyFork 142 | * pty.fork(file, args, env, cwd, cols, rows, uid, gid, onexit) 143 | */ 144 | 145 | NAN_METHOD(PtyFork) { 146 | Nan::HandleScope scope; 147 | 148 | if (info.Length() != 9 149 | || !info[0]->IsString() // file 150 | || !info[1]->IsArray() // args 151 | || !info[2]->IsArray() // env 152 | || !info[3]->IsString() // cwd 153 | || !info[4]->IsNumber() // cols 154 | || !info[5]->IsNumber() // rows 155 | || !info[6]->IsNumber() // uid 156 | || !info[7]->IsNumber() // gid 157 | || !info[8]->IsFunction() // onexit 158 | ) { 159 | return Nan::ThrowError( 160 | "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, onexit)"); 161 | } 162 | 163 | // file 164 | String::Utf8Value file(info[0]->ToString()); 165 | 166 | // args 167 | int i = 0; 168 | Local argv_ = Local::Cast(info[1]); 169 | int argc = argv_->Length(); 170 | int argl = argc + 1 + 1; 171 | char **argv = new char*[argl]; 172 | argv[0] = strdup(*file); 173 | argv[argl-1] = NULL; 174 | for (; i < argc; i++) { 175 | String::Utf8Value arg(argv_->Get(Nan::New(i))->ToString()); 176 | argv[i+1] = strdup(*arg); 177 | } 178 | 179 | // env 180 | i = 0; 181 | Local env_ = Local::Cast(info[2]); 182 | int envc = env_->Length(); 183 | char **env = new char*[envc+1]; 184 | env[envc] = NULL; 185 | for (; i < envc; i++) { 186 | String::Utf8Value pair(env_->Get(Nan::New(i))->ToString()); 187 | env[i] = strdup(*pair); 188 | } 189 | 190 | // cwd 191 | String::Utf8Value cwd_(info[3]->ToString()); 192 | char *cwd = strdup(*cwd_); 193 | 194 | // size 195 | struct winsize winp; 196 | winp.ws_col = info[4]->IntegerValue(); 197 | winp.ws_row = info[5]->IntegerValue(); 198 | winp.ws_xpixel = 0; 199 | winp.ws_ypixel = 0; 200 | 201 | // uid / gid 202 | int uid = info[6]->IntegerValue(); 203 | int gid = info[7]->IntegerValue(); 204 | 205 | // fork the pty 206 | int master = -1; 207 | char name[40]; 208 | pid_t pid = pty_forkpty(&master, name, NULL, &winp); 209 | 210 | if (pid) { 211 | for (i = 0; i < argl; i++) free(argv[i]); 212 | delete[] argv; 213 | for (i = 0; i < envc; i++) free(env[i]); 214 | delete[] env; 215 | free(cwd); 216 | } 217 | 218 | switch (pid) { 219 | case -1: 220 | return Nan::ThrowError("forkpty(3) failed."); 221 | case 0: 222 | if (strlen(cwd)) chdir(cwd); 223 | 224 | if (uid != -1 && gid != -1) { 225 | if (setgid(gid) == -1) { 226 | perror("setgid(2) failed."); 227 | _exit(1); 228 | } 229 | if (setuid(uid) == -1) { 230 | perror("setuid(2) failed."); 231 | _exit(1); 232 | } 233 | } 234 | 235 | pty_execvpe(argv[0], argv, env); 236 | 237 | perror("execvp(3) failed."); 238 | _exit(1); 239 | default: 240 | if (pty_nonblock(master) == -1) { 241 | return Nan::ThrowError("Could not set master fd to nonblocking."); 242 | } 243 | 244 | Local obj = Nan::New(); 245 | Nan::Set(obj, 246 | Nan::New("fd").ToLocalChecked(), 247 | Nan::New(master)); 248 | Nan::Set(obj, 249 | Nan::New("pid").ToLocalChecked(), 250 | Nan::New(pid)); 251 | Nan::Set(obj, 252 | Nan::New("pty").ToLocalChecked(), 253 | Nan::New(name).ToLocalChecked()); 254 | 255 | pty_baton *baton = new pty_baton(); 256 | baton->exit_code = 0; 257 | baton->signal_code = 0; 258 | baton->cb.Reset(Local::Cast(info[8])); 259 | baton->pid = pid; 260 | baton->async.data = baton; 261 | 262 | uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid); 263 | 264 | uv_thread_create(&baton->tid, pty_waitpid, static_cast(baton)); 265 | 266 | return info.GetReturnValue().Set(obj); 267 | } 268 | 269 | return info.GetReturnValue().SetUndefined(); 270 | } 271 | 272 | /** 273 | * PtyOpen 274 | * pty.open(cols, rows) 275 | */ 276 | 277 | NAN_METHOD(PtyOpen) { 278 | Nan::HandleScope scope; 279 | 280 | if (info.Length() != 2 281 | || !info[0]->IsNumber() 282 | || !info[1]->IsNumber()) { 283 | return Nan::ThrowError("Usage: pty.open(cols, rows)"); 284 | } 285 | 286 | // size 287 | struct winsize winp; 288 | winp.ws_col = info[0]->IntegerValue(); 289 | winp.ws_row = info[1]->IntegerValue(); 290 | winp.ws_xpixel = 0; 291 | winp.ws_ypixel = 0; 292 | 293 | // pty 294 | int master, slave; 295 | char name[40]; 296 | int ret = pty_openpty(&master, &slave, name, NULL, &winp); 297 | 298 | if (ret == -1) { 299 | return Nan::ThrowError("openpty(3) failed."); 300 | } 301 | 302 | if (pty_nonblock(master) == -1) { 303 | return Nan::ThrowError("Could not set master fd to nonblocking."); 304 | } 305 | 306 | if (pty_nonblock(slave) == -1) { 307 | return Nan::ThrowError("Could not set slave fd to nonblocking."); 308 | } 309 | 310 | Local obj = Nan::New(); 311 | Nan::Set(obj, 312 | Nan::New("master").ToLocalChecked(), 313 | Nan::New(master)); 314 | Nan::Set(obj, 315 | Nan::New("slave").ToLocalChecked(), 316 | Nan::New(slave)); 317 | Nan::Set(obj, 318 | Nan::New("pty").ToLocalChecked(), 319 | Nan::New(name).ToLocalChecked()); 320 | 321 | return info.GetReturnValue().Set(obj); 322 | } 323 | 324 | /** 325 | * Resize Functionality 326 | * pty.resize(fd, cols, rows) 327 | */ 328 | 329 | NAN_METHOD(PtyResize) { 330 | Nan::HandleScope scope; 331 | 332 | if (info.Length() != 3 333 | || !info[0]->IsNumber() 334 | || !info[1]->IsNumber() 335 | || !info[2]->IsNumber()) { 336 | return Nan::ThrowError("Usage: pty.resize(fd, cols, rows)"); 337 | } 338 | 339 | int fd = info[0]->IntegerValue(); 340 | 341 | struct winsize winp; 342 | winp.ws_col = info[1]->IntegerValue(); 343 | winp.ws_row = info[2]->IntegerValue(); 344 | winp.ws_xpixel = 0; 345 | winp.ws_ypixel = 0; 346 | 347 | if (ioctl(fd, TIOCSWINSZ, &winp) == -1) { 348 | return Nan::ThrowError("ioctl(2) failed."); 349 | } 350 | 351 | return info.GetReturnValue().SetUndefined(); 352 | } 353 | 354 | /** 355 | * PtyGetProc 356 | * Foreground Process Name 357 | * pty.process(fd, tty) 358 | */ 359 | 360 | NAN_METHOD(PtyGetProc) { 361 | Nan::HandleScope scope; 362 | 363 | if (info.Length() != 2 364 | || !info[0]->IsNumber() 365 | || !info[1]->IsString()) { 366 | return Nan::ThrowError("Usage: pty.process(fd, tty)"); 367 | } 368 | 369 | int fd = info[0]->IntegerValue(); 370 | 371 | String::Utf8Value tty_(info[1]->ToString()); 372 | char *tty = strdup(*tty_); 373 | char *name = pty_getproc(fd, tty); 374 | free(tty); 375 | 376 | if (name == NULL) { 377 | return info.GetReturnValue().SetUndefined(); 378 | } 379 | 380 | Local name_ = Nan::New(name).ToLocalChecked(); 381 | free(name); 382 | return info.GetReturnValue().Set(name_); 383 | } 384 | 385 | /** 386 | * execvpe 387 | */ 388 | 389 | // execvpe(3) is not portable. 390 | // http://www.gnu.org/software/gnulib/manual/html_node/execvpe.html 391 | static int 392 | pty_execvpe(const char *file, char **argv, char **envp) { 393 | char **old = environ; 394 | environ = envp; 395 | int ret = execvp(file, argv); 396 | environ = old; 397 | return ret; 398 | } 399 | 400 | /** 401 | * Nonblocking FD 402 | */ 403 | 404 | static int 405 | pty_nonblock(int fd) { 406 | int flags = fcntl(fd, F_GETFL, 0); 407 | if (flags == -1) return -1; 408 | return fcntl(fd, F_SETFL, flags | O_NONBLOCK); 409 | } 410 | 411 | /** 412 | * pty_waitpid 413 | * Wait for SIGCHLD to read exit status. 414 | */ 415 | 416 | static void 417 | pty_waitpid(void *data) { 418 | int ret; 419 | int stat_loc; 420 | 421 | pty_baton *baton = static_cast(data); 422 | 423 | errno = 0; 424 | 425 | if ((ret = waitpid(baton->pid, &stat_loc, 0)) != baton->pid) { 426 | if (ret == -1 && errno == EINTR) { 427 | return pty_waitpid(baton); 428 | } 429 | if (ret == -1 && errno == ECHILD) { 430 | // XXX node v0.8.x seems to have this problem. 431 | // waitpid is already handled elsewhere. 432 | ; 433 | } else { 434 | assert(false); 435 | } 436 | } 437 | 438 | if (WIFEXITED(stat_loc)) { 439 | baton->exit_code = WEXITSTATUS(stat_loc); // errno? 440 | } 441 | 442 | if (WIFSIGNALED(stat_loc)) { 443 | baton->signal_code = WTERMSIG(stat_loc); 444 | } 445 | 446 | uv_async_send(&baton->async); 447 | } 448 | 449 | /** 450 | * pty_after_waitpid 451 | * Callback after exit status has been read. 452 | */ 453 | 454 | static void 455 | #if NODE_VERSION_AT_LEAST(0, 11, 0) 456 | pty_after_waitpid(uv_async_t *async) { 457 | #else 458 | pty_after_waitpid(uv_async_t *async, int unhelpful) { 459 | #endif 460 | Nan::HandleScope scope; 461 | pty_baton *baton = static_cast(async->data); 462 | 463 | Local argv[] = { 464 | Nan::New(baton->exit_code), 465 | Nan::New(baton->signal_code), 466 | }; 467 | 468 | Local cb = Nan::New(baton->cb); 469 | baton->cb.Reset(); 470 | memset(&baton->cb, -1, sizeof(baton->cb)); 471 | Nan::Callback(cb).Call(Nan::GetCurrentContext()->Global(), 2, argv); 472 | 473 | uv_close((uv_handle_t *)async, pty_after_close); 474 | } 475 | 476 | /** 477 | * pty_after_close 478 | * uv_close() callback - free handle data 479 | */ 480 | 481 | static void 482 | pty_after_close(uv_handle_t *handle) { 483 | uv_async_t *async = (uv_async_t *)handle; 484 | pty_baton *baton = static_cast(async->data); 485 | delete baton; 486 | } 487 | 488 | /** 489 | * pty_getproc 490 | * Taken from tmux. 491 | */ 492 | 493 | // Taken from: tmux (http://tmux.sourceforge.net/) 494 | // Copyright (c) 2009 Nicholas Marriott 495 | // Copyright (c) 2009 Joshua Elsasser 496 | // Copyright (c) 2009 Todd Carson 497 | // 498 | // Permission to use, copy, modify, and distribute this software for any 499 | // purpose with or without fee is hereby granted, provided that the above 500 | // copyright notice and this permission notice appear in all copies. 501 | // 502 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 503 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 504 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 505 | // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 506 | // WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER 507 | // IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING 508 | // OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 509 | 510 | #if defined(__linux__) 511 | 512 | static char * 513 | pty_getproc(int fd, char *tty) { 514 | FILE *f; 515 | char *path, *buf; 516 | size_t len; 517 | int ch; 518 | pid_t pgrp; 519 | int r; 520 | 521 | if ((pgrp = tcgetpgrp(fd)) == -1) { 522 | return NULL; 523 | } 524 | 525 | r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp); 526 | if (r == -1 || path == NULL) return NULL; 527 | 528 | if ((f = fopen(path, "r")) == NULL) { 529 | free(path); 530 | return NULL; 531 | } 532 | 533 | free(path); 534 | 535 | len = 0; 536 | buf = NULL; 537 | while ((ch = fgetc(f)) != EOF) { 538 | if (ch == '\0') break; 539 | buf = (char *)realloc(buf, len + 2); 540 | if (buf == NULL) return NULL; 541 | buf[len++] = ch; 542 | } 543 | 544 | if (buf != NULL) { 545 | buf[len] = '\0'; 546 | } 547 | 548 | fclose(f); 549 | return buf; 550 | } 551 | 552 | #elif defined(__APPLE__) 553 | 554 | static char * 555 | pty_getproc(int fd, char *tty) { 556 | int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 }; 557 | size_t size; 558 | struct kinfo_proc kp; 559 | 560 | if ((mib[3] = tcgetpgrp(fd)) == -1) { 561 | return NULL; 562 | } 563 | 564 | size = sizeof kp; 565 | if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) { 566 | return NULL; 567 | } 568 | 569 | if (*kp.kp_proc.p_comm == '\0') { 570 | return NULL; 571 | } 572 | 573 | return strdup(kp.kp_proc.p_comm); 574 | } 575 | 576 | #else 577 | 578 | static char * 579 | pty_getproc(int fd, char *tty) { 580 | return NULL; 581 | } 582 | 583 | #endif 584 | 585 | /** 586 | * openpty(3) / forkpty(3) 587 | */ 588 | 589 | static int 590 | pty_openpty(int *amaster, int *aslave, char *name, 591 | const struct termios *termp, 592 | const struct winsize *winp) { 593 | #if defined(__sun) 594 | char *slave_name; 595 | int slave; 596 | int master = open("/dev/ptmx", O_RDWR | O_NOCTTY); 597 | if (master == -1) return -1; 598 | if (amaster) *amaster = master; 599 | 600 | if (grantpt(master) == -1) goto err; 601 | if (unlockpt(master) == -1) goto err; 602 | 603 | slave_name = ptsname(master); 604 | if (slave_name == NULL) goto err; 605 | if (name) strcpy(name, slave_name); 606 | 607 | slave = open(slave_name, O_RDWR | O_NOCTTY); 608 | if (slave == -1) goto err; 609 | if (aslave) *aslave = slave; 610 | 611 | ioctl(slave, I_PUSH, "ptem"); 612 | ioctl(slave, I_PUSH, "ldterm"); 613 | ioctl(slave, I_PUSH, "ttcompat"); 614 | 615 | if (termp) tcsetattr(slave, TCSAFLUSH, termp); 616 | if (winp) ioctl(slave, TIOCSWINSZ, winp); 617 | 618 | return 0; 619 | 620 | err: 621 | close(master); 622 | return -1; 623 | #else 624 | return openpty(amaster, aslave, name, (termios *)termp, (winsize *)winp); 625 | #endif 626 | } 627 | 628 | static pid_t 629 | pty_forkpty(int *amaster, char *name, 630 | const struct termios *termp, 631 | const struct winsize *winp) { 632 | #if defined(__sun) 633 | int master, slave; 634 | 635 | int ret = pty_openpty(&master, &slave, name, termp, winp); 636 | if (ret == -1) return -1; 637 | if (amaster) *amaster = master; 638 | 639 | pid_t pid = fork(); 640 | 641 | switch (pid) { 642 | case -1: 643 | close(master); 644 | close(slave); 645 | return -1; 646 | case 0: 647 | close(master); 648 | 649 | setsid(); 650 | 651 | #if defined(TIOCSCTTY) 652 | // glibc does this 653 | if (ioctl(slave, TIOCSCTTY, NULL) == -1) { 654 | _exit(1); 655 | } 656 | #endif 657 | 658 | dup2(slave, 0); 659 | dup2(slave, 1); 660 | dup2(slave, 2); 661 | 662 | if (slave > 2) close(slave); 663 | 664 | return 0; 665 | default: 666 | close(slave); 667 | return pid; 668 | } 669 | 670 | return -1; 671 | #else 672 | return forkpty(amaster, name, (termios *)termp, (winsize *)winp); 673 | #endif 674 | } 675 | 676 | /** 677 | * Init 678 | */ 679 | 680 | NAN_MODULE_INIT(init) { 681 | Nan::HandleScope scope; 682 | Nan::Set(target, 683 | Nan::New("fork").ToLocalChecked(), 684 | Nan::New(PtyFork)->GetFunction()); 685 | Nan::Set(target, 686 | Nan::New("open").ToLocalChecked(), 687 | Nan::New(PtyOpen)->GetFunction()); 688 | Nan::Set(target, 689 | Nan::New("resize").ToLocalChecked(), 690 | Nan::New(PtyResize)->GetFunction()); 691 | Nan::Set(target, 692 | Nan::New("process").ToLocalChecked(), 693 | Nan::New(PtyGetProc)->GetFunction()); 694 | } 695 | 696 | NODE_MODULE(pty, init) 697 | --------------------------------------------------------------------------------