fix(desktop): resolve unpacked spawn-helper before chmod (#71171)

This commit is contained in:
Perseus Computing 2026-07-25 14:19:29 -05:00 committed by GitHub
parent 46815f4910
commit ac327dfa38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 2 deletions

View file

@ -8,7 +8,8 @@ import {
needsExecBit,
spawnHelperCandidates,
type SpawnHelperFs,
withExecBits
withExecBits,
writableNodePtyRoot
} from './spawn-helper-perms'
interface FakeFile {
@ -56,6 +57,30 @@ function fakeFs(
}
}
test('rewrites an archived node-pty root to the matching unpacked tree exactly once', () => {
assert.equal(
writableNodePtyRoot('/Hermes.app/Contents/Resources/app.asar/dist/node_modules/node-pty'),
'/Hermes.app/Contents/Resources/app.asar.unpacked/dist/node_modules/node-pty'
)
assert.equal(
writableNodePtyRoot('/Hermes.app/Contents/Resources/app.asar.unpacked/dist/node_modules/node-pty'),
'/Hermes.app/Contents/Resources/app.asar.unpacked/dist/node_modules/node-pty'
)
})
test('uses the unpacked helper when resolution reports an app.asar node-pty root', () => {
const archivedRoot = '/Hermes.app/Contents/Resources/app.asar/dist/node_modules/node-pty'
const unpackedRoot = writableNodePtyRoot(archivedRoot)
const helper = join(unpackedRoot, 'prebuilds', 'darwin-arm64', 'spawn-helper')
const fs = fakeFs({ [helper]: { mode: 0o644 } }, { [join(unpackedRoot, 'prebuilds')]: ['darwin-arm64'] })
const result = ensureSpawnHelperExecutable(archivedRoot, fs)
assert.deepEqual(result.fixed, [helper])
assert.deepEqual(result.errors, [])
assert.deepEqual(fs.chmods, [{ path: helper, mode: 0o755 }])
})
test('needsExecBit / withExecBits treat any missing exec bit as non-executable', () => {
assert.equal(needsExecBit(0o644), true)
assert.equal(needsExecBit(0o755), false)

View file

@ -18,6 +18,14 @@ import { join } from 'node:path'
const EXEC_BITS = 0o111
// Electron exposes module paths inside app.asar even when electron-builder has
// unpacked the native payload beside it. `stat` can read an archived path, but
// chmod cannot mutate it (ENOTDIR). Native node-pty helpers belong in the
// writable app.asar.unpacked tree; leave an already-unpacked path unchanged.
export function writableNodePtyRoot(nodePtyRoot: string): string {
return nodePtyRoot.replace(/app\.asar(?!\.unpacked)/, 'app.asar.unpacked')
}
export interface SpawnHelperFs {
existsSync(path: string): boolean
readdirSync(path: string): string[]
@ -81,8 +89,9 @@ export function ensureSpawnHelperExecutable(
fs: SpawnHelperFs = defaultFs
): EnsureSpawnHelperResult {
const result: EnsureSpawnHelperResult = { fixed: [], errors: [] }
const writableRoot = writableNodePtyRoot(nodePtyRoot)
for (const path of spawnHelperCandidates(nodePtyRoot, fs)) {
for (const path of spawnHelperCandidates(writableRoot, fs)) {
if (!fs.existsSync(path)) {
continue
}