diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-03-10 17:31:23 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-03-10 17:31:23 +0100 |
| commit | 67f13cc44286432274c320360529ed28f6358a37 (patch) | |
| tree | aba4873436bc4cab25bf08125430ac98d96c02cc | |
| parent | 8be326a45b53dae1e16bd62a57ac257ed1e9cf93 (diff) | |
| parent | 25fa6c63766f6da254ebc6d252214244ca116e0e (diff) | |
| download | mullvadvpn-67f13cc44286432274c320360529ed28f6358a37.tar.xz mullvadvpn-67f13cc44286432274c320360529ed28f6358a37.zip | |
Merge branch 'disconnect-daemon-on-suspen-DES-11'
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 13 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 26 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/TransitionContainer.tsx | 167 |
4 files changed, 141 insertions, 67 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 5d24870908..966846fdf6 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -137,6 +137,7 @@ type CallFunctionArgument<T, R> = export class DaemonRpc { private client: ManagementServiceClient; private isConnectedValue = false; + private isClosed = false; private connectionObservers: ConnectionObserver[] = []; private nextSubscriptionId = 0; private subscriptions: Map<number, grpc.ClientReadableStream<grpcTypes.DaemonEvent>> = new Map(); @@ -154,6 +155,17 @@ export class DaemonRpc { return this.isConnectedValue; } + public reopen() { + if (this.isClosed) { + this.isClosed = false; + this.client = new ManagementServiceClient( + DAEMON_RPC_PATH, + grpc.credentials.createInsecure(), + this.channelOptions(), + ); + } + } + public connect(): Promise<void> { return new Promise((resolve, reject) => { this.client.waitForReady(this.deadlineFromNow(), (error) => { @@ -179,6 +191,7 @@ export class DaemonRpc { this.removeSubscription(subscriptionId); } + this.isClosed = true; this.client.close(); if (this.reconnectionTimeout) { clearTimeout(this.reconnectionTimeout); diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 7181f863bf..1e8214a277 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -1,5 +1,5 @@ import { exec, execFile } from 'child_process'; -import { app, nativeTheme, session, shell, systemPreferences } from 'electron'; +import { app, nativeTheme, powerMonitor, session, shell, systemPreferences } from 'electron'; import fs from 'fs'; import * as path from 'path'; import util from 'util'; @@ -181,6 +181,9 @@ class ApplicationMain log.info('quit received'); this.onQuit(); }); + + powerMonitor.on('suspend', this.onSuspend); + powerMonitor.on('resume', this.onResume); } public async performPostUpgradeCheck(): Promise<void> { @@ -442,6 +445,27 @@ class ApplicationMain ); }; + private onSuspend = () => { + log.info('Suspend event received, disconnecting from daemon'); + if (this.daemonEventListener) { + this.daemonRpc.unsubscribeDaemonEventListener(this.daemonEventListener); + } + + const wasConnected = this.daemonRpc.isConnected; + IpcMainEventChannel.navigation.notifyReset?.(); + this.daemonRpc.disconnect(); + this.onDaemonDisconnected(wasConnected); + }; + + private onResume = () => { + log.info('Resume event received, connecting to daemon'); + this.daemonRpc.reopen(); + this.daemonRpc.addConnectionObserver( + new ConnectionObserver(this.onDaemonConnected, this.onDaemonDisconnected), + ); + this.connectToDaemon(); + }; + private onDaemonConnected = async () => { const firstDaemonConnection = this.beforeFirstDaemonConnection; this.beforeFirstDaemonConnection = false; diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index e31066337a..cd6e07fbbd 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -59,7 +59,7 @@ export default function AppRouter() { return ( <Focus ref={focusRef}> <TransitionContainer onTransitionEnd={onNavigation} {...transition}> - <TransitionView viewId={currentLocation.key || ''} routePath={history.location.pathname}> + <TransitionView routePath={history.location.pathname}> <Switch key={currentLocation.key} location={currentLocation}> <Route exact path={RoutePath.launch} component={Launch} /> <Route exact path={RoutePath.login} component={LoginPage} /> diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx index 45e46c37e0..ce9900927f 100644 --- a/gui/src/renderer/components/TransitionContainer.tsx +++ b/gui/src/renderer/components/TransitionContainer.tsx @@ -5,7 +5,6 @@ import { ITransitionSpecification } from '../lib/history'; import { WillExit } from '../lib/will-exit'; interface ITransitioningViewProps { - viewId: string; routePath: string; children?: React.ReactNode; } @@ -33,7 +32,7 @@ interface IItemStyle { interface IState { currentItem?: ITransitionQueueItem; nextItem?: ITransitionQueueItem; - itemQueue: ITransitionQueueItem[]; + queuedItem?: ITransitionQueueItem; currentItemStyle?: IItemStyle; nextItemStyle?: IItemStyle; currentItemTransition?: Partial<IItemStyle>; @@ -91,59 +90,22 @@ export class TransitionView extends React.Component<ITransitioningViewProps> { export default class TransitionContainer extends React.Component<IProps, IState> { public state: IState = { - itemQueue: [], currentItem: TransitionContainer.makeItem(this.props), }; private isCycling = false; + private isTransitioning = false; - private currentContentRef = React.createRef<HTMLDivElement>(); - private nextContentRef = React.createRef<HTMLDivElement>(); + private currentContentRef: React.MutableRefObject<HTMLDivElement | null> = React.createRef<HTMLDivElement>(); + private nextContentRef: React.MutableRefObject<HTMLDivElement | null> = React.createRef<HTMLDivElement>(); // The item that should trigger the cycle to finish in onTransitionEnd private transitioningItemRef?: React.RefObject<HTMLDivElement>; - public static getDerivedStateFromProps(props: IProps, state: IState) { - const candidate = props.children; - - if (candidate && state.currentItem) { - // Synchronize updates to the last added child. Although the queue doesn't change, the child - // itself might need to change. That's why the queue-/next item is replaced by it again after - // calling `makeItem`. - const itemQueueCount = state.itemQueue.length; - const lastItemInQueue = itemQueueCount > 0 ? state.itemQueue[itemQueueCount - 1] : undefined; - - if (lastItemInQueue && lastItemInQueue.view.props.viewId === candidate.props.viewId) { - // Child is last item in queue. No change to the queue needed. - return { - itemQueue: [...state.itemQueue.slice(0, -1), TransitionContainer.makeItem(props)], - }; - } else if ( - itemQueueCount === 0 && - state.nextItem && - state.nextItem.view.props.viewId === candidate.props.viewId - ) { - // Child is next item, no change to the queue needed. - return { nextItem: TransitionContainer.makeItem(props) }; - } else if ( - itemQueueCount === 0 && - !state.nextItem && - state.currentItem.view.props.viewId === candidate.props.viewId - ) { - // Child is current item and there's no new child, no change to the queue needed. - return { currentItem: TransitionContainer.makeItem(props) }; - } else { - // Child is a new item and is added to the queue. - return { itemQueue: [...state.itemQueue, TransitionContainer.makeItem(props)] }; - } - } else if (candidate && !state.currentItem) { - // Child is set as current item if there's no item already. - return { currentItem: TransitionContainer.makeItem(props) }; - } else { - return null; + public componentDidUpdate(prevProps: IProps) { + if (this.props.children !== prevProps.children) { + this.updateStateFromProps(); } - } - public componentDidUpdate() { if ( this.state.currentItemStyle && this.state.currentItemTransition && @@ -167,14 +129,14 @@ export default class TransitionContainer extends React.Component<IProps, IState> } public render() { - const willExit = this.state.itemQueue.length > 0 || this.state.nextItem !== undefined; + const willExit = this.state.queuedItem !== undefined || this.state.nextItem !== undefined; return ( <StyledTransitionContainer disableUserInteraction={willExit}> {this.state.currentItem && ( - <WillExit key={this.state.currentItem.view.props.viewId} value={willExit}> + <WillExit key={this.state.currentItem.view.props.routePath} value={willExit}> <StyledTransitionContent - ref={this.currentContentRef} + ref={this.setCurrentContentRef} transition={this.state.currentItemStyle} onTransitionEnd={this.onTransitionEnd}> {this.state.currentItem.view} @@ -183,9 +145,9 @@ export default class TransitionContainer extends React.Component<IProps, IState> )} {this.state.nextItem && ( - <WillExit key={this.state.nextItem.view.props.viewId} value={false}> + <WillExit key={this.state.nextItem.view.props.routePath} value={false}> <StyledTransitionContent - ref={this.nextContentRef} + ref={this.setNextContentRef} transition={this.state.nextItemStyle} onTransitionEnd={this.onTransitionEnd}> {this.state.nextItem.view} @@ -196,8 +158,86 @@ export default class TransitionContainer extends React.Component<IProps, IState> ); } + private setCurrentContentRef = (element: HTMLDivElement) => { + this.currentContentRef.current?.removeEventListener('transitionstart', this.onTransitionStart); + this.currentContentRef.current = element; + this.currentContentRef.current?.addEventListener('transitionstart', this.onTransitionStart); + }; + + private setNextContentRef = (element: HTMLDivElement) => { + this.nextContentRef.current?.removeEventListener('transitionstart', this.onTransitionStart); + this.nextContentRef.current = element; + this.nextContentRef.current?.addEventListener('transitionstart', this.onTransitionStart); + }; + + private updateStateFromProps() { + const candidate = this.props.children; + + if (candidate && this.state.currentItem) { + // Update currentItem, nextItem, queuedItem depending on which the candidate matches. + if ( + !this.isTransitioning && + this.state.currentItem.view.props.routePath === candidate.props.routePath + ) { + // There's no transition in progress and the newest candidate has the same path as the + // current. In this sitation the app should just remain in the same view. + this.setState( + { + currentItem: TransitionContainer.makeItem(this.props), + nextItem: undefined, + queuedItem: undefined, + currentItemStyle: undefined, + nextItemStyle: undefined, + currentItemTransition: undefined, + nextItemTransition: undefined, + }, + () => (this.isCycling = false), + ); + } else if (!this.isTransitioning && this.state.nextItem) { + // There's no transition in progress but there is a next item. Abort the transition and add + // the candidate to the queue. The app shouldn't start a transition if there is another view + // to queue. + this.setState( + { + nextItem: undefined, + queuedItem: TransitionContainer.makeItem(this.props), + currentItemStyle: undefined, + nextItemStyle: undefined, + currentItemTransition: undefined, + nextItemTransition: undefined, + }, + () => (this.isCycling = false), + ); + } else if (this.state.nextItem?.view.props.routePath === candidate.props.routePath) { + // There's an update to the item that is currently being transitioned to. Update that item + // and continue the transition. + this.setState({ + nextItem: TransitionContainer.makeItem(this.props), + queuedItem: undefined, + }); + } else { + // If none of the above, initiate a transition to the new item. + this.setState({ queuedItem: TransitionContainer.makeItem(this.props) }); + } + } else if (candidate) { + // Child is set as current item if there's no item already. + this.setState({ currentItem: TransitionContainer.makeItem(this.props) }); + } + } + + private onTransitionStart = (event: TransitionEvent) => { + if ( + this.isCycling && + !this.isTransitioning && + event.target === this.transitioningItemRef?.current + ) { + this.isTransitioning = true; + } + }; + private onTransitionEnd = (event: React.TransitionEvent<HTMLDivElement>) => { if (this.isCycling && event.target === this.transitioningItemRef?.current) { + this.isTransitioning = false; this.transitioningItemRef = undefined; this.makeNextItemCurrent(() => { this.onFinishCycle(); @@ -218,11 +258,8 @@ export default class TransitionContainer extends React.Component<IProps, IState> } private cycleUnguarded = () => { - const itemQueue = this.state.itemQueue; - - if (itemQueue.length > 0) { - const nextItem = itemQueue[0]; - const transition = nextItem.transition; + if (this.state.queuedItem) { + const transition = this.state.queuedItem.transition; switch (transition.name) { case 'slide-up': @@ -277,8 +314,8 @@ export default class TransitionContainer extends React.Component<IProps, IState> private slideUp(duration: number) { this.transitioningItemRef = this.nextContentRef; this.setState((state) => ({ - nextItem: state.itemQueue[0], - itemQueue: state.itemQueue.slice(1), + nextItem: state.queuedItem, + queuedItem: undefined, currentItemStyle: { x: 0, y: 0, inFront: false }, nextItemStyle: { x: 0, y: 100, inFront: true }, currentItemTransition: { duration }, @@ -289,8 +326,8 @@ export default class TransitionContainer extends React.Component<IProps, IState> private slideDown(duration: number) { this.transitioningItemRef = this.currentContentRef; this.setState((state) => ({ - nextItem: state.itemQueue[0], - itemQueue: state.itemQueue.slice(1), + nextItem: state.queuedItem, + queuedItem: undefined, currentItemStyle: { x: 0, y: 0, inFront: true }, nextItemStyle: { x: 0, y: 0, inFront: false }, currentItemTransition: { y: 100, duration }, @@ -301,8 +338,8 @@ export default class TransitionContainer extends React.Component<IProps, IState> private push(duration: number) { this.transitioningItemRef = this.nextContentRef; this.setState((state) => ({ - nextItem: state.itemQueue[0], - itemQueue: state.itemQueue.slice(1), + nextItem: state.queuedItem, + queuedItem: undefined, currentItemStyle: { x: 0, y: 0, inFront: false }, nextItemStyle: { x: 100, y: 0, inFront: true }, currentItemTransition: { x: -50, duration }, @@ -313,8 +350,8 @@ export default class TransitionContainer extends React.Component<IProps, IState> private pop(duration: number) { this.transitioningItemRef = this.currentContentRef; this.setState((state) => ({ - nextItem: state.itemQueue[0], - itemQueue: state.itemQueue.slice(1), + nextItem: state.queuedItem, + queuedItem: undefined, currentItemStyle: { x: 0, y: 0, inFront: true }, nextItemStyle: { x: -50, y: 0, inFront: false }, currentItemTransition: { x: 100, duration }, @@ -325,9 +362,9 @@ export default class TransitionContainer extends React.Component<IProps, IState> private replace(completion: () => void) { this.setState( (state) => ({ - currentItem: state.itemQueue[0], + currentItem: state.queuedItem, nextItem: undefined, - itemQueue: state.itemQueue.slice(1), + queuedItem: undefined, currentItemStyle: { x: 0, y: 0, inFront: false, duration: 0 }, nextItemStyle: { x: 0, y: 0, inFront: true, duration: 0 }, currentItemTransition: undefined, |
