Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 89428ab

Browse files
committed
Implement session lock dialogs
1 parent 44aa589 commit 89428ab

File tree

12 files changed

+581
-14
lines changed

12 files changed

+581
-14
lines changed

res/css/_components.pcss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,10 @@
9090
@import "./structures/_UserMenu.pcss";
9191
@import "./structures/_ViewSource.pcss";
9292
@import "./structures/auth/_CompleteSecurity.pcss";
93+
@import "./structures/auth/_ConfirmSessionLockTheftView.pcss";
9394
@import "./structures/auth/_Login.pcss";
9495
@import "./structures/auth/_Registration.pcss";
96+
@import "./structures/auth/_SessionLockStolenView.pcss";
9597
@import "./structures/auth/_SetupEncryptionBody.pcss";
9698
@import "./views/audio_messages/_AudioPlayer.pcss";
9799
@import "./views/audio_messages/_PlayPauseButton.pcss";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2019-2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_ConfirmSessionLockTheftView {
18+
width: 100%;
19+
height: 100%;
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
}
24+
25+
.mx_ConfirmSessionLockTheftView_body {
26+
display: flex;
27+
flex-direction: column;
28+
max-width: 400px;
29+
align-items: center;
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_SessionLockStolenView {
18+
h1 {
19+
font-weight: var(--cpd-font-weight-semibold);
20+
font-size: $font-32px;
21+
text-align: center;
22+
}
23+
24+
h2 {
25+
margin: 0;
26+
font-weight: 500;
27+
font-size: $font-24px;
28+
text-align: center;
29+
}
30+
}

src/Lifecycle.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,41 @@ dis.register((payload) => {
8282
}
8383
});
8484

85+
/**
86+
* This is set to true by {@link #onSessionLockStolen}.
87+
*
88+
* It is used in various of the async functions to prevent races where we initialise a client after the lock is stolen.
89+
*/
90+
let sessionLockStolen = false;
91+
92+
// this is exposed solely for unit tests.
93+
// ts-prune-ignore-next
94+
export function setSessionLockNotStolen(): void {
95+
sessionLockStolen = false;
96+
}
97+
98+
/**
99+
* Handle the session lock being stolen. Stops any active Matrix Client, and aborts any ongoing client initialisation.
100+
*/
101+
export async function onSessionLockStolen(): Promise<void> {
102+
sessionLockStolen = true;
103+
stopMatrixClient();
104+
}
105+
106+
/**
107+
* Check if we still hold the session lock.
108+
*
109+
* If not, raises a {@link SessionLockStolenError}.
110+
*/
111+
function checkSessionLock(): void {
112+
if (sessionLockStolen) {
113+
throw new SessionLockStolenError("session lock has been released");
114+
}
115+
}
116+
117+
/** Error type raised by various functions in the Lifecycle workflow if session lock is stolen during execution */
118+
class SessionLockStolenError extends Error {}
119+
85120
interface ILoadSessionOpts {
86121
enableGuest?: boolean;
87122
guestHsUrl?: string;
@@ -153,6 +188,9 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
153188
if (success) {
154189
return true;
155190
}
191+
if (sessionLockStolen) {
192+
return false;
193+
}
156194

157195
if (enableGuest && guestHsUrl) {
158196
return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
@@ -166,6 +204,12 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
166204
// need to show the general failure dialog. Instead, just go back to welcome.
167205
return false;
168206
}
207+
208+
// likewise, if the session lock has been stolen while we've been trying to start
209+
if (sessionLockStolen) {
210+
return false;
211+
}
212+
169213
return handleLoadSessionFailure(e);
170214
}
171215
}
@@ -720,6 +764,7 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise<M
720764
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
721765
*/
722766
async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnabled: boolean): Promise<MatrixClient> {
767+
checkSessionLock();
723768
credentials.guest = Boolean(credentials.guest);
724769

725770
const softLogout = isSoftLogout();
@@ -750,6 +795,8 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable
750795
await abortLogin();
751796
}
752797

