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
package/lib/util.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { EOL } = require("os");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const debug = require("debug")("sshClientWrapper:debug:util");
|
|
5
|
+
const { setTimeout: setTimeoutPromise } = require("timers/promises");
|
|
6
|
+
|
|
3
7
|
const sshCmd = "ssh";
|
|
4
8
|
const rsyncCmd = "rsync";
|
|
5
9
|
|
|
@@ -62,8 +66,20 @@ const getSshOption = (hostInfo, withoutDestination)=>{
|
|
|
62
66
|
args.push(hostInfo.port);
|
|
63
67
|
}
|
|
64
68
|
if (typeof hostInfo.keyFile === "string") {
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
try {
|
|
70
|
+
const stats = fs.statSync(hostInfo.keyFile);
|
|
71
|
+
if (stats.isFile()) {
|
|
72
|
+
args.push("-i");
|
|
73
|
+
args.push(hostInfo.keyFile);
|
|
74
|
+
} else {
|
|
75
|
+
debug(`specified keyFile(${hostInfo.keyFile}) is not file. so it is ignored`);
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code !== "ENOENT") {
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
debug(`specified keyFile(${hostInfo.keyFile}) not found and ignored`);
|
|
82
|
+
}
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
if (hostInfo.noStrictHostkeyChecking) {
|
|
@@ -95,6 +111,42 @@ const isArrayOfString = (target)=>{
|
|
|
95
111
|
});
|
|
96
112
|
};
|
|
97
113
|
|
|
114
|
+
/**
|
|
115
|
+
* log and send message to pty
|
|
116
|
+
* @param {Object} pty - pty object which message will be send to
|
|
117
|
+
* @param {string} message - message
|
|
118
|
+
* @param {Function} logger - logging function
|
|
119
|
+
* @param {string} label - where this function is called
|
|
120
|
+
*/
|
|
121
|
+
const sendPty = async (pty, message, logger, label)=>{
|
|
122
|
+
logger(`${label}: ${message}`);
|
|
123
|
+
pty.write(message);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* set watch dog timer
|
|
128
|
+
* @param {number} timeout - timeout (in sec.)
|
|
129
|
+
* @param {string} label - used in both timeout and aborted message
|
|
130
|
+
* @param {Function} cb - call back function which will be called after timeout
|
|
131
|
+
* @returns {AbortController} - ac to stop watch dog timer
|
|
132
|
+
*/
|
|
133
|
+
const watchDogTimer = (timeout, label, cb)=>{
|
|
134
|
+
const ac = new AbortController();
|
|
135
|
+
const signal = ac.signal;
|
|
136
|
+
setTimeoutPromise(timeout * 1000, "neverUsedValue", { signal })
|
|
137
|
+
.then(()=>{
|
|
138
|
+
debug(`can not finish ${label} within ${timeout} sec`);
|
|
139
|
+
return typeof cb === "function" ? cb() : true;
|
|
140
|
+
})
|
|
141
|
+
.catch((err)=>{
|
|
142
|
+
if (err.name === "AbortError") {
|
|
143
|
+
debug(`timeout was aborted because ${label} has done`);
|
|
144
|
+
} else {
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return ac;
|
|
149
|
+
};
|
|
98
150
|
|
|
99
151
|
module.exports = {
|
|
100
152
|
sshCmd,
|
|
@@ -105,5 +157,7 @@ module.exports = {
|
|
|
105
157
|
buff2String,
|
|
106
158
|
getSshOption,
|
|
107
159
|
sanityCheck,
|
|
108
|
-
isArrayOfString
|
|
160
|
+
isArrayOfString,
|
|
161
|
+
sendPty,
|
|
162
|
+
watchDogTimer
|
|
109
163
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh-client-wrapper",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "wrapper library for open ssh client and rsync",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ssh",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"glob": "^10.3.3",
|
|
39
39
|
"node-pty": "^0.10.1",
|
|
40
40
|
"nyc": "^15.1.0",
|
|
41
|
-
"promise-retry": "^2.0.1"
|
|
41
|
+
"promise-retry": "^2.0.1",
|
|
42
|
+
"rewire": "^7.0.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"chai": "^4.3.7",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
process.on("unhandledRejection", console.dir); //eslint-disable-line no-console
|
|
3
|
+
Error.traceLimit = 100000;
|
|
4
|
+
|
|
5
|
+
//setup test framework
|
|
6
|
+
const chai = require("chai");
|
|
7
|
+
const { expect } = require("chai");
|
|
8
|
+
const sinon = require("sinon");
|
|
9
|
+
chai.use(require("sinon-chai"));
|
|
10
|
+
chai.use(require("chai-as-promised"));
|
|
11
|
+
|
|
12
|
+
const rewire = require("rewire");
|
|
13
|
+
|
|
14
|
+
//testee
|
|
15
|
+
const SshClientWrapper = rewire("../lib/index.js");
|
|
16
|
+
|
|
17
|
+
const sshExec = sinon.stub();
|
|
18
|
+
const canConnect = sinon.stub();
|
|
19
|
+
const disconnect = sinon.stub();
|
|
20
|
+
const ls = sinon.stub();
|
|
21
|
+
const send = sinon.stub();
|
|
22
|
+
const recv = sinon.stub();
|
|
23
|
+
|
|
24
|
+
//eslint-disable-next-line no-underscore-dangle
|
|
25
|
+
SshClientWrapper.__set__({
|
|
26
|
+
sshExec, canConnect, disconnect, ls, send, recv
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
describe("test for interface", ()=>{
|
|
31
|
+
describe("test for constructor", ()=>{
|
|
32
|
+
it("should throw error when instanciate with non-empty string host", ()=>{
|
|
33
|
+
expect(()=>{
|
|
34
|
+
//eslint-disable-next-line no-new
|
|
35
|
+
new SshClientWrapper({});
|
|
36
|
+
}).to.throw("host must be non-empty string");
|
|
37
|
+
expect(()=>{
|
|
38
|
+
//eslint-disable-next-line no-new
|
|
39
|
+
new SshClientWrapper({ host: 1 });
|
|
40
|
+
}).to.throw("host must be non-empty string");
|
|
41
|
+
expect(()=>{
|
|
42
|
+
//eslint-disable-next-line no-new
|
|
43
|
+
new SshClientWrapper({ host: "" });
|
|
44
|
+
}).to.throw("host must be non-empty string");
|
|
45
|
+
});
|
|
46
|
+
it("should successfully instanciate only with host prop", ()=>{
|
|
47
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("exec");
|
|
48
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("ls");
|
|
49
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("watch");
|
|
50
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("send");
|
|
51
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("recv");
|
|
52
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("canConnect");
|
|
53
|
+
expect(new SshClientWrapper({ host: "hoge" })).to.have.property("disconnect");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("test for public method", ()=>{
|
|
57
|
+
let ssh;
|
|
58
|
+
const hostInfoOrg = { host: "hoge" };
|
|
59
|
+
const hostInfo = { ...hostInfoOrg, masterPty: null };
|
|
60
|
+
beforeEach(()=>{
|
|
61
|
+
ssh = new SshClientWrapper(hostInfoOrg);
|
|
62
|
+
sshExec.reset();
|
|
63
|
+
canConnect.reset();
|
|
64
|
+
disconnect.reset();
|
|
65
|
+
ls.reset();
|
|
66
|
+
send.reset();
|
|
67
|
+
recv.reset();
|
|
68
|
+
});
|
|
69
|
+
describe("test for exec", ()=>{
|
|
70
|
+
it("should call sshExec with cmd", ()=>{
|
|
71
|
+
ssh.exec("ls");
|
|
72
|
+
expect(sshExec).to.be.calledWith(hostInfo, "ls", 0, null);
|
|
73
|
+
});
|
|
74
|
+
it("should call sshExec with cmd and timeout", ()=>{
|
|
75
|
+
ssh.exec("ls", 10);
|
|
76
|
+
expect(sshExec).to.be.calledWith(hostInfo, "ls", 10, null);
|
|
77
|
+
});
|
|
78
|
+
it("should call sshExec with cmd, timeout and call back function", ()=>{
|
|
79
|
+
const cb = ()=>{};
|
|
80
|
+
ssh.exec("ls", 10, cb);
|
|
81
|
+
expect(sshExec).to.be.calledWith(hostInfo, "ls", 10, cb);
|
|
82
|
+
});
|
|
83
|
+
it("should reject if cmd is not string (number)", ()=>{
|
|
84
|
+
return expect(ssh.exec(0)).to.be.rejectedWith("cmd must be string");
|
|
85
|
+
});
|
|
86
|
+
it("should reject if cmd is not string (null)", ()=>{
|
|
87
|
+
return expect(ssh.exec(null)).to.be.rejectedWith("cmd must be string");
|
|
88
|
+
});
|
|
89
|
+
it("should reject if cmd is empty string", ()=>{
|
|
90
|
+
return expect(ssh.exec("")).to.be.rejectedWith("cmd must be string");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe("test for ls", ()=>{
|
|
94
|
+
it("should call ls without timeout", ()=>{
|
|
95
|
+
ssh.ls("hoge");
|
|
96
|
+
expect(ls).to.be.calledWith(hostInfo, "hoge", [], 0);
|
|
97
|
+
});
|
|
98
|
+
it("should call ls with option", ()=>{
|
|
99
|
+
ssh.ls("hoge", ["-l", "-r", "-t"]);
|
|
100
|
+
expect(ls).to.be.calledWith(hostInfo, "hoge", ["-l", "-r", "-t"], 0);
|
|
101
|
+
});
|
|
102
|
+
it("should call ls with option and timeout", ()=>{
|
|
103
|
+
ssh.ls("hoge", ["-l", "-r", "-t"], 10);
|
|
104
|
+
expect(ls).to.be.calledWith(hostInfo, "hoge", ["-l", "-r", "-t"], 10);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("test for send", ()=>{
|
|
108
|
+
it("should call send with src and dst", ()=>{
|
|
109
|
+
ssh.send(["src"], "dst");
|
|
110
|
+
expect(send).to.be.calledWith(hostInfo, ["src"], "dst", [], 0);
|
|
111
|
+
});
|
|
112
|
+
it("should call send with src, dst, and opt", ()=>{
|
|
113
|
+
ssh.send(["src"], "dst", ["--dry-run", "--delete"]);
|
|
114
|
+
expect(send).to.be.calledWith(hostInfo, ["src"], "dst", ["--dry-run", "--delete"], 0);
|
|
115
|
+
});
|
|
116
|
+
it("should call send with src, dst, opt, and timeout", ()=>{
|
|
117
|
+
ssh.send(["src"], "dst", ["--dry-run", "--delete"], 10);
|
|
118
|
+
expect(send).to.be.calledWith(hostInfo, ["src"], "dst", ["--dry-run", "--delete"], 10);
|
|
119
|
+
});
|
|
120
|
+
it("should be rejected if src is not array of string", ()=>{
|
|
121
|
+
return expect(ssh.send(1)).to.be.rejectedWith("src must be array of string");
|
|
122
|
+
});
|
|
123
|
+
it("should be rejected if src has only empty string element", ()=>{
|
|
124
|
+
return expect(ssh.send(["", ""])).to.be.rejectedWith("src must contain non-empty string");
|
|
125
|
+
});
|
|
126
|
+
it("should be rejected if src has only empty string element", ()=>{
|
|
127
|
+
return expect(ssh.send([""])).to.be.rejectedWith("src must contain non-empty string");
|
|
128
|
+
});
|
|
129
|
+
it("should be called if src includes empty string ", ()=>{
|
|
130
|
+
ssh.send(["src", ""], "dst");
|
|
131
|
+
expect(send).to.be.calledWith(hostInfo, ["src", ""], "dst", [], 0);
|
|
132
|
+
});
|
|
133
|
+
it("should be rejected if dst is not string", ()=>{
|
|
134
|
+
return expect(ssh.send(["src"], 0)).to.be.rejectedWith("dst must be string");
|
|
135
|
+
});
|
|
136
|
+
it("should be rejected if dst is empty string", ()=>{
|
|
137
|
+
return expect(ssh.send(["src"], "")).to.be.rejectedWith("dst must be non-empty string");
|
|
138
|
+
});
|
|
139
|
+
it("should be rejected if opt is not array of string", ()=>{
|
|
140
|
+
return expect(ssh.send(["src"], "dst", [null])).to.be.rejectedWith("opt must be array of string");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("test for recv", ()=>{
|
|
144
|
+
it("should call recv with src and dst", ()=>{
|
|
145
|
+
ssh.recv(["src"], "dst");
|
|
146
|
+
expect(recv).to.be.calledWith(hostInfo, ["src"], "dst", [], 0);
|
|
147
|
+
});
|
|
148
|
+
it("should call recv with src, dst, and opt", ()=>{
|
|
149
|
+
ssh.recv(["src"], "dst", ["--dry-run", "--delete"]);
|
|
150
|
+
expect(recv).to.be.calledWith(hostInfo, ["src"], "dst", ["--dry-run", "--delete"], 0);
|
|
151
|
+
});
|
|
152
|
+
it("should call recv with src, dst, opt, and timeout", ()=>{
|
|
153
|
+
ssh.recv(["src"], "dst", ["--dry-run", "--delete"], 10);
|
|
154
|
+
expect(recv).to.be.calledWith(hostInfo, ["src"], "dst", ["--dry-run", "--delete"], 10);
|
|
155
|
+
});
|
|
156
|
+
it("should be rejected if src is not array of string", ()=>{
|
|
157
|
+
return expect(ssh.recv(1)).to.be.rejectedWith("src must be array of string");
|
|
158
|
+
});
|
|
159
|
+
it("should be rejected if src has only empty string element", ()=>{
|
|
160
|
+
return expect(ssh.recv(["", ""])).to.be.rejectedWith("src must contain non-empty string");
|
|
161
|
+
});
|
|
162
|
+
it("should be rejected if src has only empty string element", ()=>{
|
|
163
|
+
return expect(ssh.recv([""])).to.be.rejectedWith("src must contain non-empty string");
|
|
164
|
+
});
|
|
165
|
+
it("should be called if src includes empty string ", ()=>{
|
|
166
|
+
ssh.recv(["src", ""], "dst");
|
|
167
|
+
expect(recv).to.be.calledWith(hostInfo, ["src", ""], "dst", [], 0);
|
|
168
|
+
});
|
|
169
|
+
it("should be rejected if dst is not string", ()=>{
|
|
170
|
+
return expect(ssh.recv(["src"], 0)).to.be.rejectedWith("dst must be string");
|
|
171
|
+
});
|
|
172
|
+
it("should be rejected if dst is empty string", ()=>{
|
|
173
|
+
return expect(ssh.recv(["src"], "")).to.be.rejectedWith("dst must be non-empty string");
|
|
174
|
+
});
|
|
175
|
+
it("should be rejected if opt is not array of string", ()=>{
|
|
176
|
+
return expect(ssh.recv(["src"], "dst", [null])).to.be.rejectedWith("opt must be array of string");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|