/** # cp-remote   [![Build Status](https://github.com/davedoesdev/cp-remote/actions/workflows/ci.yml/badge.svg)](https://github.com/davedoesdev/cp-remote/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/davedoesdev/cp-remote/badge.png?branch=master)](https://coveralls.io/r/davedoesdev/cp-remote?branch=master) [![NPM version](https://badge.fury.io/js/cp-remote.png)](http://badge.fury.io/js/cp-remote) Node.js [`child_process`](http://nodejs.org/api/child_process.html) done remotely, IPC channel intact! Example: ```javascript var cp_remote = require('cp-remote'); var assert = require('assert'); var remote = cp_remote.run('host', '/path/on/host/to/sub.js', 'foo', { answer: 42 }); remote.on('message', function (msg) { assert.deepEqual(msg, { foo: 'bar' }); }); remote.send({ hello: 'world' }); ``` You might implement the remote script, `sub.js`, like this: ```javascript var assert = require('assert'); assert.equal(process.argv[2], 'foo') assert.deepEqual(process.argv[3], { answer: 42 }); process.on('message', function (msg) { assert.deepEqual(msg, { hello: 'world' }); process.disconnect(); }); process.send({ foo: 'bar' }); ``` The API is described [here](#tableofcontents). ## Pre-requisites Client: - SSH client (e.g. [OpenSSH](http://www.openssh.com)), configured for password-less logon to the remote host (e.g. using a private key). - [Node.js](http://www.nodejs.org) (of course) - [Bash](https://www.gnu.org/software/bash/bash.html) - [socat](http://www.dest-unreach.org/socat/) Remote host: - SSH server (e.g. [OpenSSH](http://www.openssh.com)) - Node.js (`node` command should be in the remote `PATH` of SSH sessions) - [Python](http://www.python.org) (it provides access to [`socketpair`](http://pubs.opengroup.org/onlinepubs/009695399/functions/socketpair.html), Node does not) - [socat](http://www.dest-unreach.org/socat/) ## Installation ```shell npm install cp-remote ``` ## Limitation You can't pass handles to a remote child process like you can with local child processes. ## How it works ![How it works](http://rawgit.davedoesdev.com/davedoesdev/cp-remote/master/diagrams/how_it_works.svg) - `cp-remote` calls [`child_process.spawn`](http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) to run a Bash script, [`cp-remote.sh`](cp-remote.sh). The IPC channel will be on `$NODE_CHANNEL_FD`. - `cp-remote.sh` runs `socat`, telling it to relay data between `$NODE_CHANNEL_FD` and an SSH connection to the remote host. - The SSH connection is told to start [`cp-remote.py`](cp-remote.py) on the remote host. - `cp-remote.py` calls [`socket.socketpair`](http://docs.python.org/3/library/socket.html#socket.socketpair) to create a pair of connected file descriptors (Unix domain sockets). - `cp-remote.py` starts `socat`, telling it to relay data between standard input (i.e. the SSH connection) and one of the connected file descriptors. - `cp-remote.py` sets `NODE_CHANNEL_FD` to the other connected file descriptor and starts `node`, telling it to run the module you specified. ## Licence [MIT](LICENCE) ## Test To test creating and communicating with remote child processes: ```shell grunt test --remote= --remote= ... ``` You can specify as many remote hosts as you like. The test will try to create a remote child process on each host and then communicate with each one. It assumes the `cp-remote` module is installed at the same path on each host. ## Lint ```shell grunt lint ``` ## Code Coverage ```shell grunt coverage --remote= --remote= ... ``` [c8](https://github.com/bcoe/c8) results are available [here](http://rawgit.davedoesdev.com/davedoesdev/cp-remote/master/coverage/lcov-report/index.html). Coveralls page is [here](https://coveralls.io/r/davedoesdev/cp-remote). # API */ /*jslint node: true, nomen: true */ "use strict"; var child_process = require('child_process'), path = require('path'), packed = require('./packed'); /** Run a Node.js module on a remote host and return a [`child_process.ChildProcess`](http://nodejs.org/api/child_process.html#child_process_class_childprocess) object for communication with it. @param {String} host The name (or IP address) of the remote host to run the module on. @param {String} module_path The path to the module _on the remote host_. Any arguments following `module_path` will be made available to the module in its `process.argv` (starting at the third element). @return {child_process.ChildProcess} The `ChildProcess` object for the remote process. You can do the same things with this object as a local `ChildProcess`, except send it handles (i.e. the optional `sendHandle` parameter to [`child.send`](http://nodejs.org/api/child_process.html#child_process_child_send_message_sendhandle) isn't supported). */ function run(host, module_path) { var child = child_process.spawn( path.join(__dirname, 'cp-remote.sh'), [host, module_path], { stdio: [0, 1, 2, 'ipc'] }); child.send(packed.toString()); child.send(Buffer.from(JSON.stringify( Array.prototype.slice.call(arguments, 2))).toString('hex')); return child; } exports.run = run;