From 9fc94ac6218cda9e833db30912999c096cf547c0 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Thu, 17 Sep 2026 12:49:26 +0200 Subject: [PATCH] fix: refresh SIP settings on server config change NethLink parsed config.production.js only at credential login and cached sipHost and sipPort in the account. When NethVoice reassigned the SIP port, typically after an update, the cached values went stale and the PhoneIsland could no longer register with Janus. The user was left with a permanent connection error banner, and the only way out was to log out and log back in. Autologin now re-reads the server config in both the valid-token and the expired-token path, so restarting the app is enough to pick up a new port. While the app is running, two independent triggers detect a change and reload the PhoneIsland: the socket reconnection that follows a server restart, and the webrtc_down alert raised on every failed registration. The reload is deferred to the end of an active call so that a call in progress is never dropped. Also drop PhoneIslandController.reconnect() and its RECONNECT_PHONE_ISLAND event, which had no callers. --- .../classes/controllers/AccountController.ts | 55 +++++++++++++++++-- .../controllers/PhoneIslandController.ts | 14 ----- src/main/lib/ipcEvents.ts | 54 ++++++++++++++++++ .../src/hooks/usePhoneIslandEventListeners.ts | 7 +++ src/renderer/src/pages/PhoneIslandPage.tsx | 4 -- src/shared/constants.ts | 3 +- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index a563097c..82b730ac 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -139,6 +139,10 @@ export class AccountController { return false } + // The saved SIP settings may be stale (e.g. NethVoice updated while NethLink was closed): + // refresh them from the server before the PhoneIsland is built with them + const sipChanged = await this.refreshServerConfig(lastLoggedAccount) + // Update store with the saved account (don't do a new login!) // IMPORTANT: Preserve auth.lastUser and auth.lastUserCryptPsw so they are saved to disk // IMPORTANT: Set connection: true to prevent "No internet connection" banner @@ -154,6 +158,9 @@ export class AccountController { lastUserCryptPsw: authAppData.lastUserCryptPsw } }, 'autoLogin') + if (sipChanged) { + store.saveToDisk() + } return true } else { @@ -176,15 +183,13 @@ export class AccountController { return false } - let loggedAccount: Account = { + const loggedAccount: Account = { ...lastLoggedAccount, ...tempLoggedAccount, theme: lastLoggedAccount.theme || tempLoggedAccount.theme, } - const { parseConfig } = useLogin() - const config: string = await NetworkController.instance.get(`https://${loggedAccount.host}/config/config.production.js`) - loggedAccount = parseConfig(loggedAccount, config) + await this.refreshServerConfig(loggedAccount) await this.saveLoggedAccount(loggedAccount, password) return true } catch (e) { @@ -196,6 +201,48 @@ export class AccountController { return false } + /** + * Re-read the host config.production.js and update the SIP settings of the given account in place. + * It never throws: on network or parsing errors the cached values are kept. + * @returns true when sipHost or sipPort changed + */ + async refreshServerConfig(account: Account): Promise { + try { + const { parseConfig } = useLogin() + const config: string = await NetworkController.instance.get(`https://${account.host}/config/config.production.js`) + const previous = { sipHost: account.sipHost, sipPort: account.sipPort } + parseConfig(account, config) + const changed = previous.sipHost !== account.sipHost || previous.sipPort !== account.sipPort + if (changed) { + Log.info('server SIP config changed', { previous, current: { sipHost: account.sipHost, sipPort: account.sipPort } }) + } + return changed + } catch (e) { + Log.warning('unable to refresh server config, keeping cached SIP settings:', e) + return false + } + } + + /** + * Refresh the SIP settings of the logged account from the server and persist them if changed. + * @returns true when sipHost or sipPort changed + */ + async syncLoggedAccountServerConfig(): Promise { + const account = store.store.account + if (!account) return false + const changed = await this.refreshServerConfig(account) + if (changed) { + store.set('account', account, true) + const auth = store.store.auth + if (auth) { + auth.availableAccounts[getAccountUID(account)] = account + store.set('auth', auth, true) + } + store.saveToDisk() + } + return changed + } + async saveLoggedAccount(account: Account, password: string): Promise { try { // Wait for encryption to become available (max 10 seconds with 500ms intervals) diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 4a3faf39..659268c9 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -365,20 +365,6 @@ export class PhoneIslandController { } } - reconnect() { - try { - Log.info('PHONE ISLAND RECONNECT') - this.window.emit(IPC_EVENTS.RECONNECT_PHONE_ISLAND) - once(IPC_EVENTS.LOGOUT_COMPLETED, () => { - Log.info('PHONE ISLAND RECONNECTION AFTER LOGOUT') - this.window.quit(false) - new PhoneIslandController() - }) - } catch (e) { - Log.warning('error during emitting reconnect event to the PhoneIslandWindow:', e) - } - } - muteAudio() { try { const window = this.window.getWindow() diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index fc608d14..3db2950b 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -35,6 +35,28 @@ export function isCallActive(): boolean { return hasActiveCall } +// Set when the SIP settings changed during a call: the PhoneIsland reload is deferred to the call end +let isPhoneIslandReloadPending = false + +// Minimum interval between two server config checks triggered by PhoneIsland connection errors +const SERVER_CONFIG_CHECK_MIN_INTERVAL = 10_000 +let lastServerConfigCheck = 0 + +/** + * Reload the PhoneIsland so that it registers again with the current SIP host/port. + * When a call is active the reload is deferred to the call end, since it would drop the call. + */ +function reloadPhoneIslandForSipChange(trigger: string) { + if (isCallActive()) { + Log.info(`SIP config changed (${trigger}) but a call is active - PhoneIsland reload deferred to call end`) + isPhoneIslandReloadPending = true + return + } + isPhoneIslandReloadPending = false + Log.info(`SIP config changed (${trigger}) - reloading PhoneIsland`) + PhoneIslandController.instance?.window?.getWindow()?.reload() +} + function onSyncEmitter( channel: IPC_EVENTS, asyncCallback: (...args: any[]) => Promise @@ -475,6 +497,14 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.EMIT_CALL_END, (_) => { Log.info('Call ended - setting hasActiveCall = false') hasActiveCall = false + if (isPhoneIslandReloadPending) { + // give the PhoneIsland the time to close the call UI before reloading it + setTimeout(() => { + if (isPhoneIslandReloadPending) { + reloadPhoneIslandForSipChange('call ended') + } + }, 1000) + } try { NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_CALL_END) } catch (e) { @@ -508,13 +538,37 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.RECONNECT_SOCKET, async () => { try { + const previous = { sipHost: store.store.account?.sipHost, sipPort: store.store.account?.sipPort } + // autoLogin refreshes the SIP settings from the server config too await AccountController.instance.autoLogin() NethLinkController.instance.window.emit(IPC_EVENTS.RECONNECT_SOCKET) + const account = store.store.account + if (account && (account.sipHost !== previous.sipHost || account.sipPort !== previous.sipPort)) { + reloadPhoneIslandForSipChange('socket reconnected') + } } catch (e) { Log.error('SOCKET Reconnection error on logout', e) } }) + ipcMain.on(IPC_EVENTS.CHECK_SERVER_CONFIG, async () => { + // A PhoneIsland WebRTC failure may be caused by SIP settings changed on the server (e.g. NethVoice + // update while NethLink is running): re-read them and reload the PhoneIsland if they changed. + const now = Date.now() + if (now - lastServerConfigCheck < SERVER_CONFIG_CHECK_MIN_INTERVAL) { + return + } + lastServerConfigCheck = now + try { + const changed = await AccountController.instance.syncLoggedAccountServerConfig() + if (changed) { + reloadPhoneIslandForSipChange('webrtc down') + } + } catch (e) { + Log.error('CHECK_SERVER_CONFIG error', e) + } + }) + ipcMain.on(IPC_EVENTS.FULLSCREEN_ENTER, () => { try { PhoneIslandController.instance.window.getWindow()?.setFullScreen(true); diff --git a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts index 3e0d0887..ec86f0f3 100644 --- a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts +++ b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts @@ -336,6 +336,13 @@ export const usePhoneIslandEventListener = () => { // Check if both WebRTC and Socket are ready checkAndTriggerPhoneIslandReady() }), + ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-alert-set"], (data) => { + // A WebRTC failure may be due to SIP settings changed on the server (e.g. after a NethVoice + // update): ask the main process to re-check the server config + if (data?.type === 'webrtc_down') { + window.electron.send(IPC_EVENTS.CHECK_SERVER_CONFIG) + } + }), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-all-alerts-removed"]), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-fullscreen-entered"], () => { window.electron.send(IPC_EVENTS.FULLSCREEN_ENTER) diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 510c9248..da8eb0f1 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -203,10 +203,6 @@ export function PhoneIslandPage() { }) }) - window.electron.receive(IPC_EVENTS.RECONNECT_PHONE_ISLAND, () => { - logout() - }) - window.electron.receive(IPC_EVENTS.CHANGE_DEFAULT_DEVICE, async (deviceInformationObject, force) => { Log.debug('CHANGE_DEFAULT_DEVICE', { force, deviceInformationObject, }) const changed = await NethVoiceAPI.User.default_device(deviceInformationObject, force) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 0d8d372f..f44529ed 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -65,8 +65,8 @@ export enum IPC_EVENTS { REQUEST_SHARED_STATE = "REQUEST_SHARED_STATE", GET_NETHVOICE_CONFIG = "GET_NETHVOICE_CONFIG", SET_NETHVOICE_CONFIG = "SET_NETHVOICE_CONFIG", - RECONNECT_PHONE_ISLAND = "RECONNECT_PHONE_ISLAND", RECONNECT_SOCKET = "RECONNECT_SOCKET", + CHECK_SERVER_CONFIG = "CHECK_SERVER_CONFIG", LOGOUT_COMPLETED = "LOGOUT_COMPLETED", SHOW_NO_CONNECTION = "SHOW_NO_CONNECTION", HIDE_NO_CONNECTION = "HIDE_NO_CONNECTION", @@ -237,6 +237,7 @@ export enum PHONE_ISLAND_EVENTS { 'phone-island-internet-connected' = 'phone-island-internet-connected', 'phone-island-internet-disconnected' = 'phone-island-internet-disconnected', // Alerts + 'phone-island-alert-set' = 'phone-island-alert-set', 'phone-island-all-alerts-removed' = 'phone-island-all-alerts-removed', // Videocall 'phone-island-fullscreen-entered' = 'phone-island-fullscreen-entered',