diff --git a/README.md b/README.md index 118044e815..41a978d49a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ npm run electron:build ```bash npm run test:unit npm run test-watch:unit # 監視モード +npm run test:unit -- --update # スナップショットの更新 ``` ### ブラウザ End to End テスト diff --git "a/docs/res/\343\202\250\343\203\263\343\202\270\343\203\263\345\206\215\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" "b/docs/res/\343\202\250\343\203\263\343\202\270\343\203\263\345\206\215\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" index b32d8f7c77..72fc883899 100644 --- "a/docs/res/\343\202\250\343\203\263\343\202\270\343\203\263\345\206\215\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" +++ "b/docs/res/\343\202\250\343\203\263\343\202\270\343\203\263\345\206\215\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" @@ -10,8 +10,10 @@ flowchart LR 408243["各エンジン"] --> 927120["Vuex.GET_ONLY_ENGINE_INFOS"] 927120 --> 512074["Vuex.POST_ENGINE_START"] subgraph 408243["各エンジン"] - 262932["SET_ENGINE_STATE(state=STARTING)"] --- 595264["back.RESTART_ENGINE"] - 595264 --- 920995["engine.restartEngine"] + 262932["SET_ENGINE_STATE(state=STARTING)"] --> 595264["back.RESTART_ENGINE"] + 595264 --> 920995["engine.restartEngine"] + 920995 --> 939785["runtimeInfo.setEngineInfos"] + 939785 --> 494722["runtimeInfo.exportFile"] end subgraph 512074["Vuex.POST_ENGINE_START"] 623200["Vuex.GET_ALT_PORT_INFOS"] --> 225947["各エンジン"] diff --git "a/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" "b/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" index 191115bc70..bc4dc62a35 100644 --- "a/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" +++ "b/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" @@ -28,16 +28,18 @@ flowchart end end subgraph 389651["back.start"] - 967432["engine.runEngineAll"] --> 733212 + 321984["runtimeInfo.exportFile"] --> 733212 subgraph 733212["back.createWindow"] 613440["win.loadURL"] end subgraph 548965["launchEngines"] 250263["store.get engineSettings"] --> 222321["store.set engineSettings"] 870482["store.get registeredEngineDirs"] --> 250263 - 222321 --> 967432 + 222321 --> 967432["engine.runEngineAll"] 656570["engine.fetchEngineInfos"] --> 870482 110954["engine.initializeEngineInfosAndAltPortInfo"] --> 656570 + 967432 --> 302398["runtimeInfo.setEngineInfos"] + 302398 --> 321984 subgraph 656570["engine.fetchEngineInfos"] 267019["engine.fetchAdditionalEngineInfos"] end diff --git a/src/background.ts b/src/background.ts index 612de6a9d0..b3cdf84cba 100644 --- a/src/background.ts +++ b/src/background.ts @@ -44,6 +44,7 @@ import EngineManager from "./background/engineManager"; import VvppManager, { isVvppFile } from "./background/vvppManager"; import configMigration014 from "./background/configMigration014"; import { failure, success } from "./type/result"; +import { RuntimeInfoManager } from "./background/RuntimeInfoManager"; import { ipcMainHandle, ipcMainSend } from "@/electron/ipc"; import { getConfigManager } from "@/background/electronConfig"; @@ -160,6 +161,11 @@ const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { dialog.showErrorBox("音声合成エンジンエラー", error.message); }; +const runtimeInfoManager = new RuntimeInfoManager( + path.join(app.getPath("userData"), "runtime-info.json"), + app.getVersion() +); + const configManager = getConfigManager(); const engineManager = new EngineManager({ @@ -494,6 +500,8 @@ async function launchEngines() { configManager.set("engineSettings", engineSettings); await engineManager.runEngineAll(); + runtimeInfoManager.setEngineInfos(engineInfos); + await runtimeInfoManager.exportFile(); } /** @@ -766,6 +774,9 @@ ipcMainHandle("ENGINE_INFOS", () => { */ ipcMainHandle("RESTART_ENGINE", async (_, { engineId }) => { await engineManager.restartEngine(engineId); + // TODO: setEngineInfosからexportFileはロックしたほうがより良い + runtimeInfoManager.setEngineInfos(engineManager.fetchEngineInfos()); + await runtimeInfoManager.exportFile(); }); ipcMainHandle("OPEN_ENGINE_DIRECTORY", async (_, { engineId }) => { diff --git a/src/background/RuntimeInfoManager.ts b/src/background/RuntimeInfoManager.ts new file mode 100644 index 0000000000..0fa5a0f3de --- /dev/null +++ b/src/background/RuntimeInfoManager.ts @@ -0,0 +1,104 @@ +/** + * サードパーティ向けのランタイム情報を書き出す。 + * ランタイム情報には起動しているエンジンのURLなどが含まれる。 + */ + +import fs from "fs"; +import AsyncLock from "async-lock"; +import log from "electron-log/main"; +import { EngineId, EngineInfo } from "@/type/preload"; + +/** + * ランタイム情報書き出しに必要なEngineInfo + */ +export type EngineInfoForRuntimeInfo = Pick< + EngineInfo, + "uuid" | "host" | "name" +>; + +/** + * 保存されるランタイム情報 + */ +type RuntimeInfo = { + formatVersion: number; + appVersion: string; + engineInfos: { + uuid: EngineId; + url: string; + name: string; + }[]; +}; + +/** + * サードパーティ向けのランタイム情報を書き出す + */ +export class RuntimeInfoManager { + private runtimeInfoPath: string; + private appVersion: string; + + constructor(runtimeInfoPath: string, appVersion: string) { + this.runtimeInfoPath = runtimeInfoPath; + this.appVersion = appVersion; + } + + /** + * ファイルロック用のインスタンス + */ + private lock = new AsyncLock({ + timeout: 1000, + }); + private lockKey = "write"; + + /** + * ファイルフォーマットバージョン + */ + private fileFormatVersion = 1; + + /** + * エンジン情報(書き出し用に記憶) + */ + private engineInfos: EngineInfoForRuntimeInfo[] = []; + + /** + * エンジン情報を登録する + */ + public setEngineInfos(engineInfos: EngineInfoForRuntimeInfo[]) { + this.engineInfos = engineInfos; + } + + /** + * ランタイム情報ファイルを書き出す + */ + public async exportFile() { + await this.lock.acquire(this.lockKey, async () => { + log.info( + `Runtime information file has been updated. : ${this.runtimeInfoPath}` + ); + + // データ化 + const runtimeInfoFormatFor3rdParty: RuntimeInfo = { + formatVersion: this.fileFormatVersion, + appVersion: this.appVersion, + engineInfos: this.engineInfos.map((engineInfo) => { + return { + uuid: engineInfo.uuid, + url: engineInfo.host, // NOTE: 元のEngineInfo.hostにURLが入っている + name: engineInfo.name, + }; + }), + }; + + // ファイル書き出し + try { + await fs.promises.writeFile( + this.runtimeInfoPath, + JSON.stringify(runtimeInfoFormatFor3rdParty) // FIXME: zod化する + ); + } catch (e) { + // ディスクの空き容量がない、他ツールからのファイルロック時をトラップ。 + // サードパーティ向けなのでVOICEVOX側には通知せず、エラー記録して継続 + log.error(`Failed to write file : ${e}`); + } + }); + } +} diff --git a/src/background/engineManager.ts b/src/background/engineManager.ts index dd32926c45..91ca0bf8b4 100644 --- a/src/background/engineManager.ts +++ b/src/background/engineManager.ts @@ -82,7 +82,6 @@ export class EngineManager { this.defaultEngineDir = defaultEngineDir; this.vvppEngineDir = vvppEngineDir; this.onEngineProcessError = onEngineProcessError; - this.engineProcessContainers = {}; } diff --git a/tests/unit/background/RuntimeInfo.spec.ts b/tests/unit/background/RuntimeInfo.spec.ts new file mode 100644 index 0000000000..4c8109c2a2 --- /dev/null +++ b/tests/unit/background/RuntimeInfo.spec.ts @@ -0,0 +1,54 @@ +import { tmpdir } from "os"; +import { join } from "path"; +import fs from "fs"; +import { expect, test } from "vitest"; +import { EngineId } from "@/type/preload"; + +import { RuntimeInfoManager } from "@/background/RuntimeInfoManager"; + +test("想定通りのラインタイム情報が保存されている", async () => { + const randomName = Math.random().toString(36).substring(7); + const tempFilePath = join(tmpdir(), `runtime-info-${randomName}.json`); + + const appVersion = "999.999.999"; + const runtimeInfoManager = new RuntimeInfoManager(tempFilePath, appVersion); + + // エンジン情報 + runtimeInfoManager.setEngineInfos([ + { + uuid: EngineId("00000000-0000-0000-0000-000000000001"), + host: "https://example.com/engine1", + name: "engine1", + }, + { + uuid: EngineId("00000000-0000-0000-0000-000000000002"), + host: "https://example.com/engine2", + name: "engine2", + }, + ]); + + // ファイル書き出し + await runtimeInfoManager.exportFile(); + + // ファイル読み込みしてスナップショットの比較 + // NOTE: スナップショットが変わった場合、破壊的変更ならformatVersionを上げる + const savedRuntimeInfo = JSON.parse(fs.readFileSync(tempFilePath, "utf-8")); + expect(savedRuntimeInfo).toMatchInlineSnapshot(` + { + "appVersion": "999.999.999", + "engineInfos": [ + { + "name": "engine1", + "url": "https://example.com/engine1", + "uuid": "00000000-0000-0000-0000-000000000001", + }, + { + "name": "engine2", + "url": "https://example.com/engine2", + "uuid": "00000000-0000-0000-0000-000000000002", + }, + ], + "formatVersion": 1, + } + `); +});