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

refactor: エンジン情報マネージャーとエンジンプロセスマネージャーを分ける #2260

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 26 additions & 16 deletions src/backend/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import log from "electron-log/main";
import dayjs from "dayjs";
import windowStateKeeper from "electron-window-state";
import { hasSupportedGpu } from "./device";
import EngineManager from "./manager/engineManager";
import EngineInfoManager from "./manager/engineInfoManager";
import EngineProcessManager from "./manager/engineProcessManager";
import VvppManager, { isVvppFile } from "./manager/vvppManager";
import configMigration014 from "./configMigration014";
import { RuntimeInfoManager } from "./manager/RuntimeInfoManager";
Expand Down Expand Up @@ -184,17 +185,24 @@ const runtimeInfoManager = new RuntimeInfoManager(

const configManager = getConfigManager();

const engineManager = new EngineManager({
const engineInfoManager = new EngineInfoManager({
configManager,
defaultEngineDir: appDirPath,
vvppEngineDir,
});
const engineProcessManager = new EngineProcessManager({
configManager,
onEngineProcessError,
engineInfosFetcher:
engineInfoManager.fetchEngineInfos.bind(engineInfoManager),
engineAltPortUpdater: engineInfoManager.updateAltPort.bind(engineInfoManager),
engineSettingsGetter: () => configManager.get("engineSettings"),
});
const vvppManager = new VvppManager({ vvppEngineDir });

// エンジンのフォルダを開く
function openEngineDirectory(engineId: EngineId) {
const engineDirectory = engineManager.fetchEngineDirectory(engineId);
const engineDirectory = engineInfoManager.fetchEngineDirectory(engineId);

// Windows環境だとスラッシュ区切りのパスが動かない。
// path.resolveはWindowsだけバックスラッシュ区切りにしてくれるため、path.resolveを挟む。
Expand Down Expand Up @@ -289,7 +297,7 @@ function checkMultiEngineEnabled(): boolean {
async function uninstallVvppEngine(engineId: EngineId) {
let engineInfo: EngineInfo | undefined = undefined;
try {
engineInfo = engineManager.fetchEngineInfo(engineId);
engineInfo = engineInfoManager.fetchEngineInfo(engineId);
if (!engineInfo) {
throw new Error(`No such engineInfo registered: engineId == ${engineId}`);
}
Expand Down Expand Up @@ -521,11 +529,11 @@ async function start() {

// エンジンの準備と起動
async function launchEngines() {
// エンジンの追加と削除を反映させるためEngineInfoとAltPortInfoを再生成する
engineManager.initializeEngineInfosAndAltPortInfo();
// エンジンの追加と削除を反映させるためEngineInfoとAltPortInfosを再生成する
engineInfoManager.initializeEngineInfosAndAltPortInfo();

// TODO: デフォルトエンジンの処理をConfigManagerに移してブラウザ版と共通化する
const engineInfos = engineManager.fetchEngineInfos();
const engineInfos = engineInfoManager.fetchEngineInfos();
const engineSettings = configManager.get("engineSettings");
for (const engineInfo of engineInfos) {
if (!engineSettings[engineInfo.uuid]) {
Expand All @@ -535,7 +543,7 @@ async function launchEngines() {
}
configManager.set("engineSettings", engineSettings);

await engineManager.runEngineAll();
await engineProcessManager.runEngineAll();
runtimeInfoManager.setEngineInfos(engineInfos);
await runtimeInfoManager.exportFile();
}
Expand All @@ -546,7 +554,7 @@ async function launchEngines() {
* そうでない場合は Promise を返す。
*/
function cleanupEngines(): Promise<void> | "alreadyCompleted" {
const killingProcessPromises = engineManager.killEngineAll();
const killingProcessPromises = engineProcessManager.killEngineAll();
const numLivingEngineProcess = Object.entries(killingProcessPromises).length;

// 前処理が完了している場合
Expand Down Expand Up @@ -719,7 +727,7 @@ registerIpcMainHandle<IpcMainHandle>({
},

GET_ALT_PORT_INFOS: () => {
return engineManager.altPortInfo;
return engineInfoManager.altPortInfos;
},

SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath }) => {
Expand Down Expand Up @@ -903,18 +911,20 @@ registerIpcMainHandle<IpcMainHandle>({
},

ENGINE_INFOS: () => {
// エンジン情報を設定ファイルに保存しないためにstoreは使わない
return engineManager.fetchEngineInfos();
// エンジン情報を設定ファイルに保存しないためにelectron-storeは使わない
return engineInfoManager.fetchEngineInfos();
},

/**
* エンジンを再起動する。
* エンジンの起動が開始したらresolve、起動が失敗したらreject。
*/
RESTART_ENGINE: async (_, { engineId }) => {
await engineManager.restartEngine(engineId);
// TODO: setEngineInfosからexportFileはロックしたほうがより良い
runtimeInfoManager.setEngineInfos(engineManager.fetchEngineInfos());
await engineProcessManager.restartEngine(engineId);

// ランタイム情報の更新
// TODO: setからexportの処理は排他処理にしたほうがより良い
runtimeInfoManager.setEngineInfos(engineInfoManager.fetchEngineInfos());
Comment on lines -915 to +927
Copy link
Member Author

@Hiroshiba Hiroshiba Sep 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

処理は変わってないです。
「ロックしたほうがより良い」の意味が分かりづらかったので、ロック→排他処理に置き換えました。

await runtimeInfoManager.exportFile();
},

Expand Down Expand Up @@ -998,7 +1008,7 @@ registerIpcMainHandle<IpcMainHandle>({
},

VALIDATE_ENGINE_DIR: (_, { engineDir }) => {
return engineManager.validateEngineDir(engineDir);
return engineInfoManager.validateEngineDir(engineDir);
},

RELOAD_APP: async (_, { isMultiEngineOffMode }) => {
Expand Down
229 changes: 229 additions & 0 deletions src/backend/electron/manager/engineInfoManager.ts
Copy link
Member Author

@Hiroshiba Hiroshiba Sep 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルは以前のEngineManagerからのコード移動だけになってるはず。

Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import path from "path";
import fs from "fs";
import shlex from "shlex";

import { dialog } from "electron"; // FIXME: ここでelectronをimportするのは良くない

import log from "electron-log/main";

import {
EngineInfo,
EngineDirValidationResult,
MinimumEngineManifestType,
EngineId,
minimumEngineManifestSchema,
envEngineInfoSchema,
} from "@/type/preload";
import { AltPortInfos } from "@/store/type";
import { BaseConfigManager } from "@/backend/common/ConfigManager";

/**
* デフォルトエンジンの情報を作成する
*/
function createDefaultEngineInfos(defaultEngineDir: string): EngineInfo[] {
// TODO: envから直接ではなく、envに書いたengine_manifest.jsonから情報を得るようにする
const defaultEngineInfosEnv =
import.meta.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]";

const envSchema = envEngineInfoSchema.array();
const engines = envSchema.parse(JSON.parse(defaultEngineInfosEnv));

return engines.map((engineInfo) => {
return {
...engineInfo,
isDefault: true,
type: "path",
executionFilePath: path.resolve(engineInfo.executionFilePath),
path:
engineInfo.path == undefined
? undefined
: path.resolve(defaultEngineDir, engineInfo.path),
} satisfies EngineInfo;
});
}

/** エンジンの情報を管理するクラス */
export class EngineInfoManager {
configManager: BaseConfigManager;
defaultEngineDir: string;
vvppEngineDir: string;

defaultEngineInfos: EngineInfo[] = [];
additionalEngineInfos: EngineInfo[] = [];

/** 代替ポート情報 */
public altPortInfos: AltPortInfos = {};

constructor(payload: {
configManager: BaseConfigManager;
defaultEngineDir: string;
vvppEngineDir: string;
}) {
this.configManager = payload.configManager;
this.defaultEngineDir = payload.defaultEngineDir;
this.vvppEngineDir = payload.vvppEngineDir;
}

/**
* 追加エンジンの一覧を作成する。
* FIXME: store.get("registeredEngineDirs")への副作用をEngineManager外に移動する
*/
private createAdditionalEngineInfos(): EngineInfo[] {
const engines: EngineInfo[] = [];
const addEngine = (engineDir: string, type: "vvpp" | "path") => {
const manifestPath = path.join(engineDir, "engine_manifest.json");
if (!fs.existsSync(manifestPath)) {
return "manifestNotFound";
}
let manifest: MinimumEngineManifestType;
try {
manifest = minimumEngineManifestSchema.parse(
JSON.parse(fs.readFileSync(manifestPath, { encoding: "utf8" })),
);
} catch (e) {
return "manifestParseError";
}

const [command, ...args] = shlex.split(manifest.command);

engines.push({
uuid: manifest.uuid,
host: `http://127.0.0.1:${manifest.port}`,
name: manifest.name,
path: engineDir,
executionEnabled: true,
executionFilePath: path.join(engineDir, command),
executionArgs: args,
type,
isDefault: false,
} satisfies EngineInfo);
return "ok";
};
for (const dirName of fs.readdirSync(this.vvppEngineDir)) {
const engineDir = path.join(this.vvppEngineDir, dirName);
if (!fs.statSync(engineDir).isDirectory()) {
log.log(`${engineDir} is not directory`);
continue;
}
if (dirName === ".tmp") {
continue;
}
const result = addEngine(engineDir, "vvpp");
if (result !== "ok") {
log.log(`Failed to load engine: ${result}, ${engineDir}`);
}
}
// FIXME: この関数の引数でregisteredEngineDirsを受け取り、動かないエンジンをreturnして、EngineManager外でconfig.setする
for (const engineDir of this.configManager.get("registeredEngineDirs")) {
const result = addEngine(engineDir, "path");
if (result !== "ok") {
log.log(`Failed to load engine: ${result}, ${engineDir}`);
// 動かないエンジンは追加できないので削除
// FIXME: エンジン管理UIで削除可能にする
dialog.showErrorBox(
"エンジンの読み込みに失敗しました。",
`${engineDir}を読み込めませんでした。このエンジンは削除されます。`,
);
this.configManager.set(
"registeredEngineDirs",
this.configManager
.get("registeredEngineDirs")
.filter((p) => p !== engineDir),
);
}
}
return engines;
}

/**
* 全てのエンジンの一覧を取得する。デフォルトエンジン+追加エンジン。
*/
fetchEngineInfos(): EngineInfo[] {
return [...this.defaultEngineInfos, ...this.additionalEngineInfos];
}

/**
* エンジンの情報を取得する。存在しない場合はエラーを返す。
*/
fetchEngineInfo(engineId: EngineId): EngineInfo {
const engineInfos = this.fetchEngineInfos();
const engineInfo = engineInfos.find(
(engineInfo) => engineInfo.uuid === engineId,
);
if (!engineInfo) {
throw new Error(`No such engineInfo registered: engineId == ${engineId}`);
}
return engineInfo;
}

/**
* エンジンのディレクトリを取得する。存在しない場合はエラーを返す。
*/
fetchEngineDirectory(engineId: EngineId): string {
const engineInfo = this.fetchEngineInfo(engineId);
const engineDirectory = engineInfo.path;
if (engineDirectory == undefined) {
throw new Error(`engineDirectory is undefined: engineId == ${engineId}`);
}

return engineDirectory;
}

/**
* EngineInfosとAltPortInfoを初期化する。
*/
initializeEngineInfosAndAltPortInfo() {
this.defaultEngineInfos = createDefaultEngineInfos(this.defaultEngineDir);
this.additionalEngineInfos = this.createAdditionalEngineInfos();
this.altPortInfos = {};
}

/**
* 代替ポート情報を更新する。
* エンジン起動時にポートが競合して代替ポートを使う場合に使用する。
*/
updateAltPort(engineId: EngineId, port: number) {
const engineInfo = this.fetchEngineInfo(engineId);
const url = new URL(engineInfo.host);
this.altPortInfos[engineId] = {
from: Number(url.port),
to: port,
};

url.port = port.toString();
engineInfo.host = url.toString();
}
Comment on lines +181 to +195
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、ここの関数だけは新しく作りました。
(EngineProcessManager側から叩けないといけないので)


/**
* ディレクトリがエンジンとして正しいかどうかを判定する
*/
validateEngineDir(engineDir: string): EngineDirValidationResult {
if (!fs.existsSync(engineDir)) {
return "directoryNotFound";
} else if (!fs.statSync(engineDir).isDirectory()) {
return "notADirectory";
} else if (!fs.existsSync(path.join(engineDir, "engine_manifest.json"))) {
return "manifestNotFound";
}
const manifest = fs.readFileSync(
path.join(engineDir, "engine_manifest.json"),
"utf-8",
);
let manifestContent: MinimumEngineManifestType;
try {
manifestContent = minimumEngineManifestSchema.parse(JSON.parse(manifest));
} catch (e) {
return "invalidManifest";
}

const engineInfos = this.fetchEngineInfos();
if (
engineInfos.some((engineInfo) => engineInfo.uuid === manifestContent.uuid)
) {
return "alreadyExists";
}
return "ok";
}
}

export default EngineInfoManager;
Loading
Loading