Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconnect overhaul #792

Merged
merged 13 commits into from
May 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions public/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"PLEASE_SCAN": "Scannen Sie den QR-Code mit Ihrer Threema-App",
"PLEASE_UNLOCK": "Verbindung wiederaufbauen",
"WAITING": "Auf Verbindung warten",
"RETRY": "Erneut versuchen",
"PLEASE_RELOAD": "Bitte laden Sie die Seite neu.",
"RELOAD": "Seite neu laden",
"PASSWORD": "Passwort",
Expand Down Expand Up @@ -59,6 +60,8 @@
"APP_STARTED": "Ist die Threema-App gestartet?",
"SESSION_DELETED": "Wurde diese Sitzung in der Threema-App gelöscht?",
"PHONE_ONLINE": "Ist Ihr Mobilgerät mit dem Internet verbunden?",
"UNLOCK_OR_CHARGE": "Es kann helfen, Ihr Mobilgerät zu entsperren oder an ein Ladegerät anzuschliessen.",
"PUSH_FAQ": "Möglicherweise liegt ein Problem bei der Verarbeitung von Push-Benachrichtigungen vor. Die FAQ-Artikel für <a target=\"_blank\" href=\"https://threema.ch/faq/push_andr\">Android</a> und <a target=\"_blank\" href=\"https://threema.ch/faq/push_ios\">iOS</a> helfen bei der Fehlersuche.",
"WEBCLIENT_ENABLED": "Ist Threema Web in der Threema-App aktiviert?",
"PLUGIN": "Ist in Ihrem Browser ein Plugin zum Blockieren von WebRTC installiert?",
"ADBLOCKER": "Ist in Ihrem Browser ein Ad-Blocker installiert?",
Expand Down Expand Up @@ -290,6 +293,10 @@
"PLAY_SOUND": "Ton abspielen"
}
},
"deviceUnreachable": {
"DEVICE_UNREACHABLE": "Mobilgerät nicht erreichbar",
"UNABLE_TO_CONNECT": "Eine Verbindung mit Ihrem Mobilgerät konnte nicht hergestellt werden …"
},
"version": {
"NEW_VERSION": "Neue Version Verfügbar",
"NEW_VERSION_BODY": "Eine neue Version von Threema Web ({version}) ist verfügbar. Mehr Informationen finden Sie im {changelog}. Drücken Sie \"OK\", um das Update zu aktivieren."
Expand Down
7 changes: 7 additions & 0 deletions public/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"PLEASE_SCAN": "Scan this QR code with your Threema app",
"PLEASE_UNLOCK": "Reconnecting session",
"WAITING": "Waiting for connection",
"RETRY": "Retry",
"PLEASE_RELOAD": "Please reload the page to try again.",
"RELOAD": "Reload page",
"PASSWORD": "Password",
Expand Down Expand Up @@ -59,6 +60,8 @@
"APP_STARTED": "Is the Threema app started?",
"SESSION_DELETED": "Did you delete this session on your phone?",
"PHONE_ONLINE": "Is your phone connected to the internet?",
"UNLOCK_OR_CHARGE": "It may help to unlock your device or connect it to a charger.",
"PUSH_FAQ": "Your device may be affected by Push Notification issues. See the FAQ articles for <a target=\"_blank\" href=\"https://threema.ch/faq/push_andr\">Android</a> and <a target=\"_blank\" href=\"https://threema.ch/faq/push_ios\">iOS</a> for troubleshooting.",
lgrahl marked this conversation as resolved.
Show resolved Hide resolved
"WEBCLIENT_ENABLED": "Is Threema Web enabled in the Threema app?",
"PLUGIN": "Is a privacy plugin installed in your browser which blocks WebRTC communication?",
"ADBLOCKER": "Do you use an ad blocker which also blocks WebRTC communication?",
Expand Down Expand Up @@ -290,6 +293,10 @@
"PLAY_SOUND": "Play sound"
}
},
"deviceUnreachable": {
"DEVICE_UNREACHABLE": "Device Unreachable",
"UNABLE_TO_CONNECT": "Unable to connect to your device …"
},
"version": {
"NEW_VERSION": "New Version Available",
"NEW_VERSION_BODY": "A new version of Threema Web ({version}) is available. Check out the {changelog} for more information. Click \"OK\" to activate the update."
Expand Down
145 changes: 46 additions & 99 deletions src/controllers/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,16 @@ export class StatusController {
if (oldValue === 'ok' && isWebrtc) {
this.scheduleStatusBar();
}
if (this.stateService.wasConnected) {
this.webClientService.clearIsTypingFlags();
}
if (this.stateService.wasConnected && isRelayedData) {
this.webClientService.clearIsTypingFlags();
if (isRelayedData) {
this.reconnectIos();
}
break;
case 'error':
if (this.stateService.wasConnected && isWebrtc) {
if (oldValue === 'ok') {
this.scheduleStatusBar();
}
if (isWebrtc) {
this.reconnectAndroid();
}
if (this.stateService.wasConnected && isRelayedData) {
if (this.stateService.attempt === 0 && isRelayedData) {
this.reconnectIos();
}
break;
Expand Down Expand Up @@ -166,104 +161,61 @@ export class StatusController {
* Attempt to reconnect an Android device after a connection loss.
*/
private reconnectAndroid(): void {
this.$log.warn(this.logTag, 'Connection lost (Android). Attempting to reconnect...');
this.$log.info(this.logTag, `Connection lost (Android). Reconnect attempt #${this.stateService.attempt + 1}`);

// Show expanded status bar (if on 'messenger')
if (this.$state.includes('messenger')) {
this.scheduleStatusBar();
}

// Get original keys
const originalKeyStore = this.webClientService.salty.keyStore;
const originalPeerPermanentKeyBytes = this.webClientService.salty.peerPermanentKeyBytes;

// Timeout durations
const TIMEOUT1 = 20 * 1000; // Duration per step for first reconnect
const TIMEOUT2 = 20 * 1000; // Duration per step for second reconnect

// Reconnect state
let reconnectTry: 1 | 2 = 1;

// Handler for failed reconnection attempts
const reconnectionFailed = () => {
// Collapse status bar
this.collapseStatusBar();
// Soft reconnect: Does not reset the loaded data
this.webClientService.stop({
reason: DisconnectReason.SessionStopped,
send: true,
close: false,
});
this.webClientService.init({
keyStore: originalKeyStore,
peerTrustedKey: originalPeerPermanentKeyBytes,
resume: true,
});

// Reset connection & state
this.webClientService.stop({
reason: DisconnectReason.SessionError,
send: false,
// TODO: Use welcome.error once we have it
close: 'welcome',
connectionBuildupState: 'reconnect_failed',
});
};

// Handlers for reconnecting timeout
const reconnect2Timeout = () => {
// Give up
this.$log.error(this.logTag, 'Reconnect timeout 2. Going back to initial loading screen...');
reconnectionFailed();
};
const reconnect1Timeout = () => {
// Could not connect so far.
this.$log.error(this.logTag, 'Reconnect timeout 1. Retrying...');
reconnectTry = 2;
this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
doSoftReconnect();
};

// Function to soft-reconnect. Does not reset the loaded data.
const doSoftReconnect = () => {
this.webClientService.stop({
reason: DisconnectReason.SessionStopped,
send: true,
close: false,
});
this.webClientService.init({
keyStore: originalKeyStore,
peerTrustedKey: originalPeerPermanentKeyBytes,
resume: true,
});
this.webClientService.start().then(
() => {
// Cancel timeout
this.$timeout.cancel(this.reconnectTimeout);
// Show device unreachable dialog if maximum attempts exceeded
// Note: This will not be shown on 'welcome'
const pause = this.stateService.attempt >= WebClientService.MAX_CONNECT_ATTEMPTS;
if (pause) {
this.webClientService.showDeviceUnreachableDialog();
}

// Hide expanded status bar
this.collapseStatusBar();
},
// Start
this.webClientService.start(pause)
.then(
() => { /* ignored */ },
(error) => {
this.$log.error(this.logTag, 'Error state:', error);
this.$timeout.cancel(this.reconnectTimeout);
reconnectionFailed();
// Note: The web client service has already been stopped at
// this point.
},
(progress: threema.ConnectionBuildupStateChange) => {
if (progress.state === 'peer_handshake' || progress.state === 'loading') {
this.$log.debug(this.logTag, 'Connection buildup advanced, resetting timeout');
// Restart timeout
this.$timeout.cancel(this.reconnectTimeout);
if (reconnectTry === 1) {
this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);
} else if (reconnectTry === 2) {
this.reconnectTimeout = this.$timeout(reconnect2Timeout, TIMEOUT2);
} else {
throw new Error('Invalid reconnectTry value: ' + reconnectTry);
}
}
this.$log.debug(this.logTag, 'Connection buildup advanced:', progress);
},
);
};

// Start timeout
this.reconnectTimeout = this.$timeout(reconnect1Timeout, TIMEOUT1);

// Start reconnecting process
doSoftReconnect();

// TODO: Handle server closing state
)
.finally(() => {
// Hide expanded status bar
this.collapseStatusBar();
});
++this.stateService.attempt;
}

/**
* Attempt to reconnect an iOS device after a connection loss.
*/
private reconnectIos(): void {
this.$log.info(this.logTag, 'Connection lost (iOS). Attempting to reconnect...');
this.$log.info(this.logTag, `Connection lost (iOS). Reconnect attempt #${++this.stateService.attempt}`);
dbrgn marked this conversation as resolved.
Show resolved Hide resolved

// Get original keys
const originalKeyStore = this.webClientService.salty.keyStore;
Expand Down Expand Up @@ -305,7 +257,8 @@ export class StatusController {
};
})();

this.$timeout(() => {
this.$timeout.cancel(this.reconnectTimeout);
this.reconnectTimeout = this.$timeout(() => {
if (push.send) {
this.$log.debug(`Starting new connection with push, reason: ${push.reason}`);
} else {
Expand All @@ -318,18 +271,12 @@ export class StatusController {
});

this.webClientService.start(!push.send).then(
() => { /* ok */ },
() => { /* ignored */ },
(error) => {
this.$log.error(this.logTag, 'Error state:', error);
this.webClientService.stop({
reason: DisconnectReason.SessionError,
send: false,
// TODO: Use welcome.error once we have it
close: 'welcome',
connectionBuildupState: 'reconnect_failed',
});
// Note: The web client service has already been stopped at
// this point.
},
// Progress
(progress: threema.ConnectionBuildupStateChange) => {
this.$log.debug(this.logTag, 'Connection buildup advanced:', progress);
},
Expand Down
18 changes: 18 additions & 0 deletions src/exceptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* This file is part of Threema Web.
*
* Threema Web is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
* General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
*/

export class TimeoutError extends Error {}
43 changes: 43 additions & 0 deletions src/partials/dialog.device-unreachable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<md-dialog aria-label="Device Unreachable">
<form ng-cloak>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>deviceUnreachable.DEVICE_UNREACHABLE</h2>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content">
<h3 translate>deviceUnreachable.UNABLE_TO_CONNECT</h3>
<ul class="material-icons-list">
<li class="help">
<span translate>troubleshooting.PHONE_ONLINE</span>
</li>
<li class="help">
<span translate>troubleshooting.APP_STARTED</span>
</li>
<li class="help">
<span translate>troubleshooting.WEBCLIENT_ENABLED</span>
</li>
<li class="info">
<span translate>troubleshooting.UNLOCK_OR_CHARGE</span>
</li>
<li class="info">
<span translate>troubleshooting.PUSH_FAQ</span>
</li>
</ul>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button role="button" class="md-primary reload-btn" ng-click="ctrl.reload()" aria-labelledby="aria-label-reload">
<span translate id="aria-label-reload">welcome.RELOAD</span>
</md-button>
<md-button role="button" class="md-primary reload-btn circular-progress-button" ng-click="ctrl.retry()" ng-disabled="ctrl.retrying" aria-labelledby="aria-label-retry">
<md-progress-circular ng-if="ctrl.retrying" md-mode="determinate" md-diameter="20" value="{{ctrl.progress}}"></md-progress-circular>
<i ng-if="!ctrl.retrying" class="material-icons">refresh</i>
<span translate id="aria-label-retry">welcome.RETRY</span>
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

Loading