Skip to content

Commit

Permalink
refactor: エンジン情報マネージャーとエンジンプロセスマネージャーを分ける (#2260)
Browse files Browse the repository at this point in the history
* エンジン情報マネージャーとエンジンプロセスマネージャーを分ける

* いろいろ使いやすく
  • Loading branch information
Hiroshiba committed Sep 13, 2024
1 parent a43d28a commit 8120d29
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 230 deletions.
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());
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
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();
}

/**
* ディレクトリがエンジンとして正しいかどうかを判定する
*/
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

0 comments on commit 8120d29

Please sign in to comment.