mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Open loaded Linux desktop windows when hidden rendering stalls (#6726)
* fix(desktop): show the window when ready-to-show never fires on Linux Reveal hidden Linux windows that do not receive an initial compositor frame. * fix(desktop): open loaded Linux windows without waiting for a frame A hidden Linux window can finish loading without painting, so waiting only for ready-to-show leaves the application invisible. Reveal the loaded document and retain the frame-ready path on other platforms. Co-authored-by: mistop <jhlim@mistop.org> --------- Co-authored-by: mistop <jhlim@mistop.org> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -311,6 +311,22 @@ frames non-stop, pinning ProMotion displays at 120Hz forever and draining the
|
||||
battery while the app is idle — so do not re-add it. The probe's visibility
|
||||
guards already prevent throttling from causing a false stall.
|
||||
|
||||
### Desktop Linux hidden-window fallback
|
||||
|
||||
On Wayland (seen on ChromeOS Linux) a window created with `show: false` can sit
|
||||
forever without a single compositor frame: the renderer loads and runs, but
|
||||
`ready-to-show` never fires, so the window is never shown and the app looks like
|
||||
it did not start while every process is alive. `showWindowWhenReady`
|
||||
(`packages/desktop/src/window/show-when-ready.ts`) shows the main window when
|
||||
the document finishes loading if the event has not arrived. The fallback is
|
||||
Linux-only; macOS and Windows keep waiting for `ready-to-show`.
|
||||
|
||||
Run the startup regression on Linux with an isolated display:
|
||||
|
||||
```bash
|
||||
xvfb-run -a npm run test:e2e:window-startup --workspace=@getpaseo/desktop
|
||||
```
|
||||
|
||||
### Daemon logs
|
||||
|
||||
Check `$PASEO_HOME/daemon.log` for daemon logs. The default level is `info`; set
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { _electron as electron, expect } from "playwright/test";
|
||||
|
||||
const root = await mkdtemp(path.join(tmpdir(), "paseo-window-startup-"));
|
||||
const modulePath = fileURLToPath(new URL("../dist/window/show-when-ready.js", import.meta.url));
|
||||
|
||||
async function launchWindowWithoutHiddenFrames() {
|
||||
const main = path.join(root, "main.cjs");
|
||||
await writeFile(
|
||||
main,
|
||||
`const { app, BrowserWindow } = require("electron");
|
||||
const { showWindowWhenReady } = require(${JSON.stringify(modulePath)});
|
||||
app.setPath("userData", ${JSON.stringify(path.join(root, "user-data"))});
|
||||
app.whenReady().then(async () => {
|
||||
const win = global.mainWindow = new BrowserWindow({ show: false, paintWhenInitiallyHidden: false });
|
||||
showWindowWhenReady(win, { showOnLoad: true });
|
||||
await win.loadURL("data:text/html,<h1>Desktop ready</h1>");
|
||||
global.visibleAfterLoad = win.isVisible();
|
||||
});`,
|
||||
);
|
||||
const env = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key !== "ELECTRON_RUN_AS_NODE"),
|
||||
);
|
||||
const displayArgs = process.platform === "linux" ? ["--ozone-platform=x11", "--disable-gpu"] : [];
|
||||
return electron.launch({
|
||||
args: ["--no-sandbox", ...displayArgs, main],
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
async function expectLoadedDesktopIsShown(desktop) {
|
||||
await expect.poll(() => desktop.evaluate(() => global.visibleAfterLoad)).toBe(true);
|
||||
const page = await desktop.firstWindow();
|
||||
await expect(page.getByRole("heading", { name: "Desktop ready" })).toBeVisible();
|
||||
}
|
||||
|
||||
let desktop;
|
||||
try {
|
||||
desktop = await launchWindowWithoutHiddenFrames();
|
||||
await expectLoadedDesktopIsShown(desktop);
|
||||
console.log("Loaded desktop is visible without hidden compositor frames");
|
||||
} finally {
|
||||
await desktop?.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
"verify:electron-cdp": "node ./scripts/verify-electron-cdp.mjs",
|
||||
"test": "install-electron && vitest run --exclude \"e2e/**\"",
|
||||
"typecheck": "tsgo --noEmit -p tsconfig.json",
|
||||
"test:e2e:lifecycle": "npm run build:main && node ./e2e/daemon-lifecycle.e2e.mjs"
|
||||
"test:e2e:lifecycle": "npm run build:main && node ./e2e/daemon-lifecycle.e2e.mjs",
|
||||
"test:e2e:window-startup": "npm run build:main && node ./e2e/window-startup.e2e.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
buildStandardContextMenuItems,
|
||||
} from "./window/window-manager.js";
|
||||
import { setupDarwinCompositorWatchdog } from "./window/compositor-watchdog/index.js";
|
||||
import { showWindowWhenReady } from "./window/show-when-ready.js";
|
||||
import { resolveDesktopWindowChromeMode, windowChromeModeArgument } from "./window/chrome.js";
|
||||
import { registerDialogHandlers } from "./features/dialogs.js";
|
||||
import {
|
||||
@@ -773,8 +774,8 @@ async function createWindow(
|
||||
registerBrowserWebviewNavigationGuards(contents);
|
||||
});
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
showWindowWhenReady(mainWindow, {
|
||||
showOnLoad: process.platform === "linux",
|
||||
});
|
||||
|
||||
if (!app.isPackaged) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { showWindowWhenReady } from "./show-when-ready.js";
|
||||
|
||||
class Window extends EventEmitter {
|
||||
webContents = new EventEmitter();
|
||||
shows = 0;
|
||||
destroyed = false;
|
||||
show() {
|
||||
this.shows += 1;
|
||||
}
|
||||
isDestroyed() {
|
||||
return this.destroyed;
|
||||
}
|
||||
close() {
|
||||
this.destroyed = true;
|
||||
this.emit("closed");
|
||||
}
|
||||
}
|
||||
|
||||
describe("showWindowWhenReady", () => {
|
||||
it("shows a loaded Linux window even when no initial frame was painted", () => {
|
||||
const win = new Window();
|
||||
showWindowWhenReady(win, { showOnLoad: true });
|
||||
expect(win.shows).toBe(0);
|
||||
win.webContents.emit("did-finish-load");
|
||||
expect(win.shows).toBe(1);
|
||||
win.emit("ready-to-show");
|
||||
expect(win.shows).toBe(1);
|
||||
});
|
||||
|
||||
it("shows on the first frame without showing again after load", () => {
|
||||
const win = new Window();
|
||||
showWindowWhenReady(win, { showOnLoad: true });
|
||||
win.emit("ready-to-show");
|
||||
win.webContents.emit("did-finish-load");
|
||||
expect(win.shows).toBe(1);
|
||||
});
|
||||
|
||||
it("preserves waiting for the initial frame when show on load is disabled", () => {
|
||||
const win = new Window();
|
||||
showWindowWhenReady(win, { showOnLoad: false });
|
||||
win.webContents.emit("did-finish-load");
|
||||
expect(win.shows).toBe(0);
|
||||
win.emit("ready-to-show");
|
||||
expect(win.shows).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps a closed window closed when readiness arrives later", () => {
|
||||
const win = new Window();
|
||||
showWindowWhenReady(win, { showOnLoad: true });
|
||||
win.close();
|
||||
win.webContents.emit("did-finish-load");
|
||||
win.emit("ready-to-show");
|
||||
expect(win.shows).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
interface ReadyToShowWindow {
|
||||
once(event: "ready-to-show", listener: () => void): unknown;
|
||||
webContents: {
|
||||
once(event: "did-finish-load", listener: () => void): unknown;
|
||||
};
|
||||
show(): void;
|
||||
isDestroyed(): boolean;
|
||||
}
|
||||
|
||||
export function showWindowWhenReady(
|
||||
win: ReadyToShowWindow,
|
||||
options: { showOnLoad: boolean },
|
||||
): void {
|
||||
let shown = false;
|
||||
const show = () => {
|
||||
if (shown || win.isDestroyed()) return;
|
||||
shown = true;
|
||||
win.show();
|
||||
};
|
||||
|
||||
win.once("ready-to-show", show);
|
||||
// On Linux a hidden window can wait for compositor frames until it is shown.
|
||||
// Loading the document is sufficient to reveal it without depending on a frame.
|
||||
if (options.showOnLoad) win.webContents.once("did-finish-load", show);
|
||||
}
|
||||
Reference in New Issue
Block a user