diff --git a/desktop/src/api/clip.ts b/desktop/src/api/clip.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/api/common.ts b/desktop/src/api/common.ts deleted file mode 100644 index 64a1bbdff..000000000 --- a/desktop/src/api/common.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { logError } from "../services/logging"; - - -export { logToDisk, openLogDirectory } from "../services/logging"; diff --git a/desktop/src/api/electronStore.ts b/desktop/src/api/electronStore.ts index 5f84776e1..2ee74953d 100644 --- a/desktop/src/api/electronStore.ts +++ b/desktop/src/api/electronStore.ts @@ -1,4 +1,4 @@ -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { keysStore } from "../stores/keys.store"; import { safeStorageStore } from "../stores/safeStorage.store"; import { uploadStatusStore } from "../stores/upload.store"; diff --git a/desktop/src/api/ffmpeg.ts b/desktop/src/api/ffmpeg.ts index 9d11183a8..4b077519a 100644 --- a/desktop/src/api/ffmpeg.ts +++ b/desktop/src/api/ffmpeg.ts @@ -1,7 +1,7 @@ import { ipcRenderer } from "electron"; import { existsSync } from "fs"; import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { ElectronFile } from "../types"; export async function runFFmpegCmd( diff --git a/desktop/src/api/imageProcessor.ts b/desktop/src/api/imageProcessor.ts index 9d93aecd1..b97bd7f7b 100644 --- a/desktop/src/api/imageProcessor.ts +++ b/desktop/src/api/imageProcessor.ts @@ -2,7 +2,7 @@ import { ipcRenderer } from "electron/renderer"; import { existsSync } from "fs"; import { CustomErrors } from "../constants/errors"; import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { ElectronFile } from "../types"; import { isPlatform } from "../utils/common/platform"; diff --git a/desktop/src/api/safeStorage.ts b/desktop/src/api/safeStorage.ts index 64c489195..cffb23302 100644 --- a/desktop/src/api/safeStorage.ts +++ b/desktop/src/api/safeStorage.ts @@ -1,5 +1,5 @@ import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { safeStorageStore } from "../stores/safeStorage.store"; export async function setEncryptionKey(encryptionKey: string) { diff --git a/desktop/src/api/upload.ts b/desktop/src/api/upload.ts index 280ff084f..0a31ee353 100644 --- a/desktop/src/api/upload.ts +++ b/desktop/src/api/upload.ts @@ -1,5 +1,5 @@ import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { getElectronFilesFromGoogleZip, getSavedFilePaths, diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 7ec899ec7..8a600b603 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -14,14 +14,13 @@ import serveNextAt from "next-electron-server"; import { existsSync } from "node:fs"; import * as fs from "node:fs/promises"; import * as path from "node:path"; +import { isDev } from "./main/general"; +import { logErrorSentry, setupLogging } from "./main/log"; import { initWatcher } from "./services/chokidar"; -import { logErrorSentry } from "./services/sentry"; -import { isDev } from "./utils/common"; import { addAllowOriginHeader } from "./utils/cors"; import { createWindow } from "./utils/createWindow"; import { setupAppEventEmitter } from "./utils/events"; import setupIpcComs from "./utils/ipcComms"; -import { setupLogging } from "./utils/logging"; import { handleDockIconHideOnAutoLaunch, handleDownloads, diff --git a/desktop/src/main/general.ts b/desktop/src/main/general.ts new file mode 100644 index 000000000..a92656e31 --- /dev/null +++ b/desktop/src/main/general.ts @@ -0,0 +1,42 @@ +import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */ +import { app } from "electron/main"; +import * as path from "node:path"; + +/** `true` if the app is running in development mode. */ +export const isDev = !app.isPackaged; + +/** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ +export const openDirectory = async (dirPath: string) => { + const res = await shell.openPath(path.normalize(dirPath)); + // shell.openPath resolves with a string containing the error message + // corresponding to the failure if a failure occurred, otherwise "". + if (res) throw new Error(`Failed to open directory ${dirPath}: res`); +}; + +/** + * Return the path where the logs for the app are saved. + * + * [Note: Electron app paths] + * + * By default, these paths are at the following locations: + * + * - macOS: `~/Library/Application Support/ente` + * - Linux: `~/.config/ente` + * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` + * - Windows: C:\Users\\AppData\Local\ + * + * https://www.electronjs.org/docs/latest/api/app + * + */ +const logDirectoryPath = () => app.getPath("logs"); + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +export const openLogDirectory = () => openDirectory(logDirectoryPath()); diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts new file mode 100644 index 000000000..2e2b3c004 --- /dev/null +++ b/desktop/src/main/ipc.ts @@ -0,0 +1,43 @@ +/** + * @file Listen for IPC events sent/invoked by the renderer process, and route + * them to their correct handlers. + * + * This file is meant as a sibling to `preload.ts`, but this one runs in the + * context of the main process, and can import other files from `src/`. + */ + +import { ipcMain } from "electron/main"; +import { appVersion } from "../services/appUpdater"; +import { openDirectory, openLogDirectory } from "./general"; +import { logToDisk } from "./log"; + +// - General + +export const attachIPCHandlers = () => { + // Notes: + // + // The first parameter of the handler passed to `ipcMain.handle` is the + // `event`, and is usually ignored. The rest of the parameters are the + // arguments passed to `ipcRenderer.invoke`. + // + // [Note: Catching exception during .send/.on] + // + // While we can use ipcRenderer.send/ipcMain.on for one-way communication, + // that has the disadvantage that any exceptions thrown in the processing of + // the handler are not sent back to the renderer. So we use the + // ipcRenderer.invoke/ipcMain.handle 2-way pattern even for things that are + // conceptually one way. An exception (pun intended) to this is logToDisk, + // which is a primitive, frequently used, operation and shouldn't throw, so + // having its signature by synchronous is a bit convenient. + + // - General + + ipcMain.handle("appVersion", (_) => appVersion()); + + ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath)); + + ipcMain.handle("openLogDirectory", (_) => openLogDirectory()); + + // See: [Note: Catching exception during .send/.on] + ipcMain.on("logToDisk", (_, msg) => logToDisk(msg)); +}; diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts new file mode 100644 index 000000000..014465df1 --- /dev/null +++ b/desktop/src/main/log.ts @@ -0,0 +1,34 @@ +import log from "electron-log"; +import { isDev } from "./general"; + +export function setupLogging(isDev?: boolean) { + log.transports.file.fileName = "ente.log"; + log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; + if (!isDev) { + log.transports.console.level = false; + } + log.transports.file.format = + "[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}"; +} + +export const logToDisk = (message: string) => { + log.info(message); +}; + +export const logError = logErrorSentry; + +/** Deprecated, but no alternative yet */ +export function logErrorSentry( + error: any, + msg: string, + info?: Record, +) { + logToDisk( + `error: ${error?.name} ${error?.message} ${ + error?.stack + } msg: ${msg} info: ${JSON.stringify(info)}`, + ); + if (isDev) { + console.log(error, { msg, info }); + } +} diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index a4d8e4614..1f7792bf3 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -7,24 +7,23 @@ * functions as an object on the DOM, so that the renderer process can invoke * functions that live in the main (Node.js) process if needed. * + * Ref: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload + * * Note that this script cannot import other code from `src/` - conceptually it * can be thought of as running in a separate, third, process different from * both the main or a renderer process (technically, it runs in a BrowserWindow * context that runs prior to the renderer process). * - * That said, this can be split into multiple files if we wished. However, - * that'd require us setting up a bundler to package it back up into a single JS - * file that can be used at runtime. - * * > Since enabling the sandbox disables Node.js integration in your preload * > scripts, you can no longer use require("../my-script"). In other words, * > your preload script needs to be a single file. * > * > https://www.electronjs.org/blog/breach-to-barrier * - * Since most of this is just boilerplate code providing a bridge between the - * main and renderer, we avoid introducing another moving part into the mix and - * just keep the entire preload setup in this single file. + * If we really wanted, we could setup a bundler to package this into a single + * file. However, since this is just boilerplate code providing a bridge between + * the main and renderer, we avoid introducing another moving part into the mix + * and just keep the entire preload setup in this single file. */ import { contextBridge, ipcRenderer } from "electron"; @@ -32,7 +31,6 @@ import { createWriteStream, existsSync } from "node:fs"; import * as fs from "node:fs/promises"; import { Readable } from "node:stream"; import path from "path"; -import { logToDisk, openLogDirectory } from "./api/common"; import { runFFmpegCmd } from "./api/ffmpeg"; import { getDirFiles } from "./api/fs"; import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor"; @@ -58,21 +56,44 @@ import { updateWatchMappingIgnoredFiles, updateWatchMappingSyncedFiles, } from "./api/watch"; -import { setupLogging } from "./utils/logging"; +import { logErrorSentry, setupLogging } from "./main/log"; -/* - Some of the code below has been duplicated to make this file self contained - (see the documentation at the top of why it needs to be a single file). +setupLogging(); - Enhancement: consider alternatives -*/ +// - General + +/** Return the version of the desktop app. */ +const appVersion = (): Promise => ipcRenderer.invoke("appVersion"); + +/** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ +const openDirectory = (dirPath: string): Promise => + ipcRenderer.invoke("openDirectory"); + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +const openLogDirectory = (): Promise => + ipcRenderer.invoke("openLogDirectory"); + +/** + * Log the given {@link message} to the on-disk log file maintained by the + * desktop app. + */ +const logToDisk = (message: string): void => + ipcRenderer.send("logToDisk", message); + +// - FIXME below this /* preload: duplicated logError */ -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke("log-error", error, message, info); -} - -// - +const logError = (error: Error, message: string, info?: any) => { + logErrorSentry(error, message, info); +}; /* preload: duplicated writeStream */ /** @@ -342,26 +363,6 @@ const selectDirectory = async (): Promise => { } }; -const getAppVersion = async (): Promise => { - try { - return await ipcRenderer.invoke("get-app-version"); - } catch (e) { - logError(e, "failed to get release version"); - throw e; - } -}; - -const openDirectory = async (dirPath: string): Promise => { - try { - await ipcRenderer.invoke("open-dir", dirPath); - } catch (e) { - logError(e, "error while opening directory"); - throw e; - } -}; - -// - - const clearElectronStore = () => { ipcRenderer.send("clear-electron-store"); }; @@ -382,51 +383,52 @@ const muteUpdateNotification = (version: string) => { // - -setupLogging(); - // These objects exposed here will become available to the JS code in our // renderer (the web/ code) as `window.ElectronAPIs.*` // -// - Introduction -// https://www.electronjs.org/docs/latest/tutorial/tutorial-preload -// // There are a few related concepts at play here, and it might be worthwhile to // read their (excellent) documentation to get an understanding; -// +//` // - ContextIsolation: // https://www.electronjs.org/docs/latest/tutorial/context-isolation // // - IPC https://www.electronjs.org/docs/latest/tutorial/ipc // -// // [Note: Transferring large amount of data over IPC] // // Electron's IPC implementation uses the HTML standard Structured Clone // Algorithm to serialize objects passed between processes. // https://www.electronjs.org/docs/latest/tutorial/ipc#object-serialization // -// In particular, both ArrayBuffer and the web File types are eligible for -// structured cloning. +// In particular, both ArrayBuffer is eligible for structured cloning. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm // // Also, ArrayBuffer is "transferable", which means it is a zero-copy operation // operation when it happens across threads. // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects // -// In our case though, we're not dealing with threads but separate processes, -// and it seems like there is a copy involved since the documentation for -// contextBridge explicitly calls out that "parameters, errors and return values -// are **copied** when they're sent over the bridge". -// https://www.electronjs.org/docs/latest/api/context-bridge#methods +// In our case though, we're not dealing with threads but separate processes. So +// the ArrayBuffer will be copied: +// > "parameters, errors and return values are **copied** when they're sent over +// the bridge". +// https://www.electronjs.org/docs/latest/api/context-bridge#methods // -// Related is a note from one of Electron's committers stating that even with -// copying, the IPC should be fast enough for even moderately large data: -// https://github.com/electron/electron/issues/1948#issuecomment-864191345 -// -// The main problem with transfering large amounts of data is potentially -// running out of memory, causing the app to crash as it copies it over across -// the processes. +// The copy itself is relatively fast, but the problem with transfering large +// amounts of data is potentially running out of memory during the copy. contextBridge.exposeInMainWorld("ElectronAPIs", { + // General + appVersion, + openDirectory, + + // Logging + openLogDirectory, + logToDisk, + + // - App update + updateAndRestart, + skipAppUpdate, + muteUpdateNotification, + // - Export exists, checkExistsAndCreateDir, @@ -453,7 +455,6 @@ contextBridge.exposeInMainWorld("ElectronAPIs", { isFolder, updateWatchMappingSyncedFiles, updateWatchMappingIgnoredFiles, - logToDisk, convertToJPEG, registerUpdateEventListener, @@ -465,18 +466,6 @@ contextBridge.exposeInMainWorld("ElectronAPIs", { rename, deleteFile, - // General - getAppVersion, - openDirectory, - - // Logging - openLogDirectory, - - // - App update - updateAndRestart, - skipAppUpdate, - muteUpdateNotification, - // - ML computeImageEmbedding, computeTextEmbedding, diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts index 89f428a72..ce31618b0 100644 --- a/desktop/src/services/appUpdater.ts +++ b/desktop/src/services/appUpdater.ts @@ -3,8 +3,8 @@ import { app, BrowserWindow } from "electron"; import { default as ElectronLog, default as log } from "electron-log"; import { autoUpdater } from "electron-updater"; import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; +import { logErrorSentry } from "../main/log"; import { AppUpdateInfo } from "../types"; -import { logErrorSentry } from "./sentry"; import { clearMuteUpdateNotificationVersion, clearSkipAppVersion, @@ -110,9 +110,12 @@ export function updateAndRestart() { autoUpdater.quitAndInstall(); } -export function getAppVersion() { - return `v${app.getVersion()}`; -} +/** + * Return the version of the desktop app + * + * The return value is of the form `v1.2.3`. + */ +export const appVersion = () => `v${app.getVersion()}`; export function skipAppUpdate(version: string) { setSkipAppVersion(version); diff --git a/desktop/src/services/chokidar.ts b/desktop/src/services/chokidar.ts index f0d217d09..c52ad8267 100644 --- a/desktop/src/services/chokidar.ts +++ b/desktop/src/services/chokidar.ts @@ -1,7 +1,7 @@ import chokidar from "chokidar"; import { BrowserWindow } from "electron"; import { getWatchMappings } from "../api/watch"; -import { logError } from "../services/logging"; +import { logError } from "../main/log"; export function initWatcher(mainWindow: BrowserWindow) { const mappings = getWatchMappings(); diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index e2aaa056e..14e805be4 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -7,10 +7,10 @@ import util from "util"; import { CustomErrors } from "../constants/errors"; import { Model } from "../types"; import Tokenizer from "../utils/clip-bpe-ts/mod"; -import { isDev } from "../utils/common"; +import { isDev } from "../main/general"; import { getPlatform } from "../utils/common/platform"; import { writeStream } from "./fs"; -import { logErrorSentry } from "./sentry"; +import { logErrorSentry } from "../main/log"; const shellescape = require("any-shell-escape"); const execAsync = util.promisify(require("child_process").exec); const jpeg = require("jpeg-js"); diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index 227bd310e..74158e92c 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/desktop/src/services/ffmpeg.ts @@ -5,7 +5,7 @@ import * as fs from "node:fs/promises"; import util from "util"; import { CustomErrors } from "../constants/errors"; import { generateTempFilePath, getTempDirPath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; +import { logErrorSentry } from "../main/log"; const shellescape = require("any-shell-escape"); const execAsync = util.promisify(require("child_process").exec); diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index 356b49c18..54dc6082c 100644 --- a/desktop/src/services/fs.ts +++ b/desktop/src/services/fs.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; import { Readable } from "stream"; import { ElectronFile } from "../types"; -import { logError } from "./logging"; +import { logError } from "../main/log"; const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; diff --git a/desktop/src/services/imageProcessor.ts b/desktop/src/services/imageProcessor.ts index d3a74ebb6..c0b8e3123 100644 --- a/desktop/src/services/imageProcessor.ts +++ b/desktop/src/services/imageProcessor.ts @@ -5,10 +5,10 @@ import log from "electron-log"; import * as fs from "node:fs/promises"; import path from "path"; import { CustomErrors } from "../constants/errors"; -import { isDev } from "../utils/common"; +import { isDev } from "../main/general"; import { isPlatform } from "../utils/common/platform"; import { generateTempFilePath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; +import { logErrorSentry } from "../main/log"; const shellescape = require("any-shell-escape"); const asyncExec = util.promisify(exec); diff --git a/desktop/src/services/logging.ts b/desktop/src/services/logging.ts deleted file mode 100644 index bcbacd9f5..000000000 --- a/desktop/src/services/logging.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ipcRenderer } from "electron"; -import log from "electron-log"; - -export function logToDisk(logLine: string) { - log.info(logLine); -} - -export function openLogDirectory() { - ipcRenderer.invoke("open-log-dir"); -} - -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke("log-error", error, message, info); -} diff --git a/desktop/src/services/sentry.ts b/desktop/src/services/sentry.ts deleted file mode 100644 index 4c5573152..000000000 --- a/desktop/src/services/sentry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isDev } from "../utils/common"; -import { logToDisk } from "./logging"; - -/** Deprecated, but no alternative yet */ -export function logErrorSentry( - error: any, - msg: string, - info?: Record, -) { - logToDisk( - `error: ${error?.name} ${error?.message} ${ - error?.stack - } msg: ${msg} info: ${JSON.stringify(info)}`, - ); - if (isDev) { - console.log(error, { msg, info }); - } -} diff --git a/desktop/src/utils/common/index.ts b/desktop/src/utils/common/index.ts deleted file mode 100644 index 592e7c373..000000000 --- a/desktop/src/utils/common/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { app } from "electron"; -export const isDev = !app.isPackaged; diff --git a/desktop/src/utils/createWindow.ts b/desktop/src/utils/createWindow.ts index 11f05f8c1..23dd5128c 100644 --- a/desktop/src/utils/createWindow.ts +++ b/desktop/src/utils/createWindow.ts @@ -3,9 +3,9 @@ import ElectronLog from "electron-log"; import * as path from "path"; import { isAppQuitting, rendererURL } from "../main"; import autoLauncher from "../services/autoLauncher"; -import { logErrorSentry } from "../services/sentry"; +import { logErrorSentry } from "../main/log"; import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; +import { isDev } from "../main/general"; import { isPlatform } from "./common/platform"; /** diff --git a/desktop/src/utils/error.ts b/desktop/src/utils/error.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/desktop/src/utils/ipcComms.ts b/desktop/src/utils/ipcComms.ts index 4312c3362..dcfe22056 100644 --- a/desktop/src/utils/ipcComms.ts +++ b/desktop/src/utils/ipcComms.ts @@ -10,8 +10,8 @@ import { } from "electron"; import path from "path"; import { clearElectronStore } from "../api/electronStore"; +import { attachIPCHandlers } from "../main/ipc"; import { - getAppVersion, muteUpdateNotification, skipAppUpdate, updateAndRestart, @@ -26,7 +26,6 @@ import { convertToJPEG, generateImageThumbnail, } from "../services/imageProcessor"; -import { logErrorSentry } from "../services/sentry"; import { generateTempFilePath } from "./temp"; export default function setupIpcComs( @@ -34,6 +33,8 @@ export default function setupIpcComs( mainWindow: BrowserWindow, watcher: chokidar.FSWatcher, ): void { + attachIPCHandlers(); + ipcMain.handle("select-dir", async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory"], @@ -79,10 +80,6 @@ export default function setupIpcComs( watcher.unwatch(args.dir); }); - ipcMain.handle("log-error", (_, err, msg, info?) => { - logErrorSentry(err, msg, info); - }); - ipcMain.handle("safeStorage-encrypt", (_, message) => { return safeStorage.encryptString(message); }); @@ -128,10 +125,6 @@ export default function setupIpcComs( muteUpdateNotification(version); }); - ipcMain.handle("get-app-version", () => { - return getAppVersion(); - }); - ipcMain.handle( "run-ffmpeg-cmd", (_, cmd, inputFilePath, outputFileName, dontTimeout) => { diff --git a/desktop/src/utils/logging.ts b/desktop/src/utils/logging.ts deleted file mode 100644 index e57382a65..000000000 --- a/desktop/src/utils/logging.ts +++ /dev/null @@ -1,11 +0,0 @@ -import log from "electron-log"; - -export function setupLogging(isDev?: boolean) { - log.transports.file.fileName = "ente.log"; - log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; - if (!isDev) { - log.transports.console.level = false; - } - log.transports.file.format = - "[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}"; -} diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts index 28013d18a..3a73cca00 100644 --- a/desktop/src/utils/main.ts +++ b/desktop/src/utils/main.ts @@ -8,7 +8,7 @@ import { rendererURL } from "../main"; import { setupAutoUpdater } from "../services/appUpdater"; import autoLauncher from "../services/autoLauncher"; import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; +import { isDev } from "../main/general"; import { isPlatform } from "./common/platform"; import { buildContextMenu, buildMenuBar } from "./menu"; const execAsync = util.promisify(require("child_process").exec); diff --git a/desktop/src/utils/menu.ts b/desktop/src/utils/menu.ts index c86786ff6..b2c912dde 100644 --- a/desktop/src/utils/menu.ts +++ b/desktop/src/utils/menu.ts @@ -7,6 +7,7 @@ import { } from "electron"; import ElectronLog from "electron-log"; import { setIsAppQuitting } from "../main"; +import { openDirectory, openLogDirectory } from "../main/general"; import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; import autoLauncher from "../services/autoLauncher"; import { @@ -201,15 +202,11 @@ export async function buildMenuBar(mainWindow: BrowserWindow): Promise { { type: "separator" }, { label: "View crash reports", - click: () => { - shell.openPath(app.getPath("crashDumps")); - }, + click: () => openDirectory(app.getPath("crashDumps")), }, { label: "View logs", - click: () => { - shell.openPath(app.getPath("logs")); - }, + click: openLogDirectory, }, ], }, diff --git a/web/apps/photos/src/components/Sidebar/DebugSection.tsx b/web/apps/photos/src/components/Sidebar/DebugSection.tsx index 62c038eac..b2d07bb47 100644 --- a/web/apps/photos/src/components/Sidebar/DebugSection.tsx +++ b/web/apps/photos/src/components/Sidebar/DebugSection.tsx @@ -24,7 +24,7 @@ export default function DebugSection() { useEffect(() => { const main = async () => { if (isElectron()) { - const appVersion = await ElectronAPIs.getAppVersion(); + const appVersion = await ElectronAPIs.appVersion(); setAppVersion(appVersion); } }; diff --git a/web/packages/shared/electron/types.ts b/web/packages/shared/electron/types.ts index e17cb7cdb..11db095c2 100644 --- a/web/packages/shared/electron/types.ts +++ b/web/packages/shared/electron/types.ts @@ -11,7 +11,45 @@ export enum Model { ONNX_CLIP = "onnx-clip", } +/** + * Extra APIs provided by the Node.js layer when our code is running in Electron + * + * This list is manually kept in sync with `desktop/src/preload.ts`. In case of + * a mismatch, the types may lie. + * + * These extra objects and functions will only be available when our code is + * running as the renderer process in Electron. So something in the code path + * should check for `isElectron() == true` before invoking these. + */ export interface ElectronAPIsType { + // - General + + /** Return the version of the desktop app. */ + appVersion: () => Promise; + + /** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ + openDirectory: (dirPath: string) => Promise; + + /** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ + openLogDirectory: () => Promise; + + /** + * Log the given {@link message} to the on-disk log file maintained by the + * desktop app. + * + * Note: Unlike the other functions exposed over the Electron bridge, + * logToDisk is fire-and-forge and does not return a promise. + */ + logToDisk: (message: string) => void; + exists: (path: string) => boolean; checkExistsAndCreateDir: (dirPath: string) => Promise; saveStreamToDisk: ( @@ -62,18 +100,15 @@ export interface ElectronAPIsType { clearElectronStore: () => void; setEncryptionKey: (encryptionKey: string) => Promise; getEncryptionKey: () => Promise; - logToDisk: (msg: string) => void; convertToJPEG: ( fileData: Uint8Array, filename: string, ) => Promise; - openLogDirectory: () => void; registerUpdateEventListener: ( showUpdateDialog: (updateInfo: AppUpdateInfo) => void, ) => void; updateAndRestart: () => void; skipAppUpdate: (version: string) => void; - getAppVersion: () => Promise; runFFmpegCmd: ( cmd: string[], inputFile: File | ElectronFile, @@ -87,7 +122,6 @@ export interface ElectronAPIsType { maxSize: number, ) => Promise; registerForegroundEventListener: (onForeground: () => void) => void; - openDirectory: (dirPath: string) => Promise; moveFile: (oldPath: string, newPath: string) => Promise; deleteFolder: (path: string) => Promise; deleteFile: (path: string) => Promise;