summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-03-10 17:31:23 +0100
committerOskar Nyberg <oskar@mullvad.net>2023-03-10 17:31:23 +0100
commit67f13cc44286432274c320360529ed28f6358a37 (patch)
treeaba4873436bc4cab25bf08125430ac98d96c02cc
parent8be326a45b53dae1e16bd62a57ac257ed1e9cf93 (diff)
parent25fa6c63766f6da254ebc6d252214244ca116e0e (diff)
downloadmullvadvpn-67f13cc44286432274c320360529ed28f6358a37.tar.xz
mullvadvpn-67f13cc44286432274c320360529ed28f6358a37.zip
Merge branch 'disconnect-daemon-on-suspen-DES-11'
-rw-r--r--gui/src/main/daemon-rpc.ts13
-rw-r--r--gui/src/main/index.ts26
-rw-r--r--gui/src/renderer/components/AppRouter.tsx2
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx167
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,