agent-ssh-cli 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D18-339933?logo=node.js&logoColor=white" alt="Node.js >=18"></a>
13
13
  <a href="https://www.npmjs.com/"><img src="https://img.shields.io/badge/npm-%3E%3D8-CB3837?logo=npm&logoColor=white" alt="npm >=8"></a>
14
14
  <a href="https://github.com/sleepinginsummer/agent-ssh-cli"><img src="https://img.shields.io/badge/Windows-MacOS-0078D6?labelColor=0078D6&color=C0C0C0" alt="Windows/MacOS"></a>
15
- <a href="https://github.com/sleepinginsummer/agent-ssh-cli/releases"><img src="https://img.shields.io/badge/release-v0.1.0-blue" alt="release v0.1.0"></a>
15
+ <a href="https://github.com/sleepinginsummer/agent-ssh-cli/releases"><img src="https://img.shields.io/badge/release-v0.1.3-blue" alt="release v0.1.3"></a>
16
16
  <a href="https://github.com/sleepinginsummer/agent-ssh-cli/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen" alt="PRs welcome"></a>
17
17
  </p>
18
18
 
@@ -153,6 +153,12 @@ agentsshcli list
153
153
 
154
154
  ## 卸载和清理
155
155
 
156
+ 更新到最新版:
157
+
158
+ ```bash
159
+ npm install -g agent-ssh-cli@latest
160
+ ```
161
+
156
162
  ```bash
157
163
  npm uninstall -g agent-ssh-cli
158
164
  npm cache clean --force
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-ssh-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "基于 CLI 的 SSH 代理工具,按 ssh-mcp-server 的能力一一映射",
5
5
  "type": "module",
6
6
  "engines": {
package/src/config.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
 
7
7
  const DEFAULT_CONFIG_DIR = ".agent-ssh-cli";
8
8
  const DEFAULT_CONFIG_FILE = "config.json";
9
+ const configCache = new Map();
9
10
 
10
11
  function isNonEmptyString(value) {
11
12
  return typeof value === "string" && value.trim() !== "";
@@ -23,7 +24,10 @@ function ensureRegexArray(patterns, fieldName, index) {
23
24
  throw new Error(`ssh-config.json 第 ${index + 1} 项的 ${fieldName} 必须只包含非空字符串`);
24
25
  }
25
26
  try {
26
- return new RegExp(pattern), pattern;
27
+ return {
28
+ pattern,
29
+ regex: new RegExp(pattern)
30
+ };
27
31
  } catch (error) {
28
32
  throw new Error(`ssh-config.json 第 ${index + 1} 项的 ${fieldName} 含有非法正则: ${pattern},${error.message}`);
29
33
  }
@@ -110,7 +114,13 @@ export function getDefaultConfigPath() {
110
114
  }
111
115
 
112
116
  export function loadConfig(configPath = getDefaultConfigPath()) {
113
- const raw = fs.readFileSync(configPath, "utf8");
117
+ const resolvedConfigPath = path.resolve(configPath);
118
+ const stat = fs.statSync(resolvedConfigPath);
119
+ const cached = configCache.get(resolvedConfigPath);
120
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
121
+ return cached.configs;
122
+ }
123
+ const raw = fs.readFileSync(resolvedConfigPath, "utf8");
114
124
  let parsed;
115
125
  try {
116
126
  parsed = JSON.parse(raw);
@@ -131,6 +141,11 @@ export function loadConfig(configPath = getDefaultConfigPath()) {
131
141
  }
132
142
  seenNames.add(config.name);
133
143
  }
144
+ configCache.set(resolvedConfigPath, {
145
+ mtimeMs: stat.mtimeMs,
146
+ size: stat.size,
147
+ configs
148
+ });
134
149
  return configs;
135
150
  }
136
151
 
@@ -39,8 +39,8 @@ function connectSocket(socketPath, timeoutMs = DAEMON_REQUEST_TIMEOUT_MS) {
39
39
  });
40
40
  }
41
41
 
42
- function spawnDaemon(socketPath) {
43
- const child = spawn(process.execPath, [getDaemonEntryPath(), "--socket", socketPath], {
42
+ function spawnDaemon(socketPath, configPath) {
43
+ const child = spawn(process.execPath, [getDaemonEntryPath(), "--socket", socketPath, "--config", path.resolve(configPath)], {
44
44
  detached: true,
45
45
  stdio: "ignore"
46
46
  });
@@ -63,7 +63,7 @@ async function waitForDaemon(socketPath) {
63
63
  throw new Error(`启动 SSH 缓存进程失败: ${lastError?.message || "未知错误"}`);
64
64
  }
65
65
 
66
- async function ensureDaemon(socketPath) {
66
+ async function ensureDaemon(socketPath, configPath) {
67
67
  try {
68
68
  const socket = await connectSocket(socketPath, 500);
69
69
  socket.end();
@@ -76,13 +76,13 @@ async function ensureDaemon(socketPath) {
76
76
  unlinkSocketPath(socketPath);
77
77
  }
78
78
  }
79
- spawnDaemon(socketPath);
79
+ spawnDaemon(socketPath, configPath);
80
80
  await waitForDaemon(socketPath);
81
81
  }
82
82
 
83
83
  export async function requestDaemon(configPath, request) {
84
84
  const socketPath = getSocketPath(configPath);
85
- await ensureDaemon(socketPath);
85
+ await ensureDaemon(socketPath, configPath);
86
86
  const socket = await connectSocket(socketPath);
87
87
  socket.setEncoding("utf8");
88
88
 
package/src/ssh-client.js CHANGED
@@ -1,12 +1,39 @@
1
1
  import fs from "node:fs";
2
+ import net from "node:net";
3
+ import { pipeline } from "node:stream/promises";
2
4
  import { Client } from "ssh2";
3
5
 
6
+ const SOCKS5_VERSION = 0x05;
7
+ const SOCKS5_CONNECT_COMMAND = 0x01;
8
+ const SOCKS5_RESERVED = 0x00;
9
+ const SOCKS5_AUTH_NONE = 0x00;
10
+ const SOCKS5_AUTH_PASSWORD = 0x02;
11
+ const SOCKS5_AUTH_UNACCEPTABLE = 0xff;
12
+ const SOCKS5_ADDRESS_IPV4 = 0x01;
13
+ const SOCKS5_ADDRESS_DOMAIN = 0x03;
14
+ const SOCKS5_ADDRESS_IPV6 = 0x04;
15
+ const SOCKS5_REPLY_SUCCESS = 0x00;
16
+
17
+ function getCompiledPattern(pattern, label) {
18
+ if (pattern instanceof RegExp) {
19
+ return pattern;
20
+ }
21
+ if (pattern?.regex instanceof RegExp) {
22
+ return pattern.regex;
23
+ }
24
+ try {
25
+ return new RegExp(pattern?.pattern || pattern);
26
+ } catch (error) {
27
+ throw new Error(`${label} 正则非法: ${pattern?.pattern || pattern},${error.message}`);
28
+ }
29
+ }
30
+
4
31
  function compilePatterns(patterns, label) {
5
32
  return (patterns || []).map((pattern) => {
6
33
  try {
7
- return new RegExp(pattern);
34
+ return getCompiledPattern(pattern, label);
8
35
  } catch (error) {
9
- throw new Error(`${label} 正则非法: ${pattern},${error.message}`);
36
+ throw new Error(error.message);
10
37
  }
11
38
  });
12
39
  }
@@ -23,12 +50,201 @@ export function validateCommand(connection, command) {
23
50
  }
24
51
  }
25
52
 
26
- function createConnectConfig(connection) {
53
+ function parseSocksProxy(proxy) {
54
+ const value = proxy.includes("://") ? proxy : `socks5://${proxy}`;
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(value);
58
+ } catch (error) {
59
+ throw new Error(`socksProxy 格式非法: ${proxy},${error.message}`);
60
+ }
61
+ if (parsed.protocol !== "socks5:") {
62
+ throw new Error("socksProxy 仅支持 socks5:// 协议");
63
+ }
64
+ if (!parsed.hostname || !parsed.port) {
65
+ throw new Error("socksProxy 必须包含代理主机和端口");
66
+ }
67
+ const port = Number(parsed.port);
68
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
69
+ throw new Error("socksProxy 端口非法");
70
+ }
71
+ const username = decodeURIComponent(parsed.username);
72
+ const password = decodeURIComponent(parsed.password);
73
+ if ((username && !password) || (!username && password)) {
74
+ throw new Error("socksProxy 用户名和密码必须同时提供");
75
+ }
76
+ return {
77
+ host: parsed.hostname,
78
+ port,
79
+ username: username || undefined,
80
+ password: password || undefined
81
+ };
82
+ }
83
+
84
+ function readExactly(socket, length) {
85
+ return new Promise((resolve, reject) => {
86
+ let buffer = Buffer.alloc(0);
87
+ const cleanup = () => {
88
+ socket.removeListener("data", onData);
89
+ socket.removeListener("error", onError);
90
+ socket.removeListener("close", onClose);
91
+ };
92
+ const onError = (error) => {
93
+ cleanup();
94
+ reject(error);
95
+ };
96
+ const onClose = () => {
97
+ cleanup();
98
+ reject(new Error("SOCKS5 代理连接提前关闭"));
99
+ };
100
+ const onData = (chunk) => {
101
+ buffer = Buffer.concat([buffer, chunk]);
102
+ if (buffer.length < length) {
103
+ return;
104
+ }
105
+ const result = buffer.subarray(0, length);
106
+ const rest = buffer.subarray(length);
107
+ cleanup();
108
+ if (rest.length > 0) {
109
+ socket.unshift(rest);
110
+ }
111
+ resolve(result);
112
+ };
113
+ socket.on("data", onData);
114
+ socket.once("error", onError);
115
+ socket.once("close", onClose);
116
+ });
117
+ }
118
+
119
+ function writeAll(socket, data) {
120
+ return new Promise((resolve, reject) => {
121
+ socket.write(data, (error) => {
122
+ if (error) {
123
+ reject(error);
124
+ return;
125
+ }
126
+ resolve();
127
+ });
128
+ });
129
+ }
130
+
131
+ function connectTcp(host, port) {
132
+ return new Promise((resolve, reject) => {
133
+ const socket = net.createConnection({ host, port });
134
+ const cleanup = () => {
135
+ socket.removeListener("error", onError);
136
+ };
137
+ const onError = (error) => {
138
+ cleanup();
139
+ reject(error);
140
+ };
141
+ socket.once("connect", () => {
142
+ cleanup();
143
+ resolve(socket);
144
+ });
145
+ socket.once("error", onError);
146
+ });
147
+ }
148
+
149
+ function encodeTargetAddress(host) {
150
+ const ipVersion = net.isIP(host);
151
+ if (ipVersion === 4) {
152
+ return Buffer.concat([Buffer.from([SOCKS5_ADDRESS_IPV4]), Buffer.from(host.split(".").map(Number))]);
153
+ }
154
+ const hostBuffer = Buffer.from(host, "utf8");
155
+ if (hostBuffer.length > 255) {
156
+ throw new Error("SOCKS5 目标主机名过长");
157
+ }
158
+ return Buffer.concat([Buffer.from([SOCKS5_ADDRESS_DOMAIN, hostBuffer.length]), hostBuffer]);
159
+ }
160
+
161
+ async function authenticateSocksProxy(socket, proxyConfig) {
162
+ const methods = proxyConfig.username ? [SOCKS5_AUTH_PASSWORD] : [SOCKS5_AUTH_NONE];
163
+ await writeAll(socket, Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
164
+ const response = await readExactly(socket, 2);
165
+ if (response[0] !== SOCKS5_VERSION) {
166
+ throw new Error("SOCKS5 代理响应版本非法");
167
+ }
168
+ if (response[1] === SOCKS5_AUTH_UNACCEPTABLE) {
169
+ throw new Error("SOCKS5 代理不接受当前认证方式");
170
+ }
171
+ if (response[1] === SOCKS5_AUTH_NONE) {
172
+ return;
173
+ }
174
+ if (response[1] !== SOCKS5_AUTH_PASSWORD || !proxyConfig.username) {
175
+ throw new Error("SOCKS5 代理返回了不支持的认证方式");
176
+ }
177
+ const usernameBuffer = Buffer.from(proxyConfig.username, "utf8");
178
+ const passwordBuffer = Buffer.from(proxyConfig.password, "utf8");
179
+ if (usernameBuffer.length > 255 || passwordBuffer.length > 255) {
180
+ throw new Error("SOCKS5 用户名或密码过长");
181
+ }
182
+ await writeAll(socket, Buffer.concat([
183
+ Buffer.from([0x01, usernameBuffer.length]),
184
+ usernameBuffer,
185
+ Buffer.from([passwordBuffer.length]),
186
+ passwordBuffer
187
+ ]));
188
+ const authResponse = await readExactly(socket, 2);
189
+ if (authResponse[1] !== 0x00) {
190
+ throw new Error("SOCKS5 代理认证失败");
191
+ }
192
+ }
193
+
194
+ async function readSocksConnectResponse(socket) {
195
+ const header = await readExactly(socket, 4);
196
+ if (header[0] !== SOCKS5_VERSION) {
197
+ throw new Error("SOCKS5 代理响应版本非法");
198
+ }
199
+ if (header[1] !== SOCKS5_REPLY_SUCCESS) {
200
+ throw new Error(`SOCKS5 代理连接目标失败,响应码 ${header[1]}`);
201
+ }
202
+ if (header[2] !== SOCKS5_RESERVED) {
203
+ throw new Error("SOCKS5 代理响应保留字段非法");
204
+ }
205
+ if (header[3] === SOCKS5_ADDRESS_IPV4) {
206
+ await readExactly(socket, 4);
207
+ } else if (header[3] === SOCKS5_ADDRESS_IPV6) {
208
+ await readExactly(socket, 16);
209
+ } else if (header[3] === SOCKS5_ADDRESS_DOMAIN) {
210
+ const length = (await readExactly(socket, 1))[0];
211
+ await readExactly(socket, length);
212
+ } else {
213
+ throw new Error("SOCKS5 代理响应地址类型非法");
214
+ }
215
+ await readExactly(socket, 2);
216
+ }
217
+
218
+ async function connectSocksProxy(connection) {
219
+ const proxyConfig = parseSocksProxy(connection.socksProxy);
220
+ const socket = await connectTcp(proxyConfig.host, proxyConfig.port);
221
+ try {
222
+ await authenticateSocksProxy(socket, proxyConfig);
223
+ const targetAddress = encodeTargetAddress(connection.host);
224
+ const targetPort = Buffer.alloc(2);
225
+ targetPort.writeUInt16BE(connection.port);
226
+ await writeAll(socket, Buffer.concat([
227
+ Buffer.from([SOCKS5_VERSION, SOCKS5_CONNECT_COMMAND, SOCKS5_RESERVED]),
228
+ targetAddress,
229
+ targetPort
230
+ ]));
231
+ await readSocksConnectResponse(socket);
232
+ return socket;
233
+ } catch (error) {
234
+ socket.destroy();
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ async function createConnectConfig(connection) {
27
240
  const connectConfig = {
28
241
  host: connection.host,
29
242
  port: connection.port,
30
243
  username: connection.username
31
244
  };
245
+ if (connection.socksProxy) {
246
+ connectConfig.sock = await connectSocksProxy(connection);
247
+ }
32
248
  if (connection.agent) {
33
249
  connectConfig.agent = connection.agent;
34
250
  } else if (connection.privateKey) {
@@ -46,7 +262,7 @@ function createConnectConfig(connection) {
46
262
 
47
263
  export async function connectSshClient(connection) {
48
264
  const client = new Client();
49
- const connectConfig = createConnectConfig(connection);
265
+ const connectConfig = await createConnectConfig(connection);
50
266
  await new Promise((resolve, reject) => {
51
267
  client.once("ready", resolve);
52
268
  client.once("error", reject);
@@ -77,17 +293,28 @@ export async function executeRemoteCommandWithClient(client, connection, remoteC
77
293
  let exitCode;
78
294
  let exitSignal;
79
295
  let settled = false;
80
- const timer = setTimeout(() => {
296
+ let commandStream;
297
+ const settle = (callback, value) => {
298
+ if (settled) {
299
+ return;
300
+ }
81
301
  settled = true;
82
- reject(new Error(`命令执行超时,超过 ${timeout}ms`));
302
+ clearTimeout(timer);
303
+ callback(value);
304
+ };
305
+ const timer = setTimeout(() => {
306
+ if (commandStream) {
307
+ commandStream.close();
308
+ }
309
+ settle(reject, new Error(`命令执行超时,超过 ${timeout}ms`));
83
310
  }, timeout);
84
311
 
85
312
  client.exec(remoteCommand, { pty: connection.pty ?? true }, (err, stream) => {
86
313
  if (err) {
87
- clearTimeout(timer);
88
- reject(err);
314
+ settle(reject, err);
89
315
  return;
90
316
  }
317
+ commandStream = stream;
91
318
 
92
319
  stream.on("data", (chunk) => {
93
320
  stdout += chunk.toString();
@@ -100,11 +327,9 @@ export async function executeRemoteCommandWithClient(client, connection, remoteC
100
327
  exitSignal = signal;
101
328
  });
102
329
  stream.on("close", (code, signal) => {
103
- clearTimeout(timer);
104
330
  if (settled) {
105
331
  return;
106
332
  }
107
- settled = true;
108
333
  exitCode = exitCode ?? code;
109
334
  exitSignal = exitSignal ?? signal;
110
335
  if ((exitCode ?? 0) !== 0 || exitSignal) {
@@ -121,18 +346,13 @@ export async function executeRemoteCommandWithClient(client, connection, remoteC
121
346
  if (exitSignal) {
122
347
  parts.push(`[signal] ${exitSignal}`);
123
348
  }
124
- reject(new Error(parts.join("\n") || "命令执行失败"));
349
+ settle(reject, new Error(parts.join("\n") || "命令执行失败"));
125
350
  return;
126
351
  }
127
- resolve(stdout.trimEnd());
352
+ settle(resolve, stdout.trimEnd());
128
353
  });
129
354
  stream.on("error", (streamError) => {
130
- clearTimeout(timer);
131
- if (settled) {
132
- return;
133
- }
134
- settled = true;
135
- reject(streamError);
355
+ settle(reject, streamError);
136
356
  });
137
357
  });
138
358
  });
@@ -143,37 +363,20 @@ export async function uploadFile(connection, localPath, remotePath) {
143
363
  }
144
364
 
145
365
  export async function uploadFileWithClient(client, localPath, remotePath) {
146
- return new Promise((resolve, reject) => {
366
+ const sftp = await new Promise((resolve, reject) => {
147
367
  client.sftp((err, sftp) => {
148
368
  if (err) {
149
369
  reject(err);
150
370
  return;
151
371
  }
152
- const readStream = fs.createReadStream(localPath);
153
- const writeStream = sftp.createWriteStream(remotePath);
154
- let closed = false;
155
- const cleanup = () => {
156
- if (!closed) {
157
- closed = true;
158
- sftp.end();
159
- }
160
- };
161
-
162
- readStream.on("error", (readError) => {
163
- cleanup();
164
- reject(readError);
165
- });
166
- writeStream.on("error", (writeError) => {
167
- cleanup();
168
- reject(writeError);
169
- });
170
- writeStream.on("close", () => {
171
- cleanup();
172
- resolve();
173
- });
174
- readStream.pipe(writeStream);
372
+ resolve(sftp);
175
373
  });
176
374
  });
375
+ try {
376
+ await pipeline(fs.createReadStream(localPath), sftp.createWriteStream(remotePath));
377
+ } finally {
378
+ sftp.end();
379
+ }
177
380
  }
178
381
 
179
382
  export async function downloadFile(connection, remotePath, localPath) {
@@ -181,35 +384,18 @@ export async function downloadFile(connection, remotePath, localPath) {
181
384
  }
182
385
 
183
386
  export async function downloadFileWithClient(client, remotePath, localPath) {
184
- return new Promise((resolve, reject) => {
387
+ const sftp = await new Promise((resolve, reject) => {
185
388
  client.sftp((err, sftp) => {
186
389
  if (err) {
187
390
  reject(err);
188
391
  return;
189
392
  }
190
- const readStream = sftp.createReadStream(remotePath);
191
- const writeStream = fs.createWriteStream(localPath);
192
- let closed = false;
193
- const cleanup = () => {
194
- if (!closed) {
195
- closed = true;
196
- sftp.end();
197
- }
198
- };
199
-
200
- readStream.on("error", (readError) => {
201
- cleanup();
202
- reject(readError);
203
- });
204
- writeStream.on("error", (writeError) => {
205
- cleanup();
206
- reject(writeError);
207
- });
208
- writeStream.on("close", () => {
209
- cleanup();
210
- resolve();
211
- });
212
- readStream.pipe(writeStream);
393
+ resolve(sftp);
213
394
  });
214
395
  });
396
+ try {
397
+ await pipeline(sftp.createReadStream(remotePath), fs.createWriteStream(localPath));
398
+ } finally {
399
+ sftp.end();
400
+ }
215
401
  }
package/src/ssh-daemon.js CHANGED
@@ -19,15 +19,22 @@ let lastActivityAt = Date.now();
19
19
  let exitTimer;
20
20
  let server;
21
21
  let socketPath;
22
+ let boundConfigPath;
22
23
 
23
24
  function parseArgs(argv) {
24
25
  const socketIndex = argv.indexOf("--socket");
25
- const value = socketIndex === -1 ? undefined : argv[socketIndex + 1];
26
- if (!value) {
26
+ const configIndex = argv.indexOf("--config");
27
+ const parsedSocketPath = socketIndex === -1 ? undefined : argv[socketIndex + 1];
28
+ const configPath = configIndex === -1 ? undefined : argv[configIndex + 1];
29
+ if (!parsedSocketPath) {
27
30
  throw new Error("daemon 缺少 --socket 参数");
28
31
  }
32
+ if (!configPath) {
33
+ throw new Error("daemon 缺少 --config 参数");
34
+ }
29
35
  return {
30
- socketPath: value
36
+ socketPath: parsedSocketPath,
37
+ configPath: path.resolve(configPath)
31
38
  };
32
39
  }
33
40
 
@@ -47,6 +54,7 @@ function buildConnectionKey(configPath, connection) {
47
54
  host: connection.host,
48
55
  port: connection.port,
49
56
  username: connection.username,
57
+ socksProxy: connection.socksProxy,
50
58
  auth
51
59
  });
52
60
  return crypto.createHash("sha256").update(raw).digest("hex");
@@ -141,13 +149,17 @@ async function runSerialized(entry, operation) {
141
149
  }
142
150
 
143
151
  async function executeRequest(request) {
152
+ const requestConfigPath = path.resolve(request.configPath);
153
+ if (requestConfigPath !== boundConfigPath) {
154
+ throw new Error("SSH 缓存进程拒绝访问非绑定配置文件");
155
+ }
144
156
  const ttlMs = normalizeCacheTtl(request.cacheTtlMs);
145
- const configs = loadConfig(request.configPath);
157
+ const configs = loadConfig(boundConfigPath);
146
158
  const connection = findConnection(configs, request.connectionName);
147
159
  if (request.operation === "execute") {
148
160
  validateCommand(connection, request.command);
149
161
  }
150
- const entry = await getPoolEntry(request.configPath, connection, ttlMs);
162
+ const entry = await getPoolEntry(boundConfigPath, connection, ttlMs);
151
163
 
152
164
  try {
153
165
  if (request.operation === "execute") {
@@ -221,7 +233,9 @@ function shutdown() {
221
233
  }
222
234
 
223
235
  try {
224
- ({ socketPath } = parseArgs(process.argv.slice(2)));
236
+ const parsedArgs = parseArgs(process.argv.slice(2));
237
+ socketPath = parsedArgs.socketPath;
238
+ boundConfigPath = parsedArgs.configPath;
225
239
  unlinkSocketPath(socketPath);
226
240
  server = net.createServer(handleSocket);
227
241
  server.listen(socketPath, () => {