Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions windows/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### 修复

- Windows 托盘新增安全的 injector 恢复监视:Codex 关闭重开导致原 browser identity 与 watcher 一同退出后,只有在记录的 injector 已确认停止、且新 CDP 端点仍由已注册的官方 Codex 包持有时,才会通过现有启动流程重新建立 watcher;恢复模式禁止启动或重启 Codex,避免托盘在用户主动关闭应用后把它重新拉起(#200)。
- 启动编排三处加固(#222):① 启动后的一次性验证改为 90 秒重试窗口——慢机器上 Codex 首屏尚未渲染完时不再被误判失败,进而不再连带停掉刚拉起的 watcher、也不再把 Codex 无调试口重启(此前皮肤因此完全不出现);② 启动失败回滚改用自有进程对象停止注入器并把等待延长到 15 秒——过早判定「did not stop」曾遗留互相清除对方运行时的双 watcher;③ 端点归属校验放宽到任一已注册 OpenAI.Codex 版本——商店自动更新中途换包目录后,不再拒认仍在运行的健康皮肤会话(`verify-dream-skin.ps1` 同步生效,托盘经由 start 脚本自动继承全部修复)。新增回归断言锁定重试窗口、回滚等待与版本回退逻辑。
- 修复 `--verify` 缺少 `--theme-dir` 参数的隐性错配:注入器在无该参数时回退到引擎 `assets` 内置主题作为期望值,源码安装下 watcher 应用的是暂存激活主题,二者永不一致——验证从一开始就注定失败(重试窗口暴露了这一点)。start 与 `verify-dream-skin.ps1` 现与 watcher 使用同一 `--theme-dir`(macOS 包装器一直如此),并新增断言防回退。

Expand Down
99 changes: 88 additions & 11 deletions windows/scripts/common-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,30 @@ function Get-DreamSkinProcessStartedAt {
}
}

function Stop-DreamSkinRecordedInjector {
function Get-DreamSkinRecordedInjectorProcess {
param([AllowNull()][object]$State)
if ($null -eq $State -or -not $State.injectorPid) { return $true }
if ($null -eq $State -or -not $State.injectorPid) { return $null }
$processId = [int]$State.injectorPid
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $processId" -ErrorAction SilentlyContinue
if (-not $process) { return $true }
$processHandle = Get-Process -Id $processId -ErrorAction SilentlyContinue
if ($null -eq $processHandle) { return $null }
try {
$processInfo = Get-CimInstance Win32_Process -Filter "ProcessId = $processId" -ErrorAction Stop
} catch {
if ($processHandle.HasExited) {
$processHandle.Dispose()
return $null
}
$processHandle.Dispose()
throw "The recorded injector PID $processId is running, but its identity cannot be inspected. State was preserved."
}
if (-not $processInfo) {
if ($processHandle.HasExited) {
$processHandle.Dispose()
return $null
}
$processHandle.Dispose()
throw "The recorded injector PID $processId is running, but its identity cannot be inspected. State was preserved."
}

$expectedInjector = if ($State.injectorPath) {
"$($State.injectorPath)"
Expand All @@ -844,9 +862,10 @@ function Stop-DreamSkinRecordedInjector {
} else {
$null
}
$processPath = Get-DreamSkinProcessExecutablePath -ProcessInfo $process
$commandLine = "$($process.CommandLine)"
$processPath = Get-DreamSkinProcessExecutablePath -ProcessInfo $processInfo
$commandLine = "$($processInfo.CommandLine)"
if (-not $processPath -or -not $commandLine) {
$processHandle.Dispose()
throw "The recorded injector PID $processId is running, but its identity cannot be inspected. State was preserved."
}
$isNodeExecutable = [System.IO.Path]::GetFileName("$processPath") -ieq 'node.exe'
Expand All @@ -865,18 +884,76 @@ function Stop-DreamSkinRecordedInjector {
$browserPattern = '(?:^|\s)(?i:--browser-id)(?:=|\s+)' + [regex]::Escape("$($State.browserId)") + '(?=$|\s)'
$injectorMatches = $injectorMatches -and [regex]::IsMatch($commandLine, $browserPattern)
}
$startedAt = Get-DreamSkinProcessStartedAt -ProcessId $processId
try {
$startedAt = $processHandle.StartTime.ToUniversalTime().ToString('o')
} catch {
if ($processHandle.HasExited) {
$processHandle.Dispose()
return $null
}
$processHandle.Dispose()
throw "The recorded injector PID $processId is running, but its start time cannot be inspected. State was preserved."
}
$startMatches = -not $State.injectorStartedAt -or $startedAt -eq "$($State.injectorStartedAt)"
$identityMatches = [bool]($isNodeExecutable -and $nodeMatches -and $injectorMatches -and $startMatches)

if (-not $identityMatches) {
$processHandle.Dispose()
throw "The recorded injector PID $processId is running, but its visible identity does not match the saved Dream Skin process. State was preserved."
}

Stop-Process -Id $processId -Force -ErrorAction Stop
try { Wait-Process -Id $processId -Timeout 5 -ErrorAction Stop } catch {}
if (Get-Process -Id $processId -ErrorAction SilentlyContinue) {
throw "The recorded Dream Skin injector did not stop: PID $processId"
return $processHandle
}

function Get-DreamSkinInjectorRecoveryContext {
param([AllowNull()][object]$State)
if ($null -eq $State -or -not $State.port) { return $null }
$recordedInjector = Get-DreamSkinRecordedInjectorProcess -State $State
if ($null -ne $recordedInjector) {
$recordedInjector.Dispose()
return $null
}

$port = 0
if (-not [int]::TryParse("$($State.port)", [ref]$port)) { return $null }
Assert-DreamSkinPort -Port $port
if (Test-DreamSkinPortAvailable -Port $port) { return $null }

$codex = Get-DreamSkinCodexInstallFromState -State $State
$identity = if ($null -ne $codex) {
Get-DreamSkinVerifiedCdpIdentity -Port $port -Codex $codex
} else {
$null
}
if ($null -eq $identity) {
$registered = Get-DreamSkinVerifiedCdpIdentityForAnyRegistered -Port $port
if ($null -eq $registered) { return $null }
$codex = $registered.Codex
$identity = $registered.Identity
}

return [pscustomobject]@{
Port = $port
BrowserId = $identity.BrowserId
Codex = $codex
}
}

function Stop-DreamSkinRecordedInjector {
param([AllowNull()][object]$State)
$process = Get-DreamSkinRecordedInjectorProcess -State $State
if ($null -eq $process) { return $true }
$processId = [int]$process.Id
try {
if (-not $process.HasExited) {
Stop-Process -InputObject $process -Force -ErrorAction Stop
}
[void]$process.WaitForExit(15000)
if (-not $process.HasExited) {
throw "The recorded Dream Skin injector did not stop: PID $processId"
}
} finally {
$process.Dispose()
}
return $true
}
Expand Down
29 changes: 25 additions & 4 deletions windows/scripts/start-dream-skin.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ param(
[switch]$RestartExisting,
[switch]$PromptRestart,
[string]$ProfilePath,
[switch]$ForegroundInjector
[switch]$ForegroundInjector,
[switch]$RecoverExisting
)

$ErrorActionPreference = 'Stop'
Expand All @@ -16,6 +17,9 @@ $Injector = Join-Path $PSScriptRoot 'injector.mjs'
$operationLock = Enter-DreamSkinOperationLock
try {
Assert-DreamSkinPort -Port $Port
if ($RecoverExisting -and ($RestartExisting -or $PromptRestart)) {
throw 'RecoverExisting cannot restart Codex or prompt for restart consent.'
}
if ($ProfilePath) { $ProfilePath = [System.IO.Path]::GetFullPath($ProfilePath) }
$node = Get-DreamSkinNodeRuntime
$currentCodex = Get-DreamSkinCodexInstall
Expand All @@ -29,8 +33,12 @@ try {
$VerifyPath = Join-Path $StateRoot 'verify.log'
$themePaths = Initialize-DreamSkinThemeStore -SkillRoot (Split-Path -Parent $PSScriptRoot) -StateRoot $StateRoot
$pauseWasSet = Test-DreamSkinPaused -StateRoot $StateRoot
$pauseCleared = $false

$previousState = Read-DreamSkinState -Path $StatePath
if ($RecoverExisting -and -not $ProfilePath -and $null -ne $previousState -and $previousState.profilePath) {
$ProfilePath = [System.IO.Path]::GetFullPath("$($previousState.profilePath)")
}
if (-not $PortExplicit -and $null -ne $previousState -and $previousState.port) {
$savedPort = [int]$previousState.port
Assert-DreamSkinPort -Port $savedPort
Expand Down Expand Up @@ -94,6 +102,9 @@ try {
Get-DreamSkinCodexProcesses -Codex $codexToStop
}
$closedExistingCodex = $false
if ($RecoverExisting -and -not $debugReady) {
throw 'No verified existing Codex CDP session is available for injector recovery.'
}
if (-not $debugReady -and $codexProcesses.Count -gt 0) {
$restartAuthorized = [bool]$RestartExisting
if (-not $restartAuthorized -and $PromptRestart) {
Expand All @@ -113,7 +124,11 @@ try {

$launchedWithCdp = $false
try {
if ($null -eq (Get-DreamSkinVerifiedCdpIdentity -Port $Port -Codex $codex)) {
$existingIdentity = Get-DreamSkinVerifiedCdpIdentity -Port $Port -Codex $codex
if ($RecoverExisting -and $null -eq $existingIdentity) {
throw 'The verified Codex CDP session closed before injector recovery completed.'
}
if ($null -eq $existingIdentity) {
if (-not (Test-DreamSkinPortAvailable -Port $Port)) {
if ($PortExplicit) { throw "Port $Port is already occupied by an unverified listener. Choose another port." }
$Port = Select-DreamSkinPort -PreferredPort $Port
Expand Down Expand Up @@ -175,8 +190,14 @@ try {

# Keep a paused, already-running watcher paused until all state checks and any
# restart consent have succeeded. A cancelled prompt must be side-effect free.
Set-DreamSkinPaused -Paused $false -StateRoot $StateRoot | Out-Null
$pauseCleared = $true
if ($RecoverExisting) {
if (Test-DreamSkinPaused -StateRoot $StateRoot) {
throw 'Injector recovery stopped because Dream Skin was paused before it could attach.'
}
} else {
Set-DreamSkinPaused -Paused $false -StateRoot $StateRoot | Out-Null
$pauseCleared = $true
}

if ($ForegroundInjector) {
try {
Expand Down
48 changes: 47 additions & 1 deletion windows/scripts/tray-dream-skin.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ $mutex = [System.Threading.Mutex]::new($false, "Local\CodexDreamSkin.$sid.Tray")
$acquired = $false
$notify = $null
$trayIcon = $null
$recoveryTimer = $null
$recoveryMonitor = @{
Process = $null
NextAttemptAt = [datetime]::MinValue
}
try {
try { $acquired = $mutex.WaitOne(0) } catch [System.Threading.AbandonedMutexException] { $acquired = $true }
if (-not $acquired) { exit 0 }
Expand Down Expand Up @@ -55,7 +60,31 @@ try {
$scriptToken = ConvertTo-DreamSkinProcessArgument -Value $Script
$argumentLine = '-NoProfile -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File ' + $scriptToken
if ($Arguments.Count -gt 0) { $argumentLine += ' ' + ($Arguments -join ' ') }
Start-Process -FilePath $powershell -ArgumentList $argumentLine -WindowStyle Hidden | Out-Null
return Start-Process -FilePath $powershell -ArgumentList $argumentLine -WindowStyle Hidden -PassThru
}

function Invoke-DreamSkinTrayRecovery {
if (Test-DreamSkinPaused -StateRoot $StateRoot) { return }
if ($null -ne $recoveryMonitor.Process) {
try {
if (-not $recoveryMonitor.Process.HasExited) { return }
$recoveryMonitor.Process.Dispose()
} catch {}
$recoveryMonitor.Process = $null
}
$now = Get-Date
if ($now -lt $recoveryMonitor.NextAttemptAt) { return }

$state = Read-DreamSkinState -Path $paths.State
$context = Get-DreamSkinInjectorRecoveryContext -State $state
if ($null -eq $context) { return }

# The start script repeats the official-package/CDP ownership checks and
# RecoverExisting prevents this monitor from launching or restarting Codex.
$recoveryMonitor.NextAttemptAt = $now.AddSeconds(30)
$recoveryMonitor.Process = Start-DreamSkinPowerShell -Script $startScript -Arguments @(
'-Port', "$($context.Port)", '-RecoverExisting'
)
}

function Add-DreamSkinTrayItem {
Expand Down Expand Up @@ -245,8 +274,25 @@ try {
Show-DreamSkinTrayError -Message $_.Exception.Message
}
})
$recoveryTimer = [System.Windows.Forms.Timer]::new()
$recoveryTimer.Interval = 5000
$recoveryTimer.add_Tick({
try {
Invoke-DreamSkinTrayRecovery
} catch {
$recoveryMonitor.NextAttemptAt = (Get-Date).AddSeconds(30)
}
})
$recoveryTimer.Start()
[System.Windows.Forms.Application]::Run()
} finally {
if ($null -ne $recoveryTimer) {
$recoveryTimer.Stop()
$recoveryTimer.Dispose()
}
if ($null -ne $recoveryMonitor.Process) {
try { $recoveryMonitor.Process.Dispose() } catch {}
}
if ($null -ne $notify) { $notify.Dispose() }
if ($null -ne $trayIcon) { $trayIcon.Dispose() }
if ($acquired) { try { $mutex.ReleaseMutex() } catch {} }
Expand Down
Loading
Loading