fix: gracefully handle EPERM during PTY overlay creation on Windows (#752)

This commit is contained in:
Jinwoo Hong 2026-04-18 19:37:46 -04:00 committed by GitHub
parent e8b8b5f96e
commit 5ec70ed539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 14 deletions

View file

@ -208,6 +208,16 @@ export class OpenCodeHookService {
}
const configDir = this.writePluginConfig(ptyId)
if (!configDir) {
// Why: plugin config is best-effort — return hook vars without
// OPENCODE_CONFIG_DIR so the PTY still spawns and manually launched
// opencode sessions can still report status via the hook server.
return {
ORCA_OPENCODE_HOOK_PORT: String(this.port),
ORCA_OPENCODE_HOOK_TOKEN: this.token,
ORCA_OPENCODE_PTY_ID: ptyId
}
}
// Why: OpenCode only reads the extra plugin directory at process startup.
// Inject these vars into every Orca PTY so manually launched `opencode`
@ -221,11 +231,18 @@ export class OpenCodeHookService {
}
}
private writePluginConfig(ptyId: string): string {
private writePluginConfig(ptyId: string): string | null {
const configDir = join(app.getPath('userData'), 'opencode-hooks', ptyId)
const pluginsDir = join(configDir, 'plugins')
mkdirSync(pluginsDir, { recursive: true })
writeFileSync(join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE), getOpenCodePluginSource())
try {
mkdirSync(pluginsDir, { recursive: true })
writeFileSync(join(pluginsDir, ORCA_OPENCODE_PLUGIN_FILE), getOpenCodePluginSource())
} catch {
// Why: on Windows, userData directories can be locked by antivirus or
// indexers (EPERM/EBUSY). Plugin config is non-critical — the PTY should
// still spawn without the OpenCode status plugin.
return null
}
return configDir
}
}

View file

@ -142,17 +142,36 @@ export class PiTitlebarExtensionService {
const sourceAgentDir = existingAgentDir || getDefaultPiAgentDir()
const overlayDir = this.getOverlayDir(ptyId)
rmSync(overlayDir, { recursive: true, force: true })
mkdirSync(overlayDir, { recursive: true })
this.mirrorAgentDir(sourceAgentDir, overlayDir)
try {
rmSync(overlayDir, { recursive: true, force: true })
} catch {
// Why: on Windows the overlay directory can be locked by another process
// (e.g. antivirus, indexer, or a previous Orca session that didn't clean up).
// rmSync with force:true handles ENOENT but not EPERM/EBUSY. If we can't
// remove the stale overlay, fall back to the user's own Pi agent dir so the
// terminal still spawns — the titlebar spinner is not worth blocking the PTY.
return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {}
}
const extensionsDir = join(overlayDir, 'extensions')
mkdirSync(extensionsDir, { recursive: true })
// Why: Pi auto-loads global extensions from PI_CODING_AGENT_DIR/extensions.
// Add Orca's titlebar extension alongside the user's existing extensions
// instead of replacing that directory, otherwise Orca terminals would
// silently disable the user's Pi customization inside Orca only.
writeFileSync(join(extensionsDir, ORCA_PI_EXTENSION_FILE), getPiTitlebarExtensionSource())
try {
mkdirSync(overlayDir, { recursive: true })
this.mirrorAgentDir(sourceAgentDir, overlayDir)
const extensionsDir = join(overlayDir, 'extensions')
mkdirSync(extensionsDir, { recursive: true })
// Why: Pi auto-loads global extensions from PI_CODING_AGENT_DIR/extensions.
// Add Orca's titlebar extension alongside the user's existing extensions
// instead of replacing that directory, otherwise Orca terminals would
// silently disable the user's Pi customization inside Orca only.
writeFileSync(join(extensionsDir, ORCA_PI_EXTENSION_FILE), getPiTitlebarExtensionSource())
} catch {
// Why: overlay creation is best-effort — permission errors (EPERM/EACCES)
// on Windows can occur when the userData directory is restricted or when
// symlink/junction creation fails without developer mode. Fall back to the
// user's Pi agent dir so the terminal spawns without the Orca extension.
this.clearPty(ptyId)
return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {}
}
return {
PI_CODING_AGENT_DIR: overlayDir
@ -160,7 +179,13 @@ export class PiTitlebarExtensionService {
}
clearPty(ptyId: string): void {
rmSync(this.getOverlayDir(ptyId), { recursive: true, force: true })
try {
rmSync(this.getOverlayDir(ptyId), { recursive: true, force: true })
} catch {
// Why: on Windows the overlay dir can be locked (EPERM/EBUSY) by antivirus
// or indexers. Overlay cleanup is best-effort — a stale directory in userData
// is harmless and will be overwritten on the next PTY spawn attempt.
}
}
}