Timeout provisioning socket when visibility=false
This commit is contained in:
parent
4fbf5fee57
commit
b88100d32a
5 changed files with 95 additions and 9 deletions
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue