fix: stop Cockpit work before asynchronous UI teardown

This commit is contained in:
Sameh Khamis committed 2026-09-13 02:46:57 -07:00
1 parent 947fc79182
commit 152d3e2352
11 files changed
+150 -15

No files matched your search

+68 -1
View File
@@ -75,6 +75,33 @@ try {
return unsubscribe?.();
};
}
const cockpit = ui.cockpitView;
const portal = ui._cockpitDisplayPortal;
const cockpitListenerCount = cockpit._listenerRemovers.length;
counts.cockpit = 0;
cockpit._listenerRemovers = cockpit._listenerRemovers.map(
(remove) => () => {
counts.cockpit++;
return remove();
},
);
const portalHomes = portal.records.map((record) => ({
group: record.group,
home: record.anchor.parentNode,
anchor: record.anchor,
}));
portal.setActive(true);
let releaseRestoration;
const restorationGate = new Promise((resolve) => {
releaseRestoration = resolve;
});
const restoreForDisposal = ui._contextControls.restoreForDisposal.bind(
ui._contextControls,
);
ui._contextControls.restoreForDisposal = async () => {
await restorationGate;
return restoreForDisposal();
};
const resizeHandler = ui._windowResizeHandler;
const removeEventListener = window.removeEventListener;
counts.resize = 0;
@@ -83,7 +110,21 @@ try {
return removeEventListener.call(this, type, callback, options);
};
try {
await ui.dispose();
const disposal = ui.dispose();
const focusBefore = document.activeElement;
const stoppedBeforeRestoration =
cockpit.destroyed &&
portal.stopped &&
cockpit._listenerRemovers.length === 0 &&
portal.frames.size === 0 &&
cockpit.enter() === false &&
cockpit.navigateContext(1) === false;
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
const noDeferredFocus = document.activeElement === focusBefore;
releaseRestoration();
await disposal;
const once = JSON.stringify(counts);
await ui.dispose();
window.dispatchEvent(new Event('resize'));
@@ -91,6 +132,23 @@ try {
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
return {
cockpitStoppedBeforeRestoration:
stoppedBeforeRestoration && noDeferredFocus,
cockpitReleased:
cockpit.disposed &&
cockpit.destroyed &&
counts.cockpit === cockpitListenerCount &&
cockpitListenerCount > 0 &&
portal.destroyed &&
portal.listeners.signal.aborted &&
portal.frames.size === 0 &&
ui._cockpitDisplayPortal === null &&
portalHomes.length === 4 &&
portalHomes.every(
(record) =>
record.group.parentNode === record.home &&
!record.anchor.isConnected,
),
connected: Boolean(
cctvUnsubscribe &&
resizeHandler &&
@@ -116,6 +174,7 @@ try {
idempotent: once === JSON.stringify(counts),
};
} finally {
releaseRestoration();
window.removeEventListener = removeEventListener;
}
});
@@ -143,6 +202,14 @@ try {
'Radio, Location and CCTV controls are disposed with the UI',
result.controlsReleased,
);
check(
'Cockpit actions and queued portal work stop before layer restoration',
result.cockpitStoppedBeforeRestoration,
);
check(
'Cockpit cleanup releases listeners and returns all Display groups home',
result.cockpitReleased,
);
check('repeated UI disposal is inert', result.idempotent);
check(
'disposal and subsequent resize produce no uncaught browser errors',