[web] [desktop] Refactoring IPC (#1398)

- IPC cleanup and refactoring
- Log on unhandled errors and promise rejections
This commit is contained in:
Manav Rathi 2024-04-10 14:08:07 +05:30 committed by GitHub
commit 03176911ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 396 additions and 354 deletions

View file

@ -27,7 +27,7 @@ import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
import log, { initLogging } from "./main/log";
import { createApplicationMenu } from "./main/menu";
import { isDev } from "./main/util";
import { setupAutoUpdater } from "./services/appUpdater";
import { setupAutoUpdater } from "./services/app-update";
import { initWatcher } from "./services/chokidar";
let appIsQuitting = false;
@ -142,9 +142,10 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
};
const attachEventHandlers = (mainWindow: BrowserWindow) => {
// Let ipcRenderer know when mainWindow is in the foreground.
// Let ipcRenderer know when mainWindow is in the foreground so that it can
// in turn inform the renderer process.
mainWindow.on("focus", () =>
mainWindow.webContents.send("app-in-foreground"),
mainWindow.webContents.send("mainWindowFocus"),
);
};

View file

@ -12,14 +12,11 @@ import type { FSWatcher } from "chokidar";
import { ipcMain } from "electron/main";
import {
appVersion,
muteUpdateNotification,
skipAppUpdate,
updateAndRestart,
} from "../services/appUpdater";
import {
clipImageEmbedding,
clipTextEmbedding,
} from "../services/clip-service";
updateOnNextRestart,
} from "../services/app-update";
import { clipImageEmbedding, clipTextEmbedding } from "../services/clip";
import { runFFmpegCmd } from "../services/ffmpeg";
import { getDirFiles } from "../services/fs";
import {
@ -27,9 +24,9 @@ import {
generateImageThumbnail,
} from "../services/imageProcessor";
import {
clearElectronStore,
getEncryptionKey,
setEncryptionKey,
clearStores,
encryptionKey,
saveEncryptionKey,
} from "../services/store";
import {
getElectronFilesFromGoogleZip,
@ -98,26 +95,24 @@ export const attachIPCHandlers = () => {
// See [Note: Catching exception during .send/.on]
ipcMain.on("logToDisk", (_, message) => logToDisk(message));
ipcMain.on("clear-electron-store", () => {
clearElectronStore();
});
ipcMain.on("clearStores", () => clearStores());
ipcMain.handle("setEncryptionKey", (_, encryptionKey) =>
setEncryptionKey(encryptionKey),
ipcMain.handle("saveEncryptionKey", (_, encryptionKey) =>
saveEncryptionKey(encryptionKey),
);
ipcMain.handle("getEncryptionKey", () => getEncryptionKey());
ipcMain.handle("encryptionKey", () => encryptionKey());
// - App update
ipcMain.on("update-and-restart", () => updateAndRestart());
ipcMain.on("updateAndRestart", () => updateAndRestart());
ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version));
ipcMain.on("mute-update-notification", (_, version) =>
muteUpdateNotification(version),
ipcMain.on("updateOnNextRestart", (_, version) =>
updateOnNextRestart(version),
);
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
// - Conversion
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>

View file

@ -19,6 +19,16 @@ export const initLogging = () => {
log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}";
log.transports.console.level = false;
// Log unhandled errors and promise rejections.
log.errorHandler.startCatching({
onError: ({ error, errorName }) => {
logError(errorName, error);
// Prevent the default electron-log actions (e.g. showing a dialog)
// from getting triggered.
return false;
},
});
};
/**

View file

@ -6,7 +6,7 @@ import {
shell,
} from "electron";
import { setIsAppQuitting } from "../main";
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
import { forceCheckForAppUpdates } from "../services/app-update";
import autoLauncher from "../services/autoLauncher";
import {
getHideDockIconPreference,
@ -26,8 +26,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
process.platform == "darwin" ? options : [];
const handleCheckForUpdates = () =>
forceCheckForUpdateAndNotify(mainWindow);
const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow);
const handleViewChangelog = () =>
shell.openExternal(

View file

@ -52,58 +52,55 @@ import type {
const appVersion = (): Promise<string> => ipcRenderer.invoke("appVersion");
const logToDisk = (message: string): void =>
ipcRenderer.send("logToDisk", message);
const openDirectory = (dirPath: string): Promise<void> =>
ipcRenderer.invoke("openDirectory", dirPath);
const openLogDirectory = (): Promise<void> =>
ipcRenderer.invoke("openLogDirectory");
const logToDisk = (message: string): void =>
ipcRenderer.send("logToDisk", message);
const clearStores = () => ipcRenderer.send("clearStores");
const encryptionKey = (): Promise<string | undefined> =>
ipcRenderer.invoke("encryptionKey");
const saveEncryptionKey = (encryptionKey: string): Promise<void> =>
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
const onMainWindowFocus = (cb?: () => void) => {
ipcRenderer.removeAllListeners("mainWindowFocus");
if (cb) ipcRenderer.on("mainWindowFocus", cb);
};
// - App update
const onAppUpdateAvailable = (
cb?: ((updateInfo: AppUpdateInfo) => void) | undefined,
) => {
ipcRenderer.removeAllListeners("appUpdateAvailable");
if (cb) {
ipcRenderer.on("appUpdateAvailable", (_, updateInfo: AppUpdateInfo) =>
cb(updateInfo),
);
}
};
const updateAndRestart = () => ipcRenderer.send("updateAndRestart");
const updateOnNextRestart = (version: string) =>
ipcRenderer.send("updateOnNextRestart", version);
const skipAppUpdate = (version: string) => {
ipcRenderer.send("skipAppUpdate", version);
};
const fsExists = (path: string): Promise<boolean> =>
ipcRenderer.invoke("fsExists", path);
// - AUDIT below this
const registerForegroundEventListener = (onForeground: () => void) => {
ipcRenderer.removeAllListeners("app-in-foreground");
ipcRenderer.on("app-in-foreground", onForeground);
};
const clearElectronStore = () => {
ipcRenderer.send("clear-electron-store");
};
const setEncryptionKey = (encryptionKey: string): Promise<void> =>
ipcRenderer.invoke("setEncryptionKey", encryptionKey);
const getEncryptionKey = (): Promise<string> =>
ipcRenderer.invoke("getEncryptionKey");
// - App update
const registerUpdateEventListener = (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => {
ipcRenderer.removeAllListeners("show-update-dialog");
ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
showUpdateDialog(updateInfo);
});
};
const updateAndRestart = () => {
ipcRenderer.send("update-and-restart");
};
const skipAppUpdate = (version: string) => {
ipcRenderer.send("skip-app-update", version);
};
const muteUpdateNotification = (version: string) => {
ipcRenderer.send("mute-update-notification", version);
};
// - Conversion
const convertToJPEG = (
@ -303,21 +300,19 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
contextBridge.exposeInMainWorld("electron", {
// - General
appVersion,
openDirectory,
registerForegroundEventListener,
clearElectronStore,
getEncryptionKey,
setEncryptionKey,
// - Logging
openLogDirectory,
logToDisk,
openDirectory,
openLogDirectory,
clearStores,
encryptionKey,
saveEncryptionKey,
onMainWindowFocus,
// - App update
onAppUpdateAvailable,
updateAndRestart,
updateOnNextRestart,
skipAppUpdate,
muteUpdateNotification,
registerUpdateEventListener,
// - Conversion
convertToJPEG,

View file

@ -0,0 +1,98 @@
import { compareVersions } from "compare-versions";
import { app, BrowserWindow } from "electron";
import { default as electronLog } from "electron-log";
import { autoUpdater } from "electron-updater";
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
import log from "../main/log";
import { userPreferencesStore } from "../stores/user-preferences";
import { AppUpdateInfo } from "../types/ipc";
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
autoUpdater.logger = electronLog;
autoUpdater.autoDownload = false;
const oneDay = 1 * 24 * 60 * 60 * 1000;
setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay);
checkForUpdatesAndNotify(mainWindow);
};
/**
* Check for app update check ignoring any previously saved skips / mutes.
*/
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
userPreferencesStore.delete("skipAppVersion");
userPreferencesStore.delete("muteUpdateNotificationVersion");
checkForUpdatesAndNotify(mainWindow);
};
const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
try {
const { updateInfo } = await autoUpdater.checkForUpdates();
const { version } = updateInfo;
log.debug(() => `Checking for updates found version ${version}`);
if (compareVersions(version, app.getVersion()) <= 0) {
log.debug(() => "Skipping update, already at latest version");
return;
}
if (version === userPreferencesStore.get("skipAppVersion")) {
log.info(`User chose to skip version ${version}`);
return;
}
const mutedVersion = userPreferencesStore.get(
"muteUpdateNotificationVersion",
);
if (version === mutedVersion) {
log.info(
`User has muted update notifications for version ${version}`,
);
return;
}
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
log.debug(() => "Attempting auto update");
autoUpdater.downloadUpdate();
let timeout: NodeJS.Timeout;
const fiveMinutes = 5 * 60 * 1000;
autoUpdater.on("update-downloaded", () => {
timeout = setTimeout(
() => showUpdateDialog({ autoUpdatable: true, version }),
fiveMinutes,
);
});
autoUpdater.on("error", (error) => {
clearTimeout(timeout);
log.error("Auto update failed", error);
showUpdateDialog({ autoUpdatable: false, version });
});
setIsUpdateAvailable(true);
} catch (e) {
log.error("checkForUpdateAndNotify failed", e);
}
};
/**
* Return the version of the desktop app
*
* The return value is of the form `v1.2.3`.
*/
export const appVersion = () => `v${app.getVersion()}`;
export const updateAndRestart = () => {
log.info("Restarting the app to apply update");
setIsAppQuitting(true);
autoUpdater.quitAndInstall();
};
export const updateOnNextRestart = (version: string) =>
userPreferencesStore.set("muteUpdateNotificationVersion", version);
export const skipAppUpdate = (version: string) =>
userPreferencesStore.set("skipAppVersion", version);

