ssh-client-wrapper 1.5.2 → 1.5.4

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 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
- return reject(signal);
88
+ const err = new Error("signal caught");
89
+ err.signal = signal;
90
+ return reject(err);
89
91
  }
90
- if ([0, 126, 127].includes(exitCode)) {
92
+ if (exitCode === 0) {
91
93
  return resolve(exitCode);
92
94
  }
93
- return reject(exitCode);
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/rsyncExec.js CHANGED
@@ -50,7 +50,13 @@ async function send(hostInfo, argSrc, dst, rsyncOpt = []) {
50
50
  const rshOpt = ["-e", `${sshCmd} ${sshArgs.join(" ")}`];
51
51
 
52
52
  const args = ["-avv", "--copy-unsafe-links", ...rshOpt, ...rsyncOpt, ...src, `${hostInfo.host}:${dst}`];
53
- const { dir: remoteDir } = path.parse(dst);
53
+ let remoteDir;
54
+ if (dst.endsWith("/")) {
55
+ remoteDir = dst;
56
+ } else {
57
+ const { dir } = path.parse(dst);
58
+ remoteDir = dir;
59
+ }
54
60
  debug("remoteDir=", remoteDir);
55
61
 
56
62
  if (remoteDir !== "" && remoteDir !== "/") {
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
- return retryWrapper(fork.bind(null, hostInfo, sshCmd, args, outputCallback), hostInfo);
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
+ throw e;
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
- debugVerbose(`canConnect: ${sshCmd} ${args.join(" ")}`);
63
- const rt = await retryWrapper(fork.bind(null, hostInfo, sshCmd, args, cb), hostInfo);
64
- ac.abort();
65
-
66
- if (rt !== 0) {
67
- const err = new Error(`can not connect to ${hostInfo.host}. return code=${rt}`);
68
- err.rt = rt;
69
- err.host = hostInfo.host;
70
- err.user = hostInfo.user;
71
- err.port = hostInfo.port;
72
- return err;
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
- const re = new RegExp(words);
75
- return re.test(output);
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(`disconnect: ${sshCmd} ${args.join(" ")}`);
120
+ debug(`check master session: ${sshCmd} ${args.join(" ")}`);
88
121
  await fork(hostInfo, sshCmd, args, cb);
89
122
  } catch (err) {
90
- if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
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(reject);
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
- const args = getSshOption(hostInfo);
146
- args.push("-Oexit");
186
+ if (await existsMaster(hostInfo)) {
187
+ const args = getSshOption(hostInfo);
188
+ args.push("-Oexit");
147
189
 
148
- let output = "";
149
- const cb = (e)=>{
150
- output += e;
151
- };
152
- try {
153
- debug(`disconnect: ${sshCmd} ${args.join(" ")}`);
154
- await fork(hostInfo, sshCmd, args, cb);
155
- } catch (err) {
156
- if (err !== 255 || !/Control socket connect.*: No such file or directory/.test(output)) {
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
- debug(`disconnected from ${hostInfo.host}`);
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
+
238
+ return 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "wrapper library for open ssh client and rsync",
5
5
  "keywords": [
6
6
  "ssh",
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
- after(async ()=>{
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
- let hostInfo = require("./util/hostInfo.js");
20
- const { nonExisting } = require("./util/testFiles.js");
20
+ const hostInfo = require("./util/hostInfo.js");
21
+ const { clearRemoteTestFiles, createRemoteFiles, nonExisting, remoteRoot } = 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,71 @@ describe("test for ssh execution", function() {
58
63
  });
59
64
  });
60
65
  describe("#canConnect", ()=>{
61
- let hostInfoBak;
66
+ let hostInfo2;
62
67
  beforeEach(async ()=>{
63
68
  await disconnect(hostInfo);
64
- hostInfo.masterPty = null;
65
- hostInfoBak = { ...hostInfo };
69
+ hostInfo2 = { ...hostInfo };
70
+ hostInfo2.masterPty = null;
66
71
  });
67
- afterEach(()=>{
68
- const masterPty = hostInfo.masterPty;
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(hostInfo, 2)).to.be.true;
76
+ expect(await canConnect(hostInfo2, 2)).to.be.true;
74
77
  });
75
78
  it("should be rejected if user does not exist", async ()=>{
76
- hostInfo.user = "xxxx";
77
- return expect(canConnect(hostInfo, 2)).to.be.rejected;
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
- hostInfo.password = "xxxx";
81
- return expect(canConnect(hostInfo, 2)).to.be.rejected;
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
- hostInfo.host = "foo.bar.example.com";
85
- return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
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
- hostInfo.host = "192.0.2.1";
89
- hostInfo.ConnectTimeout = 8; //please note each test will be timed out in 20 seconds
90
- return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
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
- hostInfo.port = -1;
94
- return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
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
- hostInfo.port = 65536;
98
- return expect(canConnect(hostInfo, 2)).to.be.rejectedWith(255);
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
+ }));
99
131
  });
100
132
  });
101
133
  });