ssh-client-wrapper 2.11.0 → 2.13.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/.github/dependabot.yml +11 -0
- package/.github/workflows/coverage_report.yml +1 -1
- package/.github/workflows/release.yml +29 -0
- package/.github/workflows/run_test.yml +5 -7
- package/.husky/_pre-commit +1 -0
- package/.husky/pre-commit +0 -3
- package/.releaserc.json +16 -0
- package/CHANGELOG.md +13 -0
- package/README.md +99 -0
- package/eslint.config.mjs +181 -0
- package/lib/fork.js +31 -23
- package/lib/index.js +81 -32
- package/lib/rsyncExec.js +27 -31
- package/lib/sshExec.js +76 -74
- package/lib/util.js +41 -51
- package/package.json +31 -30
- package/test/getSshOption.js +12 -12
- package/test/interface.js +77 -74
- package/test/rsyncExec.js +86 -86
- package/test/sanityCheck.js +19 -19
- package/test/sshExec.js +47 -75
- package/test/testUtil/hostInfo.js +1 -2
- package/test/testUtil/testFiles.js +33 -22
- package/.eslintrc.json +0 -34
- package/.github/workflows/npm_publish.yml +0 -22
- package/test/.eslintrc.json +0 -28
package/lib/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import Debug from "debug";
|
|
4
|
+
import * as sshExecModule from "./sshExec.js";
|
|
5
|
+
import * as rsyncExecModule from "./rsyncExec.js";
|
|
6
|
+
import { isArrayOfString, sanityCheck } from "./util.js";
|
|
7
|
+
|
|
8
|
+
const debug = Debug("sshClientWrapper:debug:interface");
|
|
9
|
+
const debugVerbose = Debug("sshClientWrapper:verbose:interface");
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Integer means integer number but it is defined as Object for now
|
|
12
13
|
* workaround for eslint-plugin-jsdoc's no-undefined-types rule bug.
|
|
13
|
-
* @typedef {
|
|
14
|
+
* @typedef {object} Integer
|
|
15
|
+
* @property
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* host info object which contain all settings for 1 remote host
|
|
18
20
|
* it will be (shallow) copied in constructor.
|
|
19
|
-
* @param {
|
|
21
|
+
* @param {object} hostInfo - option object
|
|
20
22
|
* @param {string} hostInfo.host - destination
|
|
21
23
|
* @param {string} hostInfo.user - login name
|
|
22
24
|
* @param {number} hostInfo.port - port number
|
|
@@ -24,32 +26,33 @@ const { isArrayOfString, sanityCheck } = require("./util.js");
|
|
|
24
26
|
* @param {string | Function} hostInfo.passphrase - passphrase or function which return passphrase of private key
|
|
25
27
|
* @param {string} hostInfo.keyFile - private key filename
|
|
26
28
|
* @param {boolean} hostInfo.noStrictHostKeyChecking - bypass host key checking if true
|
|
27
|
-
* @param {Integer} hostInfo.ControlPersist
|
|
28
|
-
* @param {Integer} hostInfo.ConnectTimeout
|
|
29
|
-
* @param {Integer} hostInfo.maxRetry
|
|
30
|
-
* @param {Integer} hostInfo.retryDuration
|
|
29
|
+
* @param {Integer} hostInfo.ControlPersist - how long keep master connection after last client connection has been closed (sec).
|
|
30
|
+
* @param {Integer} hostInfo.ConnectTimeout - timeout used when connectiong to ths SSH server
|
|
31
|
+
* @param {Integer} hostInfo.maxRetry - max number of retry
|
|
32
|
+
* @param {Integer} hostInfo.retryDuration - duration time (msec) between each retry
|
|
31
33
|
* @param {string} hostInfo.rcfile - rcfile path which will be sourced before executing actual command
|
|
32
34
|
* @param {string} hostInfo.prependCmd - command string which will be executed before executing actual command
|
|
33
35
|
* @param {string[]} hostInfo.sshOpt - additional options for ssh
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
|
-
const logAndReject = (message)
|
|
38
|
+
const logAndReject = (message)=>{
|
|
37
39
|
debug(message);
|
|
38
40
|
return Promise.reject(new Error(message));
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Facade class.
|
|
43
|
-
* @
|
|
45
|
+
* @class
|
|
44
46
|
*/
|
|
45
47
|
class SshClientWrapper {
|
|
46
|
-
constructor
|
|
48
|
+
constructor(hostInfo, deps = { ...sshExecModule, ...rsyncExecModule }) {
|
|
47
49
|
debug("constructor called for", hostInfo.host);
|
|
48
50
|
debugVerbose("hostInfo=", hostInfo);
|
|
49
51
|
this.hostInfo = { ...hostInfo };
|
|
50
52
|
sanityCheck(this.hostInfo);
|
|
51
53
|
this.hostInfo.masterPty = null;
|
|
52
54
|
this.hostInfo.rsyncVersion = null;
|
|
55
|
+
this.deps = deps;
|
|
53
56
|
|
|
54
57
|
if (process.env.HOME) {
|
|
55
58
|
try {
|
|
@@ -73,13 +76,46 @@ class SshClientWrapper {
|
|
|
73
76
|
*
|
|
74
77
|
* note rcfile and prependCmd override hostInfo.rcfile and hostInfo.prependCmd respectively
|
|
75
78
|
*/
|
|
76
|
-
async exec
|
|
79
|
+
async exec(cmd, timeout = 0, outputCallback = null, rcfile = null, prependCmd = null) {
|
|
77
80
|
debug("exec called", cmd);
|
|
78
81
|
|
|
79
82
|
if (typeof cmd !== "string" || cmd === "") {
|
|
80
83
|
return logAndReject("cmd must be string");
|
|
81
84
|
}
|
|
82
|
-
return sshExec(this.hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd);
|
|
85
|
+
return this.deps.sshExec(this.hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* execute command on remote host and get output text
|
|
90
|
+
* @param {string} cmd - Cmdline which will be executed.
|
|
91
|
+
* @param {Integer} timeout - timeout in secconds
|
|
92
|
+
* @param {string} rcfile - rcfile path which will be sourced before executing actual command
|
|
93
|
+
* @param {string} prependCmd - command string which will be executed before executing actual command
|
|
94
|
+
* @returns {object} - output test array (output) and return value of cmd (rt)
|
|
95
|
+
*/
|
|
96
|
+
async execAndGetOutput(cmd, timeout = 0, rcfile = null, prependCmd = null) {
|
|
97
|
+
debug("execAndGetOutput called", cmd);
|
|
98
|
+
const outputArray = [];
|
|
99
|
+
const cb = (data)=>{
|
|
100
|
+
outputArray.push(data);
|
|
101
|
+
};
|
|
102
|
+
let rt = null;
|
|
103
|
+
try {
|
|
104
|
+
rt = await this.deps.sshExec(this.hostInfo, cmd, timeout, cb, rcfile, prependCmd);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
//non-zero retrun should be allowed
|
|
107
|
+
if (typeof e.rt === "undefined" || e.rt < 0) {
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const output = outputArray.length === 0
|
|
112
|
+
? []
|
|
113
|
+
: outputArray
|
|
114
|
+
.join("\n")
|
|
115
|
+
.replace(/\n+/g, "\n")
|
|
116
|
+
.replace(/\n$/, "")
|
|
117
|
+
.split("\n");
|
|
118
|
+
return { output, rt };
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
/**
|
|
@@ -89,9 +125,22 @@ class SshClientWrapper {
|
|
|
89
125
|
* @param {Integer} timeout - timeout in secconds
|
|
90
126
|
* @returns {string[]} - output from ls
|
|
91
127
|
*/
|
|
92
|
-
async ls
|
|
128
|
+
async ls(target, lsOpt = [], timeout = 0) {
|
|
93
129
|
debug("ls called", target, lsOpt);
|
|
94
|
-
|
|
130
|
+
const { output, rt } = await this.execAndGetOutput(`ls ${lsOpt.join(" ")} ${target}`, timeout);
|
|
131
|
+
return rt === 0 ? output : rt;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* execute series of comands like expect(3)
|
|
136
|
+
* @param {string} cmd - Cmdline which will be executed.
|
|
137
|
+
* @param {Array} expects - array of expect and send string pairs
|
|
138
|
+
* @param {Integer} timeout - timeout in secconds
|
|
139
|
+
* @returns {Integer} - Return code of cmd.
|
|
140
|
+
*/
|
|
141
|
+
async expect(cmd, expects, timeout = 180) {
|
|
142
|
+
debug("expect called", expects);
|
|
143
|
+
return this.deps.expect(this.hostInfo, cmd, expects, timeout);
|
|
95
144
|
}
|
|
96
145
|
|
|
97
146
|
/**
|
|
@@ -102,13 +151,13 @@ class SshClientWrapper {
|
|
|
102
151
|
* @param {Integer} timeout - timeout in secconds
|
|
103
152
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
104
153
|
*/
|
|
105
|
-
async send
|
|
154
|
+
async send(src, dst, opt = [], timeout = 0) {
|
|
106
155
|
debug("send called", src, dst, opt);
|
|
107
156
|
|
|
108
157
|
if (!isArrayOfString(src)) {
|
|
109
158
|
return logAndReject("src must be array of string");
|
|
110
159
|
}
|
|
111
|
-
if (!src.some((e)
|
|
160
|
+
if (!src.some((e)=>{
|
|
112
161
|
return e !== "";
|
|
113
162
|
})) {
|
|
114
163
|
return logAndReject("src must contain non-empty string");
|
|
@@ -122,7 +171,7 @@ class SshClientWrapper {
|
|
|
122
171
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
123
172
|
return logAndReject("opt must be array of string");
|
|
124
173
|
}
|
|
125
|
-
return send(this.hostInfo, src, dst, opt, timeout);
|
|
174
|
+
return this.deps.send(this.hostInfo, src, dst, opt, timeout);
|
|
126
175
|
}
|
|
127
176
|
|
|
128
177
|
/**
|
|
@@ -133,13 +182,13 @@ class SshClientWrapper {
|
|
|
133
182
|
* @param {Integer} timeout - timeout in secconds
|
|
134
183
|
* @returns {Promise} - Resolved with undefined when file transfer is done.
|
|
135
184
|
*/
|
|
136
|
-
async recv
|
|
185
|
+
async recv(src, dst, opt = [], timeout = 0) {
|
|
137
186
|
debug("recv called", src, dst, opt);
|
|
138
187
|
|
|
139
188
|
if (!isArrayOfString(src)) {
|
|
140
189
|
return logAndReject("src must be array of string");
|
|
141
190
|
}
|
|
142
|
-
if (!src.some((e)
|
|
191
|
+
if (!src.some((e)=>{
|
|
143
192
|
return e !== "";
|
|
144
193
|
})) {
|
|
145
194
|
return logAndReject("src must contain non-empty string");
|
|
@@ -153,7 +202,7 @@ class SshClientWrapper {
|
|
|
153
202
|
if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
|
|
154
203
|
return logAndReject("opt must be array of string");
|
|
155
204
|
}
|
|
156
|
-
return recv(this.hostInfo, src, dst, opt, timeout);
|
|
205
|
+
return this.deps.recv(this.hostInfo, src, dst, opt, timeout);
|
|
157
206
|
}
|
|
158
207
|
|
|
159
208
|
/**
|
|
@@ -161,23 +210,23 @@ class SshClientWrapper {
|
|
|
161
210
|
* @param {Integer} timeout - timeout in secconds
|
|
162
211
|
* @returns {Promise} - Resolved with true on success, otherwise rejected with Error.
|
|
163
212
|
*/
|
|
164
|
-
async canConnect
|
|
213
|
+
async canConnect(timeout = 60) {
|
|
165
214
|
debug("canConnect called with timeout=", timeout);
|
|
166
215
|
debugVerbose("hostInfo=", this.hostInfo);
|
|
167
216
|
|
|
168
217
|
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
169
218
|
return logAndReject("timeout must be positive integer");
|
|
170
219
|
}
|
|
171
|
-
return canConnect(this.hostInfo, timeout);
|
|
220
|
+
return this.deps.canConnect(this.hostInfo, timeout);
|
|
172
221
|
}
|
|
173
222
|
|
|
174
223
|
/**
|
|
175
224
|
* Disconnect master session
|
|
176
225
|
*/
|
|
177
|
-
async disconnect
|
|
226
|
+
async disconnect() {
|
|
178
227
|
debug(`disconnect from ${this.hostInfo.host} called`);
|
|
179
|
-
disconnect(this.hostInfo);
|
|
228
|
+
this.deps.disconnect(this.hostInfo);
|
|
180
229
|
}
|
|
181
230
|
}
|
|
182
231
|
|
|
183
|
-
|
|
232
|
+
export default SshClientWrapper;
|
package/lib/rsyncExec.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const debugVerbose = require("debug")("sshClientWrapper:verbose:rsyncExec");
|
|
6
|
-
const { glob, hasMagic } = require("glob");
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import Debug from "debug";
|
|
4
|
+
import { glob, hasMagic } from "glob";
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { connect, retryWrapper, sshExec } from "./sshExec.js";
|
|
7
|
+
import { fork } from "./fork.js";
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
import { sshCmd, rsyncCmd, acceptableRsyncRetrunCodes, getSshOption } from "./util.js";
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
const debug = Debug("sshClientWrapper:debug:rsyncExec");
|
|
12
|
+
const debugVerbose = Debug("sshClientWrapper:verbose:rsyncExec");
|
|
13
|
+
|
|
14
|
+
export async function checkRsyncVersion(timeout) {
|
|
14
15
|
const rt = {};
|
|
15
|
-
await fork(null, rsyncCmd, ["--version"], timeout, (output)
|
|
16
|
+
await fork(null, rsyncCmd, ["--version"], timeout, (output)=>{
|
|
16
17
|
const result = /^rsync *version *(\d+)\.(\d+)\.(\d+).*$/m.exec(output);
|
|
17
18
|
|
|
18
19
|
if (result) {
|
|
@@ -26,7 +27,7 @@ async function checkRsyncVersion (timeout) {
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Send file or directory and its child to server.
|
|
29
|
-
* @param {
|
|
30
|
+
* @param {object} hostInfo - hostinfo object
|
|
30
31
|
* @param {[string]} argSrc - File or directory name which to be send.
|
|
31
32
|
* @param {string} dst - Destination path.
|
|
32
33
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
@@ -34,13 +35,13 @@ async function checkRsyncVersion (timeout) {
|
|
|
34
35
|
*
|
|
35
36
|
* this function execute rsync -avv --copy-unsafe-links src rsync://[user@]host[:port]/dst
|
|
36
37
|
*/
|
|
37
|
-
async function send
|
|
38
|
+
export async function send(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
38
39
|
await connect(hostInfo);
|
|
39
|
-
const promises = await Promise.allSettled(argSrc.map((e)
|
|
40
|
+
const promises = await Promise.allSettled(argSrc.map((e)=>{
|
|
40
41
|
return glob(e);
|
|
41
42
|
}));
|
|
42
43
|
const src = promises
|
|
43
|
-
.map((e, i)
|
|
44
|
+
.map((e, i)=>{
|
|
44
45
|
if (e.status !== "fulfilled") {
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
@@ -49,10 +50,10 @@ async function send (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
49
50
|
}
|
|
50
51
|
return e.value;
|
|
51
52
|
})
|
|
52
|
-
.filter((e)
|
|
53
|
+
.filter((e)=>{
|
|
53
54
|
return e !== null;
|
|
54
55
|
})
|
|
55
|
-
.flatMap((e)
|
|
56
|
+
.flatMap((e)=>{
|
|
56
57
|
return e;
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -85,14 +86,14 @@ async function send (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
88
|
* format src array to src string for recv
|
|
88
|
-
* @param {
|
|
89
|
+
* @param {object} hostInfo - hostinfo object
|
|
89
90
|
* @returns {boolean} - rsync command version is higher than 3.2.4 or not
|
|
90
91
|
*/
|
|
91
|
-
async function isNewRsync
|
|
92
|
-
if (!hostInfo.rsyncVersion
|
|
93
|
-
!Number.isInteger(hostInfo.rsyncVersion.major)
|
|
94
|
-
!Number.isInteger(hostInfo.rsyncVersion.minor)
|
|
95
|
-
!Number.isInteger(hostInfo.rsyncVersion.patch)) {
|
|
92
|
+
async function isNewRsync(hostInfo) {
|
|
93
|
+
if (!hostInfo.rsyncVersion
|
|
94
|
+
|| !Number.isInteger(hostInfo.rsyncVersion.major)
|
|
95
|
+
|| !Number.isInteger(hostInfo.rsyncVersion.minor)
|
|
96
|
+
|| !Number.isInteger(hostInfo.rsyncVersion.patch)) {
|
|
96
97
|
hostInfo.rsyncVersion = await checkRsyncVersion(3);
|
|
97
98
|
}
|
|
98
99
|
return hostInfo.rsyncVersion.major >= 3 && hostInfo.rsyncVersion.minor >= 2 && hostInfo.rsyncVersion.patch >= 4;
|
|
@@ -100,7 +101,7 @@ async function isNewRsync (hostInfo) {
|
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
103
|
* Send file or directory and its child to server.
|
|
103
|
-
* @param {
|
|
104
|
+
* @param {object} hostInfo - hostinfo object
|
|
104
105
|
* @param {[string]} argSrc - File or directory name which to be send.
|
|
105
106
|
* @param {string} dst - Destination path.
|
|
106
107
|
* @param {[string]} rsyncOpt - optional arguments for rsync
|
|
@@ -108,7 +109,7 @@ async function isNewRsync (hostInfo) {
|
|
|
108
109
|
*
|
|
109
110
|
* this function execute rsync -av --copy-unsafe-links rsync://[user@]host[:port]/src dst
|
|
110
111
|
*/
|
|
111
|
-
async function recv
|
|
112
|
+
export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
112
113
|
await connect(hostInfo);
|
|
113
114
|
const src = `${hostInfo.host}:${argSrc.join(" ")}`;
|
|
114
115
|
const sshArgs = getSshOption(hostInfo, true);
|
|
@@ -118,7 +119,7 @@ async function recv (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
118
119
|
args.unshift("--old-args");
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
//
|
|
122
|
+
//make destination directory
|
|
122
123
|
const dstDir = argSrc.len > 1 || hasMagic(argSrc[0]) ? dst : path.dirname(dst);
|
|
123
124
|
await fs.mkdir(dstDir, { recursive: true });
|
|
124
125
|
|
|
@@ -126,8 +127,3 @@ async function recv (hostInfo, argSrc, dst, rsyncOpt, timeout) {
|
|
|
126
127
|
debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
|
|
127
128
|
return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, acceptableRsyncRetrunCodes), hostInfo);
|
|
128
129
|
}
|
|
129
|
-
module.exports = {
|
|
130
|
-
checkRsyncVersion,
|
|
131
|
-
send,
|
|
132
|
-
recv
|
|
133
|
-
};
|
package/lib/sshExec.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { setTimeout as setTimeoutPromise } from "timers/promises";
|
|
3
|
+
import Debug from "debug";
|
|
4
|
+
import { sendPty, sshCmd, getSshOption, sanityCheck, watchDogTimer } from "./util.js";
|
|
5
|
+
import { fork, createMasterPty, sshLoginCallback } from "./fork.js";
|
|
6
|
+
|
|
7
|
+
const debug = Debug("sshClientWrapper:debug:sshExec");
|
|
8
|
+
const debugVerbose = Debug("sshClientWrapper:verbose:sshExec");
|
|
9
|
+
const debugSendMasterPty = Debug("sshClientWrapper:insecure:sshExec:masterPty");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef Integer {number} - integer number
|
|
13
|
+
*/
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Execute command on remote host.
|
|
12
|
-
* @param {
|
|
17
|
+
* @param {object} hostInfo - hostinfo object
|
|
13
18
|
* @param {string} cmd - Cmdline which will be executed on remote host.
|
|
14
19
|
* @param {number} timeout - timeout (in sec.)
|
|
20
|
+
* @param {Function} outputCallback - Callback routine for stdout and stderr.
|
|
15
21
|
* @param {string} rcfile - rcfile path which will be sourced before executing actual command
|
|
16
22
|
* @param {string} prependCmd - command string which will be executed before executing actual command
|
|
17
|
-
* @
|
|
23
|
+
* @returns {Integer} - return value of cmd
|
|
18
24
|
*/
|
|
19
|
-
async function sshExec
|
|
25
|
+
export async function sshExec(hostInfo, cmd, timeout, outputCallback, rcfile, prependCmd) {
|
|
20
26
|
debug("sshExec called");
|
|
21
27
|
await connect(hostInfo);
|
|
22
28
|
debug("exec", cmd, "on remote server");
|
|
@@ -44,11 +50,42 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback, rcfile, prependC
|
|
|
44
50
|
}
|
|
45
51
|
if (e.rt === 126) {
|
|
46
52
|
debug(`${cmd} got Permission deny error but ignored`);
|
|
47
|
-
return e.rt;
|
|
48
53
|
}
|
|
49
54
|
if (e.rt === 127) {
|
|
50
55
|
debug(`${cmd} not found but ignored`);
|
|
51
|
-
|
|
56
|
+
}
|
|
57
|
+
return e.rt;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function expect(hostInfo, cmd, expects, timeout) {
|
|
62
|
+
debug("expect called");
|
|
63
|
+
await connect(hostInfo);
|
|
64
|
+
debugVerbose("exec", cmd, "on remote server with", expects);
|
|
65
|
+
const args = getSshOption(hostInfo, false);
|
|
66
|
+
const compiledExpects = expects.map((e)=>{
|
|
67
|
+
return {
|
|
68
|
+
reExpect: new RegExp(e.expect),
|
|
69
|
+
send: e.send,
|
|
70
|
+
keep: e.keep
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
const reExpect = hostInfo.prompt ? new RegExp(hostInfo.prompt) : /[$#>] /;
|
|
74
|
+
compiledExpects.unshift({ reExpect, send: cmd });
|
|
75
|
+
compiledExpects.push({ reExpect, send: "exit" });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const rt = await fork(hostInfo, sshCmd, args, timeout, null, [], compiledExpects);
|
|
79
|
+
return rt;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
if (typeof e.rt === "undefined") {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
if (e.rt === 126) {
|
|
85
|
+
debug(`${cmd} got Permission deny error but ignored`);
|
|
86
|
+
}
|
|
87
|
+
if (e.rt === 127) {
|
|
88
|
+
debug(`${cmd} not found but ignored`);
|
|
52
89
|
}
|
|
53
90
|
return e.rt;
|
|
54
91
|
}
|
|
@@ -56,32 +93,32 @@ async function sshExec (hostInfo, cmd, timeout, outputCallback, rcfile, prependC
|
|
|
56
93
|
|
|
57
94
|
/**
|
|
58
95
|
* Check if you can connect to sepecified server
|
|
59
|
-
* @param {
|
|
96
|
+
* @param {object} hostInfo - hostinfo object
|
|
60
97
|
* @param {Integer} timeout - timeout in secconds
|
|
61
98
|
*/
|
|
62
|
-
async function canConnect
|
|
99
|
+
export async function canConnect(hostInfo, timeout) {
|
|
63
100
|
debug("canConnect called");
|
|
64
101
|
sanityCheck(hostInfo);
|
|
65
102
|
|
|
66
103
|
await connect(hostInfo, timeout);
|
|
67
104
|
await disconnect(hostInfo);
|
|
68
|
-
//
|
|
69
|
-
//
|
|
105
|
+
//if connection fail, Error is throwed.
|
|
106
|
+
//so, if we reach here, connection must be succeeded
|
|
70
107
|
return true;
|
|
71
108
|
}
|
|
72
109
|
|
|
73
110
|
/**
|
|
74
111
|
* check if master session exists or not
|
|
75
|
-
* @param {
|
|
112
|
+
* @param {object} hostInfo - hostinfo object
|
|
76
113
|
* @returns {boolean} - true if master session exists
|
|
77
114
|
*/
|
|
78
|
-
async function existsMaster
|
|
115
|
+
async function existsMaster(hostInfo, argTimeout) {
|
|
79
116
|
debugVerbose("existsMaster called");
|
|
80
117
|
const args = getSshOption(hostInfo);
|
|
81
118
|
args.push("-Ocheck");
|
|
82
119
|
|
|
83
120
|
let output = "";
|
|
84
|
-
const cb = (e)
|
|
121
|
+
const cb = (e)=>{
|
|
85
122
|
output += e;
|
|
86
123
|
};
|
|
87
124
|
|
|
@@ -105,9 +142,9 @@ async function existsMaster (hostInfo, argTimeout) {
|
|
|
105
142
|
|
|
106
143
|
/**
|
|
107
144
|
* create master ssh connection
|
|
108
|
-
* @param {
|
|
145
|
+
* @param {object} hostInfo - hostinfo object
|
|
109
146
|
*/
|
|
110
|
-
async function connect
|
|
147
|
+
export async function connect(hostInfo, timeout = 60) {
|
|
111
148
|
debug("connect called");
|
|
112
149
|
|
|
113
150
|
if (await existsMaster(hostInfo, timeout)) {
|
|
@@ -123,8 +160,9 @@ async function connect (hostInfo, timeout = 60) {
|
|
|
123
160
|
const re = new RegExp(`^${words}`);
|
|
124
161
|
|
|
125
162
|
let ac = null;
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
let output = null;
|
|
164
|
+
const p = new Promise((resolve, reject)=>{
|
|
165
|
+
ac = watchDogTimer(timeout, "make connection", ()=>{
|
|
128
166
|
sendPty(hostInfo.masterPty, "\n", debugSendMasterPty, 137);
|
|
129
167
|
sendPty(hostInfo.masterPty, "exit\n", debugSendMasterPty, 138);
|
|
130
168
|
|
|
@@ -132,16 +170,17 @@ async function connect (hostInfo, timeout = 60) {
|
|
|
132
170
|
err.host = hostInfo.host;
|
|
133
171
|
err.user = hostInfo.user;
|
|
134
172
|
err.port = hostInfo.port;
|
|
173
|
+
err.output = output;
|
|
135
174
|
reject(err);
|
|
136
175
|
});
|
|
137
176
|
|
|
138
177
|
let done = false;
|
|
139
|
-
hostInfo.masterPty.onData((data)
|
|
178
|
+
hostInfo.masterPty.onData((data)=>{
|
|
140
179
|
if (done) {
|
|
141
|
-
//
|
|
180
|
+
//safeguard
|
|
142
181
|
return;
|
|
143
182
|
}
|
|
144
|
-
|
|
183
|
+
output = data.toString();
|
|
145
184
|
debugVerbose(output);
|
|
146
185
|
|
|
147
186
|
if (re.test(output)) {
|
|
@@ -160,12 +199,12 @@ async function connect (hostInfo, timeout = 60) {
|
|
|
160
199
|
reject(new Error("Control socket creation failed"));
|
|
161
200
|
}
|
|
162
201
|
});
|
|
163
|
-
hostInfo.masterPty.onData((data)
|
|
202
|
+
hostInfo.masterPty.onData((data)=>{
|
|
164
203
|
const output = data.toString();
|
|
165
204
|
sshLoginCallback(output, hostInfo.masterPty, hostInfo.password, hostInfo.passphrase, debugSendMasterPty)
|
|
166
205
|
.catch(reject);
|
|
167
206
|
});
|
|
168
|
-
hostInfo.masterPty.onExit((e)
|
|
207
|
+
hostInfo.masterPty.onExit((e)=>{
|
|
169
208
|
debug("master pty exit", e);
|
|
170
209
|
resolve(e);
|
|
171
210
|
});
|
|
@@ -188,9 +227,9 @@ async function connect (hostInfo, timeout = 60) {
|
|
|
188
227
|
|
|
189
228
|
/**
|
|
190
229
|
* remove master session
|
|
191
|
-
* @param {
|
|
230
|
+
* @param {object} hostInfo - hostinfo object
|
|
192
231
|
*/
|
|
193
|
-
async function disconnect
|
|
232
|
+
export async function disconnect(hostInfo) {
|
|
194
233
|
debug("disconnect called");
|
|
195
234
|
|
|
196
235
|
if (await existsMaster(hostInfo)) {
|
|
@@ -198,7 +237,7 @@ async function disconnect (hostInfo) {
|
|
|
198
237
|
args.push("-Oexit");
|
|
199
238
|
|
|
200
239
|
let output = "";
|
|
201
|
-
const cb = (e)
|
|
240
|
+
const cb = (e)=>{
|
|
202
241
|
output += e;
|
|
203
242
|
};
|
|
204
243
|
try {
|
|
@@ -225,50 +264,22 @@ async function disconnect (hostInfo) {
|
|
|
225
264
|
}
|
|
226
265
|
}
|
|
227
266
|
|
|
228
|
-
/**
|
|
229
|
-
* execute ls command and return output
|
|
230
|
-
* @param {Object} hostInfo - hostinfo object
|
|
231
|
-
* @param {string} target - file or directory path to watch
|
|
232
|
-
* @param {string[]} lsOpt - optional arguments for ls
|
|
233
|
-
* @param {number} timeout - timeout (in sec.)
|
|
234
|
-
* @returns {string[]} - output from ls
|
|
235
|
-
*/
|
|
236
|
-
async function ls (hostInfo, target, lsOpt, timeout) {
|
|
237
|
-
debug("ls called");
|
|
238
|
-
const outputArray = [];
|
|
239
|
-
try {
|
|
240
|
-
await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, timeout, (data) => {
|
|
241
|
-
outputArray.push(data);
|
|
242
|
-
});
|
|
243
|
-
} catch (e) {
|
|
244
|
-
// non-zero retrun should be allowed
|
|
245
|
-
if (typeof e.rt === "undefined" || e.rt < 0) {
|
|
246
|
-
throw e;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return outputArray.length === 0
|
|
250
|
-
? []
|
|
251
|
-
: outputArray
|
|
252
|
-
.join("\n")
|
|
253
|
-
.replace(/\n+/g, "\n")
|
|
254
|
-
.replace(/\n$/, "")
|
|
255
|
-
.split("\n");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
267
|
/**
|
|
259
268
|
* retry fork() if it throw retryable error
|
|
260
269
|
* @param {Function} func - function to be wrapped
|
|
261
|
-
* @param {
|
|
270
|
+
* @param {object} hostInfo - hostinfo object
|
|
262
271
|
* @param {Integer} argMaxRetry - max retry number
|
|
272
|
+
* @param {Integer} argDuration - time to wait before retring
|
|
273
|
+
* @returns {Integer} - return value of func
|
|
263
274
|
*/
|
|
264
|
-
async function retryWrapper
|
|
275
|
+
export async function retryWrapper(func, hostInfo, argMaxRetry, argDuration) {
|
|
265
276
|
const maxRetry = argMaxRetry || hostInfo.maxRetry || 3;
|
|
266
277
|
const duration = argDuration || hostInfo.retryDuration || 1000;
|
|
267
278
|
if (maxRetry === 1) {
|
|
268
279
|
return func();
|
|
269
280
|
}
|
|
270
281
|
let retryCount = 0;
|
|
271
|
-
|
|
282
|
+
for (;;) {
|
|
272
283
|
let rt;
|
|
273
284
|
try {
|
|
274
285
|
rt = await func();
|
|
@@ -291,12 +302,3 @@ async function retryWrapper (func, hostInfo, argMaxRetry, argDuration) {
|
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
304
|
}
|
|
294
|
-
|
|
295
|
-
module.exports = {
|
|
296
|
-
sshExec,
|
|
297
|
-
canConnect,
|
|
298
|
-
connect,
|
|
299
|
-
disconnect,
|
|
300
|
-
ls,
|
|
301
|
-
retryWrapper
|
|
302
|
-
};
|