mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
resolveNodeExecutable() built the macOS Helper path from app.getName() (package.json
name = "omniroute-desktop"), but electron-builder names the Helper.app bundles from
build.productName ("OmniRoute"). The two diverged, so the probe never matched a real
Helper and fell through to process.execPath — spawning the main Electron binary via
ELECTRON_RUN_AS_NODE, which macOS renders as a second, inert Dock icon.
Extracted the resolution into electron/lib/resolveNodeHelper.js (pure, unit-testable),
deriving the Helper name from path.basename(process.execPath) so it always tracks the
productName-generated bundle. Registered the new module in electron build.files.
Reported by Carlos Espinoza (Carloss616) with runtime instrumentation of a packaged
.app pinpointing the exact field divergence.
This commit is contained in:
committed by
GitHub
parent
3238df3204
commit
4e57c1dc9a
1
changelog.d/fixes/7941-macos-dock-icon.md
Normal file
1
changelog.d/fixes/7941-macos-dock-icon.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(electron): resolve macOS Helper by binary name to remove second inert Dock icon (#7941)
|
||||
46
electron/lib/resolveNodeHelper.js
Normal file
46
electron/lib/resolveNodeHelper.js
Normal file
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* Resolve the macOS Helper binary used to spawn the Next.js standalone server via
|
||||
* ELECTRON_RUN_AS_NODE, so macOS treats it as a background task with no Dock icon.
|
||||
*
|
||||
* #7941: the Helper name is derived from `path.basename(execPath)` — electron-builder
|
||||
* generates BOTH the main binary and the Helper.app bundles from `build.productName`
|
||||
* ("OmniRoute"). The previous code used `app.getName()`, which reads package.json
|
||||
* `name` ("omniroute-desktop"); the two diverged, so it never matched a real Helper
|
||||
* path and the caller fell through to `process.execPath`, spawning the main Electron
|
||||
* binary and producing a second, inert Dock icon.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.execPath Absolute path to the packaged Electron binary (process.execPath).
|
||||
* @param {(p: string) => boolean} [opts.existsSync] Injectable fs.existsSync (for tests).
|
||||
* @returns {string|null} The Helper binary path, or null when none exists (caller falls back to execPath).
|
||||
*/
|
||||
function resolveDarwinHelperExecutable({ execPath, existsSync = fs.existsSync } = {}) {
|
||||
if (!execPath) return null;
|
||||
const appName = path.basename(execPath);
|
||||
|
||||
// Sibling Helper next to the main binary.
|
||||
const siblingHelper = path.join(path.dirname(execPath), `${appName} Helper`);
|
||||
if (existsSync(siblingHelper)) return siblingHelper;
|
||||
|
||||
// Electron >= 20 ships Helper bundles under Contents/Frameworks. The unsuffixed
|
||||
// Helper (not "(Renderer)"/"(GPU)"/"(Plugin)") is the one suitable for ELECTRON_RUN_AS_NODE.
|
||||
const frameworkHelper = path.join(
|
||||
path.dirname(execPath),
|
||||
"..",
|
||||
"Frameworks",
|
||||
`${appName} Helper.app`,
|
||||
"Contents",
|
||||
"MacOS",
|
||||
`${appName} Helper`
|
||||
);
|
||||
if (existsSync(frameworkHelper)) return frameworkHelper;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { resolveDarwinHelperExecutable };
|
||||
@@ -36,6 +36,7 @@ const { hasEncryptedCredentials } = require("./sqlite-inspection");
|
||||
const { loginManager } = require("./loginManager");
|
||||
const { killProcessTree } = require("./processTree");
|
||||
const { resolveServerEntry } = require("./lib/resolveServerEntry");
|
||||
const { resolveDarwinHelperExecutable } = require("./lib/resolveNodeHelper");
|
||||
|
||||
// ── Single Instance Lock ───────────────────────────────────
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -79,23 +80,16 @@ function resolveNodeExecutable(env = process.env) {
|
||||
// flash a shell window. Use the Helper binary instead — macOS treats
|
||||
// Helper processes as background tasks with no visible UI artifacts.
|
||||
if (process.platform === "darwin" && !isDev) {
|
||||
const helperPath = path.join(path.dirname(process.execPath), `${app.getName()} Helper`);
|
||||
if (fs.existsSync(helperPath)) {
|
||||
return helperPath;
|
||||
}
|
||||
// Electron \u003e= 20 may use "(Renderer)" / "(GPU)" / "(Plugin)" suffixed helpers.
|
||||
// The unsuffixed Helper is the one suitable for ELECTRON_RUN_AS_NODE.
|
||||
const frameworkHelper = path.join(
|
||||
path.dirname(process.execPath),
|
||||
"..",
|
||||
"Frameworks",
|
||||
`${app.getName()} Helper.app`,
|
||||
"Contents",
|
||||
"MacOS",
|
||||
`${app.getName()} Helper`
|
||||
);
|
||||
if (fs.existsSync(frameworkHelper)) {
|
||||
return frameworkHelper;
|
||||
// #7941: derive the Helper name from the packaged binary name
|
||||
// (path.basename(process.execPath)) rather than app.getName(). electron-builder
|
||||
// generates BOTH the main binary and the Helper.app bundles from build.productName
|
||||
// ("OmniRoute"), whereas app.getName() reads package.json `name` ("omniroute-desktop")
|
||||
// — the two diverged, so app.getName() never matched a real Helper path and this fell
|
||||
// through to process.execPath, spawning the main Electron binary and producing a
|
||||
// second, inert macOS Dock icon.
|
||||
const helper = resolveDarwinHelperExecutable({ execPath: process.execPath });
|
||||
if (helper) {
|
||||
return helper;
|
||||
}
|
||||
}
|
||||
return process.execPath;
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"processTree.js",
|
||||
"sqlite-inspection.js",
|
||||
"lib/resolveServerEntry.js",
|
||||
"lib/resolveNodeHelper.js",
|
||||
"package.json",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
|
||||
68
tests/unit/electron-resolve-node-executable-7941.test.ts
Normal file
68
tests/unit/electron-resolve-node-executable-7941.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Regression test for #7941 — macOS second, inert Dock icon.
|
||||
*
|
||||
* The Next.js standalone server is spawned via ELECTRON_RUN_AS_NODE. On macOS it must
|
||||
* run through the Helper binary (a background LSUIElement task) so no extra Dock icon
|
||||
* appears. resolveDarwinHelperExecutable() derives the Helper name from
|
||||
* path.basename(execPath) — electron-builder generates BOTH the main binary and the
|
||||
* Helper.app bundles from build.productName ("OmniRoute"). The old code used
|
||||
* app.getName() (package.json `name` = "omniroute-desktop"); the two diverged, so it
|
||||
* never matched a real Helper path and fell through to process.execPath — spawning the
|
||||
* main Electron binary and producing a second, inert Dock icon.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { resolveDarwinHelperExecutable } = require("../../electron/lib/resolveNodeHelper.js");
|
||||
|
||||
const EXEC = "/Applications/OmniRoute.app/Contents/MacOS/OmniRoute";
|
||||
|
||||
describe("#7941 — resolveDarwinHelperExecutable()", () => {
|
||||
it("resolves the productName-derived framework Helper even though app.getName() would say 'omniroute-desktop'", () => {
|
||||
const frameworkHelper = path.join(
|
||||
path.dirname(EXEC),
|
||||
"..",
|
||||
"Frameworks",
|
||||
"OmniRoute Helper.app",
|
||||
"Contents",
|
||||
"MacOS",
|
||||
"OmniRoute Helper"
|
||||
);
|
||||
const result = resolveDarwinHelperExecutable({
|
||||
execPath: EXEC,
|
||||
existsSync: (p: string) => p === frameworkHelper, // only the REAL helper exists
|
||||
});
|
||||
assert.equal(
|
||||
result,
|
||||
frameworkHelper,
|
||||
"must resolve the Helper derived from basename(execPath), NOT fall back to null/execPath"
|
||||
);
|
||||
// Guard against the exact regression: it must never hand back the main binary name.
|
||||
assert.notEqual(result, EXEC);
|
||||
});
|
||||
|
||||
it("prefers the sibling (unsuffixed) Helper next to the main binary when present", () => {
|
||||
const sibling = path.join(path.dirname(EXEC), "OmniRoute Helper");
|
||||
const result = resolveDarwinHelperExecutable({
|
||||
execPath: EXEC,
|
||||
existsSync: (p: string) => p === sibling,
|
||||
});
|
||||
assert.equal(result, sibling);
|
||||
});
|
||||
|
||||
it("returns null when no Helper bundle exists (caller then falls back to execPath)", () => {
|
||||
const result = resolveDarwinHelperExecutable({
|
||||
execPath: EXEC,
|
||||
existsSync: () => false,
|
||||
});
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
it("returns null defensively when execPath is missing", () => {
|
||||
assert.equal(resolveDarwinHelperExecutable({ execPath: "" }), null);
|
||||
assert.equal(resolveDarwinHelperExecutable({}), null);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user