[web] Systematize logout (#1729)

+ ML cleanup
This commit is contained in:
Manav Rathi 2024-05-15 13:58:17 +05:30 committed by GitHub
commit 69460418ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 313 additions and 339 deletions

View file

@ -17,7 +17,11 @@ import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
import {
attachFSWatchIPCHandlers,
attachIPCHandlers,
attachLogoutIPCHandler,
} from "./main/ipc";
import log, { initLogging } from "./main/log";
import { createApplicationMenu, createTrayContextMenu } from "./main/menu";
import { setupAutoUpdater } from "./main/services/app-update";
@ -377,8 +381,12 @@ const main = () => {
void (async () => {
// Create window and prepare for the renderer.
mainWindow = createMainWindow();
// Setup IPC and streams.
const watcher = createWatcher(mainWindow);
attachIPCHandlers();
attachFSWatchIPCHandlers(createWatcher(mainWindow));
attachFSWatchIPCHandlers(watcher);
attachLogoutIPCHandler(watcher);
registerStreamProtocol();
// Configure the renderer's environment.

View file

@ -41,16 +41,13 @@ import {
fsWriteFile,
} from "./services/fs";
import { convertToJPEG, generateImageThumbnail } from "./services/image";
import { logout } from "./services/logout";
import {
clipImageEmbedding,
clipTextEmbeddingIfAvailable,
} from "./services/ml-clip";
import { detectFaces, faceEmbedding } from "./services/ml-face";
import {
clearStores,
encryptionKey,
saveEncryptionKey,
} from "./services/store";
import { encryptionKey, saveEncryptionKey } from "./services/store";
import {
clearPendingUploads,
listZipItems,
@ -65,11 +62,9 @@ import {
watchFindFiles,
watchGet,
watchRemove,
watchReset,
watchUpdateIgnoredFiles,
watchUpdateSyncedFiles,
} from "./services/watch";
import { clearConvertToMP4Results } from "./stream";
/**
* Listen for IPC events sent/invoked by the renderer process, and route them to
@ -107,10 +102,6 @@ export const attachIPCHandlers = () => {
ipcMain.handle("selectDirectory", () => selectDirectory());
ipcMain.on("clearStores", () => clearStores());
ipcMain.on("clearConvertToMP4Results", () => clearConvertToMP4Results());
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
saveEncryptionKey(encryptionKey),
);
@ -265,6 +256,12 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
ipcMain.handle("watchFindFiles", (_, folderPath: string) =>
watchFindFiles(folderPath),
);
ipcMain.handle("watchReset", () => watchReset(watcher));
};
/**
* Sibling of {@link attachIPCHandlers} specifically for use with the logout
* event with needs access to the {@link FSWatcher} instance.
*/
export const attachLogoutIPCHandler = (watcher: FSWatcher) => {
ipcMain.handle("logout", () => logout(watcher));
};

View file

@ -0,0 +1,30 @@
import type { FSWatcher } from "chokidar";
import log from "../log";
import { clearConvertToMP4Results } from "../stream";
import { clearStores } from "./store";
import { watchReset } from "./watch";
/**
* Perform the native side logout sequence.
*
* This function is guaranteed not to throw any errors.
*
* See: [Note: Do not throw during logout].
*/
export const logout = (watcher: FSWatcher) => {
try {
watchReset(watcher);
} catch (e) {
log.error("Ignoring error during logout (FS watch)", e);
}
try {
clearConvertToMP4Results();
} catch (e) {
log.error("Ignoring error during logout (convert-to-mp4)", e);
}
try {
clearStores();
} catch (e) {
log.error("Ignoring error during logout (native stores)", e);
}
};

View file

@ -151,6 +151,15 @@ export const watchFindFiles = async (dirPath: string) => {
return paths;
};
/**
* Stop watching all existing folder watches and remove any callbacks.
*
* This function is meant to be called when the user logs out. It stops
* all existing folder watches and forgets about any "on*" callback
* functions that have been registered.
*
* The persisted state itself gets cleared via {@link clearStores}.
*/
export const watchReset = (watcher: FSWatcher) => {
watcher.unwatch(folderWatches().map((watch) => watch.folderPath));
};

View file

@ -63,10 +63,10 @@ const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory");
const selectDirectory = () => ipcRenderer.invoke("selectDirectory");
const clearStores = () => ipcRenderer.send("clearStores");
const clearConvertToMP4Results = () =>
ipcRenderer.send("clearConvertToMP4Results");
const logout = () => {
watchRemoveListeners();
ipcRenderer.send("logout");
};
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
@ -212,11 +212,10 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => {
const watchFindFiles = (folderPath: string) =>
ipcRenderer.invoke("watchFindFiles", folderPath);
const watchReset = async () => {
const watchRemoveListeners = () => {
ipcRenderer.removeAllListeners("watchAddFile");
ipcRenderer.removeAllListeners("watchRemoveFile");
ipcRenderer.removeAllListeners("watchRemoveDir");
await ipcRenderer.invoke("watchReset");
};
// - Upload
@ -308,8 +307,7 @@ contextBridge.exposeInMainWorld("electron", {
openDirectory,
openLogDirectory,
selectDirectory,
clearStores,
clearConvertToMP4Results,
logout,
encryptionKey,
saveEncryptionKey,
onMainWindowFocus,
@ -360,7 +358,6 @@ contextBridge.exposeInMainWorld("electron", {
onRemoveFile: watchOnRemoveFile,
onRemoveDir: watchOnRemoveDir,
findFiles: watchFindFiles,
reset: watchReset,
},
// - Upload

View file

@ -1,6 +1,8 @@
import { CustomHead } from "@/next/components/Head";
import { setupI18n } from "@/next/i18n";
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import { PAGES } from "@ente/accounts/constants/pages";
import { accountLogout } from "@ente/accounts/services/logout";
import { APPS, APP_TITLES } from "@ente/shared/apps/constants";
import { Overlay } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
@ -27,6 +29,7 @@ interface AppContextProps {
isMobile: boolean;
showNavBar: (show: boolean) => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
}
export const AppContext = createContext<AppContextProps>({} as AppContextProps);
@ -78,6 +81,10 @@ export default function App({ Component, pageProps }: AppProps) {
const theme = getTheme(themeColor, APPS.PHOTOS);
const logout = () => {
void accountLogout().then(() => router.push(PAGES.ROOT));
};
const title = isI18nReady
? t("TITLE", { context: APPS.ACCOUNTS })
: APP_TITLES.get(APPS.ACCOUNTS);
@ -101,6 +108,7 @@ export default function App({ Component, pageProps }: AppProps) {
showNavBar,
setDialogBoxAttributesV2:
setDialogBoxAttributesV2 as any,
logout,
}}
>
{!isI18nReady && (

View file

@ -1,4 +1,3 @@
import { logoutUser } from "@ente/accounts/services/user";
import { HorizontalFlex } from "@ente/shared/components/Container";
import { EnteLogo } from "@ente/shared/components/EnteLogo";
import NavbarBase from "@ente/shared/components/Navbar/base";
@ -11,7 +10,7 @@ import { AppContext } from "pages/_app";
import React from "react";
export default function AuthNavbar() {
const { isMobile } = React.useContext(AppContext);
const { isMobile, logout } = React.useContext(AppContext);
return (
<NavbarBase isMobile={isMobile}>
<HorizontalFlex flex={1} justifyContent={"center"}>
@ -25,7 +24,7 @@ export default function AuthNavbar() {
<OverflowMenuOption
color="critical"
startIcon={<LogoutOutlined />}
onClick={logoutUser}
onClick={logout}
>
{t("LOGOUT")}
</OverflowMenuOption>

View file

@ -4,6 +4,7 @@ import {
logStartupBanner,
logUnhandledErrorsAndRejections,
} from "@/next/log-web";
import { accountLogout } from "@ente/accounts/services/logout";
import {
APPS,
APP_TITLES,
@ -44,6 +45,7 @@ type AppContextType = {
setThemeColor: SetTheme;
somethingWentWrong: () => void;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
};
export const AppContext = createContext<AppContextType>(null);
@ -128,6 +130,10 @@ export default function App({ Component, pageProps }: AppProps) {
content: t("UNKNOWN_ERROR"),
});
const logout = () => {
void accountLogout().then(() => router.push(PAGES.ROOT));
};
const title = isI18nReady
? t("TITLE", { context: APPS.AUTH })
: APP_TITLES.get(APPS.AUTH);
@ -162,6 +168,7 @@ export default function App({ Component, pageProps }: AppProps) {
setThemeColor,
somethingWentWrong,
setDialogBoxAttributesV2,
logout,
}}
>
{(loading || !isI18nReady) && (

View file

@ -1,5 +1,4 @@
import log from "@/next/log";
import { logoutUser } from "@ente/accounts/services/user";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
import { DELETE_ACCOUNT_EMAIL } from "@ente/shared/constants/urls";
@ -43,7 +42,8 @@ const getReasonOptions = (): DropdownOption<DELETE_REASON>[] => {
};
const DeleteAccountModal = ({ open, onClose }: Iprops) => {
const { setDialogBoxAttributesV2, isMobile } = useContext(AppContext);
const { setDialogBoxAttributesV2, isMobile, logout } =
useContext(AppContext);
const { authenticateUser } = useContext(GalleryContext);
const [loading, setLoading] = useState(false);
const deleteAccountChallenge = useRef<string>();
@ -145,7 +145,7 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => {
);
const { reason, feedback } = reasonAndFeedbackRef.current;
await deleteAccount(decryptedChallenge, reason, feedback);
logoutUser();
logout();
} catch (e) {
log.error("solveChallengeAndDeleteAccount failed", e);
somethingWentWrong();

View file

@ -1,18 +1,17 @@
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import ChevronRight from "@mui/icons-material/ChevronRight";
import ScienceIcon from "@mui/icons-material/Science";
import { Box, DialogProps, Stack, Typography } from "@mui/material";
import { EnteDrawer } from "components/EnteDrawer";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
import Titlebar from "components/Titlebar";
import { MLSearchSettings } from "components/ml/MLSearchSettings";
import { t } from "i18next";
import { useContext, useEffect, useState } from "react";
import { VerticallyCenteredFlex } from "@ente/shared/components/Container";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import { CLIPIndexingStatus, clipService } from "services/clip-service";
import { formatNumber } from "utils/number/format";

View file

@ -1,13 +1,11 @@
import { t } from "i18next";
import { useContext, useState } from "react";
import { logoutUser } from "@ente/accounts/services/user";
import DeleteAccountModal from "components/DeleteAccountModal";
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useState } from "react";
export default function ExitSection() {
const { setDialogMessage } = useContext(AppContext);
const { setDialogMessage, logout } = useContext(AppContext);
const [deleteAccountModalView, setDeleteAccountModalView] = useState(false);
@ -19,7 +17,7 @@ export default function ExitSection() {
title: t("LOGOUT_MESSAGE"),
proceed: {
text: t("LOGOUT"),
action: logoutUser,
action: logout,
variant: "critical",
},
close: { text: t("CANCEL") },

View file

@ -26,7 +26,6 @@ import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { MessageContainer } from "@ente/shared/components/MessageContainer";
import AppNavbar from "@ente/shared/components/Navbar/app";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { Events, eventBus } from "@ente/shared/events";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
@ -52,7 +51,8 @@ import "photoswipe/dist/photoswipe.css";
import { createContext, useEffect, useRef, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import DownloadManager from "services/download";
import exportService, { resumeExportsIfNeeded } from "services/export";
import { resumeExportsIfNeeded } from "services/export";
import { photosLogout } from "services/logout";
import {
getMLSearchConfig,
updateMLSearchConfig,
@ -100,6 +100,7 @@ type AppContextType = {
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
isCFProxyDisabled: boolean;
setIsCFProxyDisabled: (disabled: boolean) => void;
logout: () => void;
};
export const AppContext = createContext<AppContextType>(null);
@ -188,14 +189,6 @@ export default function App({ Component, pageProps }: AppProps) {
}
};
loadMlSearchState();
try {
eventBus.on(Events.LOGOUT, () => {
setMlSearchEnabled(false);
mlWorkManager.setMlSearchEnabled(false);
});
} catch (e) {
log.error("Error while subscribing to logout event", e);
}
}, []);
useEffect(() => {
@ -213,13 +206,6 @@ export default function App({ Component, pageProps }: AppProps) {
await resumeExportsIfNeeded();
};
initExport();
try {
eventBus.on(Events.LOGOUT, () => {
exportService.disableContinuousExport();
});
} catch (e) {
log.error("Error while subscribing to logout event", e);
}
}, []);
const setUserOnline = () => setOffline(false);
@ -336,6 +322,11 @@ export default function App({ Component, pageProps }: AppProps) {
content: t("UNKNOWN_ERROR"),
});
const logout = () => {
setMlSearchEnabled(false);
void photosLogout().then(() => router.push(PAGES.ROOT));
};
const title = isI18nReady
? t("TITLE", { context: APPS.PHOTOS })
: APP_TITLES.get(APPS.PHOTOS);
@ -394,6 +385,7 @@ export default function App({ Component, pageProps }: AppProps) {
updateMapEnabled,
isCFProxyDisabled,
setIsCFProxyDisabled,
logout,
}}
>
{(loading || !isI18nReady) && (

View file

@ -3,6 +3,7 @@ import { APPS } from "@ente/shared/apps/constants";
import { CenteredFlex } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
import { CustomError } from "@ente/shared/error";
import { useFileInput } from "@ente/shared/hooks/useFileInput";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
@ -93,11 +94,7 @@ import { getLocalFiles, syncFiles } from "services/fileService";
import locationSearchService from "services/locationSearchService";
import { getLocalTrashedFiles, syncTrash } from "services/trashService";
import uploadManager from "services/upload/uploadManager";
import {
isTokenValid,
syncMapEnabled,
validateKey,
} from "services/userService";
import { isTokenValid, syncMapEnabled } from "services/userService";
import { Collection, CollectionSummaries } from "types/collection";
import { EnteFile } from "types/file";
import {
@ -249,8 +246,13 @@ export default function Gallery() {
const [tempHiddenFileIds, setTempHiddenFileIds] = useState<Set<number>>(
new Set<number>(),
);
const { startLoading, finishLoading, setDialogMessage, ...appContext } =
useContext(AppContext);
const {
startLoading,
finishLoading,
setDialogMessage,
logout,
...appContext
} = useContext(AppContext);
const [collectionSummaries, setCollectionSummaries] =
useState<CollectionSummaries>();
const [hiddenCollectionSummaries, setHiddenCollectionSummaries] =
@ -319,6 +321,19 @@ export default function Gallery() {
const [isClipSearchResult, setIsClipSearchResult] =
useState<boolean>(false);
// Ensure that the keys in local storage are not malformed by verifying that
// the recoveryKey can be decrypted with the masterKey.
// Note: This is not bullet-proof.
const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch (e) {
logout();
return false;
}
};
useEffect(() => {
appContext.showNavBar(true);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
@ -672,7 +687,7 @@ export default function Gallery() {
}, [collections, hiddenCollections]);
const showSessionExpiredMessage = () => {
setDialogMessage(getSessionExpiredMessage());
setDialogMessage(getSessionExpiredMessage(logout));
};
const syncWithRemote = async (force = false, silent = false) => {

View file

@ -1,5 +1,4 @@
import log from "@/next/log";
import { logoutUser } from "@ente/accounts/services/user";
import { APPS } from "@ente/shared/apps/constants";
import {
CenteredFlex,
@ -185,7 +184,7 @@ export default function PublicCollectionGallery() {
nonClosable: true,
proceed: {
text: t("LOGIN"),
action: logoutUser,
action: () => router.push(PAGES.ROOT),
variant: "accent",
},
});

View file

@ -80,21 +80,20 @@ class CLIPService {
this.liveEmbeddingExtractionQueue = new PQueue({
concurrency: 1,
});
eventBus.on(Events.LOGOUT, this.logoutHandler, this);
}
isPlatformSupported = () => {
return isElectron();
};
private logoutHandler = async () => {
async logout() {
if (this.embeddingExtractionInProgress) {
this.embeddingExtractionInProgress.abort();
}
if (this.onFileUploadedHandler) {
await this.removeOnFileUploadListener();
}
};
}
setupOnFileUploadListener = async () => {
try {

View file

@ -6,7 +6,6 @@ import { APPS } from "@ente/shared/apps/constants";
import ComlinkCryptoWorker from "@ente/shared/crypto";
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
import { CustomError } from "@ente/shared/error";
import { Events, eventBus } from "@ente/shared/events";
import { isPlaybackPossible } from "@ente/shared/media/video-playback";
import { Remote } from "comlink";
import isElectron from "is-electron";
@ -107,7 +106,6 @@ class DownloadManagerImpl {
// }
this.cryptoWorker = await ComlinkCryptoWorker.getInstance();
this.ready = true;
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
}
private ensureInitialized() {
@ -117,21 +115,15 @@ class DownloadManagerImpl {
);
}
private async logoutHandler() {
try {
log.info("downloadManger logoutHandler started");
this.ready = false;
this.cryptoWorker = null;
this.downloadClient = null;
this.fileObjectURLPromises.clear();
this.fileConversionPromises.clear();
this.thumbnailObjectURLPromises.clear();
this.fileDownloadProgress.clear();
this.progressUpdater = () => {};
log.info("downloadManager logoutHandler completed");
} catch (e) {
log.error("downloadManager logoutHandler failed", e);
}
async logout() {
this.ready = false;
this.cryptoWorker = null;
this.downloadClient = null;
this.fileObjectURLPromises.clear();
this.fileConversionPromises.clear();
this.thumbnailObjectURLPromises.clear();
this.fileDownloadProgress.clear();
this.progressUpdater = () => {};
}
updateToken(token: string, passwordToken?: string) {

View file

@ -0,0 +1,50 @@
import log from "@/next/log";
import { accountLogout } from "@ente/accounts/services/logout";
import { clipService } from "services/clip-service";
import DownloadManager from "./download";
import exportService from "./export";
import mlWorkManager from "./machineLearning/mlWorkManager";
/**
* Logout sequence for the photos app.
*
* This function is guaranteed not to throw any errors.
*
* See: [Note: Do not throw during logout].
*/
export const photosLogout = async () => {
await accountLogout();
try {
await DownloadManager.logout();
} catch (e) {
log.error("Ignoring error during logout (download)", e);
}
try {
await clipService.logout();
} catch (e) {
log.error("Ignoring error during logout (CLIP)", e);
}
const electron = globalThis.electron;
if (electron) {
try {
await mlWorkManager.logout();
} catch (e) {
log.error("Ignoring error during logout (ML)", e);
}
try {
exportService.disableContinuousExport();
} catch (e) {
log.error("Ignoring error during logout (export)", e);
}
try {
await electron?.logout();
} catch (e) {
log.error("Ignoring error during logout (electron)", e);
}
}
};

View file

@ -11,9 +11,9 @@ import {
} from "services/ml/types";
import { imageBitmapToBlob, warpAffineFloat32List } from "utils/image";
import ReaderService, {
fetchImageBitmap,
getFaceId,
getLocalFile,
getOriginalImageBitmap,
} from "./readerService";
class FaceService {
@ -296,7 +296,7 @@ class FaceService {
}
const file = await getLocalFile(personFace.fileId);
const imageBitmap = await getOriginalImageBitmap(file);
const imageBitmap = await fetchImageBitmap(file);
return await this.saveFaceCrop(imageBitmap, personFace, syncContext);
}
}

View file

@ -14,7 +14,6 @@ import { getLocalFiles } from "services/fileService";
import mlIDbStorage, {
ML_SEARCH_CONFIG_NAME,
ML_SYNC_CONFIG_NAME,
ML_SYNC_JOB_CONFIG_NAME,
} from "services/ml/db";
import {
BlurDetectionMethod,
@ -48,19 +47,11 @@ import dbscanClusteringService from "./dbscanClusteringService";
import FaceService from "./faceService";
import hdbscanClusteringService from "./hdbscanClusteringService";
import laplacianBlurDetectionService from "./laplacianBlurDetectionService";
import type { JobConfig } from "./mlWorkManager";
import mobileFaceNetEmbeddingService from "./mobileFaceNetEmbeddingService";
import PeopleService from "./peopleService";
import ReaderService from "./readerService";
import yoloFaceDetectionService from "./yoloFaceDetectionService";
export const DEFAULT_ML_SYNC_JOB_CONFIG: JobConfig = {
intervalSec: 5,
// TODO: finalize this after seeing effects on and from machine sleep
maxItervalSec: 960,
backoffMultiplier: 2,
};
export const DEFAULT_ML_SYNC_CONFIG: MLSyncConfig = {
batchSize: 200,
imageSource: "Original",
@ -108,13 +99,6 @@ export const DEFAULT_ML_SEARCH_CONFIG: MLSearchConfig = {
export const MAX_ML_SYNC_ERROR_COUNT = 1;
export async function getMLSyncJobConfig() {
return mlIDbStorage.getConfig(
ML_SYNC_JOB_CONFIG_NAME,
DEFAULT_ML_SYNC_JOB_CONFIG,
);
}
export async function getMLSyncConfig() {
return mlIDbStorage.getConfig(ML_SYNC_CONFIG_NAME, DEFAULT_ML_SYNC_CONFIG);
}
@ -131,14 +115,6 @@ export async function getMLSearchConfig() {
return DEFAULT_ML_SEARCH_CONFIG;
}
export async function updateMLSyncJobConfig(newConfig: JobConfig) {
return mlIDbStorage.putConfig(ML_SYNC_JOB_CONFIG_NAME, newConfig);
}
export async function updateMLSyncConfig(newConfig: MLSyncConfig) {
return mlIDbStorage.putConfig(ML_SYNC_CONFIG_NAME, newConfig);
}
export async function updateMLSearchConfig(newConfig: MLSearchConfig) {
return mlIDbStorage.putConfig(ML_SEARCH_CONFIG_NAME, newConfig);
}

View file

@ -5,12 +5,11 @@ import { eventBus, Events } from "@ente/shared/events";
import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers";
import debounce from "debounce";
import PQueue from "p-queue";
import { getMLSyncJobConfig } from "services/machineLearning/machineLearningService";
import mlIDbStorage from "services/ml/db";
import { createFaceComlinkWorker } from "services/ml/face";
import type { DedicatedMLWorker } from "services/ml/face.worker";
import { MLSyncResult } from "services/ml/types";
import { EnteFile } from "types/file";
import { getDedicatedMLWorker } from "utils/comlink/ComlinkMLWorker";
import { DedicatedMLWorker } from "worker/ml.worker";
import { logQueueStats } from "./machineLearningService";
const LIVE_SYNC_IDLE_DEBOUNCE_SEC = 30;
@ -21,32 +20,30 @@ export type JobState = "Scheduled" | "Running" | "NotScheduled";
export interface JobConfig {
intervalSec: number;
maxItervalSec: number;
backoffMultiplier: number;
}
export interface JobResult {
export interface MLSyncJobResult {
shouldBackoff: boolean;
mlSyncResult: MLSyncResult;
}
export class SimpleJob<R extends JobResult> {
private config: JobConfig;
private runCallback: () => Promise<R>;
export class MLSyncJob {
private runCallback: () => Promise<MLSyncJobResult>;
private state: JobState;
private stopped: boolean;
private intervalSec: number;
private nextTimeoutId: ReturnType<typeof setTimeout>;
constructor(config: JobConfig, runCallback: () => Promise<R>) {
this.config = config;
constructor(runCallback: () => Promise<MLSyncJobResult>) {
this.runCallback = runCallback;
this.state = "NotScheduled";
this.stopped = true;
this.intervalSec = this.config.intervalSec;
this.resetInterval();
}
public resetInterval() {
this.intervalSec = this.config.intervalSec;
this.intervalSec = 5;
}
public start() {
@ -79,10 +76,7 @@ export class SimpleJob<R extends JobResult> {
try {
const jobResult = await this.runCallback();
if (jobResult && jobResult.shouldBackoff) {
this.intervalSec = Math.min(
this.config.maxItervalSec,
this.intervalSec * this.config.backoffMultiplier,
);
this.intervalSec = Math.min(960, this.intervalSec * 2);
} else {
this.resetInterval();
}
@ -109,12 +103,6 @@ export class SimpleJob<R extends JobResult> {
}
}
export interface MLSyncJobResult extends JobResult {
mlSyncResult: MLSyncResult;
}
export class MLSyncJob extends SimpleJob<MLSyncJobResult> {}
class MLWorkManager {
private mlSyncJob: MLSyncJob;
private syncJobWorker: ComlinkWorker<typeof DedicatedMLWorker>;
@ -135,7 +123,6 @@ class MLWorkManager {
});
this.mlSearchEnabled = false;
eventBus.on(Events.LOGOUT, this.logoutHandler.bind(this), this);
this.debouncedLiveSyncIdle = debounce(
() => this.onLiveSyncIdle(),
LIVE_SYNC_IDLE_DEBOUNCE_SEC * 1000,
@ -187,26 +174,12 @@ class MLWorkManager {
}
}
// Handlers
private async appStartHandler() {
log.info("appStartHandler");
try {
this.startSyncJob();
} catch (e) {
log.error("Failed in ML appStart Handler", e);
}
}
private async logoutHandler() {
log.info("logoutHandler");
try {
this.stopSyncJob();
this.mlSyncJob = undefined;
await this.terminateLiveSyncWorker();
await mlIDbStorage.clearMLDB();
} catch (e) {
log.error("Failed in ML logout Handler", e);
}
async logout() {
this.setMlSearchEnabled(false);
this.stopSyncJob();
this.mlSyncJob = undefined;
await this.terminateLiveSyncWorker();
await mlIDbStorage.clearMLDB();
}
private async fileUploadedHandler(arg: {
@ -238,7 +211,7 @@ class MLWorkManager {
// Live Sync
private async getLiveSyncWorker() {
if (!this.liveSyncWorker) {
this.liveSyncWorker = getDedicatedMLWorker("ml-sync-live");
this.liveSyncWorker = createFaceComlinkWorker("ml-sync-live");
}
return await this.liveSyncWorker.remote;
@ -286,7 +259,7 @@ class MLWorkManager {
// Sync Job
private async getSyncJobWorker() {
if (!this.syncJobWorker) {
this.syncJobWorker = getDedicatedMLWorker("ml-sync-job");
this.syncJobWorker = createFaceComlinkWorker("ml-sync-job");
}
return await this.syncJobWorker.remote;
@ -344,11 +317,8 @@ class MLWorkManager {
log.info("User not logged in, not starting ml sync job");
return;
}
const mlSyncJobConfig = await getMLSyncJobConfig();
if (!this.mlSyncJob) {
this.mlSyncJob = new MLSyncJob(mlSyncJobConfig, () =>
this.runMLSyncJob(),
);
this.mlSyncJob = new MLSyncJob(() => this.runMLSyncJob());
}
this.mlSyncJob.start();
} catch (e) {

View file

@ -2,7 +2,7 @@ import log from "@/next/log";
import mlIDbStorage from "services/ml/db";
import { Face, MLSyncContext, Person } from "services/ml/types";
import FaceService, { isDifferentOrOld } from "./faceService";
import { getLocalFile, getOriginalImageBitmap } from "./readerService";
import { fetchImageBitmap, getLocalFile } from "./readerService";
class PeopleService {
async syncPeopleIndex(syncContext: MLSyncContext) {
@ -58,7 +58,7 @@ class PeopleService {
if (personFace && !personFace.crop?.cacheKey) {
const file = await getLocalFile(personFace.fileId);
const imageBitmap = await getOriginalImageBitmap(file);
const imageBitmap = await fetchImageBitmap(file);
await FaceService.saveFaceCrop(
imageBitmap,
personFace,

View file

@ -1,7 +1,6 @@
import { FILE_TYPE } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import log from "@/next/log";
import PQueue from "p-queue";
import DownloadManager from "services/download";
import { getLocalFiles } from "services/fileService";
import { Dimensions } from "services/ml/geom";
@ -41,7 +40,7 @@ class ReaderService {
fileContext.enteFile.metadata.fileType,
)
) {
fileContext.imageBitmap = await getOriginalImageBitmap(
fileContext.imageBitmap = await fetchImageBitmap(
fileContext.enteFile,
);
} else {
@ -106,22 +105,12 @@ export function getFaceId(detectedFace: DetectedFace, imageDims: Dimensions) {
return faceID;
}
async function getImageBlobBitmap(blob: Blob): Promise<ImageBitmap> {
return await createImageBitmap(blob);
}
export const fetchImageBitmap = async (file: EnteFile) =>
fetchRenderableBlob(file).then(createImageBitmap);
async function getOriginalFile(file: EnteFile, queue?: PQueue) {
let fileStream;
if (queue) {
fileStream = await queue.add(() => DownloadManager.getFile(file));
} else {
fileStream = await DownloadManager.getFile(file);
}
return new Response(fileStream).blob();
}
async function getOriginalConvertedFile(file: EnteFile, queue?: PQueue) {
const fileBlob = await getOriginalFile(file, queue);
async function fetchRenderableBlob(file: EnteFile) {
const fileStream = await DownloadManager.getFile(file);
const fileBlob = await new Response(fileStream).blob();
if (file.metadata.fileType === FILE_TYPE.IMAGE) {
return await getRenderableImage(file.metadata.title, fileBlob);
} else {
@ -133,17 +122,11 @@ async function getOriginalConvertedFile(file: EnteFile, queue?: PQueue) {
}
}
export async function getOriginalImageBitmap(file: EnteFile, queue?: PQueue) {
const fileBlob = await getOriginalConvertedFile(file, queue);
log.info("[MLService] Got file: ", file.id.toString());
return getImageBlobBitmap(fileBlob);
}
export async function getThumbnailImageBitmap(file: EnteFile) {
const thumb = await DownloadManager.getThumbnail(file);
log.info("[MLService] Got thumbnail: ", file.id.toString());
return getImageBlobBitmap(new Blob([thumb]));
return createImageBitmap(new Blob([thumb]));
}
export async function getLocalFileImageBitmap(
@ -152,5 +135,5 @@ export async function getLocalFileImageBitmap(
) {
let fileBlob = localFile as Blob;
fileBlob = await getRenderableImage(enteFile.metadata.title, fileBlob);
return getImageBlobBitmap(fileBlob);
return createImageBitmap(fileBlob);
}

View file

@ -12,7 +12,6 @@ import isElectron from "is-electron";
import {
DEFAULT_ML_SEARCH_CONFIG,
DEFAULT_ML_SYNC_CONFIG,
DEFAULT_ML_SYNC_JOB_CONFIG,
MAX_ML_SYNC_ERROR_COUNT,
} from "services/machineLearning/machineLearningService";
import { Face, MLLibraryData, MlFileData, Person } from "services/ml/types";
@ -27,7 +26,6 @@ export interface IndexStatus {
interface Config {}
export const ML_SYNC_JOB_CONFIG_NAME = "ml-sync-job";
export const ML_SYNC_CONFIG_NAME = "ml-sync";
export const ML_SEARCH_CONFIG_NAME = "ml-search";
@ -136,12 +134,14 @@ class MLIDbStorage {
// TODO: update configs if version is updated in defaults
db.createObjectStore("configs");
/*
await tx
.objectStore("configs")
.add(
DEFAULT_ML_SYNC_JOB_CONFIG,
ML_SYNC_JOB_CONFIG_NAME,
"ml-sync-job",
);
*/
await tx
.objectStore("configs")
.add(DEFAULT_ML_SYNC_CONFIG, ML_SYNC_CONFIG_NAME);
@ -163,6 +163,10 @@ class MLIDbStorage {
.objectStore("configs")
.delete(ML_SEARCH_CONFIG_NAME);
await tx
.objectStore("configs")
.delete("ml-sync-job");
await tx
.objectStore("configs")
.add(

View file

@ -0,0 +1,8 @@
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import type { DedicatedMLWorker } from "services/ml/face.worker";
const createFaceWebWorker = () =>
new Worker(new URL("face.worker.ts", import.meta.url));
export const createFaceComlinkWorker = (name: string) =>
new ComlinkWorker<typeof DedicatedMLWorker>(name, createFaceWebWorker());

View file

@ -1,11 +1,8 @@
import log from "@/next/log";
import { putAttributes } from "@ente/accounts/api/user";
import { logoutUser } from "@ente/accounts/services/user";
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
import { ApiError } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import { getEndpoint, getFamilyPortalURL } from "@ente/shared/network/api";
import localForage from "@ente/shared/storage/localForage";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import {
getToken,
@ -104,10 +101,6 @@ export const getRoadmapRedirectURL = async () => {
}
};
export const clearFiles = async () => {
await localForage.clear();
};
export const isTokenValid = async (token: string) => {
try {
const resp = await HTTPService.get(
@ -233,19 +226,6 @@ export const deleteAccount = async (
}
};
// Ensure that the keys in local storage are not malformed by verifying that the
// recoveryKey can be decrypted with the masterKey.
// Note: This is not bullet-proof.
export const validateKey = async () => {
try {
await getRecoveryKey();
return true;
} catch (e) {
await logoutUser();
return false;
}
};
export const getFaceSearchEnabledStatus = async () => {
try {
const token = getToken();

View file

@ -1,13 +0,0 @@
import { haveWindow } from "@/next/env";
import { ComlinkWorker } from "@/next/worker/comlink-worker";
import { type DedicatedMLWorker } from "worker/ml.worker";
export const getDedicatedMLWorker = (name: string) => {
if (haveWindow()) {
const cryptoComlinkWorker = new ComlinkWorker<typeof DedicatedMLWorker>(
name ?? "ente-ml-worker",
new Worker(new URL("worker/ml.worker.ts", import.meta.url)),
);
return cryptoComlinkWorker;
}
};

View file

@ -1,6 +1,5 @@
import { ensureElectron } from "@/next/electron";
import { AppUpdate } from "@/next/types/ipc";
import { logoutUser } from "@ente/accounts/services/user";
import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
import InfoOutlined from "@mui/icons-material/InfoRounded";
@ -121,14 +120,16 @@ export const getSubscriptionPurchaseSuccessMessage = (
),
});
export const getSessionExpiredMessage = (): DialogBoxAttributes => ({
export const getSessionExpiredMessage = (
action: () => void,
): DialogBoxAttributes => ({
title: t("SESSION_EXPIRED"),
content: t("SESSION_EXPIRED_MESSAGE"),
nonClosable: true,
proceed: {
text: t("LOGIN"),
action: logoutUser,
action,
variant: "accent",
},
});

View file

@ -43,7 +43,7 @@ export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
},
);
export const _logout = async () => {
export const logout = async () => {
try {
const token = getToken();
await HTTPService.post(`${ENDPOINT}/users/logout`, null, undefined, {

View file

@ -50,10 +50,11 @@ import {
generateSRPSetupAttributes,
loginViaSRP,
} from "../services/srp";
import { logoutUser } from "../services/user";
import { SRPAttributes } from "../types/srp";
export default function Credentials({ appContext, appName }: PageProps) {
const { logout } = appContext;
const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>();
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
const [user, setUser] = useState<User>();
@ -275,7 +276,7 @@ export default function Credentials({ appContext, appName }: PageProps) {
<LinkButton onClick={redirectToRecoverPage}>
{t("FORGOT_PASSWORD")}
</LinkButton>
<LinkButton onClick={logoutUser}>
<LinkButton onClick={logout}>
{t("CHANGE_EMAIL")}
</LinkButton>
</FormPaperFooter>

View file

@ -1,7 +1,6 @@
import log from "@/next/log";
import { putAttributes } from "@ente/accounts/api/user";
import { configureSRP } from "@ente/accounts/services/srp";
import { logoutUser } from "@ente/accounts/services/user";
import { generateKeyAndSRPAttributes } from "@ente/accounts/utils/srp";
import {
generateAndSaveIntermediateKeyAttributes,
@ -31,6 +30,8 @@ import { KeyAttributes, User } from "@ente/shared/user/types";
import { useRouter } from "next/router";
export default function Generate({ appContext, appName }: PageProps) {
const { logout } = appContext;
const [token, setToken] = useState<string>();
const [user, setUser] = useState<User>();
const [recoverModalView, setRecoveryModalView] = useState(false);
@ -113,7 +114,7 @@ export default function Generate({ appContext, appName }: PageProps) {
buttonText={t("SET_PASSPHRASE")}
/>
<FormPaperFooter>
<LinkButton onClick={logoutUser}>
<LinkButton onClick={logout}>
{t("GO_BACK")}
</LinkButton>
</FormPaperFooter>

View file

@ -2,7 +2,6 @@ import log from "@/next/log";
import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
import { PAGES } from "@ente/accounts/constants/pages";
import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import { logoutUser } from "@ente/accounts/services/user";
import { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
@ -33,6 +32,8 @@ export default function Recover({
appContext,
twoFactorType = TwoFactorType.TOTP,
}: PageProps) {
const { logout } = appContext;
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
useState<B64EncryptionResult>(null);
const [sessionID, setSessionID] = useState(null);
@ -77,7 +78,7 @@ export default function Recover({
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
) {
logoutUser();
logout();
} else {
log.error("two factor recovery page setup failed", e);
setDoesHaveEncryptedRecoveryKey(false);

View file

@ -3,7 +3,7 @@ import VerifyTwoFactor, {
VerifyTwoFactorCallback,
} from "@ente/accounts/components/two-factor/VerifyForm";
import { PAGES } from "@ente/accounts/constants/pages";
import { logoutUser } from "@ente/accounts/services/user";
import type { PageProps } from "@ente/shared/apps/types";
import { VerticallyCentered } from "@ente/shared/components/Container";
import FormPaper from "@ente/shared/components/Form/FormPaper";
@ -19,7 +19,11 @@ import { t } from "i18next";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
export const TwoFactorVerify: React.FC<PageProps> = () => {
export const TwoFactorVerify: React.FC<PageProps> = ({
appContext,
}: PageProps) => {
const { logout } = appContext;
const [sessionID, setSessionID] = useState("");
const router = useRouter();
@ -60,7 +64,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
e instanceof ApiError &&
e.httpStatusCode === HttpStatusCode.NotFound
) {
logoutUser();
logout();
} else {
throw e;
}
@ -79,7 +83,7 @@ export const TwoFactorVerify: React.FC<PageProps> = () => {
>
{t("LOST_DEVICE")}
</LinkButton>
<LinkButton onClick={logoutUser}>
<LinkButton onClick={logout}>
{t("CHANGE_EMAIL")}
</LinkButton>
</FormPaperFooter>

View file

@ -16,7 +16,7 @@ import SingleInputForm, {
import { ApiError } from "@ente/shared/error";
import { getAccountsURL } from "@ente/shared/network/api";
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
import { clearFiles } from "@ente/shared/storage/localForage/helpers";
import localForage from "@ente/shared/storage/localForage";
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
import {
getLocalReferralSource,
@ -30,10 +30,11 @@ import { useRouter } from "next/router";
import { putAttributes, sendOtt, verifyOtt } from "../api/user";
import { PAGES } from "../constants/pages";
import { configureSRP } from "../services/srp";
import { logoutUser } from "../services/user";
import { SRPSetupAttributes } from "../types/srp";
export default function VerifyPage({ appContext, appName }: PageProps) {
const { logout } = appContext;
const [email, setEmail] = useState("");
const [resend, setResend] = useState(0);
@ -121,7 +122,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
await configureSRP(srpSetupAttributes);
}
}
clearFiles();
localForage.clear();
setIsFirstLogin(true);
const redirectURL = InMemoryStore.get(MS_KEYS.REDIRECT_URL);
InMemoryStore.delete(MS_KEYS.REDIRECT_URL);
@ -191,7 +192,7 @@ export default function VerifyPage({ appContext, appName }: PageProps) {
)}
{resend === 1 && <span>{t("SENDING")}</span>}
{resend === 2 && <span>{t("SENT")}</span>}
<LinkButton onClick={logoutUser}>
<LinkButton onClick={logout}>
{t("CHANGE_EMAIL")}
</LinkButton>
</FormPaperFooter>

View file

@ -0,0 +1,50 @@
import { clearCaches } from "@/next/blob-cache";
import log from "@/next/log";
import InMemoryStore from "@ente/shared/storage/InMemoryStore";
import localForage from "@ente/shared/storage/localForage";
import { clearData } from "@ente/shared/storage/localStorage";
import { clearKeys } from "@ente/shared/storage/sessionStorage";
import { logout as remoteLogout } from "../api/user";
/**
* Logout sequence common to all apps that rely on the accounts package.
*
* [Note: Do not throw during logout]
*
* This function is guaranteed to not thrown any errors, and will try to
* independently complete all the steps in the sequence that can be completed.
* This allows the user to logout and start again even if somehow their account
* gets in an unexpected state.
*/
export const accountLogout = async () => {
try {
await remoteLogout();
} catch (e) {
log.error("Ignoring error during logout (remote)", e);
}
try {
InMemoryStore.clear();
} catch (e) {
log.error("Ignoring error during logout (in-memory store)", e);
}
try {
clearKeys();
} catch (e) {
log.error("Ignoring error during logout (session store)", e);
}
try {
clearData();
} catch (e) {
log.error("Ignoring error during logout (local storage)", e);
}
try {
await localForage.clear();
} catch (e) {
log.error("Ignoring error during logout (local forage)", e);
}
try {
await clearCaches();
} catch (e) {
log.error("Ignoring error during logout (cache)", e);
}
};

View file

@ -1,67 +0,0 @@
import { clearCaches } from "@/next/blob-cache";
import log from "@/next/log";
import { Events, eventBus } from "@ente/shared/events";
import InMemoryStore from "@ente/shared/storage/InMemoryStore";
import { clearFiles } from "@ente/shared/storage/localForage/helpers";
import { clearData } from "@ente/shared/storage/localStorage";
import { clearKeys } from "@ente/shared/storage/sessionStorage";
import router from "next/router";
import { _logout } from "../api/user";
import { PAGES } from "../constants/pages";
export const logoutUser = async () => {
try {
await _logout();
} catch (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 clearCaches();
} catch (e) {
log.error("Ignoring error when clearing caches", e);
}
try {
await clearFiles();
} catch (e) {
log.error("Ignoring error when clearing files", e);
}
const electron = globalThis.electron;
if (electron) {
try {
await electron.watch.reset();
} catch (e) {
log.error("Ignoring error when resetting native folder watches", e);
}
try {
await electron.clearConvertToMP4Results();
} catch (e) {
log.error("Ignoring error when clearing convert-to-mp4 results", e);
}
try {
await electron.clearStores();
} catch (e) {
log.error("Ignoring error when clearing native 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

@ -64,19 +64,9 @@ export interface Electron {
selectDirectory: () => Promise<string | undefined>;
/**
* Clear any stored data.
*
* This is a coarse single shot cleanup, meant for use in clearing any
* persisted Electron side state during logout.
* Perform any logout related cleanup of native side state.
*/
clearStores: () => void;
/**
* Clear an state corresponding to in-flight convert-to-mp4 requests.
*
* This is meant for use during logout.
*/
clearConvertToMP4Results: () => void;
logout: () => Promise<void>;
/**
* Return the previously saved encryption key from persistent safe storage.
@ -487,17 +477,6 @@ export interface Electron {
* The returned paths are guaranteed to use POSIX separators ('/').
*/
findFiles: (folderPath: string) => Promise<string[]>;
/**
* Stop watching all existing folder watches and remove any callbacks.
*
* This function is meant to be called when the user logs out. It stops
* all existing folder watches and forgets about any "on*" callback
* functions that have been registered.
*
* The persisted state itself gets cleared via {@link clearStores}.
*/
reset: () => Promise<void>;
};
// - Upload

View file

@ -7,6 +7,7 @@ export interface PageProps {
showNavBar: (show: boolean) => void;
isMobile: boolean;
setDialogBoxAttributesV2: SetDialogBoxAttributesV2;
logout: () => void;
};
appName: APPS;
twoFactorType?: TwoFactorType;

View file

@ -1,5 +0,0 @@
import localForage from ".";
export const clearFiles = async () => {
await localForage.clear();
};