From 1c8a966991580d1bbb106bf8262e2c04fddd0fa4 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Mon, 5 Oct 2026 00:03:13 +0800 Subject: [PATCH 1/3] fix(desktop): launch browser workers through a native UIElement bundle --- .../desktop/scripts/prepare-browser-worker.ts | 24 +++++++++++++++++ .../desktop/scripts/verify-workdsh-carrier.ts | 5 ++++ apps/desktop/src/browser-worker.ts | 10 +++++++ apps/desktop/src/workdsh-main.ts | 3 ++- apps/desktop/tests/browser-worker.spec.ts | 27 +++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/scripts/prepare-browser-worker.ts create mode 100644 apps/desktop/src/browser-worker.ts create mode 100644 apps/desktop/tests/browser-worker.spec.ts diff --git a/apps/desktop/scripts/prepare-browser-worker.ts b/apps/desktop/scripts/prepare-browser-worker.ts new file mode 100644 index 0000000000..7034a365a6 --- /dev/null +++ b/apps/desktop/scripts/prepare-browser-worker.ts @@ -0,0 +1,24 @@ +import { copyFileSync, mkdirSync, symlinkSync } from 'node:fs' +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { browserWorkerBundle } from '../src/browser-worker.ts' + +/** Share the carrier resources and Electron frameworks; copy only its small launcher. */ +export async function prepareBrowserWorker(appContents: string, mainExecutable: string): Promise { + const contents = join(appContents, 'Helpers', browserWorkerBundle, 'Contents') + mkdirSync(join(contents, 'MacOS'), { recursive: true }) + const executable = join(contents, 'MacOS', 'WorkDSH Browser') + copyFileSync(mainExecutable, executable) + copyFileSync(join(appContents, 'Info.plist'), join(contents, 'Info.plist')) + const plist = join(contents, 'Info.plist') + for (const command of [ + 'Set :CFBundleExecutable WorkDSH Browser', + 'Set :CFBundleName WorkDSH Browser', + 'Set :CFBundleIdentifier io.techflag.dsh.ssh.browser-worker', + 'Add :LSUIElement bool true', + ]) execFileSync('/usr/libexec/PlistBuddy', ['-c', command, plist]) + symlinkSync('../../../Resources', join(contents, 'Resources')) + symlinkSync('../../../Frameworks', join(contents, 'Frameworks')) + // Electron's fuse wire lives in the shared Framework, verified by the existing + // carrier gate. The helper does not introduce a second Electron installation. +} diff --git a/apps/desktop/scripts/verify-workdsh-carrier.ts b/apps/desktop/scripts/verify-workdsh-carrier.ts index 627c899496..481c296884 100644 --- a/apps/desktop/scripts/verify-workdsh-carrier.ts +++ b/apps/desktop/scripts/verify-workdsh-carrier.ts @@ -8,6 +8,7 @@ import { DSH_VERSION } from './runtime-version.mjs' import { ENTERPRISE_PACKAGES, PRODUCT_PACKAGES, RELEASE_PACKAGES } from './workdsh-package-boundary.mjs' import { verifyDefaultComposition, verifyDefaultProfile, verifyInstalledDshVersions, verifyOfficialWebPackages, verifyProfileRelease, verifyReleaseArchives } from './verify-profile-release.mjs' import { parseDeploymentConfig } from '../src/deployment-config.ts' +import { prepareBrowserWorker } from './prepare-browser-worker.ts' interface PackContext { appOutDir: string @@ -144,6 +145,10 @@ export async function afterPack(context: PackContext): Promise { throw new Error(`Packaged WorkDSH plugin inventory check failed: ${String(inventory.error ?? inventory.stderr)}`) } process.stdout.write(inventory.stdout) + if (context.electronPlatformName === 'darwin') { + const contents = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents') + await prepareBrowserWorker(contents, join(contents, 'MacOS', context.packager.appInfo.productFilename)) + } console.log(`Verified thin Electron carrier and ${names.length} root / ${instances} total Harness ${DSH_VERSION} package instances`) } diff --git a/apps/desktop/src/browser-worker.ts b/apps/desktop/src/browser-worker.ts new file mode 100644 index 0000000000..507477660c --- /dev/null +++ b/apps/desktop/src/browser-worker.ts @@ -0,0 +1,10 @@ +import { dirname, join } from 'node:path' + +export const browserWorkerBundle = 'WorkDSH Browser.app' + +/** Resolve a native UIElement bundle rather than launching the foreground app. */ +export function browserWorkerExecutable(executable: string, platform: NodeJS.Platform, development: boolean): string { + return platform === 'darwin' && !development + ? join(dirname(dirname(executable)), 'Helpers', browserWorkerBundle, 'Contents', 'MacOS', 'WorkDSH Browser') + : executable +} diff --git a/apps/desktop/src/workdsh-main.ts b/apps/desktop/src/workdsh-main.ts index d33ce8845a..0da9ad7a22 100644 --- a/apps/desktop/src/workdsh-main.ts +++ b/apps/desktop/src/workdsh-main.ts @@ -19,6 +19,7 @@ import { EnterpriseLogin, startEnterpriseAuthority, type Authority } from './ent import { desktopEnterprisePatch, enterpriseEnvironment, enterpriseSpace, materializeRuntimeProfile, markProfileUpdated, officialLauncher, officialHostLauncher } from './local-runtime.ts' import { manageCommand } from './command-management.ts' import { cleanOwnedProcessTree, stopOwnedProcess } from './owned-process.ts' +import { browserWorkerExecutable } from './browser-worker.ts' const PROFILE_NAME = 'workdsh' const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u @@ -178,7 +179,7 @@ function runtimeEnvironment(home: string): NodeJS.ProcessEnv { DSH_HOME: home, DSH_AGENTS_HOME: enterprise ? join(enterprise.root, 'agents') : join(home, 'agents'), DSH_BUNDLED_PRIMARY_RUNTIME: bundledPrimaryRuntime(), - DSH_ELECTRON_EXECUTABLE: process.execPath, + DSH_ELECTRON_EXECUTABLE: browserWorkerExecutable(process.execPath, process.platform, Boolean(process.defaultApp)), ELECTRON_RUN_AS_NODE: undefined, } } diff --git a/apps/desktop/tests/browser-worker.spec.ts b/apps/desktop/tests/browser-worker.spec.ts new file mode 100644 index 0000000000..a0145c2396 --- /dev/null +++ b/apps/desktop/tests/browser-worker.spec.ts @@ -0,0 +1,27 @@ +import { expect, it } from 'vitest' +import { execFileSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { browserWorkerExecutable } from '../src/browser-worker.ts' +import { prepareBrowserWorker } from '../scripts/prepare-browser-worker.ts' + +it('uses the native background bundle only in packaged macOS', () => { + expect(browserWorkerExecutable('/Applications/WorkDSH.app/Contents/MacOS/WorkDSH', 'darwin', false)).toBe('/Applications/WorkDSH.app/Contents/Helpers/WorkDSH Browser.app/Contents/MacOS/WorkDSH Browser') + expect(browserWorkerExecutable('/development/Electron', 'darwin', true)).toBe('/development/Electron') + expect(browserWorkerExecutable('C:/WorkDSH.exe', 'win32', false)).toBe('C:/WorkDSH.exe') +}) + +it.skipIf(process.platform !== 'darwin')('declares UIElement before launch and shares existing frameworks and resources', async () => { + const root = mkdtempSync(join(tmpdir(), 'workdsh-native-worker-')) + const contents = join(root, 'WorkDSH.app', 'Contents') + try { + mkdirSync(join(contents, 'Resources'), { recursive: true }); mkdirSync(join(contents, 'Frameworks')) + writeFileSync(join(contents, 'Info.plist'), 'CFBundleExecutableWorkDSHCFBundleNameWorkDSHCFBundleIdentifierio.techflag.dsh.ssh') + await prepareBrowserWorker(contents, join(process.cwd(), 'node_modules/electron/dist/Electron.app/Contents/MacOS/Electron')) + const helper = join(contents, 'Helpers/WorkDSH Browser.app/Contents') + expect(execFileSync('/usr/libexec/PlistBuddy', ['-c', 'Print :LSUIElement', join(helper, 'Info.plist')], { encoding: 'utf8' }).trim()).toBe('true') + expect(realpathSync(join(helper, 'Resources'))).toBe(realpathSync(join(contents, 'Resources'))) + expect(realpathSync(join(helper, 'Frameworks'))).toBe(realpathSync(join(contents, 'Frameworks'))) + } finally { rmSync(root, { recursive: true, force: true }) } +}) From 5c28c0587e55448eda4d243c5c4b0476329f9049 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Mon, 5 Oct 2026 00:09:49 +0800 Subject: [PATCH 2/3] fix(desktop): declare background startup natively and promote only the primary app --- apps/desktop/package.json | 1 + .../desktop/scripts/prepare-browser-worker.ts | 24 ----------------- .../desktop/scripts/verify-workdsh-carrier.ts | 10 +++---- apps/desktop/src/browser-worker.ts | 10 ------- apps/desktop/src/workdsh-main.ts | 6 +++-- apps/desktop/tests/browser-worker.spec.ts | 27 ------------------- 6 files changed, 10 insertions(+), 68 deletions(-) delete mode 100644 apps/desktop/scripts/prepare-browser-worker.ts delete mode 100644 apps/desktop/src/browser-worker.ts delete mode 100644 apps/desktop/tests/browser-worker.spec.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b64c134b46..91772e4a33 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -111,6 +111,7 @@ ], "category": "public.app-category.developer-tools", "extendInfo": { + "LSUIElement": true, "CFBundleAllowMixedLocalizations": true, "CFBundleDevelopmentRegion": "en", "CFBundleLocalizations": [ diff --git a/apps/desktop/scripts/prepare-browser-worker.ts b/apps/desktop/scripts/prepare-browser-worker.ts deleted file mode 100644 index 7034a365a6..0000000000 --- a/apps/desktop/scripts/prepare-browser-worker.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { copyFileSync, mkdirSync, symlinkSync } from 'node:fs' -import { execFileSync } from 'node:child_process' -import { join } from 'node:path' -import { browserWorkerBundle } from '../src/browser-worker.ts' - -/** Share the carrier resources and Electron frameworks; copy only its small launcher. */ -export async function prepareBrowserWorker(appContents: string, mainExecutable: string): Promise { - const contents = join(appContents, 'Helpers', browserWorkerBundle, 'Contents') - mkdirSync(join(contents, 'MacOS'), { recursive: true }) - const executable = join(contents, 'MacOS', 'WorkDSH Browser') - copyFileSync(mainExecutable, executable) - copyFileSync(join(appContents, 'Info.plist'), join(contents, 'Info.plist')) - const plist = join(contents, 'Info.plist') - for (const command of [ - 'Set :CFBundleExecutable WorkDSH Browser', - 'Set :CFBundleName WorkDSH Browser', - 'Set :CFBundleIdentifier io.techflag.dsh.ssh.browser-worker', - 'Add :LSUIElement bool true', - ]) execFileSync('/usr/libexec/PlistBuddy', ['-c', command, plist]) - symlinkSync('../../../Resources', join(contents, 'Resources')) - symlinkSync('../../../Frameworks', join(contents, 'Frameworks')) - // Electron's fuse wire lives in the shared Framework, verified by the existing - // carrier gate. The helper does not introduce a second Electron installation. -} diff --git a/apps/desktop/scripts/verify-workdsh-carrier.ts b/apps/desktop/scripts/verify-workdsh-carrier.ts index 481c296884..711e404159 100644 --- a/apps/desktop/scripts/verify-workdsh-carrier.ts +++ b/apps/desktop/scripts/verify-workdsh-carrier.ts @@ -8,7 +8,6 @@ import { DSH_VERSION } from './runtime-version.mjs' import { ENTERPRISE_PACKAGES, PRODUCT_PACKAGES, RELEASE_PACKAGES } from './workdsh-package-boundary.mjs' import { verifyDefaultComposition, verifyDefaultProfile, verifyInstalledDshVersions, verifyOfficialWebPackages, verifyProfileRelease, verifyReleaseArchives } from './verify-profile-release.mjs' import { parseDeploymentConfig } from '../src/deployment-config.ts' -import { prepareBrowserWorker } from './prepare-browser-worker.ts' interface PackContext { appOutDir: string @@ -21,6 +20,11 @@ export function normalizeAsarEntry(entry: string): string { } export async function afterPack(context: PackContext): Promise { + if (context.electronPlatformName === 'darwin') { + const plist = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Info.plist') + const nativeRole = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :LSUIElement', plist], { encoding: 'utf8' }) + if (nativeRole.status !== 0 || nativeRole.stdout.trim() !== 'true') throw new Error('macOS carrier must start as a native UIElement before browser worker JavaScript runs') + } const resources = context.electronPlatformName === 'darwin' ? join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources') : join(context.appOutDir, 'resources') @@ -145,10 +149,6 @@ export async function afterPack(context: PackContext): Promise { throw new Error(`Packaged WorkDSH plugin inventory check failed: ${String(inventory.error ?? inventory.stderr)}`) } process.stdout.write(inventory.stdout) - if (context.electronPlatformName === 'darwin') { - const contents = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents') - await prepareBrowserWorker(contents, join(contents, 'MacOS', context.packager.appInfo.productFilename)) - } console.log(`Verified thin Electron carrier and ${names.length} root / ${instances} total Harness ${DSH_VERSION} package instances`) } diff --git a/apps/desktop/src/browser-worker.ts b/apps/desktop/src/browser-worker.ts deleted file mode 100644 index 507477660c..0000000000 --- a/apps/desktop/src/browser-worker.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { dirname, join } from 'node:path' - -export const browserWorkerBundle = 'WorkDSH Browser.app' - -/** Resolve a native UIElement bundle rather than launching the foreground app. */ -export function browserWorkerExecutable(executable: string, platform: NodeJS.Platform, development: boolean): string { - return platform === 'darwin' && !development - ? join(dirname(dirname(executable)), 'Helpers', browserWorkerBundle, 'Contents', 'MacOS', 'WorkDSH Browser') - : executable -} diff --git a/apps/desktop/src/workdsh-main.ts b/apps/desktop/src/workdsh-main.ts index 0da9ad7a22..1e5c3b19bb 100644 --- a/apps/desktop/src/workdsh-main.ts +++ b/apps/desktop/src/workdsh-main.ts @@ -19,7 +19,6 @@ import { EnterpriseLogin, startEnterpriseAuthority, type Authority } from './ent import { desktopEnterprisePatch, enterpriseEnvironment, enterpriseSpace, materializeRuntimeProfile, markProfileUpdated, officialLauncher, officialHostLauncher } from './local-runtime.ts' import { manageCommand } from './command-management.ts' import { cleanOwnedProcessTree, stopOwnedProcess } from './owned-process.ts' -import { browserWorkerExecutable } from './browser-worker.ts' const PROFILE_NAME = 'workdsh' const READY_PATTERN = /dsh web:\s+(http:\/\/127\.0\.0\.1:\d+\/?\?token=[^\s]+)/u @@ -179,7 +178,7 @@ function runtimeEnvironment(home: string): NodeJS.ProcessEnv { DSH_HOME: home, DSH_AGENTS_HOME: enterprise ? join(enterprise.root, 'agents') : join(home, 'agents'), DSH_BUNDLED_PRIMARY_RUNTIME: bundledPrimaryRuntime(), - DSH_ELECTRON_EXECUTABLE: browserWorkerExecutable(process.execPath, process.platform, Boolean(process.defaultApp)), + DSH_ELECTRON_EXECUTABLE: process.execPath, ELECTRON_RUN_AS_NODE: undefined, } } @@ -452,6 +451,9 @@ const worker = browserWorkerRequest() if (worker !== undefined) { startBrowserWorker(worker) } else { +// The native bundle starts without a Dock icon so spawned browser workers cannot +// flash one before JavaScript runs. Only the primary application becomes foreground. +if (process.platform === 'darwin') app.setActivationPolicy('regular') const desktopUserData = process.env.WORKDSH_DESKTOP_USER_DATA if (desktopUserData) { if (!isAbsolute(desktopUserData)) throw new Error('Desktop user data override must be absolute') diff --git a/apps/desktop/tests/browser-worker.spec.ts b/apps/desktop/tests/browser-worker.spec.ts deleted file mode 100644 index a0145c2396..0000000000 --- a/apps/desktop/tests/browser-worker.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect, it } from 'vitest' -import { execFileSync } from 'node:child_process' -import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { browserWorkerExecutable } from '../src/browser-worker.ts' -import { prepareBrowserWorker } from '../scripts/prepare-browser-worker.ts' - -it('uses the native background bundle only in packaged macOS', () => { - expect(browserWorkerExecutable('/Applications/WorkDSH.app/Contents/MacOS/WorkDSH', 'darwin', false)).toBe('/Applications/WorkDSH.app/Contents/Helpers/WorkDSH Browser.app/Contents/MacOS/WorkDSH Browser') - expect(browserWorkerExecutable('/development/Electron', 'darwin', true)).toBe('/development/Electron') - expect(browserWorkerExecutable('C:/WorkDSH.exe', 'win32', false)).toBe('C:/WorkDSH.exe') -}) - -it.skipIf(process.platform !== 'darwin')('declares UIElement before launch and shares existing frameworks and resources', async () => { - const root = mkdtempSync(join(tmpdir(), 'workdsh-native-worker-')) - const contents = join(root, 'WorkDSH.app', 'Contents') - try { - mkdirSync(join(contents, 'Resources'), { recursive: true }); mkdirSync(join(contents, 'Frameworks')) - writeFileSync(join(contents, 'Info.plist'), 'CFBundleExecutableWorkDSHCFBundleNameWorkDSHCFBundleIdentifierio.techflag.dsh.ssh') - await prepareBrowserWorker(contents, join(process.cwd(), 'node_modules/electron/dist/Electron.app/Contents/MacOS/Electron')) - const helper = join(contents, 'Helpers/WorkDSH Browser.app/Contents') - expect(execFileSync('/usr/libexec/PlistBuddy', ['-c', 'Print :LSUIElement', join(helper, 'Info.plist')], { encoding: 'utf8' }).trim()).toBe('true') - expect(realpathSync(join(helper, 'Resources'))).toBe(realpathSync(join(contents, 'Resources'))) - expect(realpathSync(join(helper, 'Frameworks'))).toBe(realpathSync(join(contents, 'Frameworks'))) - } finally { rmSync(root, { recursive: true, force: true }) } -}) From b491f036fa76715180ece4144f9f7abafc9996ba Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Mon, 5 Oct 2026 00:10:24 +0800 Subject: [PATCH 3/3] test(desktop): verify primary application foreground activation --- apps/desktop/tests/connection-lifecycle.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/desktop/tests/connection-lifecycle.spec.ts b/apps/desktop/tests/connection-lifecycle.spec.ts index 9fdc994dca..af69ae313c 100644 --- a/apps/desktop/tests/connection-lifecycle.spec.ts +++ b/apps/desktop/tests/connection-lifecycle.spec.ts @@ -30,7 +30,7 @@ vi.mock('electron', async () => { return { BrowserWindow: Window, ipcMain: { handle: (name: string, fn: any) => { state.handlers[name] = fn } }, - app: Object.assign(new EventEmitter(), { setName: vi.fn(), requestSingleInstanceLock: () => true, whenReady: async () => {}, quit: vi.fn(), relaunch: vi.fn(), getPath: () => state.userData }), + app: Object.assign(new EventEmitter(), { setActivationPolicy: vi.fn(), setName: vi.fn(), requestSingleInstanceLock: () => true, whenReady: async () => {}, quit: vi.fn(), relaunch: vi.fn(), getPath: () => state.userData }), shell: { openExternal: vi.fn() }, dialog: { showErrorBox: vi.fn(), showOpenDialog: vi.fn(async () => ({ canceled: true, filePaths: [] })) }, Menu: { buildFromTemplate: (menu: any[]) => { state.menu = menu; return menu }, setApplicationMenu: vi.fn() }, } @@ -73,6 +73,9 @@ it('enforces the packaged backend, uses explicitly installed enterprise identity }) try { await import('../src/workdsh-main.ts') + const { app: startupApp } = await import('electron') + if (process.platform === 'darwin') expect(startupApp.setActivationPolicy).toHaveBeenCalledWith('regular') + else expect(startupApp.setActivationPolicy).not.toHaveBeenCalled() await vi.waitFor(() => expect(state.windows).toHaveLength(1)) const entry = state.windows[0] expect(decodeURIComponent(entry.url)).toContain('

https://company.test

')