Skip to content
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
37 changes: 37 additions & 0 deletions src/main/noble-ble-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,41 @@ describe('NobleBleManager long-session maintenance (regression)', () => {
expect(SOURCE).toContain('sessionAgeSec');
expect(SOURCE).toContain('getLongSessionHealthSnapshot');
});

/**
* On Linux, Noble is never initialized (Web Bluetooth is used in the renderer instead),
* so `sessions` stays empty for the app's whole lifetime. getLongSessionHealthSnapshot()
* used to call the throwing getSession() unconditionally, so once the hourly main-process
* health-log timer's 24h uptime gate opened, the very first tick threw an uncaught
* "Unknown noble session: meshtastic" in the main process — surfaced to the user as the
* "Mesh-Client — Unexpected Error" dialog on Linux after ~1 day of uptime.
*/
it('reports a benign not-initialized snapshot on Linux instead of throwing from getSession()', () => {
expect(SOURCE).toMatch(
/getLongSessionHealthSnapshot\(\)[\s\S]*?const linuxNotInitialized = this\.isLinuxNotInitialized\(\);/,
);
// The guard must be checked inside sessionDetail() before the throwing getSession() call.
expect(SOURCE).toMatch(
/const sessionDetail = \(sessionId: 'meshtastic' \| 'meshcore'\) => \{\s*if \(linuxNotInitialized\) \{[\s\S]*?\};\s*\}\s*const session = this\.getSession\(sessionId\);/,
);
expect(SOURCE).toContain(
'getLongSessionHealthSnapshot: skipping session detail (not initialized on Linux)',
);
});

/**
* Follow-up cleanup (code review, PR self-review): the `sessions.size === 0` Linux check was
* duplicated across getLongSessionHealthSnapshot(), stopAllScanning(), and disconnectAll().
* Centralize it so all three call sites can't drift if the detection semantics ever change.
*/
it('centralizes the Linux not-initialized check in a shared helper used by all call sites', () => {
expect(SOURCE).toMatch(
/private isLinuxNotInitialized\(\): boolean \{\s*return this\.sessions\.size === 0;\s*\}/,
);
expect(SOURCE).toContain('const linuxNotInitialized = this.isLinuxNotInitialized();');
expect(SOURCE).toContain('if (this.isLinuxNotInitialized()) return;');
expect(SOURCE).toContain('if (this.isLinuxNotInitialized()) {');
// No remaining inline `sessions.size === 0` checks — everything routes through the helper.
expect(SOURCE).not.toMatch(/(?<!return )this\.sessions\.size === 0/);
});
});
30 changes: 27 additions & 3 deletions src/main/noble-ble-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ export class NobleBleManager extends EventEmitter {
return session;
}

/** True on Linux, where Noble is never initialized (Web Bluetooth is used in renderer instead). */
private isLinuxNotInitialized(): boolean {
return this.sessions.size === 0;
}

private clearSessionState(session: NobleBleSession): void {
const peri = session.connectedPeripheral;
const mtuHandler = session.peripheralMtuHandler;
Expand Down Expand Up @@ -694,7 +699,27 @@ export class NobleBleManager extends EventEmitter {
meshcore: ReturnType<typeof sessionDetail>;
};
} {
// On Linux, Noble is not initialized (Web Bluetooth is used in renderer instead), so
// getSession() below would throw. Report a benign "not initialized" snapshot instead.
const linuxNotInitialized = this.isLinuxNotInitialized();
if (linuxNotInitialized) {
console.debug(
'[NobleBleManager] getLongSessionHealthSnapshot: skipping session detail (not initialized on Linux)',
);
}
const sessionDetail = (sessionId: 'meshtastic' | 'meshcore') => {
if (linuxNotInitialized) {
return {
connected: false,
peripheralId: null,
sessionAgeSec: null,
postWriteTimer: false,
notifyWatchdog: false,
gattInflight: false,
readPumpActive: false,
fromRadioPackets: 0,
};
}
const session = this.getSession(sessionId);
const established = session.sessionEstablishedAtMs;
return {
Expand Down Expand Up @@ -783,7 +808,7 @@ export class NobleBleManager extends EventEmitter {

/** Stop all scanning immediately — used for app quit and force-quit IPC. */
async stopAllScanning(): Promise<void> {
if (this.sessions.size === 0) return;
if (this.isLinuxNotInitialized()) return;
this.scanRequesters.clear();
await this.doStopScanning();
bleCoexistenceCoordinator.releaseScan('noble');
Expand Down Expand Up @@ -1715,8 +1740,7 @@ export class NobleBleManager extends EventEmitter {
}

async disconnectAll(): Promise<void> {
// On Linux, Noble is not initialized (Web Bluetooth is used in renderer instead)
if (this.sessions.size === 0) {
if (this.isLinuxNotInitialized()) {
console.debug('[NobleBleManager] disconnectAll: skipping (not initialized on Linux)');
return;
}
Expand Down