ssh-client-wrapper 1.8.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/coverage_report.yml +22 -0
- package/.github/workflows/{npm-publish.yml → npm_publish.yml} +2 -2
- package/.github/workflows/run_test.yml +6 -0
- package/lib/fork.js +36 -23
- package/lib/index.js +52 -35
- package/lib/rsyncExec.js +8 -6
- package/lib/sshExec.js +37 -68
- package/lib/util.js +57 -3
- package/package.json +3 -2
- package/test/interface.js +180 -0
- package/test/rsyncExec.js +67 -67
- package/test/sshExec.js +12 -9
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: report coverage data to code climate
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- 'v*'
|
|
6
|
+
jobs:
|
|
7
|
+
coverage:
|
|
8
|
+
needs:
|
|
9
|
+
- test
|
|
10
|
+
name: coverage report
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v3
|
|
14
|
+
- uses: actions/download-artifact@v3
|
|
15
|
+
with:
|
|
16
|
+
name: code-coverage-report
|
|
17
|
+
- uses: paambaati/codeclimate-action@v5.0.0
|
|
18
|
+
env:
|
|
19
|
+
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
|
20
|
+
with:
|
|
21
|
+
coverageLocations: "coverage-report.lcov:lcov"
|
|
22
|
+
debug: true
|
|
@@ -14,9 +14,9 @@ jobs:
|
|
|
14
14
|
- uses: actions/checkout@v3
|
|
15
15
|
- uses: actions/setup-node@v3
|
|
16
16
|
with:
|
|
17
|
-
node-version:
|
|
17
|
+
node-version: 20
|
|
18
18
|
registry-url: https://registry.npmjs.org/
|
|
19
19
|
- run: npm ci
|
|
20
20
|
- run: npm publish
|
|
21
21
|
env:
|
|
22
|
-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
|
22
|
+
NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
|
package/lib/fork.js
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const debug = require("debug")("sshClientWrapper:fork");
|
|
3
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
4
|
-
const debugMasterVerbose = require("debug")("sshClientWrapper:
|
|
5
|
-
const debugSendPty = require("debug")("sshClientWrapper:fork_sendpty");
|
|
2
|
+
const debug = require("debug")("sshClientWrapper:debug:fork");
|
|
3
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:fork");
|
|
4
|
+
const debugMasterVerbose = require("debug")("sshClientWrapper:insecure:fork_masterPty");
|
|
5
|
+
const debugSendPty = require("debug")("sshClientWrapper:insecure:fork_sendpty");
|
|
6
6
|
const { spawn } = require("node-pty");
|
|
7
7
|
|
|
8
|
-
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String } = require("./util.js");
|
|
8
|
+
const { rePwPrompt, rePhPrompt, reNewHostPrompt, buff2String, sendPty, watchDogTimer } = require("./util.js");
|
|
9
9
|
|
|
10
10
|
const nodePtyOpt = {
|
|
11
11
|
shell: process.platform === "win32" ? process.env.ComSpec : process.env.SHELL,
|
|
12
12
|
windowsHide: true
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
function sshLoginCallback(output, ptyProcess, pw, ph) {
|
|
15
|
+
function sshLoginCallback(output, ptyProcess, pw, ph, logger = debugSendPty) {
|
|
16
16
|
if (reNewHostPrompt.test(output)) {
|
|
17
|
-
|
|
18
|
-
ptyProcess.write("yes\n");
|
|
17
|
+
sendPty(ptyProcess, "yes\n", logger, 17);
|
|
19
18
|
return;
|
|
20
19
|
}
|
|
21
20
|
if (rePwPrompt.test(output)) {
|
|
22
21
|
if (typeof pw === "string") {
|
|
23
22
|
debug("use given password");
|
|
24
|
-
|
|
25
|
-
ptyProcess.write(`${pw}\n`);
|
|
23
|
+
sendPty(ptyProcess, `${pw}\n`, logger, 23);
|
|
26
24
|
return;
|
|
27
25
|
}
|
|
28
26
|
if (typeof pw === "function") {
|
|
29
27
|
debug("call password callback function");
|
|
30
28
|
pw()
|
|
31
29
|
.then((v)=>{
|
|
32
|
-
ptyProcess
|
|
30
|
+
sendPty(ptyProcess, `${v}\n`, logger, 30);
|
|
33
31
|
});
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
@@ -37,36 +35,49 @@ function sshLoginCallback(output, ptyProcess, pw, ph) {
|
|
|
37
35
|
if (rePhPrompt.test(output)) {
|
|
38
36
|
if (typeof ph === "string") {
|
|
39
37
|
debug("use given passphrase");
|
|
40
|
-
|
|
41
|
-
ptyProcess.write(`${ph}\n`);
|
|
38
|
+
sendPty(ptyProcess, `${ph}\n`, logger, 38);
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
if (typeof ph === "function") {
|
|
45
42
|
debug("call passphrase callback function");
|
|
46
43
|
ph()
|
|
47
44
|
.then((v)=>{
|
|
48
|
-
ptyProcess
|
|
45
|
+
sendPty(ptyProcess, `${v}\n`, logger, 45);
|
|
49
46
|
});
|
|
50
47
|
}
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
/**
|
|
52
|
+
* create pty for master ssh session and store to hostInfo.masterPty
|
|
53
|
+
*/
|
|
54
|
+
function getMasterPty(hostInfo, logger) {
|
|
55
55
|
debug("fork pty for master connection");
|
|
56
|
+
|
|
57
|
+
if (hostInfo.masterPty) {
|
|
58
|
+
debug(`kill existing master pty for ${hostInfo.host}`);
|
|
59
|
+
hostInfo.masterPty.kill();
|
|
60
|
+
}
|
|
56
61
|
const ptyProcess = spawn(nodePtyOpt.shell, [], nodePtyOpt);
|
|
57
62
|
ptyProcess.onData(buff2String.bind(null, debugMasterVerbose));
|
|
58
63
|
ptyProcess.onData((data)=>{
|
|
59
64
|
const output = data.toString();
|
|
60
|
-
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase);
|
|
65
|
+
sshLoginCallback(output, ptyProcess, hostInfo.password, hostInfo.passphrase, logger);
|
|
61
66
|
});
|
|
62
|
-
|
|
67
|
+
hostInfo.masterPty = ptyProcess;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
async function fork(hostInfo, cmd, args, outputCallback) {
|
|
66
|
-
|
|
67
|
-
const p = new Promise((resolve, reject)=>{
|
|
70
|
+
async function fork(hostInfo, cmd, args, timeout, outputCallback) {
|
|
71
|
+
return new Promise((resolve, reject)=>{
|
|
68
72
|
debug(`cmd=${cmd}: args=${args}`);
|
|
69
|
-
ptyProcess = spawn(cmd, args, nodePtyOpt);
|
|
73
|
+
const ptyProcess = spawn(cmd, args, nodePtyOpt);
|
|
74
|
+
let ac = null;
|
|
75
|
+
if (timeout > 0) {
|
|
76
|
+
ac = watchDogTimer(timeout, cmd, ()=>{
|
|
77
|
+
ptyProcess.kill();
|
|
78
|
+
reject(new Error(`watchdog timer expired ${timeout}`));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
70
81
|
ptyProcess.onData(buff2String.bind(null, debugVerbose));
|
|
71
82
|
ptyProcess.onData((data)=>{
|
|
72
83
|
const output = data.toString();
|
|
@@ -86,6 +97,10 @@ async function fork(hostInfo, cmd, args, outputCallback) {
|
|
|
86
97
|
ptyProcess.onExit(({ exitCode, signal })=>{
|
|
87
98
|
debug("onExit", exitCode, signal);
|
|
88
99
|
|
|
100
|
+
if (ac !== null) {
|
|
101
|
+
ac.abort();
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
if (signal > 0) {
|
|
90
105
|
const err = new Error("signal caught");
|
|
91
106
|
err.signal = signal;
|
|
@@ -99,8 +114,6 @@ async function fork(hostInfo, cmd, args, outputCallback) {
|
|
|
99
114
|
return reject(err);
|
|
100
115
|
});
|
|
101
116
|
});
|
|
102
|
-
p.pty = ptyProcess;
|
|
103
|
-
return p;
|
|
104
117
|
}
|
|
105
118
|
module.exports = {
|
|
106
119
|
fork,
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const debug = require("debug")("sshClientWrapper:interface");
|
|
3
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:interface");
|
|
4
4
|
const { sshExec, canConnect, disconnect, ls } = require("./sshExec.js");
|
|
5
5
|
const { send, recv } = require("./rsyncExec.js");
|
|
6
6
|
const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
@@ -30,12 +30,19 @@ const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
|
30
30
|
* @param {string[]} hostInfo.sshOpt - additional options for ssh
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
const logAndReject = (message)=>{
|
|
34
|
+
debug(message);
|
|
35
|
+
return Promise.reject(new Error(message));
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Facade class.
|
|
35
40
|
* @constructor
|
|
36
41
|
*/
|
|
37
42
|
class SshClientWrapper {
|
|
38
43
|
constructor(hostInfo) {
|
|
44
|
+
debug("constructor called for", hostInfo.host);
|
|
45
|
+
debugVerbose("hostInfo=", hostInfo);
|
|
39
46
|
sanityCheck(hostInfo);
|
|
40
47
|
this.hostInfo = { ...hostInfo };
|
|
41
48
|
this.hostInfo.masterPty = null;
|
|
@@ -44,18 +51,19 @@ class SshClientWrapper {
|
|
|
44
51
|
/**
|
|
45
52
|
* Execute command on remote host.
|
|
46
53
|
* @param {string} cmd - Cmdline which will be executed.
|
|
54
|
+
* @param {Integer} timeout - timeout in secconds
|
|
47
55
|
* @param { Function } outputCallback - call back routine for output
|
|
48
56
|
* @returns {Integer} - Return code of cmd.
|
|
49
57
|
*
|
|
50
58
|
* If stdout and stderr is array, last 5 line of stdout and stderr is stored in them.
|
|
51
59
|
*/
|
|
52
|
-
async exec(cmd, outputCallback) {
|
|
60
|
+
async exec(cmd, timeout = 0, outputCallback = null) {
|
|
53
61
|
debug("exec called", cmd);
|
|
54
62
|
|
|
55
|
-
if (typeof cmd !== "string") {
|
|
56
|
-
return
|
|
63
|
+
if (typeof cmd !== "string" || cmd === "") {
|
|
64
|
+
return logAndReject("cmd must be string");
|
|
57
65
|
}
|
|
58
|
-
return sshExec(this.hostInfo, cmd, outputCallback);
|
|
66
|
+
return sshExec(this.hostInfo, cmd, timeout, outputCallback);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
|
|
@@ -63,10 +71,12 @@ class SshClientWrapper {
|
|
|
63
71
|
* execute ls command and return output
|
|
64
72
|
* @param {string} target - file or directory path to watch
|
|
65
73
|
* @param {string[]} lsOpt - optional arguments for ls
|
|
74
|
+
* @param {Integer} timeout - timeout in secconds
|
|
66
75
|
* @returns {string[]} - output from ls
|
|
67
76
|
*/
|
|
68
|
-
async ls(target, lsOpt = []) {
|
|
69
|
-
|
|
77
|
+
async ls(target, lsOpt = [], timeout = 0) {
|
|
78
|
+
debug("ls called", target, lsOpt);
|
|
79
|
+
return ls(this.hostInfo, target, lsOpt, timeout);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/**
|
|
@@ -81,7 +91,7 @@ class SshClientWrapper {
|
|
|
81
91
|
debug("watch called", cmd, regexp, retryDelay, maxRetry);
|
|
82
92
|
|
|
83
93
|
if (typeof cmd !== "string") {
|
|
84
|
-
return
|
|
94
|
+
return logAndReject("cmd must be string");
|
|
85
95
|
}
|
|
86
96
|
if (!(regexp instanceof RegExp)) {
|
|
87
97
|
const err = new Error("illegal regexp specified");
|
|
@@ -89,7 +99,7 @@ class SshClientWrapper {
|
|
|
89
99
|
err.cmd = cmd;
|
|
90
100
|
err.retryDelay = retryDelay;
|
|
91
101
|
err.maxRetry = maxRetry;
|
|
92
|
-
return Promise.reject(err);
|
|
102
|
+
return Promise.reject(new Error("err"));
|
|
93
103
|
}
|
|
94
104
|
//not implemented yet !!!
|
|
95
105
|
return this.exec(cmd, ()=>{
|
|
@@ -101,27 +111,30 @@ class SshClientWrapper {
|
|
|
101
111
|
* @param {string[]} src - File or directory name which to be recieve.
|
|
102
112
|
* @param {string} dst - Destination path.
|
|
103
113
|
* @param {string[]} opt - option for rsync
|
|
114
|
+
* @param {Integer} timeout - timeout in secconds
|
|
104
115
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
105
116
|
*/
|
|
106
|
-
async send(src, dst, opt) {
|
|
117
|
+
async send(src, dst, opt = [], timeout = 0) {
|
|
107
118
|
debug("send called", src, dst, opt);
|
|
108
119
|
|
|
109
120
|
if (!isArrayOfString(src)) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
return logAndReject("src must be array of string");
|
|
122
|
+
}
|
|
123
|
+
if (!src.some((e)=>{
|
|
124
|
+
return e !== "";
|
|
125
|
+
})) {
|
|
126
|
+
return logAndReject("src must contain non-empty string");
|
|
113
127
|
}
|
|
114
128
|
if (typeof dst !== "string") {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
return logAndReject("dst must be string");
|
|
130
|
+
}
|
|
131
|
+
if (dst === "") {
|
|
132
|
+
return logAndReject("dst must be non-empty string");
|
|
118
133
|
}
|
|
119
134
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
120
|
-
|
|
121
|
-
debug(err);
|
|
122
|
-
return new Error(err);
|
|
135
|
+
return logAndReject("opt must be array of string");
|
|
123
136
|
}
|
|
124
|
-
return send(this.hostInfo, src, dst, opt);
|
|
137
|
+
return send(this.hostInfo, src, dst, opt, timeout);
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
/**
|
|
@@ -129,27 +142,30 @@ class SshClientWrapper {
|
|
|
129
142
|
* @param {string[]} src - File or directory name which to be recieve.
|
|
130
143
|
* @param {string} dst - Destination path.
|
|
131
144
|
* @param {string[]} opt - option for rsync
|
|
145
|
+
* @param {Integer} timeout - timeout in secconds
|
|
132
146
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
133
147
|
*/
|
|
134
|
-
async recv(src, dst, opt) {
|
|
148
|
+
async recv(src, dst, opt = [], timeout = 0) {
|
|
135
149
|
debug("recv called", src, dst, opt);
|
|
136
150
|
|
|
137
151
|
if (!isArrayOfString(src)) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
return logAndReject("src must be array of string");
|
|
153
|
+
}
|
|
154
|
+
if (!src.some((e)=>{
|
|
155
|
+
return e !== "";
|
|
156
|
+
})) {
|
|
157
|
+
return logAndReject("src must contain non-empty string");
|
|
141
158
|
}
|
|
142
159
|
if (typeof dst !== "string") {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
return logAndReject("dst must be string");
|
|
161
|
+
}
|
|
162
|
+
if (dst === "") {
|
|
163
|
+
return logAndReject("dst must be non-empty string");
|
|
146
164
|
}
|
|
147
165
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
148
|
-
|
|
149
|
-
debug(err);
|
|
150
|
-
return new Error(err);
|
|
166
|
+
return logAndReject("opt must be array of string");
|
|
151
167
|
}
|
|
152
|
-
return recv(this.hostInfo, src, dst, opt);
|
|
168
|
+
return recv(this.hostInfo, src, dst, opt, timeout);
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
/**
|
|
@@ -158,10 +174,11 @@ class SshClientWrapper {
|
|
|
158
174
|
* @returns {Promise} - Resolved with true on success, otherwise rejected with Error.
|
|
159
175
|
*/
|
|
160
176
|
async canConnect(timeout = 60) {
|
|
161
|
-
debug("canConnect called timeout=", timeout);
|
|
177
|
+
debug("canConnect called with timeout=", timeout);
|
|
178
|
+
debugVerbose("hostInfo=", this.hostInfo);
|
|
162
179
|
|
|
163
180
|
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
164
|
-
return
|
|
181
|
+
return logAndReject("timeout must be positive integer");
|
|
165
182
|
}
|
|
166
183
|
return canConnect(this.hostInfo, timeout);
|
|
167
184
|
}
|
|
@@ -170,7 +187,7 @@ class SshClientWrapper {
|
|
|
170
187
|
* Disconnect master session
|
|
171
188
|
*/
|
|
172
189
|
async disconnect() {
|
|
173
|
-
debug(
|
|
190
|
+
debug(`disconnect from ${this.hostInfo.host} called`);
|
|
174
191
|
disconnect(this.hostInfo);
|
|
175
192
|
}
|
|
176
193
|
}
|
package/lib/rsyncExec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const debug = require("debug")("sshClientWrapper:rsyncExec");
|
|
4
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
3
|
+
const debug = require("debug")("sshClientWrapper:debug:rsyncExec");
|
|
4
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:rsyncExec");
|
|
5
5
|
const { glob } = require("glob");
|
|
6
6
|
|
|
7
7
|
const { connect, retryWrapper, sshExec } = require("./sshExec.js");
|
|
@@ -16,10 +16,11 @@ const { sshCmd, rsyncCmd, getSshOption } = require("./util.js");
|
|
|
16
16
|
* @param {[string]} argSrc - File or directory name which to be send.
|
|
17
17
|
* @param {string} dst - Destination path.
|
|
18
18
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
19
|
+
* @param {Integer} timeout - timeout in secconds
|
|
19
20
|
*
|
|
20
21
|
* this function execute rsync -avv --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
21
22
|
*/
|
|
22
|
-
async function send(hostInfo, argSrc, dst, rsyncOpt
|
|
23
|
+
async function send(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
23
24
|
await connect(hostInfo);
|
|
24
25
|
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
25
26
|
return glob(e);
|
|
@@ -65,7 +66,7 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
65
66
|
}
|
|
66
67
|
debug(`send ${src} to ${hostInfo.host}:${dst}`);
|
|
67
68
|
debugVerbose(`send: ${rsyncCmd} ${args.join(" ")}`);
|
|
68
|
-
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args), hostInfo);
|
|
69
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/**
|
|
@@ -74,10 +75,11 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
74
75
|
* @param {[string]} argSrc - File or directory name which to be send.
|
|
75
76
|
* @param {string} dst - Destination path.
|
|
76
77
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
78
|
+
* @param {Integer} timeout - timeout in secconds
|
|
77
79
|
*
|
|
78
80
|
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
79
81
|
*/
|
|
80
|
-
async function recv(hostInfo, argSrc, dst, rsyncOpt
|
|
82
|
+
async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
81
83
|
await connect(hostInfo);
|
|
82
84
|
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
83
85
|
const sshArgs = getSshOption(hostInfo, true);
|
|
@@ -87,7 +89,7 @@ async function recv(hostInfo, argSrc, dst, rsyncOpt = []) {
|
|
|
87
89
|
|
|
88
90
|
debug(`recv ${src} to ${dst}`);
|
|
89
91
|
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
90
|
-
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args), hostInfo);
|
|
92
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
|
|
91
93
|
}
|
|
92
94
|
module.exports = {
|
|
93
95
|
send,
|
package/lib/sshExec.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const { setTimeout: setTimeoutPromise } = require("timers/promises");
|
|
3
2
|
const crypto = require("crypto");
|
|
4
3
|
const promiseRetry = require("promise-retry");
|
|
5
|
-
const debug = require("debug")("sshClientWrapper:sshExec");
|
|
6
|
-
const debugVerbose = require("debug")("sshClientWrapper:
|
|
7
|
-
const
|
|
4
|
+
const debug = require("debug")("sshClientWrapper:debug:sshExec");
|
|
5
|
+
const debugVerbose = require("debug")("sshClientWrapper:verbose:sshExec");
|
|
6
|
+
const debugSendMasterPty = require("debug")("sshClientWrapper:insecure:sshExec_masterPty");
|
|
7
|
+
const { sendPty, sshCmd, getSshOption, sanityCheck, watchDogTimer } = require("./util.js");
|
|
8
8
|
const { fork, getMasterPty } = require("./fork.js");
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Execute command on remote host.
|
|
12
12
|
* @param {Object} hostInfo - hostinfo object
|
|
13
13
|
* @param {string} cmd - Cmdline which will be executed on remote host.
|
|
14
|
+
* @param {number} timeout - timeout (in sec.)
|
|
14
15
|
* @param {Function} outputCallback - Callback routine for stdout and stderr.
|
|
15
16
|
*/
|
|
16
|
-
async function sshExec(hostInfo, cmd, outputCallback) {
|
|
17
|
+
async function sshExec(hostInfo, cmd, timeout, outputCallback) {
|
|
17
18
|
await connect(hostInfo);
|
|
18
19
|
debug("exec", cmd, "on remote server");
|
|
19
20
|
const args = getSshOption(hostInfo, false);
|
|
@@ -21,7 +22,7 @@ async function sshExec(hostInfo, cmd, outputCallback) {
|
|
|
21
22
|
debugVerbose(`exec: ${sshCmd} ${args.join(" ")}`);
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
|
-
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, outputCallback), hostInfo);
|
|
25
|
+
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, timeout, outputCallback), hostInfo);
|
|
25
26
|
return rt;
|
|
26
27
|
} catch (e) {
|
|
27
28
|
if (typeof e.rt === "undefined") {
|
|
@@ -64,9 +65,7 @@ async function canConnect(hostInfo, timeout = 60) {
|
|
|
64
65
|
* @returns {boolean} - true if master session exists
|
|
65
66
|
*/
|
|
66
67
|
async function existsMaster(hostInfo) {
|
|
67
|
-
|
|
68
|
-
const signal = ac.signal;
|
|
69
|
-
|
|
68
|
+
debugVerbose("existsMaster called");
|
|
70
69
|
const args = getSshOption(hostInfo);
|
|
71
70
|
args.push("-Ocheck");
|
|
72
71
|
|
|
@@ -75,31 +74,12 @@ async function existsMaster(hostInfo) {
|
|
|
75
74
|
output += e;
|
|
76
75
|
};
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
let p;
|
|
77
|
+
const rt = true; //if timeout occurred during ssh -Ocheck, it will turn to false
|
|
80
78
|
const timeout = hostInfo.ConnectTimeout || 60;
|
|
81
|
-
|
|
82
|
-
.then(()=>{
|
|
83
|
-
debug(`can not finish master session check within ${timeout} sec`);
|
|
84
|
-
|
|
85
|
-
if (p.pty) {
|
|
86
|
-
p.pty.kill();
|
|
87
|
-
}
|
|
88
|
-
rt = false;
|
|
89
|
-
})
|
|
90
|
-
.catch((err)=>{
|
|
91
|
-
if (err.name === "AbortError") {
|
|
92
|
-
debug("timeout was aborted because master session check has done");
|
|
93
|
-
} else {
|
|
94
|
-
throw err;
|
|
95
|
-
}
|
|
96
|
-
});
|
|
79
|
+
debug(`check master session: ${sshCmd} ${args.join(" ")}`);
|
|
97
80
|
|
|
98
81
|
try {
|
|
99
|
-
|
|
100
|
-
p = fork(hostInfo, sshCmd, args, cb);
|
|
101
|
-
await p;
|
|
102
|
-
ac.abort();
|
|
82
|
+
await fork(hostInfo, sshCmd, args, timeout, cb);
|
|
103
83
|
} catch (err) {
|
|
104
84
|
if (err.rt !== 255) {
|
|
105
85
|
debug("check master session failed", err);
|
|
@@ -119,36 +99,22 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
119
99
|
debugVerbose("master connection exists");
|
|
120
100
|
return;
|
|
121
101
|
}
|
|
122
|
-
debug(`create master ssh session to ${hostInfo.host}`);
|
|
123
102
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
debug(`master pty for ${hostInfo.host} exists`);
|
|
129
|
-
}
|
|
103
|
+
debug(`create master pty for ${hostInfo.host}`);
|
|
104
|
+
getMasterPty(hostInfo, debugSendMasterPty);
|
|
105
|
+
|
|
106
|
+
debug(`create master ssh session to ${hostInfo.host}`);
|
|
130
107
|
|
|
131
|
-
const ac =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.then(()=>{
|
|
135
|
-
debug(`can not connect within ${timeout} sec`);
|
|
136
|
-
hostInfo.masterPty.write("\n");
|
|
137
|
-
hostInfo.masterPty.write("exit\n");
|
|
108
|
+
const ac = watchDogTimer(timeout, "make connection", ()=>{
|
|
109
|
+
sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
|
|
110
|
+
sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
|
|
138
111
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.catch((err)=>{
|
|
146
|
-
if (err.name === "AbortError") {
|
|
147
|
-
debug("timeout was aborted because connection check has done");
|
|
148
|
-
} else {
|
|
149
|
-
throw err;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
112
|
+
const err = new Error(`can not connect within ${timeout} sec`);
|
|
113
|
+
err.host = hostInfo.host;
|
|
114
|
+
err.user = hostInfo.user;
|
|
115
|
+
err.port = hostInfo.port;
|
|
116
|
+
throw err;
|
|
117
|
+
});
|
|
152
118
|
|
|
153
119
|
const words = crypto.randomUUID();
|
|
154
120
|
const re = new RegExp(`^${words}`);
|
|
@@ -164,7 +130,6 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
164
130
|
|
|
165
131
|
if (re.test(output)) {
|
|
166
132
|
debug("test done. call exit on remotehost");
|
|
167
|
-
hostInfo.masterPty.write("exit\n");
|
|
168
133
|
done = true;
|
|
169
134
|
resolve();
|
|
170
135
|
} else if (/Permission denied \(/.test(output)) {
|
|
@@ -186,11 +151,14 @@ async function connect(hostInfo, timeout = 60) {
|
|
|
186
151
|
const args = getSshOption(hostInfo);
|
|
187
152
|
args.push(`echo ${words}`);
|
|
188
153
|
const cmd = `${sshCmd} ${args.join(" ")}\n`;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
154
|
+
sendPty(hostInfo.masterPty, cmd, debugSendMasterPty, 190);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await p;
|
|
158
|
+
debug(`connected to ${hostInfo.host}`);
|
|
159
|
+
} finally {
|
|
160
|
+
ac.abort();
|
|
161
|
+
}
|
|
194
162
|
}
|
|
195
163
|
|
|
196
164
|
/**
|
|
@@ -208,7 +176,7 @@ async function disconnect(hostInfo) {
|
|
|
208
176
|
};
|
|
209
177
|
try {
|
|
210
178
|
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
211
|
-
await fork(hostInfo, sshCmd, args, cb);
|
|
179
|
+
await fork(hostInfo, sshCmd, args, 0, cb);
|
|
212
180
|
} catch (err) {
|
|
213
181
|
if (err.rt === 255) {
|
|
214
182
|
return;
|
|
@@ -225,7 +193,7 @@ async function disconnect(hostInfo) {
|
|
|
225
193
|
|
|
226
194
|
if (hostInfo.masterPty) {
|
|
227
195
|
debug("exit masterPty");
|
|
228
|
-
hostInfo.masterPty
|
|
196
|
+
sendPty(hostInfo.masterPty, "exit \n", debugSendMasterPty, 228);
|
|
229
197
|
hostInfo.masterPty = null;
|
|
230
198
|
}
|
|
231
199
|
}
|
|
@@ -235,12 +203,13 @@ async function disconnect(hostInfo) {
|
|
|
235
203
|
* @param {Object} hostInfo - hostinfo object
|
|
236
204
|
* @param {string} target - file or directory path to watch
|
|
237
205
|
* @param {string[]} lsOpt - optional arguments for ls
|
|
206
|
+
* @param {number} timeout - timeout (in sec.)
|
|
238
207
|
* @returns {string[]} - output from ls
|
|
239
208
|
*/
|
|
240
|
-
async function ls(hostInfo, target, lsOpt
|
|
209
|
+
async function ls(hostInfo, target, lsOpt, timeout) {
|
|
241
210
|
const outputArray = [];
|
|
242
211
|
try {
|
|
243
|
-
await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, (data)=>{
|
|
212
|
+
await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, timeout, (data)=>{
|
|
244
213
|
outputArray.push(data);
|
|
245
214
|
});
|
|
246
215
|
} catch (e) {
|