fix(playback): withdraw the keep-awake vote when the renderer crashes

A crash emits render-process-gone while the WebContents object stays
alive, so the destroyed listener alone missed it: without a follow-up
reload the display stayed pinned awake. Review finding by Codex.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-10 22:19:34 +02:00
1 parent 7349ed6511
commit f97c5e6923
2 files changed
+19 -2

No files matched your search

@@ -98,6 +98,17 @@ describe('playback keep-awake service', () => {
expect(mockedBlocker.stop).toHaveBeenCalledWith(7);
});
it('releases the blocker when the renderer process crashes', () => {
const stub = createSenderStub(1);
setPlaybackKeepAwake(stub.sender, true);
// A crash emits render-process-gone while the WebContents object
// stays alive; without a reload no other lifetime event follows.
stub.emit('render-process-gone', {}, { reason: 'crashed' });
expect(mockedBlocker.stop).toHaveBeenCalledWith(7);
});
it('releases the blocker on a main-frame navigation (reload)', () => {
const stub = createSenderStub(1);
@@ -127,10 +138,12 @@ describe('playback keep-awake service', () => {
setPlaybackKeepAwake(stub.sender, true);
expect(stub.listenerCount('destroyed')).toBe(1);
expect(stub.listenerCount('render-process-gone')).toBe(1);
expect(stub.listenerCount('did-start-navigation')).toBe(1);
setPlaybackKeepAwake(stub.sender, false);
expect(stub.listenerCount('destroyed')).toBe(0);
expect(stub.listenerCount('render-process-gone')).toBe(0);
expect(stub.listenerCount('did-start-navigation')).toBe(0);
});
@@ -13,8 +13,10 @@ import { powerSaveBlocker, WebContents } from 'electron';
*
* The renderer's flag must not outlive the page that set it: a reload or a
* crashed render process would otherwise pin the display awake until app
* quit. Each activating WebContents therefore gets destroy/navigation
* listeners that withdraw its vote.
* quit. Each activating WebContents therefore gets destroy/crash/navigation
* listeners that withdraw its vote — a crash emits `render-process-gone`
* while the WebContents object stays alive, so `destroyed` alone would miss
* it when no reload follows.
*/
const activeSenders = new Set<number>();
@@ -62,9 +64,11 @@ function watchSenderLifetime(sender: WebContents): void {
}
};
sender.on('destroyed', clear);
sender.on('render-process-gone', clear);
sender.on('did-start-navigation', onNavigation);
senderCleanups.set(senderId, () => {
sender.off('destroyed', clear);
sender.off('render-process-gone', clear);
sender.off('did-start-navigation', onNavigation);
});
}