ssh-client-wrapper 1.5.3 → 1.6.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 +7 -3
- package/lib/index.js +12 -1
- package/lib/sshExec.js +122 -45
- package/package.json +1 -1
- package/test/rsyncExec.js +1 -3
- package/test/sshExec.js +60 -25
package/lib/fork.js
CHANGED
|
@@ -85,12 +85,16 @@ async function fork(hostInfo, cmd, args, outputCallback) {
|
|
|
85
85
|
debug("onExit", exitCode, signal);
|
|
86
86
|
|
|
87
87
|
if (signal > 0) {
|
|
88
|
-
|
|
88
|
+
const err = new Error("signal caught");
|
|
89
|
+
err.signal = signal;
|
|
90
|
+
return reject(err);
|
|
89
91
|
}
|
|
90
|
-
if (
|
|
92
|
+
if (exitCode === 0) {
|
|
91
93
|
return resolve(exitCode);
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
const err = new Error("exit with non-zero");
|
|
96
|
+
err.rt = exitCode;
|
|
97
|
+
return reject(err);
|
|
94
98
|
});
|
|
95
99
|
});
|
|
96
100
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const debugLib = require("debug");
|
|
3
3
|
const debug = debugLib("sshClientWrapper:interface");
|
|
4
|
-
const { sshExec, canConnect, disconnect } = require("./sshExec.js");
|
|
4
|
+
const { sshExec, canConnect, disconnect, ls } = require("./sshExec.js");
|
|
5
5
|
const { send, recv } = require("./rsyncExec.js");
|
|
6
6
|
const { isArrayOfString } = require("./util.js");
|
|
7
7
|
|
|
@@ -60,6 +60,17 @@ class SshClientWrapper {
|
|
|
60
60
|
return sshExec(this.hostInfo, cmd, outputCallback);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* execute ls command and return output
|
|
66
|
+
* @param {string} target - file or directory path to watch
|
|
67
|
+
* @param {string[]} lsOpt - optional arguments for ls
|
|
68
|
+
* @returns {string[]} - output from ls
|
|
69
|
+
*/
|
|
70
|
+
async ls(target, lsOpt = []) {
|
|
71
|
+
return ls(this.hostInfo, target, lsOpt);
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
75
|
* Execute command repeatedly until specified keyword is found in stdout and/or stderr.
|
|
65
76
|
* @param {string} cmd - Cmdline which will be executed.
|
package/lib/sshExec.js
CHANGED
|
@@ -20,7 +20,23 @@ async function sshExec(hostInfo, cmd, outputCallback) {
|
|
|
20
20
|
args.push(cmd);
|
|
21
21
|
debugVerbose(`exec: ${sshCmd} ${args.join(" ")}`);
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
try {
|
|
24
|
+
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, outputCallback), hostInfo);
|
|
25
|
+
return rt;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (typeof e.rt === "undefined") {
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
if (e.rt === 126) {
|
|
31
|
+
debug(`${cmd} got Permission deny error but ignored`);
|
|
32
|
+
return e.rt;
|
|
33
|
+
}
|
|
34
|
+
if (e.rt === 127) {
|
|
35
|
+
debug(`${cmd} not found but ignored`);
|
|
36
|
+
return e.rt;
|
|
37
|
+
}
|
|
38
|
+
return e.rt;
|
|
39
|
+
}
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
/**
|
|
@@ -29,19 +45,7 @@ async function sshExec(hostInfo, cmd, outputCallback) {
|
|
|
29
45
|
* @param {Integer} timeout - timeout in secconds
|
|
30
46
|
*/
|
|
31
47
|
async function canConnect(hostInfo, timeout = 60) {
|
|
32
|
-
await connect(hostInfo);
|
|
33
48
|
debug("check connectibity");
|
|
34
|
-
const words = crypto.randomUUID();
|
|
35
|
-
const args = getSshOption(hostInfo);
|
|
36
|
-
if (Number.isInteger(timeout) && timeout > 0) {
|
|
37
|
-
args.push(`-oConnectTimeout=${timeout}`);
|
|
38
|
-
}
|
|
39
|
-
args.push(`echo ${words}`);
|
|
40
|
-
let output = "";
|
|
41
|
-
const cb = (e)=>{
|
|
42
|
-
output += e;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
49
|
const ac = new AbortController();
|
|
46
50
|
const signal = ac.signal;
|
|
47
51
|
|
|
@@ -59,22 +63,51 @@ async function canConnect(hostInfo, timeout = 60) {
|
|
|
59
63
|
}
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
if (await existsMaster(hostInfo)) {
|
|
67
|
+
const words = crypto.randomUUID();
|
|
68
|
+
const args = getSshOption(hostInfo);
|
|
69
|
+
if (Number.isInteger(timeout) && timeout > 0) {
|
|
70
|
+
args.push(`-oConnectTimeout=${timeout}`);
|
|
71
|
+
}
|
|
72
|
+
args.push(`echo ${words}`);
|
|
73
|
+
let output = "";
|
|
74
|
+
const cb = (e)=>{
|
|
75
|
+
output += e;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
debugVerbose(`canConnect: ${sshCmd} ${args.join(" ")}`);
|
|
79
|
+
const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, cb), hostInfo);
|
|
80
|
+
ac.abort();
|
|
81
|
+
|
|
82
|
+
if (rt !== 0) {
|
|
83
|
+
const err = new Error(`can not connect to ${hostInfo.host}. return code=${rt}`);
|
|
84
|
+
err.rt = rt;
|
|
85
|
+
err.host = hostInfo.host;
|
|
86
|
+
err.user = hostInfo.user;
|
|
87
|
+
err.port = hostInfo.port;
|
|
88
|
+
return err;
|
|
89
|
+
}
|
|
90
|
+
const re = new RegExp(words);
|
|
91
|
+
if (!re.test(output)) {
|
|
92
|
+
throw new Error("can not get echo result correctly");
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
73
95
|
}
|
|
74
|
-
|
|
75
|
-
|
|
96
|
+
//do NOT catch following try
|
|
97
|
+
//if connect throw error, canConnect must be throw error
|
|
98
|
+
try {
|
|
99
|
+
await connect(hostInfo);
|
|
100
|
+
} finally {
|
|
101
|
+
ac.abort();
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
76
104
|
}
|
|
77
105
|
|
|
106
|
+
/**
|
|
107
|
+
* check if master session exists or not
|
|
108
|
+
* @param {Object} hostInfo - hostinfo object
|
|
109
|
+
* @returns {boolean} - true if master session exists
|
|
110
|
+
*/
|
|
78
111
|
async function existsMaster(hostInfo) {
|
|
79
112
|
const args = getSshOption(hostInfo);
|
|
80
113
|
args.push("-Ocheck");
|
|
@@ -84,10 +117,10 @@ async function existsMaster(hostInfo) {
|
|
|
84
117
|
output += e;
|
|
85
118
|
};
|
|
86
119
|
try {
|
|
87
|
-
debug(`
|
|
120
|
+
debug(`check master session: ${sshCmd} ${args.join(" ")}`);
|
|
88
121
|
await fork(hostInfo, sshCmd, args, cb);
|
|
89
122
|
} catch (err) {
|
|
90
|
-
if (err !== 255
|
|
123
|
+
if (err.rt !== 255) {
|
|
91
124
|
throw err;
|
|
92
125
|
}
|
|
93
126
|
}
|
|
@@ -96,6 +129,7 @@ async function existsMaster(hostInfo) {
|
|
|
96
129
|
|
|
97
130
|
/**
|
|
98
131
|
* create master ssh connection
|
|
132
|
+
* @param {Object} hostInfo - hostinfo object
|
|
99
133
|
*/
|
|
100
134
|
async function connect(hostInfo) {
|
|
101
135
|
if (await existsMaster(hostInfo)) {
|
|
@@ -110,13 +144,11 @@ async function connect(hostInfo) {
|
|
|
110
144
|
const words = crypto.randomUUID();
|
|
111
145
|
const re = new RegExp(`^${words}`);
|
|
112
146
|
|
|
113
|
-
const args = getSshOption(hostInfo);
|
|
114
|
-
args.push(`echo ${words}`);
|
|
115
|
-
const cmd = `${sshCmd} ${args.join(" ")}\n`;
|
|
116
|
-
pty.write(cmd);
|
|
117
147
|
const p = new Promise((resolve, reject)=>{
|
|
118
148
|
pty.onData((data)=>{
|
|
119
149
|
const output = data.toString();
|
|
150
|
+
debugVerbose(output);
|
|
151
|
+
|
|
120
152
|
if (re.test(output)) {
|
|
121
153
|
debug("call exit on remotehost");
|
|
122
154
|
pty.write("exit\n");
|
|
@@ -131,8 +163,17 @@ async function connect(hostInfo) {
|
|
|
131
163
|
reject(new Error("Operation timed out"));
|
|
132
164
|
}
|
|
133
165
|
});
|
|
134
|
-
pty.onExit(
|
|
166
|
+
pty.onExit((e)=>{
|
|
167
|
+
debug("master pty exit", e);
|
|
168
|
+
resolve(e);
|
|
169
|
+
});
|
|
135
170
|
});
|
|
171
|
+
|
|
172
|
+
const args = getSshOption(hostInfo);
|
|
173
|
+
args.push(`echo ${words}`);
|
|
174
|
+
const cmd = `${sshCmd} ${args.join(" ")}\n`;
|
|
175
|
+
debugVerbose(cmd);
|
|
176
|
+
pty.write(cmd);
|
|
136
177
|
await p;
|
|
137
178
|
debug(`connected to ${hostInfo.host}`);
|
|
138
179
|
}
|
|
@@ -142,20 +183,29 @@ async function connect(hostInfo) {
|
|
|
142
183
|
* @param {Object} hostInfo - hostinfo object
|
|
143
184
|
*/
|
|
144
185
|
async function disconnect(hostInfo) {
|
|
145
|
-
|
|
146
|
-
|
|
186
|
+
if (await existsMaster(hostInfo)) {
|
|
187
|
+
const args = getSshOption(hostInfo);
|
|
188
|
+
args.push("-Oexit");
|
|
147
189
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
190
|
+
let output = "";
|
|
191
|
+
const cb = (e)=>{
|
|
192
|
+
output += e;
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
|
|
196
|
+
await fork(hostInfo, sshCmd, args, cb);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
if (err.rt === 255) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (/Control socket connect.*: No such file or directory/.test(output)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
157
204
|
throw err;
|
|
158
205
|
}
|
|
206
|
+
debug(`disconnected from ${hostInfo.host}`);
|
|
207
|
+
} else {
|
|
208
|
+
debug(`disconnect: master session to ${hostInfo.host} does not exist`);
|
|
159
209
|
}
|
|
160
210
|
|
|
161
211
|
if (hostInfo.masterPty) {
|
|
@@ -163,7 +213,33 @@ async function disconnect(hostInfo) {
|
|
|
163
213
|
hostInfo.masterPty.write("exit \n");
|
|
164
214
|
hostInfo.masterPty = null;
|
|
165
215
|
}
|
|
166
|
-
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* execute ls command and return output
|
|
220
|
+
* @param {Object} hostInfo - hostinfo object
|
|
221
|
+
* @param {string} target - file or directory path to watch
|
|
222
|
+
* @param {string[]} lsOpt - optional arguments for ls
|
|
223
|
+
* @returns {string[]} - output from ls
|
|
224
|
+
*/
|
|
225
|
+
async function ls(hostInfo, target, lsOpt = []) {
|
|
226
|
+
const outputArray = [];
|
|
227
|
+
try {
|
|
228
|
+
await sshExec(hostInfo, `ls ${lsOpt.join(" ")} ${target}`, (data)=>{
|
|
229
|
+
outputArray.push(data);
|
|
230
|
+
});
|
|
231
|
+
} catch (e) {
|
|
232
|
+
//non-zero retrun should be allowed
|
|
233
|
+
if (typeof e.rt === "undefined" || e.rt < 0) {
|
|
234
|
+
throw e;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return outputArray.length === 0 ? []
|
|
238
|
+
: outputArray
|
|
239
|
+
.join("\n")
|
|
240
|
+
.replace(/\n+/g, "\n")
|
|
241
|
+
.replace(/\n$/, "")
|
|
242
|
+
.split("\n");
|
|
167
243
|
}
|
|
168
244
|
|
|
169
245
|
/**
|
|
@@ -192,5 +268,6 @@ module.exports = {
|
|
|
192
268
|
canConnect,
|
|
193
269
|
connect,
|
|
194
270
|
disconnect,
|
|
271
|
+
ls,
|
|
195
272
|
retryWrapper
|
|
196
273
|
};
|
package/package.json
CHANGED
package/test/rsyncExec.js
CHANGED
|
@@ -63,9 +63,7 @@ describe("test rsync exec", function() {
|
|
|
63
63
|
await clearRemoteTestFiles(hostInfo);
|
|
64
64
|
await clearLocalTestFiles();
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
await disconnect(hostInfo);
|
|
68
|
-
});
|
|
66
|
+
await disconnect(hostInfo);
|
|
69
67
|
});
|
|
70
68
|
describe("#send", async ()=>{
|
|
71
69
|
describe("send single file", ()=>{
|
package/test/sshExec.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const path = require("path");
|
|
2
3
|
|
|
3
4
|
process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
|
|
4
5
|
Error.traceLimit = 100000;
|
|
@@ -13,19 +14,23 @@ chai.use(require("chai-as-promised"));
|
|
|
13
14
|
const sshout = sinon.stub();
|
|
14
15
|
|
|
15
16
|
//testee
|
|
16
|
-
const { sshExec, canConnect, disconnect } = require("../lib/sshExec.js");
|
|
17
|
+
const { sshExec, canConnect, disconnect, ls } = require("../lib/sshExec.js");
|
|
17
18
|
|
|
18
19
|
//test helpers
|
|
19
|
-
|
|
20
|
-
const { nonExisting } = require("./util/testFiles.js");
|
|
20
|
+
const hostInfo = require("./util/hostInfo.js");
|
|
21
|
+
const { clearRemoteTestFiles, createRemoteFiles, nonExisting, remoteRoot, remoteEmptyDir } = require("./util/testFiles.js");
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
describe("test for ssh execution", function() {
|
|
24
25
|
this.timeout(20000);//eslint-disable-line no-invalid-this
|
|
25
|
-
beforeEach(()=>{
|
|
26
|
+
beforeEach(async ()=>{
|
|
26
27
|
sshout.reset();
|
|
28
|
+
await clearRemoteTestFiles(hostInfo);
|
|
27
29
|
});
|
|
28
30
|
after(async ()=>{
|
|
31
|
+
if (!process.env.TEST_KEEP_FILES) {
|
|
32
|
+
await clearRemoteTestFiles(hostInfo);
|
|
33
|
+
}
|
|
29
34
|
await disconnect(hostInfo);
|
|
30
35
|
});
|
|
31
36
|
describe("#exec", ()=>{
|
|
@@ -58,44 +63,74 @@ describe("test for ssh execution", function() {
|
|
|
58
63
|
});
|
|
59
64
|
});
|
|
60
65
|
describe("#canConnect", ()=>{
|
|
61
|
-
let
|
|
66
|
+
let hostInfo2;
|
|
62
67
|
beforeEach(async ()=>{
|
|
63
68
|
await disconnect(hostInfo);
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
hostInfo2 = { ...hostInfo };
|
|
70
|
+
hostInfo2.masterPty = null;
|
|
66
71
|
});
|
|
67
|
-
afterEach(()=>{
|
|
68
|
-
|
|
69
|
-
hostInfo = { ...hostInfoBak };
|
|
70
|
-
hostInfo.masterPty = masterPty;
|
|
72
|
+
afterEach(async ()=>{
|
|
73
|
+
await disconnect(hostInfo2);
|
|
71
74
|
});
|
|
72
75
|
it("should be resolved with true", async ()=>{
|
|
73
|
-
expect(await canConnect(
|
|
76
|
+
expect(await canConnect(hostInfo2, 2)).to.be.true;
|
|
74
77
|
});
|
|
75
78
|
it("should be rejected if user does not exist", async ()=>{
|
|
76
|
-
|
|
77
|
-
return expect(canConnect(
|
|
79
|
+
hostInfo2.user = "xxxx";
|
|
80
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejected;
|
|
78
81
|
});
|
|
79
82
|
it("should be rejected if password is wrong", async ()=>{
|
|
80
|
-
|
|
81
|
-
return expect(canConnect(
|
|
83
|
+
hostInfo2.password = "xxxx";
|
|
84
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejected;
|
|
82
85
|
});
|
|
83
86
|
it("should be rejected if host does not exist", async ()=>{
|
|
84
|
-
|
|
85
|
-
return expect(canConnect(
|
|
87
|
+
hostInfo2.host = "foo.bar.example.com";
|
|
88
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
|
|
86
89
|
});
|
|
87
90
|
it("should be rejected if host(ip address) does not exist", async ()=>{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return expect(canConnect(
|
|
91
|
+
hostInfo2.host = "192.0.2.1";
|
|
92
|
+
hostInfo2.ConnectTimeout = 8; //please note each test will be timed out in 20 seconds
|
|
93
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
|
|
91
94
|
});
|
|
92
95
|
it("should be rejected if port number is out of range(-1)", async ()=>{
|
|
93
|
-
|
|
94
|
-
return expect(canConnect(
|
|
96
|
+
hostInfo2.port = -1;
|
|
97
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
|
|
95
98
|
});
|
|
96
99
|
it("should be rejected if port number is out of range(65536)", async ()=>{
|
|
97
|
-
|
|
98
|
-
return expect(canConnect(
|
|
100
|
+
hostInfo2.port = 65536;
|
|
101
|
+
return expect(canConnect(hostInfo2, 2)).to.be.rejectedWith(255);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("#ls", ()=>{
|
|
105
|
+
beforeEach(async ()=>{
|
|
106
|
+
await createRemoteFiles(hostInfo);
|
|
107
|
+
});
|
|
108
|
+
it("should return array of file and directory names in the specified directory", async ()=>{
|
|
109
|
+
expect(await ls(hostInfo, remoteRoot)).to.have.members(["foo", "bar", "baz", "hoge", "huga"]);
|
|
110
|
+
});
|
|
111
|
+
it("should return array which has only specified file", async ()=>{
|
|
112
|
+
expect(await ls(hostInfo, path.posix.join(remoteRoot, "foo"))).to.eql([path.posix.join(remoteRoot, "foo")]);
|
|
113
|
+
});
|
|
114
|
+
it("should return 'No such file or directory' if non-existing path is specified", async ()=>{
|
|
115
|
+
expect(await ls(hostInfo, path.posix.join(remoteRoot, nonExisting))).to.match(/No such file or directory/);
|
|
116
|
+
});
|
|
117
|
+
it("should return only matched filenames, if glob is specified", async ()=>{
|
|
118
|
+
expect(await ls(hostInfo, path.posix.join(remoteRoot, "b*"))).to.have.members(["bar", "baz"].map((e)=>{
|
|
119
|
+
return path.posix.join(remoteRoot, e);
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
it("should return only matched filenames src contains multipl glob pattern", async ()=>{
|
|
123
|
+
expect(await ls(hostInfo, path.posix.join(remoteRoot, "h*", "p[iu]yo"))).to.have.members(["piyo", "puyo"].map((e)=>{
|
|
124
|
+
return path.posix.join(remoteRoot, "hoge", e);
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
it("should return only matched filenames, if specified glob contains /", async ()=>{
|
|
128
|
+
expect(await ls(hostInfo, path.posix.join(remoteRoot, "hoge/*yo"))).to.have.members(["piyo", "puyo", "poyo"].map((e)=>{
|
|
129
|
+
return path.posix.join(remoteRoot, "hoge", e);
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
132
|
+
it("should return empty array if specified target is empty directory", async ()=>{
|
|
133
|
+
expect(await ls(hostInfo, remoteEmptyDir)).to.be.an("array").that.is.empty;
|
|
99
134
|
});
|
|
100
135
|
});
|
|
101
136
|
});
|