From c0a01182369f99de171b242232f5e49a0eed0d81 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 1 Oct 2026 22:12:52 -0700 Subject: [PATCH 1/2] Avoid redirected profile daemon trap Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- GraphcodeKit/Sources/SupportDirectory.swift | 18 ++- .../RedirectedUserProfileDaemon.Tests.ps1 | 145 ++++++++++++++++++ Tools/windows/validate.ps1 | 11 ++ graphcode/Tests/SupportDirectoryTests.swift | 10 ++ windows-tests/WindowsDaemonTests.swift | 10 ++ 5 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 diff --git a/GraphcodeKit/Sources/SupportDirectory.swift b/GraphcodeKit/Sources/SupportDirectory.swift index ec245361..2f0f008e 100644 --- a/GraphcodeKit/Sources/SupportDirectory.swift +++ b/GraphcodeKit/Sources/SupportDirectory.swift @@ -147,9 +147,21 @@ public enum SupportDirectory { /// Where graphcode kept its state before this moved. Read only by the migration below. static var legacyURL: URL { - FileManager.default - .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] - .appendingPathComponent("graphcode", isDirectory: true) + legacyURL( + applicationSupportDirectories: FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask), + fallback: defaultURL) + } + + static func legacyURL( + applicationSupportDirectories: [URL], + fallback: URL + ) -> URL { + guard let applicationSupport = applicationSupportDirectories.first else { + return fallback + } + return applicationSupport.appendingPathComponent("graphcode", isDirectory: true) } public static var binDirectory: URL { diff --git a/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 b/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 new file mode 100644 index 00000000..d23bdacd --- /dev/null +++ b/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 @@ -0,0 +1,145 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $DaemonExecutable, + [Parameter(Mandatory)] + [string] $ScratchRoot +) + +$ErrorActionPreference = "Stop" +$DaemonExecutable = (Resolve-Path -LiteralPath $DaemonExecutable).Path +$ScratchRoot = [IO.Path]::GetFullPath($ScratchRoot) +if (-not (Test-Path -LiteralPath $DaemonExecutable -PathType Leaf)) { + throw "real graphcoded.exe is missing: $DaemonExecutable" +} + +$nonce = [guid]::NewGuid().ToString("N") +$runRoot = Join-Path $ScratchRoot "redirected-userprofile-$nonce" +$redirectedProfile = Join-Path $runRoot "profile" +$supportDirectory = Join-Path $runRoot "support" +$tempDirectory = Join-Path $runRoot "temp" +$evidenceDirectory = Join-Path $ScratchRoot "redirected-userprofile-evidence" +$shutdownEventName = "Local\GraphCode-redirected-userprofile-$PID-$nonce" +$shutdownEvent = $null +$process = $null +$stdoutTask = $null +$stderrTask = $null +$successful = $false + +function Format-ExitCode([int] $code) { + $unsigned = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$code), 0) + return "0x{0:X8}" -f $unsigned +} + +New-Item -ItemType Directory -Force -Path @( + $runRoot, + $redirectedProfile, + $supportDirectory, + $tempDirectory, + $evidenceDirectory + ) | Out-Null + +try { + $shutdownEvent = [Threading.EventWaitHandle]::new( + $false, + [Threading.EventResetMode]::ManualReset, + $shutdownEventName + ) + $startInfo = [Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $DaemonExecutable + $startInfo.WorkingDirectory = Split-Path -Parent $DaemonExecutable + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + foreach ($name in @( + "GRAPHCODE_SOCKET", + "GRAPHCODE_DAEMON_PIPE", + "GRAPHCODE_DAEMON_STARTUP_EVENT", + "GRAPHCODE_DAEMON_HANDOFF_READY_EVENT", + "GRAPHCODE_DAEMON_HANDOFF_TEST_STATE")) { + [void] $startInfo.Environment.Remove($name) + } + $startInfo.Environment["USERPROFILE"] = $redirectedProfile + $startInfo.Environment["GRAPHCODE_SUPPORT_DIR"] = $supportDirectory + $startInfo.Environment["TEMP"] = $tempDirectory + $startInfo.Environment["TMP"] = $tempDirectory + $startInfo.Environment["GRAPHCODE_DAEMON_SHUTDOWN_EVENT"] = $shutdownEventName + + $process = [Diagnostics.Process]::new() + $process.StartInfo = $startInfo + if (-not $process.Start()) { + throw "could not start graphcoded.exe" + } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + + if ($process.WaitForExit(3000)) { + $process.Refresh() + throw ( + "graphcoded.exe exited with $(Format-ExitCode $process.ExitCode) under redirected " + + "USERPROFILE; stdout='$($stdoutTask.Result.Trim())'; " + + "stderr='$($stderrTask.Result.Trim())'") + } + + [void] $shutdownEvent.Set() + if (-not $process.WaitForExit(10000)) { + throw "graphcoded.exe ignored its owned shutdown event" + } + $process.Refresh() + $stdout = $stdoutTask.Result + $stderr = $stderrTask.Result + if ($process.ExitCode -ne 0) { + throw "graphcoded.exe shutdown returned $(Format-ExitCode $process.ExitCode)" + } + if ($stdout -notmatch "graphcoded: listening on namedPipe") { + throw "graphcoded.exe did not report a listening endpoint: $stdout" + } + if (-not [string]::IsNullOrWhiteSpace($stderr)) { + throw "graphcoded.exe wrote unexpected stderr: $stderr" + } + if (-not (Test-Path -LiteralPath ( + Join-Path $supportDirectory ".graphcode-rendezvous.secret") -PathType Leaf)) { + throw "graphcoded.exe did not initialize the explicit support directory" + } + if (@(Get-ChildItem -LiteralPath $redirectedProfile -Force).Count -ne 0) { + throw "graphcoded.exe wrote into redirected USERPROFILE despite explicit support isolation" + } + + $summary = @( + "REDIRECTED_USERPROFILE_DAEMON: PASS" + "executable=$DaemonExecutable" + "support=$supportDirectory" + "profile=$redirectedProfile" + "exit=$(Format-ExitCode $process.ExitCode)" + "stdout=$($stdout.Trim())" + "stderr=$($stderr.Trim())" + ) + $evidencePath = Join-Path $evidenceDirectory "redirected-userprofile-$nonce.txt" + [IO.File]::WriteAllLines($evidencePath, $summary, [Text.UTF8Encoding]::new($false)) + $summary + "evidence=$evidencePath" + $successful = $true +} finally { + if ($process -and -not $process.HasExited) { + if ($shutdownEvent) { + [void] $shutdownEvent.Set() + [void] $process.WaitForExit(3000) + } + if (-not $process.HasExited) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + [void] $process.WaitForExit(3000) + } + } + if ($process) { + $process.Dispose() + } + if ($shutdownEvent) { + $shutdownEvent.Dispose() + } + if ($successful) { + Remove-Item -LiteralPath $runRoot -Recurse -Force + } else { + Write-Output "Failure artifacts retained at $runRoot" + } +} diff --git a/Tools/windows/validate.ps1 b/Tools/windows/validate.ps1 index b3242890..a5d6e8d4 100644 --- a/Tools/windows/validate.ps1 +++ b/Tools/windows/validate.ps1 @@ -793,6 +793,17 @@ function Invoke-Task([string] $name) { New-Item -ItemType Directory -Force $smokeSupport | Out-Null $installedBin = Join-Path $smokeSupport "bin" Install-AtomicRuntimePackage $releaseBin $installedBin + $redirectedProfileScratch = Join-Path $env:TEMP "graphcode-g558-$([guid]::NewGuid())" + try { + Invoke-Native "Redirected USERPROFILE daemon regression" { + & (Join-Path $repoRoot "Tools\windows\Tests\RedirectedUserProfileDaemon.Tests.ps1") ` + -DaemonExecutable (Join-Path $installedBin "graphcoded.exe") ` + -ScratchRoot $redirectedProfileScratch + } + } finally { + Remove-Item -LiteralPath $redirectedProfileScratch -Recurse -Force ` + -ErrorAction SilentlyContinue + } $daemonProcess = $null $secondDaemonProcess = $null $cliProcess = $null diff --git a/graphcode/Tests/SupportDirectoryTests.swift b/graphcode/Tests/SupportDirectoryTests.swift index 8d673488..e0134ec5 100644 --- a/graphcode/Tests/SupportDirectoryTests.swift +++ b/graphcode/Tests/SupportDirectoryTests.swift @@ -55,6 +55,16 @@ struct SupportDirectoryTests { // MARK: - Migration + @Test + func missingLegacyApplicationSupportFallsBackWithoutTrapping() { + let fallback = URL(fileURLWithPath: "/Users/test-user/.graphcode", isDirectory: true) + + #expect( + SupportDirectory.legacyURL( + applicationSupportDirectories: [], + fallback: fallback) == fallback) + } + /// A scratch pair of paths — neither created — plus cleanup. private func withScratchPaths(_ body: (_ legacy: URL, _ destination: URL) throws -> Void) throws { let root = FileManager.default.temporaryDirectory diff --git a/windows-tests/WindowsDaemonTests.swift b/windows-tests/WindowsDaemonTests.swift index 90089a5e..7f60ff1f 100644 --- a/windows-tests/WindowsDaemonTests.swift +++ b/windows-tests/WindowsDaemonTests.swift @@ -87,6 +87,16 @@ final class WindowsDaemonTests: XCTestCase { XCTAssertEqual(ProjectRegistry.presencePollDelay(runningLoops: 0), .seconds(60)) } + func testMissingLegacyApplicationSupportFallsBackWithoutTrapping() { + let fallback = URL(fileURLWithPath: "C:\\Users\\test-user\\.graphcode", isDirectory: true) + + XCTAssertEqual( + SupportDirectory.legacyURL( + applicationSupportDirectories: [], + fallback: fallback), + fallback) + } + func testShellPredicateUsesPowerShellAndProjectWorkingDirectory() async throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("graphcode-predicate-\(UUID().uuidString)", isDirectory: true) From 285b73e8e5a7634a9033b8e72f950efc703b4f60 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Thu, 1 Oct 2026 22:15:48 -0700 Subject: [PATCH 2/2] Normalize redirected profile regression line endings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- .../RedirectedUserProfileDaemon.Tests.ps1 | 290 +++++++++--------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 b/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 index d23bdacd..5a004f5f 100644 --- a/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 +++ b/Tools/windows/Tests/RedirectedUserProfileDaemon.Tests.ps1 @@ -1,145 +1,145 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [string] $DaemonExecutable, - [Parameter(Mandatory)] - [string] $ScratchRoot -) - -$ErrorActionPreference = "Stop" -$DaemonExecutable = (Resolve-Path -LiteralPath $DaemonExecutable).Path -$ScratchRoot = [IO.Path]::GetFullPath($ScratchRoot) -if (-not (Test-Path -LiteralPath $DaemonExecutable -PathType Leaf)) { - throw "real graphcoded.exe is missing: $DaemonExecutable" -} - -$nonce = [guid]::NewGuid().ToString("N") -$runRoot = Join-Path $ScratchRoot "redirected-userprofile-$nonce" -$redirectedProfile = Join-Path $runRoot "profile" -$supportDirectory = Join-Path $runRoot "support" -$tempDirectory = Join-Path $runRoot "temp" -$evidenceDirectory = Join-Path $ScratchRoot "redirected-userprofile-evidence" -$shutdownEventName = "Local\GraphCode-redirected-userprofile-$PID-$nonce" -$shutdownEvent = $null -$process = $null -$stdoutTask = $null -$stderrTask = $null -$successful = $false - -function Format-ExitCode([int] $code) { - $unsigned = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$code), 0) - return "0x{0:X8}" -f $unsigned -} - -New-Item -ItemType Directory -Force -Path @( - $runRoot, - $redirectedProfile, - $supportDirectory, - $tempDirectory, - $evidenceDirectory - ) | Out-Null - -try { - $shutdownEvent = [Threading.EventWaitHandle]::new( - $false, - [Threading.EventResetMode]::ManualReset, - $shutdownEventName - ) - $startInfo = [Diagnostics.ProcessStartInfo]::new() - $startInfo.FileName = $DaemonExecutable - $startInfo.WorkingDirectory = Split-Path -Parent $DaemonExecutable - $startInfo.UseShellExecute = $false - $startInfo.CreateNoWindow = $true - $startInfo.RedirectStandardOutput = $true - $startInfo.RedirectStandardError = $true - foreach ($name in @( - "GRAPHCODE_SOCKET", - "GRAPHCODE_DAEMON_PIPE", - "GRAPHCODE_DAEMON_STARTUP_EVENT", - "GRAPHCODE_DAEMON_HANDOFF_READY_EVENT", - "GRAPHCODE_DAEMON_HANDOFF_TEST_STATE")) { - [void] $startInfo.Environment.Remove($name) - } - $startInfo.Environment["USERPROFILE"] = $redirectedProfile - $startInfo.Environment["GRAPHCODE_SUPPORT_DIR"] = $supportDirectory - $startInfo.Environment["TEMP"] = $tempDirectory - $startInfo.Environment["TMP"] = $tempDirectory - $startInfo.Environment["GRAPHCODE_DAEMON_SHUTDOWN_EVENT"] = $shutdownEventName - - $process = [Diagnostics.Process]::new() - $process.StartInfo = $startInfo - if (-not $process.Start()) { - throw "could not start graphcoded.exe" - } - $stdoutTask = $process.StandardOutput.ReadToEndAsync() - $stderrTask = $process.StandardError.ReadToEndAsync() - - if ($process.WaitForExit(3000)) { - $process.Refresh() - throw ( - "graphcoded.exe exited with $(Format-ExitCode $process.ExitCode) under redirected " + - "USERPROFILE; stdout='$($stdoutTask.Result.Trim())'; " + - "stderr='$($stderrTask.Result.Trim())'") - } - - [void] $shutdownEvent.Set() - if (-not $process.WaitForExit(10000)) { - throw "graphcoded.exe ignored its owned shutdown event" - } - $process.Refresh() - $stdout = $stdoutTask.Result - $stderr = $stderrTask.Result - if ($process.ExitCode -ne 0) { - throw "graphcoded.exe shutdown returned $(Format-ExitCode $process.ExitCode)" - } - if ($stdout -notmatch "graphcoded: listening on namedPipe") { - throw "graphcoded.exe did not report a listening endpoint: $stdout" - } - if (-not [string]::IsNullOrWhiteSpace($stderr)) { - throw "graphcoded.exe wrote unexpected stderr: $stderr" - } - if (-not (Test-Path -LiteralPath ( - Join-Path $supportDirectory ".graphcode-rendezvous.secret") -PathType Leaf)) { - throw "graphcoded.exe did not initialize the explicit support directory" - } - if (@(Get-ChildItem -LiteralPath $redirectedProfile -Force).Count -ne 0) { - throw "graphcoded.exe wrote into redirected USERPROFILE despite explicit support isolation" - } - - $summary = @( - "REDIRECTED_USERPROFILE_DAEMON: PASS" - "executable=$DaemonExecutable" - "support=$supportDirectory" - "profile=$redirectedProfile" - "exit=$(Format-ExitCode $process.ExitCode)" - "stdout=$($stdout.Trim())" - "stderr=$($stderr.Trim())" - ) - $evidencePath = Join-Path $evidenceDirectory "redirected-userprofile-$nonce.txt" - [IO.File]::WriteAllLines($evidencePath, $summary, [Text.UTF8Encoding]::new($false)) - $summary - "evidence=$evidencePath" - $successful = $true -} finally { - if ($process -and -not $process.HasExited) { - if ($shutdownEvent) { - [void] $shutdownEvent.Set() - [void] $process.WaitForExit(3000) - } - if (-not $process.HasExited) { - Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue - [void] $process.WaitForExit(3000) - } - } - if ($process) { - $process.Dispose() - } - if ($shutdownEvent) { - $shutdownEvent.Dispose() - } - if ($successful) { - Remove-Item -LiteralPath $runRoot -Recurse -Force - } else { - Write-Output "Failure artifacts retained at $runRoot" - } -} +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $DaemonExecutable, + [Parameter(Mandatory)] + [string] $ScratchRoot +) + +$ErrorActionPreference = "Stop" +$DaemonExecutable = (Resolve-Path -LiteralPath $DaemonExecutable).Path +$ScratchRoot = [IO.Path]::GetFullPath($ScratchRoot) +if (-not (Test-Path -LiteralPath $DaemonExecutable -PathType Leaf)) { + throw "real graphcoded.exe is missing: $DaemonExecutable" +} + +$nonce = [guid]::NewGuid().ToString("N") +$runRoot = Join-Path $ScratchRoot "redirected-userprofile-$nonce" +$redirectedProfile = Join-Path $runRoot "profile" +$supportDirectory = Join-Path $runRoot "support" +$tempDirectory = Join-Path $runRoot "temp" +$evidenceDirectory = Join-Path $ScratchRoot "redirected-userprofile-evidence" +$shutdownEventName = "Local\GraphCode-redirected-userprofile-$PID-$nonce" +$shutdownEvent = $null +$process = $null +$stdoutTask = $null +$stderrTask = $null +$successful = $false + +function Format-ExitCode([int] $code) { + $unsigned = [BitConverter]::ToUInt32([BitConverter]::GetBytes([int32]$code), 0) + return "0x{0:X8}" -f $unsigned +} + +New-Item -ItemType Directory -Force -Path @( + $runRoot, + $redirectedProfile, + $supportDirectory, + $tempDirectory, + $evidenceDirectory + ) | Out-Null + +try { + $shutdownEvent = [Threading.EventWaitHandle]::new( + $false, + [Threading.EventResetMode]::ManualReset, + $shutdownEventName + ) + $startInfo = [Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $DaemonExecutable + $startInfo.WorkingDirectory = Split-Path -Parent $DaemonExecutable + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + foreach ($name in @( + "GRAPHCODE_SOCKET", + "GRAPHCODE_DAEMON_PIPE", + "GRAPHCODE_DAEMON_STARTUP_EVENT", + "GRAPHCODE_DAEMON_HANDOFF_READY_EVENT", + "GRAPHCODE_DAEMON_HANDOFF_TEST_STATE")) { + [void] $startInfo.Environment.Remove($name) + } + $startInfo.Environment["USERPROFILE"] = $redirectedProfile + $startInfo.Environment["GRAPHCODE_SUPPORT_DIR"] = $supportDirectory + $startInfo.Environment["TEMP"] = $tempDirectory + $startInfo.Environment["TMP"] = $tempDirectory + $startInfo.Environment["GRAPHCODE_DAEMON_SHUTDOWN_EVENT"] = $shutdownEventName + + $process = [Diagnostics.Process]::new() + $process.StartInfo = $startInfo + if (-not $process.Start()) { + throw "could not start graphcoded.exe" + } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + + if ($process.WaitForExit(3000)) { + $process.Refresh() + throw ( + "graphcoded.exe exited with $(Format-ExitCode $process.ExitCode) under redirected " + + "USERPROFILE; stdout='$($stdoutTask.Result.Trim())'; " + + "stderr='$($stderrTask.Result.Trim())'") + } + + [void] $shutdownEvent.Set() + if (-not $process.WaitForExit(10000)) { + throw "graphcoded.exe ignored its owned shutdown event" + } + $process.Refresh() + $stdout = $stdoutTask.Result + $stderr = $stderrTask.Result + if ($process.ExitCode -ne 0) { + throw "graphcoded.exe shutdown returned $(Format-ExitCode $process.ExitCode)" + } + if ($stdout -notmatch "graphcoded: listening on namedPipe") { + throw "graphcoded.exe did not report a listening endpoint: $stdout" + } + if (-not [string]::IsNullOrWhiteSpace($stderr)) { + throw "graphcoded.exe wrote unexpected stderr: $stderr" + } + if (-not (Test-Path -LiteralPath ( + Join-Path $supportDirectory ".graphcode-rendezvous.secret") -PathType Leaf)) { + throw "graphcoded.exe did not initialize the explicit support directory" + } + if (@(Get-ChildItem -LiteralPath $redirectedProfile -Force).Count -ne 0) { + throw "graphcoded.exe wrote into redirected USERPROFILE despite explicit support isolation" + } + + $summary = @( + "REDIRECTED_USERPROFILE_DAEMON: PASS" + "executable=$DaemonExecutable" + "support=$supportDirectory" + "profile=$redirectedProfile" + "exit=$(Format-ExitCode $process.ExitCode)" + "stdout=$($stdout.Trim())" + "stderr=$($stderr.Trim())" + ) + $evidencePath = Join-Path $evidenceDirectory "redirected-userprofile-$nonce.txt" + [IO.File]::WriteAllLines($evidencePath, $summary, [Text.UTF8Encoding]::new($false)) + $summary + "evidence=$evidencePath" + $successful = $true +} finally { + if ($process -and -not $process.HasExited) { + if ($shutdownEvent) { + [void] $shutdownEvent.Set() + [void] $process.WaitForExit(3000) + } + if (-not $process.HasExited) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + [void] $process.WaitForExit(3000) + } + } + if ($process) { + $process.Dispose() + } + if ($shutdownEvent) { + $shutdownEvent.Dispose() + } + if ($successful) { + Remove-Item -LiteralPath $runRoot -Recurse -Force + } else { + Write-Output "Failure artifacts retained at $runRoot" + } +}