Skip to content

Commit e0c06ce

Browse files
committed
fix(compositor): bypass Electron's asar-transparent fs when probing for real asset files
`resolveSceneAssetPath`'s candidate-probe loop (from the previous commit) used plain `fs.existsSync`, which Electron patches to read *through* an asar archive — from JS's point of view, `.../app.asar/dist/wallpapers/x.jpg` "exists". So the probe locked onto the VITE_PUBLIC candidate (pointing into the asar) on every call and never reached the real `resourcesPath` files, making the previous fix a no-op in the actual packaged app: rebuilt and relaunched it, and the exact same "wallpaper image ... introuvable" errors came right back. Confirmed the mechanism by running a probe script inside the packaged binary via `ELECTRON_RUN_AS_NODE=1`: patched `fs.existsSync` on the asar-internal path answered `true`; the native addon (raw `fopen`/`CreateFile`, no knowledge of asar) still gets ENOENT on that same path. `original-fs` is Electron's own escape hatch for this — same API, unpatched. Route the existence check through it, falling back to plain `fs` where `original-fs` doesn't exist (outside Electron, i.e. the vitest suite), since plain Node's `fs` was never patched in the first place there. Re-verified live after rebuilding: the same project's bundled wallpaper and themed cursor sprite now render in the packaged app, and the "introuvable"/ENOENT log lines are gone.
1 parent f7a020d commit e0c06ce

1 file changed

Lines changed: 24 additions & 9 deletions

File tree

electron/native-bridge/services/compositorViewService.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,30 @@ import type {
2424
const localRequire: NodeRequire = createRequire(import.meta.url) as unknown as NodeRequire;
2525

2626
/**
27-
* The native compositor is a separate process and can only read absolute
28-
* filesystem paths — it can't resolve renderer-relative asset URLs like
29-
* `/wallpapers/wallpaper1.jpg` or fetch `http(s)://`/`data:` URLs. Bundled
30-
* wallpapers live under `process.env.VITE_PUBLIC` (dev: `<root>/public`,
31-
* packaged: the renderer dist), so rewrite an image background's root-relative
32-
* path to that absolute location before handing the scene to the addon. Other
33-
* schemes (data:, http:) are left as-is; the native side falls back to a flat
34-
* colour when it can't load them. Malformed JSON passes through untouched.
27+
* `existsSync` that answers for the REAL filesystem, not Electron's asar-transparent view of it.
28+
*
29+
* Electron patches `fs` so JS can read `.../app.asar/dist/wallpapers/x.jpg` as if it were a plain
30+
* file — that's the entire point of asar. But `existsSync` inherits the same patch: called on an
31+
* asar-internal path it returns `true`, even though nothing outside Electron's own patched `fs`
32+
* can ever open that path — the Rust addon calls the raw OS `fopen`/`CreateFile`, which has no
33+
* concept of asar and gets ENOENT. A candidate-probe loop built on the patched `fs.existsSync`
34+
* therefore locks onto the WRONG candidate (VITE_PUBLIC, pointing into the asar) before ever
35+
* trying the right one (`resourcesPath`, real files on disk) — confirmed by running this exact
36+
* check inside the packaged binary: patched `existsSync` on the asar path answered `true`.
37+
*
38+
* `original-fs` is Electron's escape hatch for precisely this: the same API, unpatched. It only
39+
* exists inside Electron, so fall back to plain `node:fs` where it doesn't — under plain Node
40+
* (tests) there is no asar patch to route around in the first place, so plain `fs` already tells
41+
* the truth there.
3542
*/
43+
function realExistsSync(candidate: string): boolean {
44+
try {
45+
return (localRequire("original-fs") as typeof fs).existsSync(candidate);
46+
} catch {
47+
return fs.existsSync(candidate);
48+
}
49+
}
50+
3651
/**
3752
* Bases holding the `wallpapers/` and `cursors/` trees, most specific first.
3853
*
@@ -64,7 +79,7 @@ function sceneAssetBaseDirs(): string[] {
6479
export function resolveSceneAssetPath(relativePath: string): string | null {
6580
for (const base of sceneAssetBaseDirs()) {
6681
const candidate = path.join(base, relativePath);
67-
if (fs.existsSync(candidate)) {
82+
if (realExistsSync(candidate)) {
6883
return candidate;
6984
}
7085
}

0 commit comments

Comments
 (0)