diff --git a/src/backend/electron/ipcMainHandle.ts b/src/backend/electron/ipcMainHandle.ts new file mode 100644 index 0000000000..527a21329d --- /dev/null +++ b/src/backend/electron/ipcMainHandle.ts @@ -0,0 +1,351 @@ +import fs from "fs"; +import path from "path"; +import { app, nativeTheme, shell } from "electron"; +import { hasSupportedGpu } from "./device"; +import { getConfigManager } from "./electronConfig"; +import { getEngineAndVvppController } from "./engineAndVvppController"; +import { writeFileSafely } from "./fileHelper"; +import { IpcMainHandle } from "./ipc"; +import { getEngineInfoManager } from "./manager/engineInfoManager"; +import { getEngineProcessManager } from "./manager/engineProcessManager"; +import { getWindowManager } from "./manager/windowManager"; +import { AssetTextFileNames } from "@/type/staticResources"; +import { failure, success } from "@/type/result"; +import { + defaultToolbarButtonSetting, + EngineId, + SystemError, + TextAsset, +} from "@/type/preload"; + +// エンジンのフォルダを開く +function openEngineDirectory(engineId: EngineId) { + const engineDirectory = getEngineInfoManager().fetchEngineDirectory(engineId); + + // Windows環境だとスラッシュ区切りのパスが動かない。 + // path.resolveはWindowsだけバックスラッシュ区切りにしてくれるため、path.resolveを挟む。 + void shell.openPath(path.resolve(engineDirectory)); +} + +/** + * 保存に適した場所を選択するかキャンセルするまでダイアログを繰り返し表示する。 + * アンインストール等で消えうる場所などを避ける。 + * @param showDialogFunction ダイアログを表示する関数 + */ +async function retryShowSaveDialogWhileSafeDir< + T extends Electron.OpenDialogReturnValue | Electron.SaveDialogReturnValue, +>(showDialogFunction: () => Promise, appDirPath: string): Promise { + /** + * 指定されたパスが安全でないかどうかを判断する + */ + const isUnsafePath = (filePath: string) => { + const unsafeSaveDirs = [appDirPath, app.getPath("userData")]; // アンインストールで消えうるフォルダ + return unsafeSaveDirs.some((unsafeDir) => { + const relativePath = path.relative(unsafeDir, filePath); + return !( + path.isAbsolute(relativePath) || + relativePath.startsWith(`..${path.sep}`) || + relativePath === ".." + ); + }); + }; + + /** + * 警告ダイアログを表示し、ユーザーが再試行を選択したかどうかを返す + */ + const showWarningDialog = async () => { + const windowManager = getWindowManager(); + const productName = app.getName().toUpperCase(); + const warningResult = await windowManager.showMessageBox({ + message: `指定された保存先は${productName}により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。`, + type: "warning", + buttons: ["保存場所を変更", "無視して保存"], + defaultId: 0, + title: "警告", + cancelId: 0, + }); + return warningResult.response === 0 ? "retry" : "forceSave"; + }; + + while (true) { + const result = await showDialogFunction(); + // キャンセルされた場合、結果を直ちに返す + if (result.canceled) return result; + + // 選択されたファイルパスを取得 + const filePath = + "filePaths" in result ? result.filePaths[0] : result.filePath; + + // 選択されたパスが安全かどうかを確認 + if (isUnsafePath(filePath)) { + const result = await showWarningDialog(); + if (result === "retry") continue; // ユーザーが保存場所を変更を選択した場合 + } + return result; // 安全なパスが選択された場合 + } +} + +export function getIpcMainHandle( + appStateGetter: () => { willQuit: boolean }, + staticDirPath: string, + appDirPath: string, + initialFilePath: string | undefined, +): IpcMainHandle { + const configManager = getConfigManager(); + const engineAndVvppController = getEngineAndVvppController(); + const engineInfoManager = getEngineInfoManager(); + const engineProcessManager = getEngineProcessManager(); + const windowManager = getWindowManager(); + return { + GET_TEXT_ASSET: async (_, textType) => { + const fileName = path.join(staticDirPath, AssetTextFileNames[textType]); + const text = await fs.promises.readFile(fileName, "utf-8"); + if (textType === "OssLicenses" || textType === "UpdateInfos") { + return JSON.parse(text) as TextAsset[typeof textType]; + } + return text; + }, + + GET_ALT_PORT_INFOS: () => { + return engineInfoManager.altPortInfos; + }, + + GET_INITIAL_PROJECT_FILE_PATH: async () => { + if (initialFilePath && initialFilePath.endsWith(".vvproj")) { + return initialFilePath; + } + }, + + /** + * 保存先になるディレクトリを選ぶダイアログを表示する。 + */ + SHOW_SAVE_DIRECTORY_DIALOG: async (_, { title }) => { + const result = await retryShowSaveDialogWhileSafeDir( + () => + windowManager.showOpenDialog({ + title, + properties: [ + "openDirectory", + "createDirectory", + "treatPackageAsDirectory", + ], + }), + appDirPath, + ); + if (result.canceled) { + return undefined; + } + return result.filePaths[0]; + }, + + /** + * ディレクトリ選択ダイアログを表示する。 + * 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。 + */ + SHOW_OPEN_DIRECTORY_DIALOG: async (_, { title }) => { + const result = await windowManager.showOpenDialog({ + title, + properties: [ + "openDirectory", + "createDirectory", + "treatPackageAsDirectory", + ], + }); + if (result.canceled) { + return undefined; + } + return result.filePaths[0]; + }, + + SHOW_WARNING_DIALOG: (_, { title, message }) => { + return windowManager.showMessageBox({ + type: "warning", + title, + message, + }); + }, + + SHOW_ERROR_DIALOG: (_, { title, message }) => { + return windowManager.showMessageBox({ + type: "error", + title, + message, + }); + }, + + SHOW_OPEN_FILE_DIALOG: (_, { title, name, extensions, defaultPath }) => { + return windowManager.showOpenDialogSync({ + title, + defaultPath, + filters: [{ name, extensions }], + properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], + })?.[0]; + }, + + SHOW_SAVE_FILE_DIALOG: async ( + _, + { title, defaultPath, name, extensions }, + ) => { + const result = await retryShowSaveDialogWhileSafeDir( + () => + windowManager.showSaveDialog({ + title, + defaultPath, + filters: [{ name, extensions }], + properties: ["createDirectory"], + }), + appDirPath, + ); + if (result.canceled) { + return undefined; + } + return result.filePath; + }, + + IS_AVAILABLE_GPU_MODE: () => { + return hasSupportedGpu(process.platform); + }, + + IS_MAXIMIZED_WINDOW: () => { + return windowManager.isMaximized(); + }, + + CLOSE_WINDOW: () => { + const appState = appStateGetter(); + appState.willQuit = true; + windowManager.destroyWindow(); + }, + + MINIMIZE_WINDOW: () => { + windowManager.minimize(); + }, + + TOGGLE_MAXIMIZE_WINDOW: () => { + windowManager.toggleMaximizeWindow(); + }, + + TOGGLE_FULLSCREEN: () => { + windowManager.toggleFullScreen(); + }, + + /** UIの拡大 */ + ZOOM_IN: () => { + windowManager.zoomIn(); + }, + + /** UIの縮小 */ + ZOOM_OUT: () => { + windowManager.zoomOut(); + }, + + /** UIの拡大率リセット */ + ZOOM_RESET: () => { + windowManager.zoomReset(); + }, + + OPEN_LOG_DIRECTORY: () => { + void shell.openPath(app.getPath("logs")); + }, + + ENGINE_INFOS: () => { + // エンジン情報を設定ファイルに保存しないためにelectron-storeは使わない + return engineInfoManager.fetchEngineInfos(); + }, + + RESTART_ENGINE: async (_, { engineId }) => { + return engineProcessManager.restartEngine(engineId); + }, + + OPEN_ENGINE_DIRECTORY: async (_, { engineId }) => { + openEngineDirectory(engineId); + }, + + HOTKEY_SETTINGS: (_, { newData }) => { + if (newData != undefined) { + const hotkeySettings = configManager.get("hotkeySettings"); + const hotkeySetting = hotkeySettings.find( + (hotkey) => hotkey.action == newData.action, + ); + if (hotkeySetting != undefined) { + hotkeySetting.combination = newData.combination; + } + configManager.set("hotkeySettings", hotkeySettings); + } + return configManager.get("hotkeySettings"); + }, + + ON_VUEX_READY: () => { + windowManager.show(); + }, + + CHECK_FILE_EXISTS: (_, { file }) => { + return fs.existsSync(file); + }, + + CHANGE_PIN_WINDOW: () => { + windowManager.togglePinWindow(); + }, + + GET_DEFAULT_TOOLBAR_SETTING: () => { + return defaultToolbarButtonSetting; + }, + + GET_SETTING: (_, key) => { + return configManager.get(key); + }, + + SET_SETTING: (_, key, newValue) => { + configManager.set(key, newValue); + return configManager.get(key); + }, + + SET_ENGINE_SETTING: async (_, engineId, engineSetting) => { + engineAndVvppController.updateEngineSetting(engineId, engineSetting); + }, + + SET_NATIVE_THEME: (_, source) => { + nativeTheme.themeSource = source; + }, + + INSTALL_VVPP_ENGINE: async (_, path: string) => { + return await engineAndVvppController.installVvppEngine(path); + }, + + UNINSTALL_VVPP_ENGINE: async (_, engineId: EngineId) => { + return await engineAndVvppController.uninstallVvppEngine(engineId); + }, + + VALIDATE_ENGINE_DIR: (_, { engineDir }) => { + return engineInfoManager.validateEngineDir(engineDir); + }, + + RELOAD_APP: async (_, { isMultiEngineOffMode }) => { + await windowManager.reload(isMultiEngineOffMode); + }, + + WRITE_FILE: (_, { filePath, buffer }) => { + try { + writeFileSafely( + filePath, + new DataView(buffer instanceof Uint8Array ? buffer.buffer : buffer), + ); + return success(undefined); + } catch (e) { + // throwだと`.code`の情報が消えるのでreturn + const a = e as SystemError; + return failure(a.code, a); + } + }, + + READ_FILE: async (_, { filePath }) => { + try { + const result = await fs.promises.readFile(filePath); + return success(result); + } catch (e) { + // throwだと`.code`の情報が消えるのでreturn + const a = e as SystemError; + return failure(a.code, a); + } + }, + }; +} diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index 4338d1d092..f18cbde217 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -4,20 +4,13 @@ import path from "path"; import fs from "fs"; import { pathToFileURL } from "url"; -import { app, dialog, Menu, nativeTheme, net, protocol, shell } from "electron"; +import { app, dialog, Menu, net, protocol, shell } from "electron"; import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; import electronLog from "electron-log/main"; import dayjs from "dayjs"; -import { hasSupportedGpu } from "./device"; -import { - getEngineInfoManager, - initializeEngineInfoManager, -} from "./manager/engineInfoManager"; -import { - getEngineProcessManager, - initializeEngineProcessManager, -} from "./manager/engineProcessManager"; +import { initializeEngineInfoManager } from "./manager/engineInfoManager"; +import { initializeEngineProcessManager } from "./manager/engineProcessManager"; import { initializeVvppManager, isVvppFile } from "./manager/vvppManager"; import { getWindowManager, @@ -28,16 +21,8 @@ import { initializeRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { registerIpcMainHandle, ipcMainSendProxy, IpcMainHandle } from "./ipc"; import { getConfigManager } from "./electronConfig"; import { getEngineAndVvppController } from "./engineAndVvppController"; -import { writeFileSafely } from "./fileHelper"; -import { failure, success } from "@/type/result"; -import { AssetTextFileNames } from "@/type/staticResources"; -import { - EngineInfo, - SystemError, - defaultToolbarButtonSetting, - EngineId, - TextAsset, -} from "@/type/preload"; +import { getIpcMainHandle } from "./ipcMainHandle"; +import { EngineInfo } from "@/type/preload"; import { isMac, isProduction } from "@/helpers/platform"; import { createLogger } from "@/helpers/log"; @@ -214,19 +199,8 @@ initializeVvppManager({ vvppEngineDir, tmpDir: app.getPath("temp") }); const configManager = getConfigManager(); const windowManager = getWindowManager(); -const engineInfoManager = getEngineInfoManager(); -const engineProcessManager = getEngineProcessManager(); const engineAndVvppController = getEngineAndVvppController(); -// エンジンのフォルダを開く -function openEngineDirectory(engineId: EngineId) { - const engineDirectory = engineInfoManager.fetchEngineDirectory(engineId); - - // Windows環境だとスラッシュ区切りのパスが動かない。 - // path.resolveはWindowsだけバックスラッシュ区切りにしてくれるため、path.resolveを挟む。 - void shell.openPath(path.resolve(engineDirectory)); -} - /** * マルチエンジン機能が有効だった場合はtrueを返す。 * 無効だった場合はダイアログを表示してfalseを返す。 @@ -290,313 +264,10 @@ if (isMac) { } } -/** - * 保存に適した場所を選択するかキャンセルするまでダイアログを繰り返し表示する。 - * アンインストール等で消えうる場所などを避ける。 - * @param showDialogFunction ダイアログを表示する関数 - */ -const retryShowSaveDialogWhileSafeDir = async < - T extends Electron.OpenDialogReturnValue | Electron.SaveDialogReturnValue, ->( - showDialogFunction: () => Promise, -): Promise => { - /** - * 指定されたパスが安全でないかどうかを判断する - */ - const isUnsafePath = (filePath: string) => { - const unsafeSaveDirs = [appDirPath, app.getPath("userData")]; // アンインストールで消えうるフォルダ - return unsafeSaveDirs.some((unsafeDir) => { - const relativePath = path.relative(unsafeDir, filePath); - return !( - path.isAbsolute(relativePath) || - relativePath.startsWith(`..${path.sep}`) || - relativePath === ".." - ); - }); - }; - - /** - * 警告ダイアログを表示し、ユーザーが再試行を選択したかどうかを返す - */ - const showWarningDialog = async () => { - const productName = app.getName().toUpperCase(); - const warningResult = await windowManager.showMessageBox({ - message: `指定された保存先は${productName}により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。`, - type: "warning", - buttons: ["保存場所を変更", "無視して保存"], - defaultId: 0, - title: "警告", - cancelId: 0, - }); - return warningResult.response === 0 ? "retry" : "forceSave"; - }; - - while (true) { - const result = await showDialogFunction(); - // キャンセルされた場合、結果を直ちに返す - if (result.canceled) return result; - - // 選択されたファイルパスを取得 - const filePath = - "filePaths" in result ? result.filePaths[0] : result.filePath; - - // 選択されたパスが安全かどうかを確認 - if (isUnsafePath(filePath)) { - const result = await showWarningDialog(); - if (result === "retry") continue; // ユーザーが保存場所を変更を選択した場合 - } - return result; // 安全なパスが選択された場合 - } -}; - // プロセス間通信 -registerIpcMainHandle({ - GET_TEXT_ASSET: async (_, textType) => { - const fileName = path.join(__static, AssetTextFileNames[textType]); - const text = await fs.promises.readFile(fileName, "utf-8"); - if (textType === "OssLicenses" || textType === "UpdateInfos") { - return JSON.parse(text) as TextAsset[typeof textType]; - } - return text; - }, - - GET_ALT_PORT_INFOS: () => { - return engineInfoManager.altPortInfos; - }, - - GET_INITIAL_PROJECT_FILE_PATH: async () => { - if (initialFilePath && initialFilePath.endsWith(".vvproj")) { - return initialFilePath; - } - }, - - /** - * 保存先になるディレクトリを選ぶダイアログを表示する。 - */ - SHOW_SAVE_DIRECTORY_DIALOG: async (_, { title }) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - windowManager.showOpenDialog({ - title, - properties: [ - "openDirectory", - "createDirectory", - "treatPackageAsDirectory", - ], - }), - ); - if (result.canceled) { - return undefined; - } - return result.filePaths[0]; - }, - - /** - * ディレクトリ選択ダイアログを表示する。 - * 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。 - */ - SHOW_OPEN_DIRECTORY_DIALOG: async (_, { title }) => { - const result = await windowManager.showOpenDialog({ - title, - properties: [ - "openDirectory", - "createDirectory", - "treatPackageAsDirectory", - ], - }); - if (result.canceled) { - return undefined; - } - return result.filePaths[0]; - }, - - SHOW_WARNING_DIALOG: (_, { title, message }) => { - return windowManager.showMessageBox({ - type: "warning", - title, - message, - }); - }, - - SHOW_ERROR_DIALOG: (_, { title, message }) => { - return windowManager.showMessageBox({ - type: "error", - title, - message, - }); - }, - - SHOW_OPEN_FILE_DIALOG: (_, { title, name, extensions, defaultPath }) => { - return windowManager.showOpenDialogSync({ - title, - defaultPath, - filters: [{ name, extensions }], - properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], - })?.[0]; - }, - - SHOW_SAVE_FILE_DIALOG: async ( - _, - { title, defaultPath, name, extensions }, - ) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - windowManager.showSaveDialog({ - title, - defaultPath, - filters: [{ name, extensions }], - properties: ["createDirectory"], - }), - ); - if (result.canceled) { - return undefined; - } - return result.filePath; - }, - - IS_AVAILABLE_GPU_MODE: () => { - return hasSupportedGpu(process.platform); - }, - - IS_MAXIMIZED_WINDOW: () => { - return windowManager.isMaximized(); - }, - - CLOSE_WINDOW: () => { - appState.willQuit = true; - windowManager.destroyWindow(); - }, - - MINIMIZE_WINDOW: () => { - windowManager.minimize(); - }, - - TOGGLE_MAXIMIZE_WINDOW: () => { - windowManager.toggleMaximizeWindow(); - }, - - TOGGLE_FULLSCREEN: () => { - windowManager.toggleFullScreen(); - }, - - /** UIの拡大 */ - ZOOM_IN: () => { - windowManager.zoomIn(); - }, - - /** UIの縮小 */ - ZOOM_OUT: () => { - windowManager.zoomOut(); - }, - - /** UIの拡大率リセット */ - ZOOM_RESET: () => { - windowManager.zoomReset(); - }, - - OPEN_LOG_DIRECTORY: () => { - void shell.openPath(app.getPath("logs")); - }, - - ENGINE_INFOS: () => { - // エンジン情報を設定ファイルに保存しないためにelectron-storeは使わない - return engineInfoManager.fetchEngineInfos(); - }, - - RESTART_ENGINE: async (_, { engineId }) => { - return engineProcessManager.restartEngine(engineId); - }, - - OPEN_ENGINE_DIRECTORY: async (_, { engineId }) => { - openEngineDirectory(engineId); - }, - - HOTKEY_SETTINGS: (_, { newData }) => { - if (newData != undefined) { - const hotkeySettings = configManager.get("hotkeySettings"); - const hotkeySetting = hotkeySettings.find( - (hotkey) => hotkey.action == newData.action, - ); - if (hotkeySetting != undefined) { - hotkeySetting.combination = newData.combination; - } - configManager.set("hotkeySettings", hotkeySettings); - } - return configManager.get("hotkeySettings"); - }, - - ON_VUEX_READY: () => { - windowManager.show(); - }, - - CHECK_FILE_EXISTS: (_, { file }) => { - return fs.existsSync(file); - }, - - CHANGE_PIN_WINDOW: () => { - windowManager.togglePinWindow(); - }, - - GET_DEFAULT_TOOLBAR_SETTING: () => { - return defaultToolbarButtonSetting; - }, - - GET_SETTING: (_, key) => { - return configManager.get(key); - }, - - SET_SETTING: (_, key, newValue) => { - configManager.set(key, newValue); - return configManager.get(key); - }, - - SET_ENGINE_SETTING: async (_, engineId, engineSetting) => { - engineAndVvppController.updateEngineSetting(engineId, engineSetting); - }, - - SET_NATIVE_THEME: (_, source) => { - nativeTheme.themeSource = source; - }, - - INSTALL_VVPP_ENGINE: async (_, path: string) => { - return await engineAndVvppController.installVvppEngine(path); - }, - - UNINSTALL_VVPP_ENGINE: async (_, engineId: EngineId) => { - return await engineAndVvppController.uninstallVvppEngine(engineId); - }, - - VALIDATE_ENGINE_DIR: (_, { engineDir }) => { - return engineInfoManager.validateEngineDir(engineDir); - }, - - RELOAD_APP: async (_, { isMultiEngineOffMode }) => { - await windowManager.reload(isMultiEngineOffMode); - }, - - WRITE_FILE: (_, { filePath, buffer }) => { - try { - writeFileSafely( - filePath, - new DataView(buffer instanceof Uint8Array ? buffer.buffer : buffer), - ); - return success(undefined); - } catch (e) { - // throwだと`.code`の情報が消えるのでreturn - const a = e as SystemError; - return failure(a.code, a); - } - }, - - READ_FILE: async (_, { filePath }) => { - try { - const result = await fs.promises.readFile(filePath); - return success(result); - } catch (e) { - // throwだと`.code`の情報が消えるのでreturn - const a = e as SystemError; - return failure(a.code, a); - } - }, -}); +registerIpcMainHandle( + getIpcMainHandle(() => appState, __static, appDirPath, initialFilePath), +); // app callback app.on("web-contents-created", (_e, contents) => {