Timeout provisioning socket when visibility=false

This commit is contained in:
Fedor Indutny 2024-11-05 15:51:25 -08:00 committed by GitHub
parent 4fbf5fee57
commit b88100d32a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 95 additions and 9 deletions

View file

@ -1,8 +1,9 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReactElement } from 'react'; import React, { type ReactElement, useEffect, useCallback } from 'react';
import React from 'react'; import { noop } from 'lodash';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser'; import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
@ -27,9 +28,31 @@ export function InstallScreenErrorStep({
}: Props): ReactElement { }: Props): ReactElement {
let errorMessage: string; let errorMessage: string;
let buttonText = i18n('icu:installTryAgain'); let buttonText = i18n('icu:installTryAgain');
let onClickButton = () => tryAgain(); let onClickButton = useCallback(() => tryAgain(), [tryAgain]);
let shouldShowQuitButton = false; let shouldShowQuitButton = false;
useEffect(() => {
if (error !== InstallScreenError.InactiveTimeout) {
return noop;
}
const cleanup = () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
const onVisibilityChange = () => {
if (document.hidden) {
return;
}
cleanup();
tryAgain();
};
document.addEventListener('visibilitychange', onVisibilityChange);
return cleanup;
}, [error, tryAgain]);
switch (error) { switch (error) {
case InstallScreenError.TooManyDevices: case InstallScreenError.TooManyDevices:
errorMessage = i18n('icu:installTooManyDevices'); errorMessage = i18n('icu:installTooManyDevices');
@ -43,6 +66,7 @@ export function InstallScreenErrorStep({
shouldShowQuitButton = true; shouldShowQuitButton = true;
break; break;
case InstallScreenError.ConnectionFailed: case InstallScreenError.ConnectionFailed:
case InstallScreenError.InactiveTimeout:
errorMessage = i18n('icu:installConnectionFailed'); errorMessage = i18n('icu:installConnectionFailed');
break; break;
case InstallScreenError.QRCodeFailed: case InstallScreenError.QRCodeFailed:

View file

@ -19,7 +19,7 @@ import { strictAssert } from '../../util/assert';
import { SECOND } from '../../util/durations'; import { SECOND } from '../../util/durations';
import * as Registration from '../../util/registration'; import * as Registration from '../../util/registration';
import { isBackupEnabled } from '../../util/isBackupEnabled'; import { isBackupEnabled } from '../../util/isBackupEnabled';
import { HTTPError } from '../../textsecure/Errors'; import { HTTPError, InactiveTimeoutError } from '../../textsecure/Errors';
import { import {
Provisioner, Provisioner,
type PrepareLinkDataOptionsType, type PrepareLinkDataOptionsType,
@ -196,7 +196,10 @@ function startInstaller(): ThunkAction<
const { server } = window.textsecure; const { server } = window.textsecure;
strictAssert(server, 'Expected a server'); strictAssert(server, 'Expected a server');
const provisioner = new Provisioner(server, window.getVersion()); const provisioner = new Provisioner({
server,
appVersion: window.getVersion(),
});
const abortController = new AbortController(); const abortController = new AbortController();
const { signal } = abortController; const { signal } = abortController;
@ -295,6 +298,14 @@ function startInstaller(): ThunkAction<
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
if (error instanceof InactiveTimeoutError) {
dispatch({
type: SET_ERROR,
payload: InstallScreenError.InactiveTimeout,
});
return;
}
dispatch({ dispatch({
type: SET_ERROR, type: SET_ERROR,
payload: InstallScreenError.ConnectionFailed, payload: InstallScreenError.ConnectionFailed,

View file

@ -315,3 +315,9 @@ export class IncorrectSenderKeyAuthError extends Error {}
export class WarnOnlyError extends Error {} export class WarnOnlyError extends Error {}
export class NoSenderKeyError extends Error {} export class NoSenderKeyError extends Error {}
export class InactiveTimeoutError extends Error {
constructor() {
super('Closing socket due to inactivity');
}
}

View file

@ -10,6 +10,7 @@ import { strictAssert } from '../util/assert';
import { normalizeAci } from '../util/normalizeAci'; import { normalizeAci } from '../util/normalizeAci';
import { normalizeDeviceName } from '../util/normalizeDeviceName'; import { normalizeDeviceName } from '../util/normalizeDeviceName';
import { isLinkAndSyncEnabled } from '../util/isLinkAndSyncEnabled'; import { isLinkAndSyncEnabled } from '../util/isLinkAndSyncEnabled';
import { MINUTE } from '../util/durations';
import { MAX_DEVICE_NAME_LENGTH } from '../types/InstallScreen'; import { MAX_DEVICE_NAME_LENGTH } from '../types/InstallScreen';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
import { import {
@ -33,6 +34,7 @@ import {
type IncomingWebSocketRequest, type IncomingWebSocketRequest,
ServerRequestType, ServerRequestType,
} from './WebsocketResources'; } from './WebsocketResources';
import { InactiveTimeoutError } from './Errors';
enum Step { enum Step {
Idle = 'Idle', Idle = 'Idle',
@ -72,16 +74,25 @@ export type PrepareLinkDataOptionsType = Readonly<{
backupFile?: Uint8Array; backupFile?: Uint8Array;
}>; }>;
export type ProvisionerOptionsType = Readonly<{
server: WebAPIType;
appVersion: string;
}>;
const INACTIVE_SOCKET_TIMEOUT = 30 * MINUTE;
export class Provisioner { export class Provisioner {
private readonly cipher = new ProvisioningCipher(); private readonly cipher = new ProvisioningCipher();
private readonly server: WebAPIType;
private readonly appVersion: string;
private state: StateType = { step: Step.Idle }; private state: StateType = { step: Step.Idle };
private wsr: IWebSocketResource | undefined; private wsr: IWebSocketResource | undefined;
constructor( constructor(options: ProvisionerOptionsType) {
private readonly server: WebAPIType, this.server = options.server;
private readonly appVersion: string this.appVersion = options.appVersion;
) {} }
public close(error = new Error('Provisioner closed')): void { public close(error = new Error('Provisioner closed')): void {
try { try {
@ -122,6 +133,32 @@ export class Provisioner {
}); });
this.wsr = wsr; this.wsr = wsr;
let inactiveTimer: NodeJS.Timeout | undefined;
const onVisibilityChange = (): void => {
// Visible
if (!document.hidden) {
if (inactiveTimer != null) {
clearTimeout(inactiveTimer);
}
inactiveTimer = undefined;
return;
}
// Invisible, but already has a timer
if (inactiveTimer != null) {
return;
}
inactiveTimer = setTimeout(() => {
inactiveTimer = undefined;
this.close(new InactiveTimeoutError());
}, INACTIVE_SOCKET_TIMEOUT);
};
document.addEventListener('visibilitychange', onVisibilityChange);
if (this.state.step !== Step.Connecting) { if (this.state.step !== Step.Connecting) {
this.close(); this.close();
throw new Error('Provisioner closed early'); throw new Error('Provisioner closed early');
@ -133,6 +170,13 @@ export class Provisioner {
}; };
wsr.addEventListener('close', ({ code, reason }) => { wsr.addEventListener('close', ({ code, reason }) => {
// Unsubscribe from visibility changes
document.removeEventListener('visibilitychange', onVisibilityChange);
if (inactiveTimer != null) {
clearTimeout(inactiveTimer);
}
inactiveTimer = undefined;
if (this.state.step === Step.ReadyToLink) { if (this.state.step === Step.ReadyToLink) {
// WebSocket close is not an issue since we no longer need it // WebSocket close is not an issue since we no longer need it
return; return;

View file

@ -22,6 +22,7 @@ export enum InstallScreenError {
TooOld = 'TooOld', TooOld = 'TooOld',
ConnectionFailed = 'ConnectionFailed', ConnectionFailed = 'ConnectionFailed',
QRCodeFailed = 'QRCodeFailed', QRCodeFailed = 'QRCodeFailed',
InactiveTimeout = 'InactiveTimeout',
} }
export enum InstallScreenQRCodeError { export enum InstallScreenQRCodeError {