-
Notifications
You must be signed in to change notification settings - Fork 313
/
Copy pathportManager.ts
255 lines (225 loc) · 7.73 KB
/
portManager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import { execFileSync } from "child_process";
import { createServer } from "net";
import log from "electron-log/main";
const isWindows = process.platform === "win32";
export type HostInfo = {
protocol: string;
hostname: string;
port: number;
};
const portLog = (port: number, message: string, isNested = false) =>
log.info(`${isNested ? "| " : ""}PORT ${port}: ${message}`);
const portWarn = (port: number, message: string, isNested = false) =>
log.warn(`${isNested ? "| " : ""}PORT ${port}: ${message}`);
export function url2HostInfo(url: URL): HostInfo {
return {
protocol: url.protocol,
hostname: url.hostname,
port: Number(url.port),
};
}
/**
* "netstat -ano" の stdout から, 指定したポートを LISTENING しているプロセスの id を取得します.
*
* ex) stdout:
* ``` cmd
* TCP 127.0.0.1:5173 127.0.0.1:50170 TIME_WAIT 0
* TCP 127.0.0.1:6463 0.0.0.0:0 LISTENING 18692
* TCP 127.0.0.1:50021 0.0.0.0:0 LISTENING 17320
* ```
* -> `17320`
*
* @param stdout netstat の stdout
* @param hostInfo ホスト情報
* @returns `process id` or `undefined` (ポートが割り当て可能なとき)
*/
function netstatStdout2pid(
stdout: string,
hostInfo: HostInfo
): number | undefined {
const lines = stdout.split("\n");
for (const line of lines) {
if (line.includes(`${hostInfo.hostname}:${hostInfo.port}`)) {
const parts = line.trim().split(/\s+/);
const pid = parts[parts.length - 1];
const tcpState = parts[parts.length - 2];
if (tcpState === "LISTENING") return Number(pid);
}
}
return undefined;
}
export async function getPidFromPort(
hostInfo: HostInfo,
isNested = false // ログ整形用の引数
): Promise<number | undefined> {
// Windows の場合は, hostname が以下のループバックアドレスが割り当てられているか確認
const parse4windows = (stdout: string): string | undefined => {
// それぞれのループバックアドレスに対して pid を取得
const loopbackAddr = ["localhost", "127.0.0.1", "0.0.0.0", "[::1]"];
if (!loopbackAddr.includes(hostInfo.hostname)) {
portLog(
hostInfo.port,
`Hostname is not loopback address; Getting process id from ${hostInfo.hostname}...`,
isNested
);
return netstatStdout2pid(stdout, hostInfo)?.toString();
} else {
portLog(
hostInfo.port,
"Hostname is loopback address; Getting process id from all loopback addresses...",
isNested
);
const pidArr: (number | undefined)[] = [];
loopbackAddr.forEach((hostname) => {
// netstat の stdout から pid を取得
const pid = netstatStdout2pid(stdout, {
protocol: hostInfo.protocol,
hostname,
port: hostInfo.port,
});
portLog(
hostInfo.port,
`| ${hostname}\t-> ${
pid == undefined ? "Assignable" : `pid=${pid} uses this port`
}`,
isNested
);
pidArr.push(pid);
});
// pid が undefined (= 割り当て可能) でないものを取得 → 1つ目を取得
return pidArr.filter((pid) => pid != undefined)[0]?.toString();
}
};
portLog(hostInfo.port, "Getting process id...", isNested);
const exec = isWindows
? {
cmd: "netstat",
args: ["-ano"],
}
: {
cmd: "lsof",
args: ["-i", `:${hostInfo.port}`, "-t", "-sTCP:LISTEN"],
};
portLog(
hostInfo.port,
`Running command: "${exec.cmd} ${exec.args.join(" ")}"...`,
isNested
);
// lsofは、ポートを使用しているプロセスが存在しない場合は
// エラーを返すので、エラーを無視して割り当て可能として扱う
// FIXME: lsof以外のエラーだった場合のエラーハンドリングを追加する
let stdout: string;
try {
stdout = execFileSync(exec.cmd, exec.args, {
shell: true,
}).toString();
} catch (e) {
portLog(hostInfo.port, "Assignable; Nobody uses this port!", isNested);
return undefined;
}
// Windows の場合は, lsof のように port と pid が 1to1 で取れないので, netstat の stdout をパース
const pid = isWindows ? parse4windows(stdout) : stdout;
if (pid == undefined || !pid.length) {
portLog(hostInfo.port, "Assignable; Nobody uses this port!", isNested);
return undefined;
}
portWarn(
hostInfo.port,
`Nonassignable; pid=${pid} uses this port!`,
isNested
);
return Number(pid);
}
export async function getProcessNameFromPid(
hostInfo: HostInfo,
pid: number
): Promise<string> {
portLog(hostInfo.port, `Getting process name from pid=${pid}...`);
const exec = isWindows
? {
cmd: "wmic",
args: ["process", "where", `"ProcessID=${pid}"`, "get", "name"],
}
: {
cmd: "ps",
args: ["-p", pid.toString(), "-o", "comm="],
};
const stdout = execFileSync(exec.cmd, exec.args, { shell: true }).toString();
/*
* ex) stdout:
* ```
* Name
* node.exe
* ```
* -> `node.exe`
*/
const processName = isWindows ? stdout.split("\n")[1] : stdout;
portLog(hostInfo.port, `Found process name: ${processName}`);
return processName.trim();
}
/**
* ポートが割り当て可能かどうか実際にlistenして接続したポート番号を返します。
* 0番ポートを指定した場合はランダムな接続可能ポート番号を返します。
* @param port 確認するポート番号
* @param hostname 確認するホスト名
* @returns 割り当て不能だった場合`undefined`を返します。割り当て可能だった場合ポート番号を返します。
*/
function findOrCheckPort(
port: number,
hostname: string
): Promise<number | undefined> {
return new Promise((resolve, reject) => {
const server = createServer();
server.on("error", () => {
server.close();
resolve(undefined);
});
server.on("listening", () => {
const address = server.address();
server.close();
if (address == undefined || typeof address === "string") {
reject(new Error(`'address' is null or string: ${address}`));
return;
}
resolve(address.port);
});
server.listen(port, hostname);
});
}
/**
* 割り当て可能な他のポートを探します
* @param basePort 元のポート番号
* @param hostname 割り当てるホスト名
* @returns 割り当て可能なポート番号 or `undefined` (割り当て可能なポートが見つからなかったとき)
*/
export async function findAltPort(
basePort: number,
hostname: string
): Promise<number | undefined> {
portLog(basePort, `Find another assignable port from ${basePort}...`);
// エンジン指定のポート + 100番までを探索 エフェメラルポートの範囲の最大は超えないようにする
const altPortMax = Math.min(basePort + 100, 65535);
for (let altPort = basePort + 1; altPort <= altPortMax; altPort++) {
portLog(basePort, `Trying whether port ${altPort} is assignable...`);
if (await isAssignablePort(altPort, hostname)) {
return altPort;
}
}
// 指定のポート + 100番まで見つからなかった場合ランダムなポートを使用する
portWarn(basePort, `No alternative port found! ${basePort}...${altPortMax}`);
const altPort = await findOrCheckPort(0, hostname);
if (altPort == undefined) {
portWarn(basePort, "No alternative port found!");
} else {
portLog(altPort, "Assignable");
}
return altPort;
}
/**
* ポートが割り当て可能か確認します
* @param port 確認するポート番号
* @param hostname 確認するホスト名
*/
export async function isAssignablePort(port: number, hostname: string) {
return (await findOrCheckPort(port, hostname)) != undefined;
}