View file

@ -1,120 +0,0 @@
import { compareVersions } from "compare-versions";
import { app, BrowserWindow } from "electron";
import { default as electronLog } from "electron-log";
import { autoUpdater } from "electron-updater";
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
import log from "../main/log";
import { AppUpdateInfo } from "../types/ipc";
import {
clearMuteUpdateNotificationVersion,
clearSkipAppVersion,
getMuteUpdateNotificationVersion,
getSkipAppVersion,
setMuteUpdateNotificationVersion,
setSkipAppVersion,
} from "./userPreference";
const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000;
const ONE_DAY_IN_MICROSECOND = 1 * 24 * 60 * 60 * 1000;
export function setupAutoUpdater(mainWindow: BrowserWindow) {
autoUpdater.logger = electronLog;
autoUpdater.autoDownload = false;
checkForUpdateAndNotify(mainWindow);
setInterval(
() => checkForUpdateAndNotify(mainWindow),
ONE_DAY_IN_MICROSECOND,
);
}
export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) {
try {
clearSkipAppVersion();
clearMuteUpdateNotificationVersion();
checkForUpdateAndNotify(mainWindow);
} catch (e) {
log.error("forceCheckForUpdateAndNotify failed", e);
}
}
async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
try {
log.debug(() => "checkForUpdateAndNotify");
const { updateInfo } = await autoUpdater.checkForUpdates();
log.debug(() => `Update version ${updateInfo.version}`);
if (compareVersions(updateInfo.version, app.getVersion()) <= 0) {
log.debug(() => "Skipping update, already at latest version");
return;
}
const skipAppVersion = getSkipAppVersion();
if (skipAppVersion && updateInfo.version === skipAppVersion) {
log.info(`User chose to skip version ${updateInfo.version}`);
return;
}
let timeout: NodeJS.Timeout;
log.debug(() => "Attempting auto update");
autoUpdater.downloadUpdate();
const muteUpdateNotificationVersion =
getMuteUpdateNotificationVersion();
if (
muteUpdateNotificationVersion &&
updateInfo.version === muteUpdateNotificationVersion
) {
log.info(
`User has muted update notifications for version ${updateInfo.version}`,
);
return;
}
autoUpdater.on("update-downloaded", () => {
timeout = setTimeout(
() =>
showUpdateDialog(mainWindow, {
autoUpdatable: true,
version: updateInfo.version,
}),
FIVE_MIN_IN_MICROSECOND,
);
});
autoUpdater.on("error", (error) => {
clearTimeout(timeout);
log.error("Auto update failed", error);
showUpdateDialog(mainWindow, {
autoUpdatable: false,
version: updateInfo.version,
});
});
setIsUpdateAvailable(true);
} catch (e) {
log.error("checkForUpdateAndNotify failed", e);
}
}
export function updateAndRestart() {
log.info("user quit the app");
setIsAppQuitting(true);
autoUpdater.quitAndInstall();
}
/**
* 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);
}
export function muteUpdateNotification(version: string) {
setMuteUpdateNotificationVersion(version);
}
function showUpdateDialog(
mainWindow: BrowserWindow,
updateInfo: AppUpdateInfo,
) {
mainWindow.webContents.send("show-update-dialog", updateInfo);
}

View file

@ -4,23 +4,22 @@ import { safeStorageStore } from "../stores/safeStorage.store";
import { uploadStatusStore } from "../stores/upload.store";
import { watchStore } from "../stores/watch.store";
export const clearElectronStore = () => {
export const clearStores = () => {
uploadStatusStore.clear();
keysStore.clear();
safeStorageStore.clear();
watchStore.clear();
};
export async function setEncryptionKey(encryptionKey: string) {
export const saveEncryptionKey = async (encryptionKey: string) => {
const encryptedKey: Buffer = await safeStorage.encryptString(encryptionKey);
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
safeStorageStore.set("encryptionKey", b64EncryptedKey);
}
};
export async function getEncryptionKey(): Promise<string> {
export const encryptionKey = async (): Promise<string | undefined> => {
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
if (b64EncryptedKey) {
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
return await safeStorage.decryptString(keyBuffer);
}
}
if (!b64EncryptedKey) return undefined;
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
return await safeStorage.decryptString(keyBuffer);
};

View file

@ -1,4 +1,4 @@
import { userPreferencesStore } from "../stores/userPreferences.store";
import { userPreferencesStore } from "../stores/user-preferences";
export function getHideDockIconPreference() {
return userPreferencesStore.get("hideDockIcon");
@ -7,27 +7,3 @@ export function getHideDockIconPreference() {
export function setHideDockIconPreference(shouldHideDockIcon: boolean) {
userPreferencesStore.set("hideDockIcon", shouldHideDockIcon);
}
export function getSkipAppVersion() {
return userPreferencesStore.get("skipAppVersion");
}
export function setSkipAppVersion(version: string) {
userPreferencesStore.set("skipAppVersion", version);
}
export function getMuteUpdateNotificationVersion() {
return userPreferencesStore.get("muteUpdateNotificationVersion");
}
export function setMuteUpdateNotificationVersion(version: string) {
userPreferencesStore.set("muteUpdateNotificationVersion", version);
}
export function clearSkipAppVersion() {
userPreferencesStore.delete("skipAppVersion");
}
export function clearMuteUpdateNotificationVersion() {
userPreferencesStore.delete("muteUpdateNotificationVersion");
}

View file

@ -1,7 +1,12 @@
import Store, { Schema } from "electron-store";
import type { UserPreferencesType } from "../types/main";
const userPreferencesSchema: Schema<UserPreferencesType> = {
interface UserPreferencesSchema {
hideDockIcon: boolean;
skipAppVersion?: string;
muteUpdateNotificationVersion?: string;
}
const userPreferencesSchema: Schema<UserPreferencesSchema> = {
hideDockIcon: {
type: "boolean",
},

View file

@ -29,9 +29,3 @@ export const FILE_PATH_KEYS: {
export interface SafeStorageStoreType {
encryptionKey: string;
}
export interface UserPreferencesType {
hideDockIcon: boolean;
skipAppVersion: string;
muteUpdateNotificationVersion: string;
}

View file

@ -1,5 +1,6 @@
import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n";
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
@ -54,6 +55,8 @@ export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
setupI18n().finally(() => setIsI18nReady(true));
logUnhandledErrorsAndRejections(true);
return () => logUnhandledErrorsAndRejections(false);
}, []);
const setupPackageName = () => {

View file

@ -1,6 +1,9 @@
import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n";
import { logStartupBanner } from "@/next/log-web";
import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} from "@/next/log-web";
import {
APPS,
APP_TITLES,
@ -68,9 +71,11 @@ export default function App({ Component, pageProps }: AppProps) {
setupI18n().finally(() => setIsI18nReady(true));
const userId = (getData(LS_KEYS.USER) as User)?.id;
logStartupBanner(APPS.AUTH, userId);
logUnhandledErrorsAndRejections(true);
HTTPService.setHeaders({
"X-Client-Package": CLIENT_PACKAGE_NAMES.get(APPS.AUTH),
});
return () => logUnhandledErrorsAndRejections(false);
}, []);
const setUserOnline = () => setOffline(false);

View file

@ -1,12 +1,20 @@
import { CustomHead } from "@/next/components/Head";
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
import { CssBaseline, ThemeProvider } from "@mui/material";
import type { AppProps } from "next/app";
import { useEffect } from "react";
import "styles/global.css";
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
logUnhandledErrorsAndRejections(true);
return () => logUnhandledErrorsAndRejections(false);
}, []);
return (
<>
<CustomHead title={APP_TITLES.get(APPS.PHOTOS)} />

View file

@ -1,7 +1,10 @@
import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n";
import log from "@/next/log";
import { logStartupBanner } from "@/next/log-web";
import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} from "@/next/log-web";
import { AppUpdateInfo } from "@/next/types/ipc";
import {
APPS,
@ -147,35 +150,35 @@ export default function App({ Component, pageProps }: AppProps) {
setupI18n().finally(() => setIsI18nReady(true));
const userId = (getData(LS_KEYS.USER) as User)?.id;
logStartupBanner(APPS.PHOTOS, userId);
logUnhandledErrorsAndRejections(true);
HTTPService.setHeaders({
"X-Client-Package": CLIENT_PACKAGE_NAMES.get(APPS.PHOTOS),
});
return () => logUnhandledErrorsAndRejections(false);
}, []);
useEffect(() => {
const electron = globalThis.electron;
if (electron) {
const showUpdateDialog = (updateInfo: AppUpdateInfo) => {
if (updateInfo.autoUpdatable) {
setDialogMessage(
getUpdateReadyToInstallMessage(updateInfo),
);
} else {
setNotificationAttributes({
endIcon: <ArrowForward />,
variant: "secondary",
message: t("UPDATE_AVAILABLE"),
onClick: () =>
setDialogMessage(
getUpdateAvailableForDownloadMessage(
updateInfo,
),
),
});
}
};
electron.registerUpdateEventListener(showUpdateDialog);
}
if (!electron) return;
const showUpdateDialog = (updateInfo: AppUpdateInfo) => {
if (updateInfo.autoUpdatable) {
setDialogMessage(getUpdateReadyToInstallMessage(updateInfo));
} else {
setNotificationAttributes({
endIcon: <ArrowForward />,
variant: "secondary",
message: t("UPDATE_AVAILABLE"),
onClick: () =>
setDialogMessage(
getUpdateAvailableForDownloadMessage(updateInfo),
),
});
}
};
electron.onAppUpdateAvailable(showUpdateDialog);
return () => electron.onAppUpdateAvailable(undefined);
}, []);
useEffect(() => {

View file

@ -363,16 +363,14 @@ export default function Gallery() {
}, SYNC_INTERVAL_IN_MICROSECONDS);
if (electron) {
void clipService.setupOnFileUploadListener();
electron.registerForegroundEventListener(() => {
syncWithRemote(false, true);
});
electron.onMainWindowFocus(() => syncWithRemote(false, true));
}
};
main();
return () => {
clearInterval(syncInterval.current);
if (electron) {
electron.registerForegroundEventListener(() => {});
electron.onMainWindowFocus(undefined);
clipService.removeOnFileUploadListener();
}
};

View file

@ -133,9 +133,9 @@ export default function LandingPage() {
const electron = globalThis.electron;
if (!key && electron) {
try {
key = await electron.getEncryptionKey();
key = await electron.encryptionKey();
} catch (e) {
log.error("getEncryptionKey failed", e);
log.error("Failed to get encryption key from electron", e);
}
if (key) {
await saveKeyInSessionStore(

View file

@ -1,3 +1,4 @@
import { ensureElectron } from "@/next/electron";
import { AppUpdateInfo } from "@/next/types/ipc";
import { logoutUser } from "@ente/accounts/services/user";
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
@ -52,35 +53,34 @@ export const getTrashFileMessage = (deleteFileHelper): DialogBoxAttributes => ({
close: { text: t("CANCEL") },
});
export const getUpdateReadyToInstallMessage = (
updateInfo: AppUpdateInfo,
): DialogBoxAttributes => ({
export const getUpdateReadyToInstallMessage = ({
version,
}: AppUpdateInfo): DialogBoxAttributes => ({
icon: <AutoAwesomeOutlinedIcon />,
title: t("UPDATE_AVAILABLE"),
content: t("UPDATE_INSTALLABLE_MESSAGE"),
proceed: {
action: () => globalThis.electron?.updateAndRestart(),
action: () => ensureElectron().updateAndRestart(),
text: t("INSTALL_NOW"),
variant: "accent",
},
close: {
text: t("INSTALL_ON_NEXT_LAUNCH"),
variant: "secondary",
action: () =>
globalThis.electron?.muteUpdateNotification(updateInfo.version),
action: () => ensureElectron().updateOnNextRestart(version),
},
});
export const getUpdateAvailableForDownloadMessage = (
updateInfo: AppUpdateInfo,
): DialogBoxAttributes => ({
export const getUpdateAvailableForDownloadMessage = ({
version,
}: AppUpdateInfo): DialogBoxAttributes => ({
icon: <AutoAwesomeOutlinedIcon />,
title: t("UPDATE_AVAILABLE"),
content: t("UPDATE_AVAILABLE_MESSAGE"),
close: {
text: t("IGNORE_THIS_VERSION"),
variant: "secondary",
action: () => globalThis.electron?.skipAppUpdate(updateInfo.version),
action: () => ensureElectron().skipAppUpdate(version),
},
proceed: {
action: downloadApp,

View file

@ -1,4 +1,3 @@
import log from "@/next/log";
import {
RecoveryKey,
TwoFactorRecoveryResponse,
@ -62,7 +61,6 @@ export const _logout = async () => {
) {
return;
}
log.error("/users/logout failed", e);
throw e;
}
};

View file

@ -70,9 +70,9 @@ export default function Credentials({ appContext, appName }: PageProps) {
const electron = globalThis.electron;
if (!key && electron) {
try {
key = await electron.getEncryptionKey();
key = await electron.encryptionKey();
} catch (e) {
log.error("getEncryptionKey failed", e);
log.error("Failed to get encryption key from electron", e);
}
if (key) {
await saveKeyInSessionStore(

View file

@ -11,49 +11,44 @@ import { PAGES } from "../constants/pages";
export const logoutUser = async () => {
try {
try {
await _logout();
} catch (e) {
// ignore
}
try {
InMemoryStore.clear();
} catch (e) {
// ignore
log.error("clear InMemoryStore failed", e);
}
try {
clearKeys();
} catch (e) {
log.error("clearKeys failed", e);
}
try {
clearData();
} catch (e) {
log.error("clearData failed", e);
}
try {
await deleteAllCache();
} catch (e) {
log.error("deleteAllCache failed", e);
}
try {
await clearFiles();
} catch (e) {
log.error("clearFiles failed", e);
}
try {
globalThis.electron?.clearElectronStore();
} catch (e) {
log.error("clearElectronStore failed", e);
}
try {
eventBus.emit(Events.LOGOUT);
} catch (e) {
log.error("Error in logout handlers", e);
}
router.push(PAGES.ROOT);
await _logout();
} catch (e) {
log.error("logoutUser failed", e);
log.error("Ignoring error during POST /users/logout", e);
}
try {
InMemoryStore.clear();
} catch (e) {
log.error("Ignoring error when clearing in-memory store", e);
}
try {
clearKeys();
} catch (e) {
log.error("Ignoring error when clearing keys", e);
}
try {
clearData();
} catch (e) {
log.error("Ignoring error when clearing data", e);
}
try {
await deleteAllCache();
} catch (e) {
log.error("Ignoring error when clearing caches", e);
}
try {
await clearFiles();
} catch (e) {
log.error("Ignoring error when clearing files", e);
}
try {
globalThis.electron?.clearStores();
} catch (e) {
log.error("Ignoring error when clearing electron stores", e);
}
try {
eventBus.emit(Events.LOGOUT);
} catch (e) {
log.error("Ignoring error in event-bus logout handlers", e);
}
router.push(PAGES.ROOT);
};

View file

@ -18,6 +18,33 @@ export const logStartupBanner = (appId: string, userId?: number) => {
log.info(`Starting ente-${appIdL}-web ${buildId}uid ${userId ?? 0}`);
};
/**
* Attach handlers to log any unhandled exceptions and promise rejections.
*
* @param attach If true, attach handlers, and if false, remove them. This
* allows us to use this in a React hook that cleans up after itself.
*/
export const logUnhandledErrorsAndRejections = (attach: boolean) => {
const handleError = (event: ErrorEvent) => {
log.error("Unhandled error", event.error);
};
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
log.error("Unhandled promise rejection", event.reason);
};
if (attach) {
window.addEventListener("error", handleError);
window.addEventListener("unhandledrejection", handleUnhandledRejection);
} else {
window.removeEventListener("error", handleError);
window.removeEventListener(
"unhandledrejection",
handleUnhandledRejection,
);
}
};
interface LogEntry {
timestamp: number;
logLine: string;

View file

@ -37,9 +37,22 @@ export enum PICKED_UPLOAD_TYPE {
export interface Electron {
// - General
/** Return the version of the desktop app. */
/**
* Return the version of the desktop app.
*
* The return value is of the form `v1.2.3`.
*/
appVersion: () => Promise<string>;
/**
* 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-forget and does not return a promise.
*/
logToDisk: (message: string) => void;
/**
* Open the given {@link dirPath} in the system's folder viewer.
*
@ -55,13 +68,75 @@ export interface Electron {
openLogDirectory: () => Promise<void>;
/**
* Log the given {@link message} to the on-disk log file maintained by the
* desktop app.
* Clear any stored data.
*
* Note: Unlike the other functions exposed over the Electron bridge,
* logToDisk is fire-and-forget and does not return a promise.
* This is a coarse single shot cleanup, meant for use in clearing any
* Electron side state during logout.
*/
logToDisk: (message: string) => void;
clearStores: () => void;
/**
* Return the previously saved encryption key from persistent safe storage.
*
* If no such key is found, return `undefined`.
*
* @see {@link saveEncryptionKey}.
*/
encryptionKey: () => Promise<string | undefined>;
/**
* Save the given {@link encryptionKey} into persistent safe storage.
*/
saveEncryptionKey: (encryptionKey: string) => Promise<void>;
/**
* Set or clear the callback {@link cb} to invoke whenever the app comes
* into the foreground. More precisely, the callback gets invoked when the
* main window gets focus.
*
* Note: Setting a callback clears any previous callbacks.
*
* @param cb The function to call when the main window gets focus. Pass
* `undefined` to clear the callback.
*/
onMainWindowFocus: (cb?: () => void) => void;
// - App update
/**
* Set or clear the callback {@link cb} to invoke whenever a new
* (actionable) app update is available. This allows the Node.js layer to
* ask the renderer to show an "Update available" dialog to the user.
*
* Note: Setting a callback clears any previous callbacks.
*/
onAppUpdateAvailable: (
cb?: ((updateInfo: AppUpdateInfo) => void) | undefined,
) => void;
/**
* Restart the app to apply the latest available update.
*
* This is expected to be called in response to {@link onAppUpdateAvailable}
* if the user so wishes.
*/
updateAndRestart: () => void;
/**
* Mute update notifications for the given {@link version}. This allows us
* to implement the "Install on next launch" functionality in response to
* the {@link onAppUpdateAvailable} event.
*/
updateOnNextRestart: (version: string) => void;
/**
* Skip the app update with the given {@link version}.
*
* This is expected to be called in response to {@link onAppUpdateAvailable}
* if the user so wishes. It will remember this {@link version} as having
* been marked as skipped so that we don't prompt the user again.
*/
skipAppUpdate: (version: string) => void;
/**
* A subset of filesystem access APIs.
@ -98,28 +173,6 @@ export interface Electron {
* the dataflow.
*/
// - General
registerForegroundEventListener: (onForeground: () => void) => void;
clearElectronStore: () => void;
setEncryptionKey: (encryptionKey: string) => Promise<void>;
getEncryptionKey: () => Promise<string>;
// - App update
updateAndRestart: () => void;
skipAppUpdate: (version: string) => void;
muteUpdateNotification: (version: string) => void;
registerUpdateEventListener: (
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
) => void;
// - Conversion
convertToJPEG: (

View file

@ -103,7 +103,7 @@ export const saveKeyInSessionStore = async (
setKey(keyType, sessionKeyAttributes);
const electron = globalThis.electron;
if (electron && !fromDesktop && keyType === SESSION_KEYS.ENCRYPTION_KEY) {
electron.setEncryptionKey(key);
electron.saveEncryptionKey(key);
}
};