mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(desktop): clear macOS quarantine from node-pty cache
Remove macOS download quarantine/provenance attributes from the packaged node-pty runtime cache before loading the native module. Existing matching caches are repaired before require, and read-only helper permissions are restored after cleanup.\n\nTested: bun test desktop/electron/services/terminal.test.ts\nTested: cd desktop && bun run build:electron\nTested: git diff --check\nTested: prepared the installed 0.4.3 node-pty cache and required it with Electron's Node runtime\nScope-risk: narrow\nConfidence: high
This commit is contained in:
parent
6715e75161
commit
2c1af7a843
@ -1,3 +1,4 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
@ -54,6 +55,7 @@ class FakePty implements TerminalPtyProcess {
|
||||
}
|
||||
|
||||
const tempDirs: string[] = []
|
||||
const itOnDarwin = process.platform === 'darwin' ? it : it.skip
|
||||
|
||||
function tempDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-haha-terminal-'))
|
||||
@ -168,6 +170,25 @@ describe('Electron terminal service', () => {
|
||||
expect(fs.readFileSync(path.join(cache, 'index.js'), 'utf8')).toBe('module.exports = { spawn() { return "source" } }\n')
|
||||
})
|
||||
|
||||
itOnDarwin('removes stale macOS quarantine attributes from the node-pty runtime cache', () => {
|
||||
const source = tempDir()
|
||||
const cache = path.join(tempDir(), 'node-pty-cache')
|
||||
fs.writeFileSync(path.join(source, 'package.json'), JSON.stringify({ name: 'node-pty', main: 'index.js' }))
|
||||
fs.writeFileSync(path.join(source, 'index.js'), 'module.exports = { spawn() { return "source" } }\n')
|
||||
|
||||
prepareNodePtyRuntime(source, cache)
|
||||
|
||||
const cachedEntry = path.join(cache, 'index.js')
|
||||
execFileSync('/usr/bin/xattr', ['-w', 'com.apple.quarantine', '0381;00000000;Chrome;CC-HAHA-TEST', cachedEntry])
|
||||
execFileSync('/usr/bin/xattr', ['-p', 'com.apple.quarantine', cachedEntry], { stdio: 'ignore' })
|
||||
fs.chmodSync(cachedEntry, 0o500)
|
||||
|
||||
prepareNodePtyRuntime(source, cache)
|
||||
|
||||
expect(() => execFileSync('/usr/bin/xattr', ['-p', 'com.apple.quarantine', cachedEntry], { stdio: 'ignore' })).toThrow()
|
||||
expect(fs.statSync(cachedEntry).mode & 0o777).toBe(0o500)
|
||||
})
|
||||
|
||||
it('spawns a PTY, forwards events, and controls the active session', async () => {
|
||||
const dir = tempDir()
|
||||
const fakePty = new FakePty()
|
||||
|
||||
@ -11,6 +11,7 @@ const TERMINAL_CONFIG_FILE = 'terminal-config.json'
|
||||
const MIN_TERMINAL_COLS = 20
|
||||
const MIN_TERMINAL_ROWS = 8
|
||||
const NODE_PTY_MANIFEST_FILE = '.cc-haha-node-pty-manifest.json'
|
||||
const MACOS_DOWNLOAD_XATTRS = ['com.apple.quarantine', 'com.apple.provenance']
|
||||
|
||||
export type TerminalSpawnInput = {
|
||||
cols?: number
|
||||
@ -367,6 +368,47 @@ function chmodNodePtyDirectories(moduleDir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function stripMacosDownloadAttributes(moduleDir: string): void {
|
||||
if (process.platform !== 'darwin') return
|
||||
|
||||
for (const attr of MACOS_DOWNLOAD_XATTRS) {
|
||||
try {
|
||||
execFileSync('/usr/bin/xattr', ['-dr', attr, moduleDir], { stdio: 'ignore' })
|
||||
} catch {
|
||||
// Best effort: the attribute may be absent, but stale quarantine blocks copied .node files.
|
||||
}
|
||||
}
|
||||
|
||||
for (const filePath of walkNodePtyFiles(moduleDir)) {
|
||||
let originalMode: number | null = null
|
||||
try {
|
||||
const stat = fs.statSync(filePath)
|
||||
const mode = stat.mode & 0o777
|
||||
if ((mode & 0o200) === 0) {
|
||||
originalMode = mode
|
||||
fs.chmodSync(filePath, mode | 0o200)
|
||||
}
|
||||
for (const attr of MACOS_DOWNLOAD_XATTRS) {
|
||||
try {
|
||||
execFileSync('/usr/bin/xattr', ['-d', attr, filePath], { stdio: 'ignore' })
|
||||
} catch {
|
||||
// Best effort: only files that still carry download xattrs need this fallback.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best effort: a partially rebuilt cache will be removed and copied again later.
|
||||
} finally {
|
||||
if (originalMode != null) {
|
||||
try {
|
||||
fs.chmodSync(filePath, originalMode)
|
||||
} catch {
|
||||
// Best effort: helper executable bits are restored separately before loading node-pty.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNodePtyCacheCurrent(sourceManifest: NodePtyIntegrityManifest, cacheDir: string): boolean {
|
||||
const cacheManifest = readNodePtyManifest(cacheDir)
|
||||
if (!cacheManifest || !manifestsEqual(sourceManifest, cacheManifest)) return false
|
||||
@ -385,9 +427,11 @@ export function prepareNodePtyRuntime(sourceDir: string, cacheDir: string): stri
|
||||
const sourceManifest = buildNodePtyManifest(sourceDir)
|
||||
|
||||
if (preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
||||
stripMacosDownloadAttributes(cacheDir)
|
||||
return cacheDir
|
||||
}
|
||||
if (!preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
||||
stripMacosDownloadAttributes(cacheDir)
|
||||
preparedNodePtyDirs.add(cacheDir)
|
||||
return cacheDir
|
||||
}
|
||||
@ -396,6 +440,7 @@ export function prepareNodePtyRuntime(sourceDir: string, cacheDir: string): stri
|
||||
fs.mkdirSync(path.dirname(cacheDir), { recursive: true, mode: 0o700 })
|
||||
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
|
||||
fs.cpSync(sourceDir, cacheDir, { recursive: true })
|
||||
stripMacosDownloadAttributes(cacheDir)
|
||||
ensureNodePtyHelpersExecutable(cacheDir)
|
||||
chmodNodePtyDirectories(cacheDir)
|
||||
if (!manifestsEqual(sourceManifest, buildNodePtyManifest(cacheDir))) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user