798+
// check the session lock just before creating the new client
799+
checkSessionLock();
753800
MatrixClientPeg.replaceUsingCreds(credentials);
754801
const client = MatrixClientPeg.safeGet();
755802

@@ -782,6 +829,7 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable
782829
} else {
783830
logger.warn("No local storage available: can't persist session!");
784831
}
832+
checkSessionLock();
785833

786834
dis.fire(Action.OnLoggedIn);
787835
await startMatrixClient(client, /*startSyncing=*/ !softLogout);
@@ -977,6 +1025,8 @@ async function startMatrixClient(client: MatrixClient, startSyncing = true): Pro
9771025
await MatrixClientPeg.assign();
9781026
}
9791027

1028+
checkSessionLock();
1029+
9801030
// Run the migrations after the MatrixClientPeg has been assigned
9811031
SettingsStore.runMigrations();
9821032

src/PosthogTrackers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type InteractionName = InteractionEvent["name"];
2727

2828
const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
2929
[Views.LOADING]: "Loading",
30+
[Views.CONFIRM_LOCK_THEFT]: "ConfirmStartup",
3031
[Views.WELCOME]: "Welcome",
3132
[Views.LOGIN]: "Login",
3233
[Views.REGISTER]: "Register",
@@ -35,6 +36,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
3536
[Views.COMPLETE_SECURITY]: "CompleteSecurity",
3637
[Views.E2E_SETUP]: "E2ESetup",
3738
[Views.SOFT_LOGOUT]: "SoftLogout",
39+
[Views.LOCK_STOLEN]: "SessionLockStolen",
3840
};
3941

4042
const loggedInPageTypeMap: Record<PageType, ScreenName> = {

src/Views.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ enum Views {
2020
// trying to re-animate a matrix client or register as a guest.
2121
LOADING,
2222

23+
// Another tab holds the lock.
24+
CONFIRM_LOCK_THEFT,
25+
2326
// we are showing the welcome view
2427
WELCOME,
2528

@@ -48,6 +51,9 @@ enum Views {
4851
// We are logged out (invalid token) but have our local state again. The user
4952
// should log back in to rehydrate the client.
5053
SOFT_LOGOUT,
54+
55+
// Another instance of the application has started up. We just show an error page.
56+
LOCK_STOLEN,
5157
}
5258

5359
export default Views;

src/components/structures/MatrixChat.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ import { Linkify } from "../../HtmlUtils";
146146
import { NotificationColor } from "../../stores/notifications/NotificationColor";
147147
import { UserTab } from "../views/dialogs/UserTab";
148148
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption";
149+
import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
150+
import { SessionLockStolenView } from "./auth/SessionLockStolenView";
151+
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
149152

150153
// legacy export
151154
export { default as Views } from "../../Views";
@@ -306,11 +309,23 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
306309

307310
initSentry(SdkConfig.get("sentry"));
308311

312+
if (!checkSessionLockFree()) {
313+
// another instance holds the lock; confirm its theft before proceeding
314+
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
315+
} else {
316+
this.startInitSession();
317+
}
318+
}
319+
320+
/**
321+
* Kick off a call to {@link initSession}, and handle any errors
322+
*/
323+
private startInitSession = (): void => {
309324
this.initSession().catch((err) => {
310325
// TODO: show an error screen, rather than a spinner of doom
311326
logger.error("Error initialising Matrix session", err);
312327
});
313-
}
328+
};
314329

315330
/**
316331
* Do what we can to establish a Matrix session.
@@ -323,6 +338,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
323338
* * If all else fails, present a login screen.
324339
*/
325340
private async initSession(): Promise<void> {
341+
// we may previously been on the "confirm lock theft" page, so switch to the loading spinner.
342+
this.setState({ view: Views.LOADING });
343+
344+
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
345+
// make sure we are the only Element instance in town (on this browser/domain).
346+
if (!(await getSessionLock(() => this.onSessionLockStolen()))) {
347+
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
348+
return;
349+
}
350+
326351
// If the user was soft-logged-out, we want to make the SoftLogout component responsible for doing any
327352
// token auth (rather than Lifecycle.attemptDelegatedAuthLogin), since SoftLogout knows about submitting the
328353
// device ID and preserving the session.
@@ -377,6 +402,18 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
377402
}
378403
}
379404

