ssh-client-wrapper 2.2.10 → 2.4.0
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/lib/fork.js +4 -1
- package/lib/index.js +1 -2
- package/lib/rsyncExec.js +2 -2
- package/lib/sshExec.js +29 -15
- package/lib/util.js +5 -6
- package/package.json +1 -1
- package/run_test_all.sh +12 -18
- package/test/interface.js +6 -1
- package/test/util.js +31 -8
package/lib/fork.js
CHANGED
|
@@ -69,7 +69,7 @@ function createMasterPty (hostInfo) {
|
|
|
69
69
|
hostInfo.masterPty = ptyProcess;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
async function fork (hostInfo, cmd, args, timeout, outputCallback) {
|
|
72
|
+
async function fork (hostInfo, cmd, args, timeout, outputCallback, retryableExitCodes = []) {
|
|
73
73
|
return new Promise((resolve, reject) => {
|
|
74
74
|
debug(`cmd=${cmd}: args=${args}`);
|
|
75
75
|
const ptyProcess = spawn(cmd, args, nodePtyOpt);
|
|
@@ -119,6 +119,9 @@ async function fork (hostInfo, cmd, args, timeout, outputCallback) {
|
|
|
119
119
|
}
|
|
120
120
|
const err = new Error("exit with non-zero");
|
|
121
121
|
err.rt = exitCode;
|
|
122
|
+
if (retryableExitCodes.includes(exitCode)) {
|
|
123
|
+
err.retryable = true;
|
|
124
|
+
}
|
|
122
125
|
return reject(err);
|
|
123
126
|
});
|
|
124
127
|
});
|
package/lib/index.js
CHANGED
|
@@ -27,8 +27,7 @@ const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
|
27
27
|
* @param {Integer} hostInfo.ControlPersist=180 - how long keep master connection after last client connection has been closed (sec).
|
|
28
28
|
* @param {Integer} hostInfo.ConnectTimeout=60 - timeout used when connectiong to ths SSH server
|
|
29
29
|
* @param {Integer} hostInfo.maxRetry=3 - max number of retry
|
|
30
|
-
* @param {Integer} hostInfo.
|
|
31
|
-
* @param {Integer} hostInfo.retryMaxTimeout=300 - maximum duration between each retry
|
|
30
|
+
* @param {Integer} hostInfo.retryDuration=1000 - duration time (msec) between each retry
|
|
32
31
|
* @param {string[]} hostInfo.sshOpt - additional options for ssh
|
|
33
32
|
*/
|
|
34
33
|
|
package/lib/rsyncExec.js
CHANGED
|
@@ -79,7 +79,7 @@ async function send (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
79
79
|
}
|
|
80
80
|
debug(`send ${src} to ${hostInfo.host}:${dst}`);
|
|
81
81
|
debugVerbose(`send: ${rsyncCmd} ${args.join(" ")}`);
|
|
82
|
-
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
|
|
82
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, [12]), hostInfo);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/**
|
|
@@ -119,7 +119,7 @@ async function recv (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
119
119
|
|
|
120
120
|
debug(`recv ${src} to ${dst}`);
|
|
121
121
|
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
122
|
-
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout), hostInfo);
|
|
122
|
+
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, [12]), hostInfo);
|
|
123
123
|
}
|
|
124
124
|
module.exports = {
|
|
125
125
|
checkRsyncVersion,
|
package/lib/sshExec.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const crypto = require("crypto");
|
|
3
|
-
const
|
|
3
|
+
const { setTimeout: setTimeoutPromise } = require("timers/promises");
|
|
4
4
|
const debug = require("debug")("sshClientWrapper:debug:sshExec");
|
|
5
5
|
const debugVerbose = require("debug")("sshClientWrapper:verbose:sshExec");
|
|
6
6
|
const debugSendMasterPty = require("debug")("sshClientWrapper:insecure:sshExec_masterPty");
|
|
@@ -242,22 +242,36 @@ async function ls (hostInfo, target, lsOpt, timeout) {
|
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
244
|
* retry fork() if it throw retryable error
|
|
245
|
-
* @param {Function}
|
|
245
|
+
* @param {Function} func - function to be wrapped
|
|
246
|
+
* @param {Object} hostInfo - hostinfo object
|
|
247
|
+
* @param {Integer} argMaxRetry - max retry number
|
|
246
248
|
*/
|
|
247
|
-
async function retryWrapper (func, hostInfo) {
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
async function retryWrapper (func, hostInfo, argMaxRetry, argDuration) {
|
|
250
|
+
const maxRetry = argMaxRetry || hostInfo.maxRetry || 1;
|
|
251
|
+
const duration = argDuration || hostInfo.retryDuration || 1000;
|
|
252
|
+
if (maxRetry === 1) {
|
|
253
|
+
return func();
|
|
254
|
+
}
|
|
255
|
+
let retryCount = 0;
|
|
256
|
+
while (true) {
|
|
257
|
+
let rt;
|
|
258
|
+
try {
|
|
259
|
+
rt = await func();
|
|
260
|
+
return rt;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
if (!e.retryable) {
|
|
258
263
|
throw e;
|
|
259
|
-
}
|
|
260
|
-
|
|
264
|
+
}
|
|
265
|
+
if (retryCount > maxRetry) {
|
|
266
|
+
debug(`max retry count exceeded: ${retryCount}`);
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
retryCount++;
|
|
270
|
+
debug(`retryable error occurred ${e.message}`);
|
|
271
|
+
debug(`retrying: ${retryCount} times`);
|
|
272
|
+
await setTimeoutPromise(duration, true);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
261
275
|
}
|
|
262
276
|
|
|
263
277
|
module.exports = {
|
package/lib/util.js
CHANGED
|
@@ -16,6 +16,7 @@ const Ajv = require("ajv");
|
|
|
16
16
|
const ajv = new Ajv({
|
|
17
17
|
allErrors: true,
|
|
18
18
|
coerceTypes: true,
|
|
19
|
+
useDefaults: "empty",
|
|
19
20
|
logger: {
|
|
20
21
|
log: debug,
|
|
21
22
|
warn: debug,
|
|
@@ -33,11 +34,10 @@ const hostInfoSchema = {
|
|
|
33
34
|
port: { type: "number", minimum: 0, maximum: 65535 },
|
|
34
35
|
keyFile: { type: "string", pattern: "\\S+", transform: ["trim"] },
|
|
35
36
|
noStrictHostkeyChecking: { type: "boolean" },
|
|
36
|
-
ControlPersist: { type: "number", minimum: 0 },
|
|
37
|
+
ControlPersist: { type: "number", minimum: 0, default: 180 },
|
|
37
38
|
ConnectTimeout: { type: "number", minimum: 0 },
|
|
38
|
-
maxRetry: { type: "number", minimum: 0 },
|
|
39
|
-
|
|
40
|
-
retryMaxTimeout: { type: "number", minimum: 0 },
|
|
39
|
+
maxRetry: { type: "number", minimum: 0, default: 3 },
|
|
40
|
+
retryDuration: { type: "number", minimum: 0, default: 1000 },
|
|
41
41
|
sshOpt: { type: "array", minItems: 1, items: { type: "string", pattern: "\\S+", transform: ["trim"] } }
|
|
42
42
|
},
|
|
43
43
|
required: ["host"]
|
|
@@ -51,8 +51,7 @@ const numberOptions = [
|
|
|
51
51
|
"ControlPersist",
|
|
52
52
|
"ConnectTimeout",
|
|
53
53
|
"maxRetry",
|
|
54
|
-
"
|
|
55
|
-
"retryMaxTimeout"
|
|
54
|
+
"retryDuration"
|
|
56
55
|
];
|
|
57
56
|
|
|
58
57
|
const validate = ajv.compile(hostInfoSchema);
|
package/package.json
CHANGED
package/run_test_all.sh
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
function cleanup(){
|
|
4
|
+
docker-compose rm --stop --force testbed
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
trap cleanup EXIT
|
|
8
|
+
set -e -o pipefail
|
|
9
|
+
|
|
2
10
|
docker-compose up testbed -d
|
|
3
11
|
export TEST_HOST=localhost
|
|
4
12
|
export TEST_USER=testuser
|
|
@@ -7,27 +15,13 @@ export TEST_PORT=4000
|
|
|
7
15
|
ssh-keygen -R '[localhost]:4000'
|
|
8
16
|
|
|
9
17
|
npm run test
|
|
10
|
-
|
|
11
|
-
echo local test failed
|
|
12
|
-
exit 1
|
|
13
|
-
fi
|
|
18
|
+
echo local test success
|
|
14
19
|
|
|
15
20
|
docker-compose run --build --rm tester
|
|
16
|
-
|
|
17
|
-
echo test on latest ubuntu failed
|
|
18
|
-
exit 2
|
|
19
|
-
fi
|
|
21
|
+
echo test on latest ubuntu success
|
|
20
22
|
|
|
21
23
|
docker-compose run --build --rm tester-bullseye
|
|
22
|
-
|
|
23
|
-
echo test on bullseye failed
|
|
24
|
-
exit 3
|
|
25
|
-
fi
|
|
24
|
+
echo test on bullseye success
|
|
26
25
|
|
|
27
26
|
docker-compose run --build --rm tester-buster
|
|
28
|
-
|
|
29
|
-
echo test on buster failed
|
|
30
|
-
exit 4
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
docker-compose rm --stop --force testbed
|
|
27
|
+
echo test on buster success
|
package/test/interface.js
CHANGED
|
@@ -49,7 +49,12 @@ describe("test for interface", () => {
|
|
|
49
49
|
});
|
|
50
50
|
describe("test for public method", () => {
|
|
51
51
|
let ssh;
|
|
52
|
-
const hostInfoOrg = {
|
|
52
|
+
const hostInfoOrg = {
|
|
53
|
+
host: "hoge",
|
|
54
|
+
ControlPersist: 180,
|
|
55
|
+
maxRetry: 3,
|
|
56
|
+
retryDuration: 1000
|
|
57
|
+
};
|
|
53
58
|
const hostInfo = { ...hostInfoOrg, masterPty: null, rsyncVersion: null };
|
|
54
59
|
beforeEach(() => {
|
|
55
60
|
ssh = new SshClientWrapper(hostInfoOrg);
|
package/test/util.js
CHANGED
|
@@ -11,6 +11,11 @@ const { sanityCheck } = require("../lib/util.js");
|
|
|
11
11
|
|
|
12
12
|
describe("test for sanityCheck", () => {
|
|
13
13
|
const host = "testHostName";
|
|
14
|
+
const defaultValues = {
|
|
15
|
+
ControlPersist: 180,
|
|
16
|
+
maxRetry: 3,
|
|
17
|
+
retryDuration: 1000
|
|
18
|
+
};
|
|
14
19
|
it("should throw error for empty host", () => {
|
|
15
20
|
expect(sanityCheck.bind(null, { host: " ", user: " ", port: 22 })).to.throw(/empty host is not allowed/);
|
|
16
21
|
});
|
|
@@ -18,10 +23,29 @@ describe("test for sanityCheck", () => {
|
|
|
18
23
|
expect(sanityCheck.bind(null, { user: " ", port: 22 })).to.throw(/host is required/);
|
|
19
24
|
});
|
|
20
25
|
it("should just remove empty string props", () => {
|
|
21
|
-
expect(sanityCheck({
|
|
26
|
+
expect(sanityCheck({
|
|
27
|
+
host,
|
|
28
|
+
user: " ",
|
|
29
|
+
port: 22
|
|
30
|
+
})).to.deep.equal({
|
|
31
|
+
host,
|
|
32
|
+
...defaultValues,
|
|
33
|
+
port: 22
|
|
34
|
+
});
|
|
22
35
|
});
|
|
23
36
|
it("should remove empty string member in sshOpt", () => {
|
|
24
|
-
expect(sanityCheck({
|
|
37
|
+
expect(sanityCheck({
|
|
38
|
+
host,
|
|
39
|
+
sshOpt: ["foo", " ", "bar"],
|
|
40
|
+
user: "user",
|
|
41
|
+
port: 33
|
|
42
|
+
})).to.deep.equal({
|
|
43
|
+
host,
|
|
44
|
+
...defaultValues,
|
|
45
|
+
sshOpt: ["foo", "bar"],
|
|
46
|
+
user: "user",
|
|
47
|
+
port: 33
|
|
48
|
+
});
|
|
25
49
|
});
|
|
26
50
|
it("should just change type if string value specified for number", () => {
|
|
27
51
|
expect(sanityCheck({
|
|
@@ -30,16 +54,14 @@ describe("test for sanityCheck", () => {
|
|
|
30
54
|
ControlPersist: "22",
|
|
31
55
|
ConnectTimeout: "33",
|
|
32
56
|
maxRetry: "44",
|
|
33
|
-
|
|
34
|
-
retryMaxTimeout: "66"
|
|
57
|
+
retryDuration: "55"
|
|
35
58
|
})).to.deep.equal({
|
|
36
59
|
host,
|
|
37
60
|
port: 11,
|
|
38
61
|
ControlPersist: 22,
|
|
39
62
|
ConnectTimeout: 33,
|
|
40
63
|
maxRetry: 44,
|
|
41
|
-
|
|
42
|
-
retryMaxTimeout: 66
|
|
64
|
+
retryDuration: 55
|
|
43
65
|
});
|
|
44
66
|
});
|
|
45
67
|
it("should remove out of range members", () => {
|
|
@@ -48,8 +70,7 @@ describe("test for sanityCheck", () => {
|
|
|
48
70
|
ControlPersist: "-1",
|
|
49
71
|
ConnectTimeout: "-1",
|
|
50
72
|
maxRetry: "-1",
|
|
51
|
-
|
|
52
|
-
retryMaxTimeout: "-1"
|
|
73
|
+
retryDuration: "-1"
|
|
53
74
|
})).to.deep.equal({
|
|
54
75
|
host
|
|
55
76
|
});
|
|
@@ -60,6 +81,7 @@ describe("test for sanityCheck", () => {
|
|
|
60
81
|
noStrictHostkeyChecking: "true"
|
|
61
82
|
})).to.deep.equal({
|
|
62
83
|
host,
|
|
84
|
+
...defaultValues,
|
|
63
85
|
noStrictHostkeyChecking: true
|
|
64
86
|
});
|
|
65
87
|
});
|
|
@@ -69,6 +91,7 @@ describe("test for sanityCheck", () => {
|
|
|
69
91
|
noStrictHostkeyChecking: 0
|
|
70
92
|
})).to.deep.equal({
|
|
71
93
|
host,
|
|
94
|
+
...defaultValues,
|
|
72
95
|
noStrictHostkeyChecking: false
|
|
73
96
|
});
|
|
74
97
|
});
|