-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
135 lines (96 loc) · 5.12 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
# 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=<host1> --remote=<host2> ...
```
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=<host1> --remote=<host2> ...
```
[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;