mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +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 fs from 'node:fs'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
@ -54,6 +55,7 @@ class FakePty implements TerminalPtyProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tempDirs: string[] = []
|
const tempDirs: string[] = []
|
||||||
|
const itOnDarwin = process.platform === 'darwin' ? it : it.skip
|
||||||
|
|
||||||
function tempDir() {
|
function tempDir() {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-haha-terminal-'))
|
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')
|
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 () => {
|
it('spawns a PTY, forwards events, and controls the active session', async () => {
|
||||||
const dir = tempDir()
|
const dir = tempDir()
|
||||||
const fakePty = new FakePty()
|
const fakePty = new FakePty()
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const TERMINAL_CONFIG_FILE = 'terminal-config.json'
|
|||||||
const MIN_TERMINAL_COLS = 20
|
const MIN_TERMINAL_COLS = 20
|
||||||
const MIN_TERMINAL_ROWS = 8
|
const MIN_TERMINAL_ROWS = 8
|
||||||
const NODE_PTY_MANIFEST_FILE = '.cc-haha-node-pty-manifest.json'
|
const NODE_PTY_MANIFEST_FILE = '.cc-haha-node-pty-manifest.json'
|
||||||
|
const MACOS_DOWNLOAD_XATTRS = ['com.apple.quarantine', 'com.apple.provenance']
|
||||||
|
|
||||||
export type TerminalSpawnInput = {
|
export type TerminalSpawnInput = {
|
||||||
cols?: number
|
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 {
|
function isNodePtyCacheCurrent(sourceManifest: NodePtyIntegrityManifest, cacheDir: string): boolean {
|
||||||
const cacheManifest = readNodePtyManifest(cacheDir)
|
const cacheManifest = readNodePtyManifest(cacheDir)
|
||||||
if (!cacheManifest || !manifestsEqual(sourceManifest, cacheManifest)) return false
|
if (!cacheManifest || !manifestsEqual(sourceManifest, cacheManifest)) return false
|
||||||
@ -385,9 +427,11 @@ export function prepareNodePtyRuntime(sourceDir: string, cacheDir: string): stri
|
|||||||
const sourceManifest = buildNodePtyManifest(sourceDir)
|
const sourceManifest = buildNodePtyManifest(sourceDir)
|
||||||
|
|
||||||
if (preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
if (preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
||||||
|
stripMacosDownloadAttributes(cacheDir)
|
||||||
return cacheDir
|
return cacheDir
|
||||||
}
|
}
|
||||||
if (!preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
if (!preparedNodePtyDirs.has(cacheDir) && isNodePtyCacheCurrent(sourceManifest, cacheDir)) {
|
||||||
|
stripMacosDownloadAttributes(cacheDir)
|
||||||
preparedNodePtyDirs.add(cacheDir)
|
preparedNodePtyDirs.add(cacheDir)
|
||||||
return 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(path.dirname(cacheDir), { recursive: true, mode: 0o700 })
|
||||||
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
|
fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
|
||||||
fs.cpSync(sourceDir, cacheDir, { recursive: true })
|
fs.cpSync(sourceDir, cacheDir, { recursive: true })
|
||||||
|
stripMacosDownloadAttributes(cacheDir)
|
||||||
ensureNodePtyHelpersExecutable(cacheDir)
|
ensureNodePtyHelpersExecutable(cacheDir)
|
||||||
chmodNodePtyDirectories(cacheDir)
|
chmodNodePtyDirectories(cacheDir)
|
||||||
if (!manifestsEqual(sourceManifest, buildNodePtyManifest(cacheDir))) {
|
if (!manifestsEqual(sourceManifest, buildNodePtyManifest(cacheDir))) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user