mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
A Windows tray-icon GUID binds to the executable's full path unless the binary is code-signed with an organization in the signature subject. The Store (MSIX) and portable/scoop builds run from versioned directories whose path changes on every update, so the GUID goes stale and Shell_NotifyIcon(NIM_ADD) fails silently (Electron raises no JS error). new Tray(image, guid) still returns a live Tray, so the try/catch and ensureIndicator() truthiness guards never detected the failure — the window hid to an invisible, unreachable tray. Use the path-bound GUID only for the stable-path NSIS build; create the tray without a GUID on Store and portable, so Windows identifies the icon by window handle and it stays visible across updates. The NSIS path is untouched, preserving its taskbar-pin persistence. Refs: https://www.electronjs.org/docs/latest/api/tray (guid)
This commit is contained in:
parent
2f808a7a26
commit
31b7c3aac7
2 changed files with 85 additions and 29 deletions
|
|
@ -164,9 +164,58 @@ test.beforeEach(() => {
|
|||
test.afterEach(() => {
|
||||
Module._load = originalModuleLoad;
|
||||
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
|
||||
// Windows distribution-channel signals read by getDistChannel(); clear so
|
||||
// they never leak into the next test.
|
||||
delete process.windowsStore;
|
||||
delete process.env.PORTABLE_EXECUTABLE_DIR;
|
||||
resetModule();
|
||||
});
|
||||
|
||||
// Windows tray icon GUIDs are bound to the executable path. NSIS installs to a
|
||||
// stable location, so it keeps its GUID; Store (MSIX) and portable/scoop run
|
||||
// from versioned paths where a stale GUID makes the icon silently invisible
|
||||
// (#7282), so those build the tray without a GUID.
|
||||
const NSIS_TRAY_GUID = 'a2512177-8bee-4b70-a0a8-f3d18e0eab90';
|
||||
|
||||
const initIndicatorOnWindows = () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: 'win32',
|
||||
});
|
||||
const { initIndicator } = loadIndicatorModule();
|
||||
initIndicator({
|
||||
showApp: () => {},
|
||||
quitApp: () => {},
|
||||
ICONS_FOLDER: '/icons/',
|
||||
forceDarkTray: false,
|
||||
app: { on: () => {} },
|
||||
});
|
||||
};
|
||||
|
||||
test('Windows NSIS build creates the tray with a stable GUID', () => {
|
||||
// Neither windowsStore nor PORTABLE_EXECUTABLE_DIR set -> win-nsis.
|
||||
initIndicatorOnWindows();
|
||||
|
||||
assert.equal(createdTrayArgs.length, 1);
|
||||
assert.equal(createdTrayArgs[0][1], NSIS_TRAY_GUID);
|
||||
});
|
||||
|
||||
test('Windows Store (MSIX) build creates the tray without a GUID', () => {
|
||||
process.windowsStore = true;
|
||||
initIndicatorOnWindows();
|
||||
|
||||
assert.equal(createdTrayArgs.length, 1);
|
||||
assert.equal(createdTrayArgs[0][1], undefined);
|
||||
});
|
||||
|
||||
test('Windows portable build creates the tray without a GUID', () => {
|
||||
process.env.PORTABLE_EXECUTABLE_DIR = 'C:\\Users\\test\\sp-portable';
|
||||
initIndicatorOnWindows();
|
||||
|
||||
assert.equal(createdTrayArgs.length, 1);
|
||||
assert.equal(createdTrayArgs[0][1], undefined);
|
||||
});
|
||||
|
||||
test('initIndicator uses NativeImage for Linux tray creation and updates', () => {
|
||||
const { initIndicator } = loadIndicatorModule();
|
||||
|
||||
|
|
|
|||
|
|
@ -62,30 +62,27 @@ const IS_MAC = process.platform === 'darwin';
|
|||
const IS_LINUX = process.platform === 'linux';
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
// Stable GUIDs per Windows distribution type.
|
||||
// Windows ties tray icon GUIDs to the executable path (when unsigned).
|
||||
// Different distribution types have different exe paths, so they need
|
||||
// separate GUIDs to avoid silent tray creation failures.
|
||||
// WARNING: These GUIDs must never change once deployed per distribution type.
|
||||
// Changing a GUID makes Windows treat it as a new icon, resetting it to the
|
||||
// overflow area. Users would have to manually re-show it on the taskbar.
|
||||
// See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
|
||||
const WINDOWS_TRAY_GUIDS = {
|
||||
portable: 'f7c06d50-4d3e-4f8d-b9a0-2c8e7f5a1b3d',
|
||||
nsis: 'a2512177-8bee-4b70-a0a8-f3d18e0eab90',
|
||||
store: '19b9d3fe-aa50-4792-917e-60ada97f3088',
|
||||
} as const;
|
||||
// Stable GUID for the NSIS (installer) build only.
|
||||
// Per Electron's Tray docs, a tray-icon GUID binds to the code-signing
|
||||
// signature only when that signature carries an organization in its subject;
|
||||
// otherwise it binds to the executable's full path, and changing the path
|
||||
// breaks tray-icon creation until a new GUID is used. The GitHub NSIS build is
|
||||
// signed and installs to a fixed path, so its GUID stays valid. The Store
|
||||
// (MSIX) and portable/scoop builds run from versioned directories whose path
|
||||
// changes on every update, so the GUID goes stale and Shell_NotifyIcon(NIM_ADD)
|
||||
// fails silently (Electron raises no JS error) — leaving an invisible tray icon
|
||||
// and an unreachable window (#7282). Those builds therefore create the tray
|
||||
// without a GUID, so Windows identifies it by window handle and it stays visible.
|
||||
// WARNING: This GUID must never change once deployed; Windows would treat a new
|
||||
// value as a new icon and reset it to the overflow area.
|
||||
// Retired GUIDs (shipped in v18.10.0, now GUID-less — never reuse for a new icon):
|
||||
// portable f7c06d50-4d3e-4f8d-b9a0-2c8e7f5a1b3d, store 19b9d3fe-aa50-4792-917e-60ada97f3088
|
||||
// See https://www.electronjs.org/docs/latest/api/tray (guid) and
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
|
||||
const WINDOWS_TRAY_NSIS_GUID = 'a2512177-8bee-4b70-a0a8-f3d18e0eab90';
|
||||
|
||||
const getWindowsTrayGuid = (): string => {
|
||||
const channel = getDistChannel();
|
||||
if (channel === 'win-portable') {
|
||||
return WINDOWS_TRAY_GUIDS.portable;
|
||||
}
|
||||
if (channel === 'win-store') {
|
||||
return WINDOWS_TRAY_GUIDS.store;
|
||||
}
|
||||
return WINDOWS_TRAY_GUIDS.nsis;
|
||||
};
|
||||
const getWindowsTrayGuid = (): string | undefined =>
|
||||
getDistChannel() === 'win-nsis' ? WINDOWS_TRAY_NSIS_GUID : undefined;
|
||||
|
||||
export const initIndicator = ({
|
||||
showApp,
|
||||
|
|
@ -155,13 +152,23 @@ const createTray = (): Tray => {
|
|||
let nextTray: Tray;
|
||||
if (IS_WINDOWS) {
|
||||
const guid = getWindowsTrayGuid();
|
||||
try {
|
||||
nextTray = new Tray(trayIcon, guid);
|
||||
log('Tray created on Windows with GUID:', guid);
|
||||
} catch (e) {
|
||||
log('Tray creation with GUID failed, retrying without GUID:', e);
|
||||
if (guid) {
|
||||
try {
|
||||
nextTray = new Tray(trayIcon, guid);
|
||||
log('Tray created on Windows with GUID:', guid);
|
||||
} catch (e) {
|
||||
// Log only the message, not the full error: log history is exportable
|
||||
// and the error/stack can embed the install path (e.g. C:\Users\<name>\).
|
||||
log(
|
||||
'Tray creation with GUID failed, retrying without GUID:',
|
||||
e instanceof Error ? e.message : e,
|
||||
);
|
||||
nextTray = new Tray(trayIcon);
|
||||
log('Tray created on Windows without GUID');
|
||||
}
|
||||
} else {
|
||||
nextTray = new Tray(trayIcon);
|
||||
log('Tray created on Windows without GUID');
|
||||
log('Tray created on Windows without GUID (versioned install path)');
|
||||
}
|
||||
} else {
|
||||
nextTray = new Tray(trayIcon);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue