[web] [desktop] Refactoring IPC (#1398)
- IPC cleanup and refactoring - Log on unhandled errors and promise rejections
This commit is contained in:
commit
03176911ee
25 changed files with 396 additions and 354 deletions
|
@ -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"),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
98
desktop/src/services/app-update.ts
Normal file
98
desktop/src/services/app-update.ts
Normal 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);
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
|
@ -29,9 +29,3 @@ export const FILE_PATH_KEYS: {
|
|||
export interface SafeStorageStoreType {
|
||||
encryptionKey: string;
|
||||
}
|
||||
|
||||
export interface UserPreferencesType {
|
||||
hideDockIcon: boolean;
|
||||
skipAppVersion: string;
|
||||
muteUpdateNotificationVersion: string;
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue