Skip to content
15 changes: 15 additions & 0 deletions src/BasePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,19 @@ export default abstract class BasePlatform {
* Begin update polling, if applicable
*/
public startUpdater(): void {}

/**
* Checks if the current session is lock-free, i.e., no other instance is holding the session lock.
* Platforms that support session locking should override this method.
* @returns {boolean} True if the session is lock-free, false otherwise.
*/
public abstract checkSessionLockFree(): boolean;
/**
* Attempts to acquire a session lock for this instance.
* If another instance is detected, calls the provided callback.
* Platforms that support session locking should override this method.
* @param _onNewInstance Callback to invoke if a new instance is detected.
* @returns {Promise<boolean>} True if the lock was acquired, false otherwise.
*/
public abstract getSessionLock(_onNewInstance: () => Promise<void>): Promise<boolean>;
}
7 changes: 4 additions & 3 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ import { NotificationLevel } from "../../stores/notifications/NotificationLevel"
import { type UserTab } from "../views/dialogs/UserTab";
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
import { Filter } from "../views/dialogs/spotlight/Filter";
import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
import { SessionLockStolenView } from "./auth/SessionLockStolenView";
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
import { LoginSplashView } from "./auth/LoginSplashView";
Expand Down Expand Up @@ -314,7 +313,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private async initSession(): Promise<void> {
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
// make sure we are the only Element instance in town (on this browser/domain).
if (!(await getSessionLock(() => this.onSessionLockStolen()))) {
const platform = PlatformPeg.get();
if (platform && !(await platform.getSessionLock(() => this.onSessionLockStolen()))) {
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
return;
}
Expand Down Expand Up @@ -479,7 +479,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// mounted.
if (!this.sessionLoadStarted) {
this.sessionLoadStarted = true;
if (!checkSessionLockFree()) {
const platform = PlatformPeg.get();
if (platform && !platform.checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
Expand Down
8 changes: 8 additions & 0 deletions src/vector/platform/ElectronPlatform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,12 @@ export default class ElectronPlatform extends BasePlatform {
}
return url;
}

public checkSessionLockFree(): boolean {
return true;
}

public async getSessionLock(_onNewInstance: () => Promise<void>): Promise<boolean> {
return true;
}
}
9 changes: 9 additions & 0 deletions src/vector/platform/WebPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ToastStore from "../../stores/ToastStore.ts";
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
import SdkConfig from "../../SdkConfig.ts";
import type { ActionPayload } from "../../dispatcher/payloads.ts";
import * as SessionLock from "../../utils/SessionLock.ts";

const POKE_RATE_MS = 10 * 60 * 1000; // 10 min

Expand Down Expand Up @@ -268,4 +269,12 @@ export default class WebPlatform extends BasePlatform {
public reload(): void {
window.location.reload();
}

public checkSessionLockFree(): boolean {
return SessionLock.checkSessionLockFree();
}

public async getSessionLock(onNewInstance: () => Promise<void>): Promise<boolean> {
return SessionLock.getSessionLock(onNewInstance);
}
}
9 changes: 9 additions & 0 deletions test/test-utils/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type MethodLikeKeys, mocked, type MockedObject } from "jest-mock";

import BasePlatform from "../../src/BasePlatform";
import PlatformPeg from "../../src/PlatformPeg";
import * as SessionLock from "../../src/utils/SessionLock";

// doesn't implement abstract
// @ts-ignore
Expand All @@ -18,6 +19,14 @@ class MockPlatform extends BasePlatform {
super();
Object.assign(this, platformMocks);
}

public checkSessionLockFree(): boolean {
return SessionLock.checkSessionLockFree();
}

public async getSessionLock(onNewInstance: () => Promise<void>): Promise<boolean> {
return SessionLock.getSessionLock(onNewInstance);
}
}
/**
* Mock Platform Peg
Expand Down
6 changes: 6 additions & 0 deletions test/unit-tests/components/structures/MatrixChat-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1632,6 +1632,10 @@ describe("<MatrixChat />", () => {
});

describe("Multi-tab lockout", () => {
beforeEach(() => {
mockPlatformPeg();
});

afterEach(() => {
Lifecycle.setSessionLockNotStolen();
});
Expand Down Expand Up @@ -1677,6 +1681,8 @@ describe("<MatrixChat />", () => {
beforeEach(() => {
// make sure we start from a clean DOM for each of these tests
document.body.replaceChildren();
// use the MockPlatform
mockPlatformPeg();
});

function simulateSessionLockClaim() {
Expand Down
Loading