Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

エンジンが使うポートが割り当てできなければ、他の空いているポートで起動してユーザーに通知する #1267

Merged
merged 24 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3adb7c1
Add: ポートが割り当て済みかどうかをまず検知
wappon28dev Mar 30, 2023
057e95f
Add: 使っているプロセスIDと名前の取得
wappon28dev Mar 30, 2023
619809b
Fix: dependencies 消し忘れ
wappon28dev Mar 30, 2023
f05a87a
Add: 空いているポートを探してエンジンに port を伝える
wappon28dev Mar 30, 2023
2f81e09
Fix: 代替ポートが見つからないときのエラーメッセージを追加
wappon28dev Mar 30, 2023
f39ef13
Add: ChangePortInfosの追加
wappon28dev Apr 2, 2023
0060e97
Add: トースト通知と代替ポート用にstateを追加
wappon28dev Apr 3, 2023
e445bfd
Add: IPCに代替ポートの情報を生やした
wappon28dev Apr 6, 2023
7b0ac15
Fix: 変なdiff削除
wappon28dev Apr 8, 2023
44a5ffc
Add: "今後この通知をしない" オプション追加
wappon28dev Apr 8, 2023
3f5e18e
Enh: コメントいろいろ追加
wappon28dev Apr 8, 2023
78b9f80
Enh: 良い書き方あった
wappon28dev Apr 8, 2023
824e411
Merge branch 'VOICEVOX:main' into fix/handle-port-50021-used
wappon28dev Apr 16, 2023
220068f
Fix: 古いコメント削除
wappon28dev Apr 16, 2023
9f78aa7
Fix: 色, CSS変数の名前
wappon28dev Apr 18, 2023
bc5a40a
コメント追加
wappon28dev Apr 23, 2023
8d711ff
TODO 追加
wappon28dev Apr 24, 2023
eed8d24
Enh: getUrlを直接実行へ
wappon28dev Apr 24, 2023
dc85487
Enh: AltPortInfo -> AltPortInfos
wappon28dev Apr 24, 2023
21a4d9a
Enh: 早期リターンの削除
wappon28dev Apr 24, 2023
6b71976
Enh: noticeAltPortInfo -> engineStartedOnAltPort
wappon28dev Apr 24, 2023
324dcbc
Add: ループバックアドレスの探索追加
wappon28dev Apr 24, 2023
2857f01
Add: 探索まわりのコメントを追加
wappon28dev Apr 24, 2023
a9ca4fa
Typo: loopBack -> loopback
wappon28dev Apr 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/background/engineManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import treeKill from "tree-kill";
import Store from "electron-store";
import shlex from "shlex";

import { BrowserWindow, dialog } from "electron";
import { app, BrowserWindow, dialog } from "electron";

import log from "electron-log";
import { z } from "zod";
import { PortManager } from "./portManager";
import { ipcMainSend } from "@/electron/ipc";

