From da73bcdeaefafad59ce28b13f2deefd7ae01b70d Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Sun, 30 Jul 2023 04:15:23 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=9F=E3=81=82=E3=81=A8=E3=81=AE?= =?UTF-8?q?=E3=82=A2=E3=83=97=E3=83=AA=E3=82=B1=E3=83=BC=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E5=86=8D=E8=B5=B7=E5=8B=95=E3=82=92=E4=B8=8D?= =?UTF-8?q?=E8=A6=81=E3=81=AB=E3=81=99=E3=82=8B=20(#1400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nanashi. --- ...61\343\203\263\343\202\271\345\233\263.md" | 75 ++++--- ...61\343\203\263\343\202\271\345\233\263.md" | 32 +-- ...56\346\255\251\343\201\215\346\226\271.md" | 9 +- package-lock.json | 24 +++ package.json | 2 + src/background.ts | 200 +++++++++++------- src/background/vvppManager.ts | 19 +- src/browser/sandbox.ts | 4 +- src/components/AcceptTermsDialog.vue | 4 +- src/components/EngineManageDialog.vue | 26 +-- src/components/MenuBar.vue | 32 +-- src/components/MenuButton.vue | 12 +- src/components/MinMaxCloseButtons.vue | 2 +- src/electron/preload.ts | 8 +- src/plugins/ipcMessageReceiverPlugin.ts | 4 +- src/store/type.ts | 17 +- src/store/ui.ts | 46 +++- src/type/ipc.ts | 11 +- src/type/preload.ts | 2 +- src/views/EditorHome.vue | 13 +- tests/unit/store/Vuex.spec.ts | 1 + 21 files changed, 351 insertions(+), 192 deletions(-) diff --git "a/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" "b/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" index 06f91c9bb9..cb7761e068 100644 --- "a/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" +++ "b/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" @@ -1,46 +1,57 @@ # 終了シーケンス図 ```mermaid -flowchart - node_1>"アプリ実行中"] -->|"アプリを再起動"| node_3["back.RESTART_APP"] +flowchart TD + node_1>"アプリ実行中"] -.->|"MacのCmd+Q"| subgraph_2["app.before_quit"] style node_1 fill:#ffbbbb,stroke:#ff0000 - node_3 --> node_4(["willRestart=true"]) - node_1 -.->|"MacのCmd+Q"| subgraph_2["app.before_quit"] node_21{{"winがclose済みか"}} -.->|"NO"| subgraph_1["win.close"] - node_1 -->|"アプリ起動直後のVVPPインストール後の再起動"| 132024(["willRestart=true"]) - 132024 --> 512010["app.quit"] - 512010 -.-> subgraph_2 - 438128["win.close"] -.-> subgraph_1 - node_1 -.->|"ウィンドウを閉じる"| subgraph_1 - node_4 --> 438128 - 442878["event.preventDefault"] --> 571782["Vuex.CHECK_EDITED_AND_NOT_WAVE"] - node_9["event.preventDefault"] --> 571782 + node_1 -.->|"MacのCmd+W・WinのAlt+F4"| subgraph_1 node_15["app.quit"] -.-> subgraph_2 - a{{"willQuit"}} -.-> subgraph_2 - node_8["win.destroy"] -.-> subgraph_1 - 701221{{"willRestart"}} -->|"false"| node_21 + a{{"willQuit"}} -.->|"true"| subgraph_2 node_21 -.->|"YES"| node_23>"アプリ終了"] style node_23 fill:#bbbbff,stroke:#0000ff - subgraph 571782["Vuex.CHECK_EDITED_AND_NOT_WAVE"] - node_5["back.CLOSE_WINDOW"] --> node_7(["willQuit=true"]) - node_7 --> node_8 - 453066{" "} -->|"キャンセル"| node_6>"アプリ実行中に戻る"] - 453066 --> node_5 + 846215{" "} -->|"reload"| 186768["RELOAD_APP"] + node_1 -->|"×ボタン"| 295190(["close"]) + node_1 -->|"アプリを再読み込み"| 929152(["reload"]) + 846215 -->|"close"| 208965["back.CLOSE_WINDOW"] + node_9["event.preventDefault"] --> 295190 + 295190 --> 571782["Vuex.CHECK_EDITED_AND_NOT_SAVE"] + 929152 --> 571782 + 177756{{"alreadyCompleted?"}} -->|"true"| node_21 + 442878["event.preventDefault"] --> 295190 + node_8["win.destroy"] -.-> subgraph_2 + 705785[["cleanupEngines"]] ~~~ 793927["cleanupEngines"] + 198115["win.loadURL"] --> 562861>"UIの描画"] + 454139[["cleanupEngines"]] ~~~ 793927 + subgraph 186768["RELOAD_APP"] + 705785 --> 700765{{"alreadyCompleted?"}} + 700765 -->|"false"| 462289["await"] + 700765 -->|"true"| 464405[["launchEngines"]] + 462289 --> 464405 + 464405 --> 198115 + 494918["win.loadURL(dummy)"] --> 705785 end - subgraph subgraph_2["app.before_quit"] - c["willQuit"] -->|"false"| node_9 - node_13["event.preventDefault"] --> node_14["全エンジンkill待機"] - node_19(["willRestart=false, willQuit=false"]) --> node_20>"back.start"] + subgraph 208965["back.CLOSE_WINDOW"] + node_7(["willQuit=true"]) --> node_8 + end + subgraph 571782["Vuex.CHECK_EDITED_AND_NOT_SAVE"] + 846215 -->|"キャンセル"| node_6>"アプリ実行中に戻る"] + end + subgraph 793927["cleanupEngines"] node_12["engine.killEngineAll"] --> 889691{{"numLivingEngineProcess"}} - 889691 -->|">0"| 701221 - 701221 -->|"true"| node_17["event.preventDefault"] - c -->|"true"| node_12 889691 -->|"0"| 916552{{"hasMarkedEngineDirs"}} - 916552 -->|"false"| 701221 - 916552 -->|"true"| node_13 - node_17 --> node_19 - node_14 --> node_18["vvpp.handleMarkedEngineDirs"] - node_18 --> node_15 + node_14["全エンジンkill待機"] --> node_18["vvpp.handleMarkedEngineDirs"] + 889691 -->|">0"| node_14 + 916552 -->|"false"| node_14 + 916552 -->|"true"| 655722["何もしない"] + end + subgraph subgraph_2["app.before_quit"] + c["willQuit"] -->|"false"| node_9 + c -->|"true"| 454139 + 454139 --> 177756 + 177756 -->|"false"| node_13["event.preventDefault"] + node_13 --> 322763["await"] + 322763 --> node_15 end subgraph subgraph_1["win.close"] a -->|"false"| 442878 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 561956e4a8..191115bc70 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" @@ -3,20 +3,17 @@ ```mermaid flowchart 174170["back.installVvppEngineWithWarning"] --> 786961["back.installVvppEngine"] - 786961 --> 409576{{"再起動するか"}} - 409576 -->|"する"| 173803>"アプリ起動直後のVVPPインストール後の再起動へ"] 764022["(画面読み込み)"] --> 698565["App.vue"] 764022 --> 332024["EditorHome.vue"] - 389651["back.start"] --> 733212["back.createWindow"] - 733212 -.-> 764022 + 733212["back.createWindow"] -.-> 764022 448821>"アプリ停止中"] -.-> 430173["app.ready"] style 448821 fill:#ffbbbb,stroke:#ff0000 430173 -->|"ある"| 174170 - 409576 -->|"しない"| 389651 - 430173 -->|"ない"| 389651 + 430173 -->|"ない"| 389651["back.start"] 698565 -.-> 704891>"アプリ実行中"] style 704891 fill:#bbbbff,stroke:#0000ff 332024 -.-> 704891 + 786961 --> 389651 subgraph 332024["EditorHome.vue"] 709863["Vuex.GET_ENGINE_INFOS"] --> 773040["Vuex.POST_ENGINE_START"] subgraph 773040["Vuex.POST_ENGINE_START"] @@ -31,13 +28,19 @@ flowchart end end subgraph 389651["back.start"] - 250263["store.get engineSettings"] --> 222321["store.set engineSettings"] - 870482["store.get registeredEngineDirs"] --> 250263 - 222321 --> 967432["engine.runEngineAll"] - 656570["engine.fetchEngineInfos"] --> 870482 - 110954["engine.initializeEngineInfosAndAltPortInfo"] --> 656570 - subgraph 656570["engine.fetchEngineInfos"] - 267019["engine.fetchAdditionalEngineInfos"] + 967432["engine.runEngineAll"] --> 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 + 656570["engine.fetchEngineInfos"] --> 870482 + 110954["engine.initializeEngineInfosAndAltPortInfo"] --> 656570 + subgraph 656570["engine.fetchEngineInfos"] + 267019["engine.fetchAdditionalEngineInfos"] + end end end subgraph 430173["app.ready"] @@ -49,7 +52,4 @@ flowchart 225701["win.show"] end end - subgraph 733212["back.createWindow"] - 613440["win.loadURL"] - end ``` diff --git "a/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" "b/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" index 0ed42e2a27..bef776ded1 100644 --- "a/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" +++ "b/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" @@ -112,12 +112,17 @@ flowchart LR 945949 -->|"条件2"| 871294["受動的な処理は点線\n(イベント発生など)"] 366994 --> 617606["大きい関数(subgraph)"] 871294 -.->|"発生条件"| 617606 - 617606 --> 314405>"なんらかの状態"] - 617606 --> 230853>"シーケンスの終了"] + 572391[["外に定義した関数"]] --> 314405>"なんらかの状態"] + 572391 --> 230853>"シーケンスの終了"] style 230853 fill:#bbbbff,stroke:#0000ff + 617606 --> 572391 + 572391 ~~~|"透明な線で結んでもよし"| 513532["外に定義した関数"] subgraph 617606["大きい関数(subgraph)"] 776462["処理開始"] --> 992786["処理終了"] end + subgraph 513532["外に定義した関数"] + 327013["処理"] + end ``` 処理の prefix 一覧 diff --git a/package-lock.json b/package-lock.json index bf64f1140a..039efdeda0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "async-lock": "1.4.0", "buffer": "6.0.3", "clone-deep": "4.0.1", "core-js": "3.12.1", @@ -43,6 +44,7 @@ "@openapitools/openapi-generator-cli": "2.3.3", "@playwright/test": "1.32.1", "@quasar/vite-plugin": "1.3.0", + "@types/async-lock": "1.4.0", "@types/clone-deep": "4.0.1", "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", @@ -1572,6 +1574,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==", + "dev": true + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -3259,6 +3267,11 @@ "node": ">=0.12.0" } }, + "node_modules/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -15594,6 +15607,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==", + "dev": true + }, "@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -16969,6 +16988,11 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index 1d29a21a0d..aa41d2df42 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "async-lock": "1.4.0", "buffer": "6.0.3", "clone-deep": "4.0.1", "core-js": "3.12.1", @@ -72,6 +73,7 @@ "@openapitools/openapi-generator-cli": "2.3.3", "@playwright/test": "1.32.1", "@quasar/vite-plugin": "1.3.0", + "@types/async-lock": "1.4.0", "@types/clone-deep": "4.0.1", "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", diff --git a/src/background.ts b/src/background.ts index 8d3864beb6..7675625e73 100644 --- a/src/background.ts +++ b/src/background.ts @@ -128,9 +128,18 @@ if (isDevelopment) { __static = __dirname; } +// ソフトウェア起動時はプロトコルを app にする protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); +if (process.env.VITE_DEV_SERVER_URL == undefined) { + // FIXME: registerFileProtocolが非推奨になったので対策を考える + protocol.registerFileProtocol("app", (request, callback) => { + const filePath = new URL(request.url).pathname; + callback(path.join(__dirname, filePath)); + }); +} +const firstUrl = process.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; // 設定ファイル const electronStoreJsonSchema = zodToJsonSchema(electronStoreSchema); @@ -255,10 +264,10 @@ async function installVvppEngine(vvppPath: string) { */ async function installVvppEngineWithWarning({ vvppPath, - restartNeeded, + reloadNeeded, }: { vvppPath: string; - restartNeeded: boolean; + reloadNeeded: boolean; }) { const result = dialog.showMessageBoxSync(win, { type: "warning", @@ -274,21 +283,22 @@ async function installVvppEngineWithWarning({ await installVvppEngine(vvppPath); - if (restartNeeded) { + if (reloadNeeded) { dialog .showMessageBox(win, { type: "info", - title: "再起動が必要です", + title: "再読み込みが必要です", message: - "VVPPファイルを読み込みました。反映にはVOICEVOXの再起動が必要です。今すぐ再起動しますか?", - buttons: ["再起動", "キャンセル"], + "VVPPファイルを読み込みました。反映には再読み込みが必要です。今すぐ再読み込みしますか?", + buttons: ["再読み込み", "キャンセル"], noLink: true, cancelId: 1, }) .then((result) => { if (result.response === 0) { - appState.willRestart = true; - app.quit(); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "reload", + }); } }); } @@ -436,8 +446,6 @@ migrateHotkeySettings(); const appState = { willQuit: false, - willRestart: false, - isMultiEngineOffMode: false, }; let filePathOnMac: string | undefined = undefined; // create window @@ -466,7 +474,7 @@ async function createWindow() { icon: path.join(__static, "icon.png"), }); - let projectFilePath: string | undefined = ""; + let projectFilePath = ""; if (isMac) { if (filePathOnMac) { if (filePathOnMac.endsWith(".vvproj")) { @@ -487,21 +495,8 @@ async function createWindow() { } } - const parameter = - "#/home?isMultiEngineOffMode=" + - appState.isMultiEngineOffMode + - "&projectFilePath=" + - projectFilePath; + await loadUrl({ projectFilePath }); - if (process.env.VITE_DEV_SERVER_URL) { - await win.loadURL(process.env.VITE_DEV_SERVER_URL + parameter); - } else { - protocol.registerFileProtocol("app", (request, callback) => { - const filePath = new URL(request.url).pathname; - callback(path.join(__dirname, filePath)); - }); - win.loadURL("app://./index.html" + parameter); - } if (isDevelopment && !isTest) win.webContents.openDevTools(); win.on("maximize", () => win.webContents.send("DETECT_MAXIMIZED")); @@ -520,7 +515,9 @@ async function createWindow() { win.on("close", (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE"); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "close", + }); return; } }); @@ -536,8 +533,31 @@ async function createWindow() { mainWindowState.manage(win); } -// UI処理を開始。その他の準備が完了した後に呼ばれる。 +/** + * 画面の読み込みを開始する。 + * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 + * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 + * @returns ロードの完了を待つPromise。 + */ +async function loadUrl(obj: { + isMultiEngineOffMode?: boolean; + projectFilePath?: string; +}) { + const fragment = + "#/home" + + `?isMultiEngineOffMode=${obj?.isMultiEngineOffMode ?? false}` + + `&projectFilePath=${obj?.projectFilePath ?? ""}`; + return win.loadURL(`${firstUrl}${fragment}`); +} + +// 開始。その他の準備が完了した後に呼ばれる。 async function start() { + await launchEngines(); + await createWindow(); +} + +// エンジンの準備と起動 +async function launchEngines() { // エンジンの追加と削除を反映させるためEngineInfoとAltPortInfoを再生成する。 engineManager.initializeEngineInfosAndAltPortInfo(); const engineInfos = engineManager.fetchEngineInfos(); @@ -551,7 +571,50 @@ async function start() { store.set("engineSettings", engineSettings); await engineManager.runEngineAll(); - await createWindow(); +} + +/** + * エンジンの停止とエンジン終了後処理を行う。 + * 全処理が完了済みの場合 alreadyCompleted を返す。 + * そうでない場合は Promise を返す。 + */ +function cleanupEngines(): Promise | "alreadyCompleted" { + const killingProcessPromises = engineManager.killEngineAll(); + const numLivingEngineProcess = Object.entries(killingProcessPromises).length; + + // 前処理が完了している場合 + if (numLivingEngineProcess === 0 && !vvppManager.hasMarkedEngineDirs()) { + return "alreadyCompleted"; + } + + let numEngineProcessKilled = 0; + + // 非同期的にすべてのエンジンプロセスをキル + const waitingKilledPromises: Promise[] = Object.entries( + killingProcessPromises + ).map(([engineId, promise]) => { + return promise + .catch((error) => { + // TODO: 各エンジンプロセスキルの失敗をUIに通知する + log.error(`ENGINE ${engineId}: Error during killing process: ${error}`); + // エディタを終了するため、エラーが起きてもエンジンプロセスをキルできたとみなす + }) + .finally(() => { + numEngineProcessKilled++; + log.info( + `ENGINE ${engineId}: Process killed. ${numEngineProcessKilled} / ${numLivingEngineProcess} processes killed` + ); + }); + }); + + // すべてのエンジンプロセスキル処理が完了するまで待機 + return Promise.all(waitingKilledPromises).then(() => { + // エンジン終了後の処理を実行 + log.info( + "All ENGINE process kill operations done. Running post engine kill process" + ); + return vvppManager.handleMarkedEngineDirs(); + }); } const menuTemplateForMac: Electron.MenuItemConstructorOptions[] = [ @@ -879,10 +942,25 @@ ipcMainHandle("VALIDATE_ENGINE_DIR", (_, { engineDir }) => { return engineManager.validateEngineDir(engineDir); }); -ipcMainHandle("RESTART_APP", async (_, { isMultiEngineOffMode }) => { - appState.willRestart = true; - appState.isMultiEngineOffMode = isMultiEngineOffMode; - win.close(); +ipcMainHandle("RELOAD_APP", async (_, { isMultiEngineOffMode }) => { + win.hide(); // FIXME: ダミーページ表示のほうが良い + + // FIXME: 同じようなURLだとスーパーリロードされないことがあるので一度ダミーページを読み込む + await win.loadURL(firstUrl + "dummypage"); + + log.info("Checking ENGINE status before reload app"); + const engineCleanupResult = cleanupEngines(); + + // エンジンの停止とエンジン終了後処理の待機 + if (engineCleanupResult != "alreadyCompleted") { + await engineCleanupResult; + } + log.info("Post engine kill process done. Now reloading app"); + + await launchEngines(); + + await loadUrl({ isMultiEngineOffMode: !!isMultiEngineOffMode }); + win.show(); }); ipcMainHandle("WRITE_FILE", (_, { filePath, buffer }) => { @@ -928,31 +1006,16 @@ app.on("window-all-closed", () => { app.on("before-quit", async (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE"); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { closeOrReload: "close" }); return; } log.info("Checking ENGINE status before app quit"); + const engineCleanupResult = cleanupEngines(); - const killingProcessPromises = engineManager.killEngineAll(); - const numLivingEngineProcess = Object.entries(killingProcessPromises).length; - - // すべてのエンジンプロセスが停止している - if (numLivingEngineProcess === 0 && !vvppManager.hasMarkedEngineDirs()) { - if (appState.willRestart) { - // 再起動フラグが立っている場合はフラグを戻して再起動する - log.info( - "Post engine kill process done. Now restarting app because of willRestart flag" - ); - event.preventDefault(); - - appState.willRestart = false; - appState.willQuit = false; - - start(); - } else { - log.info("Post engine kill process done. Now quit app"); - } + // エンジンの停止とエンジン終了後処理が完了している + if (engineCleanupResult == "alreadyCompleted") { + log.info("Post engine kill process done. Now quit app"); return; } @@ -962,34 +1025,7 @@ app.on("before-quit", async (event) => { log.info("Interrupt app quit to kill ENGINE processes"); event.preventDefault(); - let numEngineProcessKilled = 0; - - // 非同期的にすべてのエンジンプロセスをキル - const waitingKilledPromises: Array> = Object.entries( - killingProcessPromises - ).map(([engineId, promise]) => { - return promise - .catch((error) => { - // TODO: 各エンジンプロセスキルの失敗をUIに通知する - log.error(`ENGINE ${engineId}: Error during killing process: ${error}`); - // エディタを終了するため、エラーが起きてもエンジンプロセスをキルできたとみなす - }) - .finally(() => { - numEngineProcessKilled++; - log.info( - `ENGINE ${engineId}: Process killed. ${numEngineProcessKilled} / ${numLivingEngineProcess} processes killed` - ); - }); - }); - - // すべてのエンジンプロセスキル処理が完了するまで待機 - await Promise.all(waitingKilledPromises); - - // エンジン終了後の処理を実行 - log.info( - "All ENGINE process kill operations done. Running post engine kill process" - ); - await vvppManager.handleMarkedEngineDirs(); + await engineCleanupResult; // アプリケーションの終了を再試行する log.info("Attempting to quit app again"); @@ -1047,7 +1083,7 @@ app.on("ready", async () => { if (checkMultiEngineEnabled()) { await installVvppEngineWithWarning({ vvppPath: filePath, - restartNeeded: true, // FIXME: 再インストールでない限り再起動は不要 + reloadNeeded: false, }); } } @@ -1066,7 +1102,7 @@ app.on("second-instance", async (event, argv, workDir, rawData) => { if (checkMultiEngineEnabled()) { await installVvppEngineWithWarning({ vvppPath: data.filePath, - restartNeeded: true, + reloadNeeded: true, }); } } else if (data.filePath.endsWith(".vvproj")) { diff --git a/src/background/vvppManager.ts b/src/background/vvppManager.ts index bfc1725a2c..e85805dd0e 100644 --- a/src/background/vvppManager.ts +++ b/src/background/vvppManager.ts @@ -6,6 +6,7 @@ import { moveFile } from "move-file"; import { app, dialog } from "electron"; import MultiStream from "multistream"; import glob, { glob as callbackGlob } from "glob"; +import AsyncLock from "async-lock"; import { EngineId, EngineInfo, @@ -41,18 +42,19 @@ export const isVvppFile = (filePath: string) => { ); }; +const lockKey = "lock-key-for-vvpp-manager"; + // # 軽い概要 // // フォルダ名:"エンジン名+UUID" // エンジン名にフォルダ名に使用できない文字が含まれている場合は"_"に置換する。連続する"_"は1つにする。 // 拡張子は".vvpp"または".vvppp"。".vvppp"は分割されているファイルであることを示す。 // engine.0.vvppp、engine.1.vvppp、engine.2.vvppp、...というように分割されている。 -// UUIDはengine_manifest.jsonのuuidを使用する +// UUIDはengine_manifest.jsonのuuidを使用し、同一エンジンの判定にはこれを使用する。 // // 追加: // * エンジンを仮フォルダ(vvpp-engines/.tmp/現在の時刻)に展開する // * エンジンが既に存在しているか確認する -// 最後のUUIDで比較する // - 存在していた場合:上書き処理を行う // - 存在していなかった場合:仮フォルダをvvpp-engines/エンジン名+UUIDに移動する // @@ -72,6 +74,8 @@ export class VvppManager { willDeleteEngineIds: Set; willReplaceEngineDirs: Array<{ from: string; to: string }>; + private lock = new AsyncLock(); + constructor({ vvppEngineDir }: { vvppEngineDir: string }) { this.vvppEngineDir = vvppEngineDir; this.willDeleteEngineIds = new Set(); @@ -115,7 +119,7 @@ export class VvppManager { return true; } - async extractVvpp( + private async extractVvpp( vvppLikeFilePath: string ): Promise<{ outputDir: string; manifest: MinimumEngineManifest }> { const nonce = new Date().getTime().toString(); @@ -257,7 +261,13 @@ export class VvppManager { } } + /** + * 追加 + */ async install(vvppPath: string) { + await this.lock.acquire(lockKey, () => this._install(vvppPath)); + } + private async _install(vvppPath: string) { const { outputDir, manifest } = await this.extractVvpp(vvppPath); const dirName = this.toValidDirName(manifest); const engineDirectory = path.join(this.vvppEngineDir, dirName); @@ -280,6 +290,9 @@ export class VvppManager { } async handleMarkedEngineDirs() { + await this.lock.acquire(lockKey, () => this._handleMarkedEngineDirs()); + } + private async _handleMarkedEngineDirs() { await Promise.all( [...this.willDeleteEngineIds].map(async (engineId) => { let deletingEngineDir: string | undefined = undefined; diff --git a/src/browser/sandbox.ts b/src/browser/sandbox.ts index adccaadc32..5e3388d190 100644 --- a/src/browser/sandbox.ts +++ b/src/browser/sandbox.ts @@ -308,7 +308,7 @@ export const api: Sandbox = { validateEngineDir(/* engineDir: string */) { throw new Error(`Not supported on Browser version: validateEngineDir`); }, - restartApp(/* obj: { isMultiEngineOffMode: boolean } */) { - throw new Error(`Not supported on Browser version: restartApp`); + reloadApp(/* obj: { isMultiEngineOffMode: boolean } */) { + throw new Error(`Not supported on Browser version: reloadApp`); }, }; diff --git a/src/components/AcceptTermsDialog.vue b/src/components/AcceptTermsDialog.vue index c7e96baa6a..6ad2047f28 100644 --- a/src/components/AcceptTermsDialog.vue +++ b/src/components/AcceptTermsDialog.vue @@ -86,7 +86,9 @@ const handler = (acceptTerms: boolean) => { store.dispatch("SET_ACCEPT_TERMS", { acceptTerms: acceptTerms ? "Accepted" : "Rejected", }); - !acceptTerms ? store.dispatch("CHECK_EDITED_AND_NOT_SAVE") : undefined; + !acceptTerms + ? store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { closeOrReload: "close" }) + : undefined; modelValueComputed.value = false; }; diff --git a/src/components/EngineManageDialog.vue b/src/components/EngineManageDialog.vue index cd43468f4d..541af31d10 100644 --- a/src/components/EngineManageDialog.vue +++ b/src/components/EngineManageDialog.vue @@ -507,8 +507,8 @@ const addEngine = () => { }) ); - requireRestart( - "エンジンを追加しました。反映にはVOICEVOXの再起動が必要です。今すぐ再起動しますか?" + requireReload( + "エンジンを追加しました。反映には再読み込みが必要です。今すぐ再読み込みしますか?" ); } else { const success = await lockUi( @@ -516,8 +516,8 @@ const addEngine = () => { store.dispatch("INSTALL_VVPP_ENGINE", vvppFilePath.value) ); if (success) { - requireRestart( - "エンジンを追加しました。反映にはVOICEVOXの再起動が必要です。今すぐ再起動しますか?" + requireReload( + "エンジンを追加しました。反映には再読み込みが必要です。今すぐ再読み込みしますか?" ); } } @@ -551,8 +551,8 @@ const deleteEngine = () => { engineDir, }) ); - requireRestart( - "エンジンを削除しました。反映にはVOICEVOXの再起動が必要です。今すぐ再起動しますか?" + requireReload( + "エンジンを削除しました。反映には再読み込みが必要です。今すぐ再読み込みしますか?" ); break; } @@ -562,8 +562,8 @@ const deleteEngine = () => { store.dispatch("UNINSTALL_VVPP_ENGINE", selectedId.value) ); if (success) { - requireRestart( - "エンジンの削除にはVOICEVOXの再起動が必要です。今すぐ再起動しますか?" + requireReload( + "エンジンの削除には再読み込みが必要です。今すぐ再読み込みしますか?" ); } break; @@ -592,9 +592,9 @@ const restartSelectedEngine = () => { }); }; -const requireRestart = (message: string) => { +const requireReload = (message: string) => { $q.dialog({ - title: "VOICEVOXの再起動が必要です", + title: "再読み込みが必要です", message: message, noBackdropDismiss: true, cancel: { @@ -603,14 +603,16 @@ const requireRestart = (message: string) => { flat: true, }, ok: { - label: "再起動", + label: "再読み込み", flat: true, textColor: "warning", }, }) .onOk(() => { toInitialState(); - store.dispatch("RESTART_APP", {}); + store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "reload", + }); }) .onCancel(() => { toInitialState(); diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue index a85adc7511..1448a985b4 100644 --- a/src/components/MenuBar.vue +++ b/src/components/MenuBar.vue @@ -54,6 +54,7 @@ export type MenuItemRoot = MenuItemBase<"root"> & { icon?: string; disabled?: boolean; disableWhenUiLocked: boolean; + disablreloadingLocked?: boolean; }; export type MenuItemButton = MenuItemBase<"button"> & { @@ -61,6 +62,7 @@ export type MenuItemButton = MenuItemBase<"button"> & { icon?: string; disabled?: boolean; disableWhenUiLocked: boolean; + disablreloadingLocked?: boolean; }; export type MenuItemData = MenuItemSeparator | MenuItemRoot | MenuItemButton; @@ -528,6 +530,20 @@ async function updateEngines() { disableWhenUiLocked: false, }); } + // マルチエンジンオフモードの解除 + if (store.state.isMultiEngineOffMode) { + engineMenu.subMenu.push({ + type: "button", + label: "マルチエンジンをオンにして再読み込み", + onClick() { + store.dispatch("RELOAD_APP", { + isMultiEngineOffMode: false, + }); + }, + disableWhenUiLocked: false, + disablreloadingLocked: true, + }); + } } // engineInfos、engineManifests、enableMultiEngineを見て動的に更新できるようにする // FIXME: computedにする @@ -535,22 +551,6 @@ watch([engineInfos, engineManifests, enableMultiEngine], updateEngines, { immediate: true, }); -// マルチエンジンオフモードの解除 -if (store.state.isMultiEngineOffMode) { - ( - menudata.value.find((data) => data.label === "エンジン") as MenuItemRoot - ).subMenu.push({ - type: "button", - label: "マルチエンジンをオンにして再起動", - onClick() { - store.dispatch("RESTART_APP", { - isMultiEngineOffMode: false, - }); - }, - disableWhenUiLocked: false, - }); -} - // 「最近開いたプロジェクト」の更新 async function updateRecentProjects() { const projectsMenu = menudata.value.find( diff --git a/src/components/MenuButton.vue b/src/components/MenuButton.vue index 82898f7ecb..7e4fcac61e 100644 --- a/src/components/MenuButton.vue +++ b/src/components/MenuButton.vue @@ -31,9 +31,7 @@ :key="index" v-model:selected="subMenuOpenFlags[index]" :menudata="menu" - :disable=" - uiLocked && menu.type !== 'separator' && menu.disableWhenUiLocked - " + :disable="isDisabledMenuItem(menu)" @mouseenter="reassignSubMenuOpen(index)" @mouseleave="reassignSubMenuOpen.cancel()" /> @@ -67,6 +65,7 @@ const emit = const store = useStore(); const uiLocked = computed(() => store.getters.UI_LOCKED); +const reloadingLocked = computed(() => store.state.reloadingLock); const selectedComputed = computed({ get: () => props.selected, set: (val) => emit("update:selected", val), @@ -78,6 +77,13 @@ const subMenuOpenFlags = ref( : [] ); +const isDisabledMenuItem = computed(() => (menu: MenuItemData) => { + if (menu.type === "separator") return false; + if (menu.disableWhenUiLocked && uiLocked.value) return true; + if (menu.disablreloadingLocked && reloadingLocked.value) return true; + return false; +}); + const reassignSubMenuOpen = debounce((idx: number) => { if (subMenuOpenFlags.value[idx]) return; if (props.menudata.type !== "root") return; diff --git a/src/components/MinMaxCloseButtons.vue b/src/components/MinMaxCloseButtons.vue index f9a3c6edb8..116c0994ad 100644 --- a/src/components/MinMaxCloseButtons.vue +++ b/src/components/MinMaxCloseButtons.vue @@ -100,7 +100,7 @@ import { useStore } from "@/store"; const store = useStore(); const closeWindow = async () => { - store.dispatch("CHECK_EDITED_AND_NOT_SAVE"); + store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { closeOrReload: "close" }); }; const minimizeWindow = () => window.electron.minimizeWindow(); const maximizeWindow = () => window.electron.maximizeWindow(); diff --git a/src/electron/preload.ts b/src/electron/preload.ts index bf6432a7c8..44bffb49ea 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -262,8 +262,12 @@ const api: Sandbox = { return await ipcRendererInvoke("VALIDATE_ENGINE_DIR", { engineDir }); }, - restartApp: ({ isMultiEngineOffMode }: { isMultiEngineOffMode: boolean }) => { - ipcRendererInvoke("RESTART_APP", { isMultiEngineOffMode }); + /** + * アプリを再読み込みする。 + * 画面以外の情報を刷新する。 + */ + reloadApp: async ({ isMultiEngineOffMode }) => { + return await ipcRendererInvoke("RELOAD_APP", { isMultiEngineOffMode }); }, }; diff --git a/src/plugins/ipcMessageReceiverPlugin.ts b/src/plugins/ipcMessageReceiverPlugin.ts index c21788d017..27224113cd 100644 --- a/src/plugins/ipcMessageReceiverPlugin.ts +++ b/src/plugins/ipcMessageReceiverPlugin.ts @@ -44,8 +44,8 @@ export const ipcMessageReceiver: Plugin = { options.store.dispatch("DETECT_LEAVE_FULLSCREEN") ); - window.electron.onReceivedIPCMsg("CHECK_EDITED_AND_NOT_SAVE", () => { - options.store.dispatch("CHECK_EDITED_AND_NOT_SAVE"); + window.electron.onReceivedIPCMsg("CHECK_EDITED_AND_NOT_SAVE", (_, obj) => { + options.store.dispatch("CHECK_EDITED_AND_NOT_SAVE", obj); }); window.electron.onReceivedIPCMsg( diff --git a/src/store/type.ts b/src/store/type.ts index 652fb1a2fa..3efb79c29b 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1129,6 +1129,7 @@ export type SettingStoreTypes = { export type UiStoreState = { uiLockCount: number; dialogLockCount: number; + reloadingLock: boolean; inheritAudioInfo: boolean; activePointScrollMode: ActivePointScrollMode; isHelpDialogOpen: boolean; @@ -1185,6 +1186,11 @@ export type UiStoreTypes = { action(): void; }; + LOCK_RELOADING: { + mutation: undefined; + action(): void; + }; + SHOULD_SHOW_PANES: { getter: boolean; }; @@ -1270,10 +1276,17 @@ export type UiStoreTypes = { }; CHECK_EDITED_AND_NOT_SAVE: { - action(): Promise; + action( + obj: + | { closeOrReload: "close" } + | { + closeOrReload: "reload"; + isMultiEngineOffMode?: boolean; + } + ): Promise; }; - RESTART_APP: { + RELOAD_APP: { action(obj: { isMultiEngineOffMode?: boolean }): void; }; diff --git a/src/store/ui.ts b/src/store/ui.ts index 9ee1c03d07..4b10c73383 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -36,6 +36,7 @@ export function withProgress( export const uiStoreState: UiStoreState = { uiLockCount: 0, dialogLockCount: 0, + reloadingLock: false, inheritAudioInfo: true, activePointScrollMode: "OFF", isHelpDialogOpen: false, @@ -125,6 +126,18 @@ export const uiStore = createPartialStore({ }, }, + /** + * 再読み込み中。UNLOCKされることはない。 + */ + LOCK_RELOADING: { + mutation(state) { + state.reloadingLock = true; + }, + action({ commit }) { + commit("LOCK_RELOADING"); + }, + }, + SHOULD_SHOW_PANES: { getter(_, getters) { return getters.ACTIVE_AUDIO_KEY != undefined; @@ -303,7 +316,12 @@ export const uiStore = createPartialStore({ }, CHECK_EDITED_AND_NOT_SAVE: { - async action({ dispatch, getters }) { + /** + * プロジェクトファイル未保存の場合、保存するかどうかを確認する。 + * 保存後にウィンドウを閉じるか、アプリを再読み込みする。 + * 保存がキャンセルされた場合は何もしない。 + */ + async action({ dispatch, getters }, obj) { if (getters.IS_EDITED) { const result = await dispatch("SAVE_OR_DISCARD_PROJECT_FILE", {}); if (result == "canceled") { @@ -311,16 +329,28 @@ export const uiStore = createPartialStore({ } } - window.electron.closeWindow(); + if (obj.closeOrReload == "close") { + window.electron.closeWindow(); + } else if (obj.closeOrReload == "reload") { + await dispatch("RELOAD_APP", { + isMultiEngineOffMode: obj.isMultiEngineOffMode, + }); + } }, }, - RESTART_APP: { - action(_, { isMultiEngineOffMode }: { isMultiEngineOffMode?: boolean }) { - window.electron.restartApp({ - isMultiEngineOffMode: !!isMultiEngineOffMode, - }); - }, + RELOAD_APP: { + action: createUILockAction( + async ( + { dispatch }, + { isMultiEngineOffMode }: { isMultiEngineOffMode?: boolean } + ) => { + await dispatch("LOCK_RELOADING"); + await window.electron.reloadApp({ + isMultiEngineOffMode: !!isMultiEngineOffMode, + }); + } + ), }, START_PROGRESS: { diff --git a/src/type/ipc.ts b/src/type/ipc.ts index f3b81a6c10..24e76f342b 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -287,8 +287,8 @@ export type IpcIHData = { return: EngineDirValidationResult; }; - RESTART_APP: { - args: [obj: { isMultiEngineOffMode: boolean }]; + RELOAD_APP: { + args: [obj: { isMultiEngineOffMode?: boolean }]; return: void; }; @@ -348,7 +348,12 @@ export type IpcSOData = { }; CHECK_EDITED_AND_NOT_SAVE: { - args: []; + args: [ + obj: { + closeOrReload: "close" | "reload"; + isMultiEngineOffMode?: boolean; + } + ]; return: void; }; diff --git a/src/type/preload.ts b/src/type/preload.ts index 0681b486f7..70612bb46f 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -222,7 +222,7 @@ export interface Sandbox { installVvppEngine(path: string): Promise; uninstallVvppEngine(engineId: EngineId): Promise; validateEngineDir(engineDir: string): Promise; - restartApp(obj: { isMultiEngineOffMode: boolean }): void; + reloadApp(obj: { isMultiEngineOffMode?: boolean }): Promise; } export type AppInfos = { diff --git a/src/views/EditorHome.vue b/src/views/EditorHome.vue index 9e8d59d691..3b42fd0c67 100644 --- a/src/views/EditorHome.vue +++ b/src/views/EditorHome.vue @@ -29,9 +29,10 @@ - マルチエンジンをオフにして再起動する Q&Aを見る @@ -217,6 +218,7 @@ const $q = useQuasar(); const audioKeys = computed(() => store.state.audioKeys); const uiLocked = computed(() => store.getters.UI_LOCKED); +const reloadingLocked = computed(() => store.state.reloadingLock); const isMultipleEngine = computed(() => store.state.engineIds.length > 1); @@ -687,8 +689,11 @@ watch( } ); -const restartAppWithMultiEngineOffMode = () => { - store.dispatch("RESTART_APP", { isMultiEngineOffMode: true }); +const reloadAppWithMultiEngineOffMode = () => { + store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "reload", + isMultiEngineOffMode: true, + }); }; const openQa = () => { diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index a812f25167..8598a7a9b3 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -36,6 +36,7 @@ describe("store/vuex.js test", () => { audioPlayStartPoint: 0, uiLockCount: 0, dialogLockCount: 0, + reloadingLock: false, nowPlayingContinuously: false, undoCommands: [], redoCommands: [],