fix: close and recreate macOS windows safely

This commit is contained in:
鲨鱼辣椒
2026-09-29 19:50:25 +08:00
parent f443f3bc66
commit b5e6c663ef
19 changed files with 1079 additions and 172 deletions

View File

@@ -312,6 +312,7 @@ export class AgentBrowserModule {
}
open(input: AgentBrowserOpenInput): Promise<AgentBrowserSnapshot> {
const epoch = this.queueEpoch;
return this.serialize(async () => {
this.assertAvailable();
const projectPath = normalizeRequiredPath(input.projectPath);
@@ -322,6 +323,7 @@ export class AgentBrowserModule {
if (this.record && !samePath(this.record.projectPath, projectPath)) {
await this.closeInternal();
this.assertCommandEpoch(epoch);
}
if (this.record) {
const record = this.record;
@@ -335,6 +337,7 @@ export class AgentBrowserModule {
}
if (record.state !== 'attached') {
await this.closeInternal();
this.assertCommandEpoch(epoch);
}
}
if (this.record) {
@@ -346,6 +349,7 @@ export class AgentBrowserModule {
: Boolean(record.previewData);
if (shouldReplacePreview) {
await this.closeInternal(record);
this.assertCommandEpoch(epoch);
}
}
if (this.record) {
@@ -357,6 +361,7 @@ export class AgentBrowserModule {
try {
if (diagnosticOwner) {
await this.updateDiagnosticOwner(record, diagnosticOwner, true);
this.assertCommandEpoch(epoch);
}
if (bounds) {
this.applyPresentation(record, input.visible ?? true, bounds);
@@ -403,9 +408,14 @@ export class AgentBrowserModule {
this.registerListeners(record);
record.view.webContents.denyWindowOpen();
if (bounds) record.view.setBounds(bounds);
record.view.setVisible(record.visible);
this.adapter.mount(record.view);
try {
if (bounds) record.view.setBounds(bounds);
record.view.setVisible(record.visible);
this.adapter.mount(record.view);
} catch (error) {
await this.closeInternal(record);
throw error;
}
try {
await this.runWhileActive(
@@ -413,12 +423,15 @@ export class AgentBrowserModule {
record.view.webContents.loadURL(RENDERER_PRIME_URL),
OPEN_TIMEOUT_MS,
);
this.assertCommandEpoch(epoch);
await this.runWhileActive(
record,
this.attachDebugger(record, false),
OPEN_TIMEOUT_MS,
);
this.assertCommandEpoch(epoch);
if (injectProjectData) await this.installPreviewData(record, undefined, targetUrl);
this.assertCommandEpoch(epoch);
} catch (error) {
await this.closeInternal(record);
throw toFault(
@@ -432,6 +445,7 @@ export class AgentBrowserModule {
try {
await this.loadPageUntilReady(record, targetUrl);
this.assertCommandEpoch(epoch);
try {
record.view.webContents.navigationHistory.clear();
} catch {
@@ -658,8 +672,18 @@ export class AgentBrowserModule {
async close(projectPath?: string): Promise<AgentBrowserSnapshot> {
if (this.record && projectPath) this.assertProject(this.record, projectPath);
this.preemptCommands('开发浏览器已关闭。');
await this.closeInternal();
return this.closedSnapshot();
const previousBarrier = this.lifecycleBarrier;
let releaseBarrier!: () => void;
this.lifecycleBarrier = new Promise<void>((resolveBarrier) => {
releaseBarrier = resolveBarrier;
});
try {
await previousBarrier;
await this.closeInternal();
return this.closedSnapshot();
} finally {
releaseBarrier();
}
}
async resetProfile(projectPath: string): Promise<AgentBrowserSnapshot> {
@@ -1709,6 +1733,7 @@ export class AgentBrowserModule {
if (expected && this.record !== expected) return;
record.state = 'closing';
await this.invalidatePreviewData(record, 'preview_closed');
if (this.record !== record) return;
this.notifyLifecycle({
type: 'closed',
projectId: record.projectId,
@@ -1812,15 +1837,7 @@ export class AgentBrowserModule {
try {
await barrier;
if (callerSettled) return;
if (epoch !== this.queueEpoch) {
throw new AgentBrowserFault(
'CLOSED',
'开发浏览器操作已被新的生命周期取代。',
true,
this.record?.generation,
'unknown',
);
}
this.assertCommandEpoch(epoch);
const value = await operation();
if (!callerSettled) {
callerSettled = true;
@@ -1842,6 +1859,18 @@ export class AgentBrowserModule {
return caller;
}
private assertCommandEpoch(epoch: number): void {
if (epoch !== this.queueEpoch) {
throw new AgentBrowserFault(
'CLOSED',
'开发浏览器操作已被新的生命周期取代。',
true,
this.record?.generation,
'unknown',
);
}
}
private serializeWithTimeout<T>(
operation: () => Promise<T>,
timeoutMs: number,