import {
Expand Down Expand Up @@ -209,6 +210,7 @@ export class EngineManager {
const engineInfo = engineInfos.find(
(engineInfo) => engineInfo.uuid === engineId
);

if (!engineInfo)
throw new Error(`No such engineInfo registered: engineId == ${engineId}`);

Expand All @@ -224,6 +226,50 @@ export class EngineManager {
return;
}

const engineInfoUrl = new URL(engineInfo.host);
const portManager = new PortManager(
engineInfoUrl.hostname,
parseInt(engineInfoUrl.port)
);

log.info(
`ENGINE ${engineId}: Checking whether port ${engineInfoUrl.port} is assignable...`
);

// ポートを既に割り当てられているプロセスidの取得: undefined → ポートが空いている
const processId = await portManager.getProcessIdFromPort();

if (processId !== undefined) {
const processName = await portManager.getProcessNameFromPid(processId);
log.warn(
`ENGINE ${engineId}: Port ${engineInfoUrl.port} has already been assigned by ${processName} (pid=${processId})`
);

const altPort = await portManager.findAltPort();

// TODO: エディターでトースト通知を出す:
// > XXXXX番ポートが使用中であるため、〇〇はYYYYY番ポートで起動しました

// 代替ポートが見つからないとき
if (!altPort) {
dialog.showErrorBox(
`${engineInfo.name} の起動に失敗しました`,
`${engineInfoUrl.port}番ポートの代わりに利用可能なポートが見つかりませんでした。PCを再起動してください。`
);
app.exit(1);
throw new Error("No Alternative Port Found");
}

// 代替ポートを設定
engineInfo.host = new PortManager(
engineInfoUrl.hostname,
altPort
).getUrl();
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved
log.warn(
`ENGINE ${engineId}: Applied Alternative Port: ${engineInfoUrl.port} -> ${altPort}`
);
}

log.info(`ENGINE ${engineId}: Starting process`);

if (!(engineId in this.engineProcessContainers)) {
Expand All @@ -244,7 +290,12 @@ export class EngineManager {

// エンジンプロセスの起動
const enginePath = engineInfo.executionFilePath;
const args = engineInfo.executionArgs.concat(useGpu ? ["--use_gpu"] : []);
const args = engineInfo.executionArgs.concat(useGpu ? ["--use_gpu"] : [], [
"--host",
new URL(engineInfo.host).hostname,
"--port",
new URL(engineInfo.host).port,
]);
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved

log.info(`ENGINE ${engineId} path: ${enginePath}`);
log.info(`ENGINE ${engineId} args: ${JSON.stringify(args)}`);
Expand Down
137 changes: 137 additions & 0 deletions src/background/portManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { execFileSync } from "child_process";
import log from "electron-log";

const isWindows = process.platform === "win32";

export class PortManager {
constructor(private hostname: string, private port: number) {}

/**
* ex) url: `http://localhost:50021`
* host -> `localhost`
* port -> `50021`
*/

// eslint-disable-next-line @typescript-eslint/no-explicit-any
portLog = (...message: any) => log.info(`PORT ${this.port}: ${message}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
portWarn = (...message: any) => log.warn(`PORT ${this.port}: ${message}`);

public getUrl(isHttps = false): string {
return `${isHttps ? "https" : "http"}://${this.hostname}:${this.port}`;
}

/**
* "netstat -ano" の stdout から, 指定したポートを使用しているプロセスの process 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
* @returns `process id` or `undefined` (ポートが使用されていないとき)
*/
private stdout2processId(stdout: string): number | undefined {
const lines = stdout.split("\n");
for (const line of lines) {
if (line.includes(`${this.hostname}:${this.port}`)) {
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved
const parts = line.trim().split(/\s+/);
return parseInt(parts[parts.length - 1], 10);
}
}
return undefined;
}

async getProcessIdFromPort(): Promise<number | undefined> {
this.portLog("Getting process id...");
const exec = isWindows
? {
cmd: "netstat",
args: ["-ano"],
}
: {
cmd: "lsof",
args: ["-i", `:${this.port}`, "-t", "-sTCP:LISTEN"],
};

this.portLog(`Running command: "${exec.cmd} ${exec.args.join(" ")}"`);

let stdout = execFileSync(exec.cmd, exec.args, {
shell: true,
}).toString();

// windows
if (isWindows) {
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved
stdout = this.stdout2processId(stdout)?.toString() ?? "";
}

// bash
if (!stdout || !stdout.length) {
this.portLog("Assignable; Nobody uses this port!");
return undefined;
}
this.portWarn(`Nonassignable; pid=${stdout} uses this port!`);
return parseInt(stdout);
}

async getProcessNameFromPid(pid: number): Promise<string> {
this.portLog(`Getting process name from pid=${pid}...`);
const exec = isWindows
? // TODO: `TCPポートの除外範囲` というものがあって, そこに該当しているかを確認する
{
cmd: "wmic",
args: ["process", "where", `"ProcessID=${pid}"`, "get", "name"],
}
: // TODO: MacOS だとroot権限が必要かも? 場合によって, 代替コマンド `ss` や `fuser` を使うかも
{
cmd: "ps",
args: ["-p", pid.toString(), "-o", "comm="],
};

let stdout = execFileSync(exec.cmd, exec.args, { shell: true }).toString();

if (isWindows) {
/*
* ex) stdout:
* ```
* Name
* node.exe
* ```
* -> `node.exe`
*/
stdout = stdout.split("\r\n")[1];
}

this.portLog(`Found process name: ${stdout}`);

return stdout.trim();
}

/**
* 割り当て可能な他のポートを探します
*
* @returns 割り当て可能なポート番号 or `undefined` (割り当て可能なポートが見つからなかったとき)
*/
async findAltPort(): Promise<number | undefined> {
this.portLog(`Find another assignable port from ${this.port}...`);
const altPortMax = 50100;

for (let altPort = this.port + 1; altPort <= altPortMax; altPort++) {
this.portLog(`Trying whether port ${altPort} is assignable...`);
const altPid = await new PortManager(
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved
this.hostname,
altPort
).getProcessIdFromPort();

// ポートを既に割り当てられているプロセスidの取得: undefined → ポートが空いている
if (altPid === undefined) return altPort;
}

this.portWarn(`No alternative port found! ${this.port}...${altPortMax}`);
return undefined;
}
wappon28dev marked this conversation as resolved.
Show resolved Hide resolved
}