@linxin666/dsh-ssh 0.1.15 → 0.1.16

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/index.js CHANGED
@@ -1352,23 +1352,24 @@ function connectClient(config) {
1352
1352
  return new Promise((resolve, reject) => {
1353
1353
  const client = new Client();
1354
1354
  let settled = false;
1355
- client.once("ready", () => {
1355
+ const fail = (error) => {
1356
1356
  if (settled) return;
1357
1357
  settled = true;
1358
- resolve(client);
1359
- });
1360
- client.once("error", (error) => {
1358
+ try {
1359
+ client.destroy();
1360
+ } catch {}
1361
+ reject(error instanceof Error ? error : new Error(String(error)));
1362
+ };
1363
+ client.once("ready", () => {
1361
1364
  if (settled) return;
1362
1365
  settled = true;
1363
- reject(error instanceof Error ? error : new Error(String(error)));
1366
+ resolve(client);
1364
1367
  });
1368
+ client.on("error", fail);
1365
1369
  try {
1366
1370
  client.connect(config);
1367
1371
  } catch (error) {
1368
- if (!settled) {
1369
- settled = true;
1370
- reject(error instanceof Error ? error : new Error(String(error)));
1371
- }
1372
+ fail(error);
1372
1373
  }
1373
1374
  });
1374
1375
  }
@@ -1414,13 +1415,18 @@ async function connectChain(engine, entry) {
1414
1415
  });
1415
1416
  });
1416
1417
  }
1418
+ let target;
1417
1419
  try {
1420
+ target = await connectClient(buildConnectConfig(entry, sock));
1418
1421
  return {
1419
- client: await connectClient(buildConnectConfig(entry, sock)),
1422
+ client: target,
1420
1423
  hops
1421
1424
  };
1422
1425
  } catch (error) {
1423
1426
  for (const client of hops) client.end();
1427
+ if (target !== void 0) try {
1428
+ target.destroy();
1429
+ } catch {}
1424
1430
  throw error;
1425
1431
  }
1426
1432
  }
@@ -48,6 +48,8 @@ export interface PoolEngine {
48
48
  }
49
49
  /** Build the ssh2 connect config for one entry (key read from disk). */
50
50
  export declare function buildConnectConfig(entry: SshHostEntry, sock?: ConnectConfig['sock']): ConnectConfig;
51
+ /** Connect one ssh2 client (resolve on ready, reject on error/close). */
52
+ export declare function connectClient(config: ConnectConfig): Promise<Client>;
51
53
  /** Cap captured output at the configured byte budget (marks truncation). */
52
54
  export declare function appendOutput(target: {
53
55
  text: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-ssh",
3
3
  "description": "Remote SSH operations for the dsh web GUI: host config store (~/.dsh/dsh-ssh.json, import from ~/.ssh/config), a persistent ssh2 connection pool with jump-host support, exec / PTY web terminal / SFTP transfer / local port-forward tunnels / cluster execution, plus agent tools (ssh_list, ssh_exec, ssh_upload, ssh_download, ssh_tunnel, ssh_cluster). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
- "version": "0.1.15",
4
+ "version": "0.1.16",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -88,27 +88,31 @@ export function buildConnectConfig(entry: SshHostEntry, sock?: ConnectConfig['so
88
88
  }
89
89
 
90
90
  /** Connect one ssh2 client (resolve on ready, reject on error/close). */
91
- function connectClient(config: ConnectConfig): Promise<Client> {
91
+ export function connectClient(config: ConnectConfig): Promise<Client> {
92
92
  return new Promise((resolve, reject) => {
93
93
  const client = new Client()
94
94
  let settled = false
95
- client.once('ready', () => {
95
+ const fail = (error: unknown): void => {
96
96
  if (settled) return
97
97
  settled = true
98
- resolve(client)
99
- })
100
- client.once('error', (error) => {
98
+ try { client.destroy() } catch { /* already closed */ }
99
+ reject(error instanceof Error ? error : new Error(String(error)))
100
+ }
101
+ client.once('ready', () => {
101
102
  if (settled) return
102
103
  settled = true
103
- reject(error instanceof Error ? error : new Error(String(error)))
104
+ resolve(client)
104
105
  })
106
+ // Keep an error listener attached after the handshake: when TCP connects
107
+ // but the handshake drops, ssh2 can emit a second 'error' after a single
108
+ // once-listener is gone, which would surface as an unhandled 'error' event.
109
+ // The settled guard turns later emissions into a no-op; other callers
110
+ // (like doAcquire) attach their own error listeners and all fire together.
111
+ client.on('error', fail)
105
112
  try {
106
113
  client.connect(config)
107
114
  } catch (error) {
108
- if (!settled) {
109
- settled = true
110
- reject(error instanceof Error ? error : new Error(String(error)))
111
- }
115
+ fail(error)
112
116
  }
113
117
  })
114
118
  }
@@ -159,11 +163,18 @@ export async function connectChain(engine: PoolEngine, entry: SshHostEntry): Pro
159
163
  })
160
164
  })
161
165
  }
166
+ let target: Client | undefined
162
167
  try {
163
- const client = await connectClient(buildConnectConfig(entry, sock))
164
- return { client, hops }
168
+ target = await connectClient(buildConnectConfig(entry, sock))
169
+ return { client: target, hops }
165
170
  } catch (error) {
166
171
  for (const client of hops) client.end()
172
+ // connectClient already destroys the failed target on its own failure
173
+ // path; destroy defensively only when a reference leaked through, and
174
+ // guard it so a second destroy is never an issue.
175
+ if (target !== undefined) {
176
+ try { target.destroy() } catch { /* already destroyed */ }
177
+ }
167
178
  throw error
168
179
  }
169
180
  }