405+
private async onSessionLockStolen(): Promise<void> {
406+
// switch to the LockStolenView. We deliberately do this immediately, rather than going through the dispatcher,
407+
// because there can be a substantial queue in the dispatcher, and some of the events in it might require an
408+
// active MatrixClient.
409+
await new Promise<void>((resolve) => {
410+
this.setState({ view: Views.LOCK_STOLEN }, resolve);
411+
});
412+
413+
// now we can tell the Lifecycle routines to abort any active startup, and to stop the active client.
414+
await Lifecycle.onSessionLockStolen();
415+
}
416+
380417
private async postLoginSetup(): Promise<void> {
381418
const cli = MatrixClientPeg.safeGet();
382419
const cryptoEnabled = cli.isCryptoEnabled();
@@ -434,6 +471,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
434471
| (Pick<IState, K> | IState | null),
435472
callback?: () => void,
436473
): void {
474+
console.log(`setState: ${JSON.stringify(state)}`);
437475
if (this.shouldTrackPageChange(this.state, { ...this.state, ...state })) {
438476
this.startPageChangeTimer();
439477
}
@@ -575,6 +613,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
575613
}
576614

577615
private onAction = (payload: ActionPayload): void => {
616+
// once the session lock has been stolen, don't try to do anything.
617+
if (this.state.view === Views.LOCK_STOLEN) {
618+
return;
619+
}
620+
578621
// Start the onboarding process for certain actions
579622
if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) {
580623
// This will cause `payload` to be dispatched later, once a
@@ -2048,6 +2091,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
20482091
<Spinner />
20492092
</div>
20502093
);
2094+
} else if (this.state.view === Views.CONFIRM_LOCK_THEFT) {
2095+
view = <ConfirmSessionLockTheftView onConfirm={this.startInitSession} />;
20512096
} else if (this.state.view === Views.COMPLETE_SECURITY) {
20522097
view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
20532098
} else if (this.state.view === Views.E2E_SETUP) {
@@ -2154,6 +2199,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
21542199
);
21552200
} else if (this.state.view === Views.USE_CASE_SELECTION) {
21562201
view = <UseCaseSelection onFinished={(useCase): Promise<void> => this.onShowPostLoginScreen(useCase)} />;
2202+
} else if (this.state.view === Views.LOCK_STOLEN) {
2203+
view = <SessionLockStolenView />;
21572204
} else {
21582205
logger.error(`Unknown view ${this.state.view}`);
21592206
return null;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
19+
import { _t } from "../../../languageHandler";
20+
import SdkConfig from "../../../SdkConfig";
21+
import AccessibleButton from "../../views/elements/AccessibleButton";
22+
23+
interface Props {
24+
/** Callback which the view will call if the user confirms they want to use this window */
25+
onConfirm: () => void;
26+
}
27+
28+
/**
29+
* Component shown by {@link MatrixChat} when another session is already active in the same browser and we need to
30+
* confirm if we should steal its lock
31+
*/
32+
export function ConfirmSessionLockTheftView(props: Props): JSX.Element {
33+
const brand = SdkConfig.get().brand;
34+
35+
return (
36+
<div className="mx_ConfirmSessionLockTheftView">
37+
<div className="mx_ConfirmSessionLockTheftView_body">
38+
<p>
39+
{_t(
40+
'%(brand)s is open in another window. Click "%(label)s" to use %(brand)s here and disconnect the other window.',
41+
{ brand, label: _t("Continue") },
42+
)}
43+
</p>
44+
45+
<AccessibleButton kind="primary" onClick={props.onConfirm}>
46+
{_t("Continue")}
47+
</AccessibleButton>
48+
</div>
49+
</div>
50+
);
51+
}

0 commit comments

Comments
 (0)