Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only allow one window to run export per user #8123

Open
wants to merge 2 commits into
base: export-integration
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/common/desktop/ApplicationWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class ApplicationWindow {
private readonly localShortcut: LocalShortcutManager,
private readonly themeFacade: DesktopThemeFacade,
private readonly remoteBridge: RemoteBridge,
private desktopExportLocks: Set<string>,
hrb-hub marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

napkin pitch: what if we make the "window cleanup" mechanism more generic and inject some listener here instead? and coordination of these listeners can be outside of the poor ApplicationWindow.

noAutoLogin?: boolean | null,
preloadOverridePath?: string,
) {
Expand Down Expand Up @@ -312,7 +313,7 @@ export class ApplicationWindow {

this._browserWindow
.on("closed", async () => {
await this.closeDb()
await this.cleanup()
})
.on("focus", () => this.localShortcut.enableAll(this._browserWindow))
.on("blur", (_: FocusEvent) => this.localShortcut.disableAll(this._browserWindow))
Expand Down Expand Up @@ -390,7 +391,7 @@ export class ApplicationWindow {
}

async reload(queryParams: Record<string, string | boolean>) {
await this.closeDb()
await this.cleanup()
// try to do this asap as to not get the window destroyed on us
this.remoteBridge.unsubscribe(this._browserWindow.webContents.ipc)
this.userId = null
Expand All @@ -399,8 +400,9 @@ export class ApplicationWindow {
await this._browserWindow.loadURL(url)
}

private async closeDb() {
private async cleanup() {
if (this.userId) {
this.desktopExportLocks.delete(this.userId)
log.debug(TAG, `closing offline db for userId ${this.userId}`)
await this._sqlCipherFacade.closeDb()
}
Expand Down
6 changes: 4 additions & 2 deletions src/common/desktop/DesktopMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ async function createComponents(): Promise<Components> {
manageDownloadsForSession(session, dictUrl)
})

const wm = new WindowManager(conf, tray, notifier, electron, shortcutManager, appIcon)
const lockedUserIdsForExport = new Set<Id>()

const wm = new WindowManager(conf, tray, notifier, electron, shortcutManager, lockedUserIdsForExport, appIcon)
const themeFacade = new DesktopThemeFacade(conf, wm, electron.nativeTheme)
const schedulerImpl = new SchedulerImpl(dateProvider, global, global)
const alarmScheduler = new AlarmScheduler(dateProvider, schedulerImpl)
Expand Down Expand Up @@ -270,7 +272,7 @@ async function createComponents(): Promise<Components> {
const dispatcher = new DesktopGlobalDispatcher(
desktopCommonSystemFacade,
new DesktopDesktopSystemFacade(wm, window, sock),
new DesktopExportFacade(tfs, electron, conf, window, dragIcons, mailboxExportPersistence, fs, dateProvider),
new DesktopExportFacade(tfs, electron, conf, window, dragIcons, mailboxExportPersistence, fs, dateProvider, lockedUserIdsForExport),
new DesktopExternalCalendarFacade(),
new DesktopFileFacade(window, conf, dateProvider, customFetch, electron, tfs, fs),
new DesktopInterWindowEventFacade(window, wm),
Expand Down
11 changes: 9 additions & 2 deletions src/common/desktop/DesktopWindowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class WindowManager {
notifier: DesktopNotifier,
electron: ElectronExports,
localShortcut: LocalShortcutManager,
lockedExportUserIds: Set<Id>,
private readonly icon: NativeImage,
private readonly preloadOverride?: string,
) {
Expand All @@ -79,7 +80,7 @@ export class WindowManager {
this._notifier = notifier
this._electron = electron

this._newWindowFactory = (noAutoLogin) => this._newWindow(electron, localShortcut, noAutoLogin ?? null)
this._newWindowFactory = (noAutoLogin) => this._newWindow(electron, localShortcut, noAutoLogin ?? null, lockedExportUserIds)
// this is the old default for window placement & scale
// should never be used because the config now contains
// a new default value.
Expand Down Expand Up @@ -282,7 +283,12 @@ export class WindowManager {
this.saveBounds(result)
}

private async _newWindow(electron: ElectronExports, localShortcut: LocalShortcutManager, noAutoLogin: boolean | null): Promise<ApplicationWindow> {
private async _newWindow(
electron: ElectronExports,
localShortcut: LocalShortcutManager,
noAutoLogin: boolean | null,
lockedExportUserIds: Set<Id>,
): Promise<ApplicationWindow> {
const absoluteWebAssetsPath = this._electron.app.getAppPath()
// custom builds get the dicts from us as well
return new ApplicationWindow(
Expand All @@ -293,6 +299,7 @@ export class WindowManager {
localShortcut,
this.themeFacade,
this.remoteBridge,
lockedExportUserIds,
noAutoLogin,
this.preloadOverride,
)
Expand Down
36 changes: 28 additions & 8 deletions src/common/desktop/export/DesktopExportFacade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { MailboxExportPersistence, MailboxExportState } from "./MailboxExportPer
import { DateProvider } from "../../api/common/DateProvider.js"
import { formatSortableDate } from "@tutao/tutanota-utils"
import { FileOpenError } from "../../api/common/error/FileOpenError.js"
import { LockedError } from "../../api/common/error/RestError"

const EXPORT_DIR = "export"

Expand All @@ -33,6 +34,7 @@ export class DesktopExportFacade implements ExportFacade {
private readonly mailboxExportPersistence: MailboxExportPersistence,
private readonly fs: typeof FsModule,
private readonly dateProvider: DateProvider,
private readonly lockedUserIds: Set<Id>,
) {}

async checkFileExistsInExportDir(fileName: string): Promise<boolean> {
Expand Down Expand Up @@ -84,6 +86,11 @@ export class DesktopExportFacade implements ExportFacade {
}

async startMailboxExport(userId: string, mailboxId: string, mailBagId: string, mailId: string): Promise<void> {
if (this.lockedUserIds.has(userId)) {
throw new LockedError(`Export is locked for user: ${userId}`)
hrb-hub marked this conversation as resolved.
Show resolved Hide resolved
} else {
this.lockedUserIds.add(userId)
}
const previousExportState = await this.mailboxExportPersistence.getStateForUser(userId)
if (previousExportState != null && previousExportState.type !== "finished") {
throw new Error("Export is already running for this user")
Expand Down Expand Up @@ -134,20 +141,32 @@ export class DesktopExportFacade implements ExportFacade {
}

async getMailboxExportState(userId: string): Promise<MailboxExportState | null> {
return await this.mailboxExportPersistence.getStateForUser(userId)
const state = await this.mailboxExportPersistence.getStateForUser(userId)
if (state && state.type === "running") {
if (this.lockedUserIds.has(userId)) {
return {
type: "locked",
userId,
}
} else {
this.lockedUserIds.add(userId)
}
}
return state
}

async endMailboxExport(userId: string): Promise<void> {
const previousExportState = await this.mailboxExportPersistence.getStateForUser(userId)
if (previousExportState == null) {
if (previousExportState && previousExportState.type === "running") {
await this.mailboxExportPersistence.setStateForUser({
type: "finished",
userId,
exportDirectoryPath: previousExportState.exportDirectoryPath,
mailboxId: previousExportState.mailboxId,
})
} else {
throw new ProgrammingError("An Export was not previously running")
}
await this.mailboxExportPersistence.setStateForUser({
type: "finished",
userId,
exportDirectoryPath: previousExportState.exportDirectoryPath,
mailboxId: previousExportState.mailboxId,
})
}

async saveMailboxExport(bundle: MailBundle, userId: string, mailBagId: string, mailId: string): Promise<void> {
Expand Down Expand Up @@ -180,6 +199,7 @@ export class DesktopExportFacade implements ExportFacade {

async clearExportState(userId: string): Promise<void> {
await this.mailboxExportPersistence.clearStateForUser(userId)
this.lockedUserIds.delete(userId)
}

async openExportDirectory(userId: string): Promise<void> {
Expand Down
4 changes: 4 additions & 0 deletions src/common/desktop/export/MailboxExportPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export type MailboxExportState =
mailboxId: Id
exportDirectoryPath: string
}
| {
type: "locked"
userId: Id
}

export class MailboxExportPersistence {
constructor(private readonly conf: DesktopConfig) {}
Expand Down
1 change: 1 addition & 0 deletions src/common/misc/TranslationKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1817,3 +1817,4 @@ export type TranslationKeyType =
| "you_label"
| "emptyString_msg"
| "exportFinished_label"
| "exportRunningElsewhere_label"
2 changes: 1 addition & 1 deletion src/mail-app/mailLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ class MailLocator {
readonly mailExportController: () => Promise<MailExportController> = lazyMemoized(async () => {
const { htmlSanitizer } = await import("../common/misc/HtmlSanitizer")
const { MailExportController } = await import("./native/main/MailExportController.js")
return new MailExportController(this.mailExportFacade, htmlSanitizer, this.exportFacade, this.logins, this.mailboxModel)
return new MailExportController(this.mailExportFacade, htmlSanitizer, this.exportFacade, this.logins, this.mailboxModel, await this.scheduler())
})

/**
Expand Down
23 changes: 18 additions & 5 deletions src/mail-app/native/main/MailExportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import Stream from "mithril/stream"
import stream from "mithril/stream"
import { MailBag } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { GENERATED_MAX_ID, getElementId, isSameId } from "../../../common/api/common/utils/EntityUtils.js"
import { assertNotNull, delay, isNotNull, lastThrow, ofClass, promiseMap } from "@tutao/tutanota-utils"
import { assertNotNull, delay, isNotNull, lastThrow } from "@tutao/tutanota-utils"
import { HtmlSanitizer } from "../../../common/misc/HtmlSanitizer.js"
import { ExportFacade } from "../../../common/native/common/generatedipc/ExportFacade.js"
import { LoginController } from "../../../common/api/main/LoginController.js"
import { FileController } from "../../../common/file/FileController.js"
import { CancelledError } from "../../../common/api/common/error/CancelledError.js"
import { BulkMailLoader } from "../../workerUtils/index/BulkMailLoader.js"
import { FileOpenError } from "../../../common/api/common/error/FileOpenError.js"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
import { NotFoundError } from "../../../common/api/common/error/RestError.js"
import { MailExportFacade } from "../../../common/api/worker/facades/lazy/MailExportFacade.js"
import { Scheduler } from "../../../common/api/common/utils/Scheduler"
import { LockedError } from "../../../common/api/common/error/RestError"

export type MailExportState =
| { type: "idle" }
| { type: "exporting"; mailboxDetail: MailboxDetail; progress: number; exportedMails: number }
| { type: "locked" }
| { type: "error"; message: string }
| {
type: "finished"
Expand All @@ -43,6 +43,7 @@ export class MailExportController {
private readonly exportFacade: ExportFacade,
private readonly logins: LoginController,
private readonly mailboxModel: MailboxModel,
private readonly scheduler: Scheduler,
) {}

get state(): Stream<MailExportState> {
Expand All @@ -66,6 +67,9 @@ export class MailExportController {
if (e instanceof CancelledError) {
console.log("Export start cancelled")
return
} else if (e instanceof LockedError) {
this._state({ type: "locked" })
return
} else {
throw e
}
Expand All @@ -88,7 +92,12 @@ export class MailExportController {
await this.cancelExport()
return
}
this._state({ type: "exporting", mailboxDetail: mailboxDetail, progress: 0, exportedMails: exportState.exportedMails })
this._state({
type: "exporting",
mailboxDetail: mailboxDetail,
progress: 0,
exportedMails: exportState.exportedMails,
})
await this.resumeExport(mailboxDetail, exportState.mailBagId, exportState.mailId)
} else if (exportState.type === "finished") {
const mailboxDetail = await this.mailboxModel.getMailboxDetailByMailboxId(exportState.mailboxId)
Expand All @@ -98,6 +107,9 @@ export class MailExportController {
return
}
this._state({ type: "finished", mailboxDetail: mailboxDetail })
} else if (exportState.type === "locked") {
this._state({ type: "locked" })
this.scheduler.scheduleAfter(() => this.resumeIfNeeded(), 1000 * 60 * 5) // 5 min
}
}
}
Expand Down Expand Up @@ -187,6 +199,7 @@ export class MailExportController {
} catch (e) {
if (isOfflineError(e)) {
console.log(TAG, "Offline, will retry later")
// FIXME: should we use scheduler here too?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there isn't a strong reason to, maybe only to make it testable

await delay(1000 * 60) // 1 min
console.log(TAG, "Trying to continue with export")
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/mail-app/settings/MailExportSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ export class MailExportSettings implements Component<MailExportSettingsAttrs> {
}),
]),
]
case "locked":
return [
m(".flex-space-between.items-center.mt.mb-s.button-height", [
m("small.noselect", `${lang.get("exportRunningElsewhere_label")} ${lang.get("pleaseWait_msg")}`),
]),
]
}
}

Expand Down
1 change: 1 addition & 0 deletions src/mail-app/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,5 +1837,6 @@ export default {
"yourMessage_label": "Deine Nachricht",
"you_label": "Du",
"exportFinished_label": "Export finished",
"exportRunningElsewhere_label": "Export may be running in another window."
}
}
1 change: 1 addition & 0 deletions src/mail-app/translations/de_sie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,5 +1837,6 @@ export default {
"yourMessage_label": "Ihre Nachricht",
"you_label": "Sie",
"exportFinished_label": "Export finished",
"exportRunningElsewhere_label": "Export may be running in another window."
}
}
1 change: 1 addition & 0 deletions src/mail-app/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1833,5 +1833,6 @@ export default {
"yourMessage_label": "Your message",
"you_label": "You",
"exportFinished_label": "Export finished",
"exportRunningElsewhere_label": "Export may be running in another window."
}
}
Loading
Loading