Skip to content

Commit

Permalink
🐛 (electron) reconnect to sockets if connection lost (actualbudget#1694)
Browse files Browse the repository at this point in the history
  • Loading branch information
MatissJanis authored Oct 5, 2023
1 parent bedfa08 commit d651414
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 19 deletions.
64 changes: 48 additions & 16 deletions packages/desktop-electron/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require('./security');

const { fork } = require('child_process');
const path = require('path');
const http = require('http');

require('./setRequireHook');

Expand All @@ -55,10 +56,8 @@ const WindowState = require('./window-state.js');
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let clientWin;
let serverWin; // eslint-disable-line @typescript-eslint/no-unused-vars
let serverProcess;
let serverSocket;
let IS_QUITTING = false;

updater.onEvent((type, data) => {
// Notify both the app and the about window
Expand Down Expand Up @@ -98,9 +97,48 @@ function createBackgroundProcess(socketName) {
console.log('Unknown server message: ' + msg.type);
}
});

return serverProcess;
}

const isPortFree = port =>
new Promise(resolve => {
const server = http
.createServer()
.listen(port, () => {
server.close();
resolve(true);
})
.on('error', () => {
resolve(false);
});
});

async function createSocketConnection() {
if (!serverSocket) serverSocket = await getRandomPort();

// Spawn the child process if it is not already running
// (sometimes long child processes die, so we need to set them
// up again)
const isFree = await isPortFree(serverSocket);
if (isFree) {
await createBackgroundProcess(serverSocket);
}

if (!clientWin) {
return;
}

// Send a heartbeat to the client whenever we attempt to create a new
// sockets connection
clientWin.webContents.executeJavaScript(
`window.__actionsForMenu && window.__actionsForMenu.reconnect(${serverSocket})`,
);
}

async function createWindow() {
await createSocketConnection();

const windowState = await WindowState.get();

// Create the browser window.
Expand Down Expand Up @@ -137,15 +175,6 @@ async function createWindow() {
win.loadURL(`app://actual/`);
}

win.on('close', () => {
// We don't want to close the budget on exit because that will
// clear the state which re-opens the last budget automatically on
// startup
if (!IS_QUITTING) {
clientWin.webContents.executeJavaScript('__actionsForMenu.closeBudget()');
}
});

win.on('closed', () => {
clientWin = null;
updateMenu(false);
Expand Down Expand Up @@ -233,8 +262,6 @@ function updateMenu(isBudgetOpen) {
app.setAppUserModelId('com.shiftreset.actual');

app.on('ready', async () => {
serverSocket = await getRandomPort();

// Install an `app://` protocol that always returns the base HTML
// file no matter what URL it is. This allows us to use react-router
// on the frontend
Expand Down Expand Up @@ -277,8 +304,6 @@ app.on('ready', async () => {
require('electron').powerMonitor.on('suspend', () => {
console.log('Suspending', new Date());
});

createBackgroundProcess(serverSocket);
});

app.on('window-all-closed', () => {
Expand All @@ -289,7 +314,6 @@ app.on('window-all-closed', () => {
});

app.on('before-quit', () => {
IS_QUITTING = true;
if (serverProcess) {
serverProcess.kill();
serverProcess = null;
Expand All @@ -302,6 +326,14 @@ app.on('activate', () => {
}
});

app.on('did-become-active', () => {
// Reconnect whenever the window becomes active;
// We don't know what might have happened in-between, so it's better
// to be safe than sorry; the client can then decide if it wants to
// reconnect or not.
createSocketConnection();
});

ipcMain.on('get-bootstrap-data', event => {
event.returnValue = {
version: app.getVersion(),
Expand Down
8 changes: 7 additions & 1 deletion packages/loot-core/src/client/actions/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send } from '../../platform/client/fetch';
import { init as initConnection, send } from '../../platform/client/fetch';
import * as constants from '../constants';
import type {
AppState,
Expand All @@ -15,6 +15,12 @@ export function setAppState(state: Partial<AppState>): SetAppStateAction {
};
}

export function reconnect(connectionName: string) {
return () => {
initConnection(connectionName);
};
}

export function updateApp() {
return async (dispatch: Dispatch) => {
global.Actual.applyAppUpdate();
Expand Down
14 changes: 12 additions & 2 deletions packages/loot-core/src/platform/client/fetch/index.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ let replyHandlers = new Map();
let listeners = new Map();
let messageQueue = [];
let socketClient = null;
let activePort = null;

function connectSocket(port, onOpen) {
// Do nothing if connection to this port is already active
if (socketClient && port === activePort) {
return;
}

let client = new WebSocket('ws://localhost:' + port);
socketClient = client;
activePort = port;

client.onmessage = event => {
const msg = JSON.parse(event.data);
Expand Down Expand Up @@ -62,7 +70,6 @@ function connectSocket(port, onOpen) {
};

client.onopen = event => {
socketClient = client;
// Send any messages that were queued while closed
if (messageQueue.length > 0) {
messageQueue.forEach(msg => {
Expand All @@ -73,10 +80,13 @@ function connectSocket(port, onOpen) {

onOpen();
};

client.onclose = () => {
socketClient = null;
};
}

export const init: T.Init = async function (socketName) {
await clearServer();
return new Promise(resolve => connectSocket(socketName, resolve));
};

Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/1694.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [MatissJanis]
---

Desktop: reconnect to websockets if connection lost or server restarted

0 comments on commit d651414

Please sign in to comment.