From 44b7835ce3ec934dbfb23b63fdca4a90e00d9050 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Sat, 29 Aug 2026 14:41:49 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EA=B0=9C=20=EB=9F=B0=ED=83=80?= =?UTF-8?q?=EC=9E=84=20=EB=B9=8C=EB=93=9C=20=EC=A2=85=EB=A3=8C=20=ED=8C=90?= =?UTF-8?q?=EC=A0=95=EC=9D=84=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/start-public-runtime.ps1 | 7 ++ scripts/test_start_public_runtime_contract.py | 79 ++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/scripts/start-public-runtime.ps1 b/scripts/start-public-runtime.ps1 index eb029af..1c31566 100644 --- a/scripts/start-public-runtime.ps1 +++ b/scripts/start-public-runtime.ps1 @@ -1574,6 +1574,10 @@ if (!$SkipWebRestart) { -WorkingDirectory $WebDir ` -NoNewWindow ` -PassThru + # Windows PowerShell 5.1의 Start-Process -PassThru는 Process.Handle을 먼저 + # 열지 않으면 timed WaitForExit 뒤에도 ExitCode가 $null로 남을 수 있다. + # 성공한 빌드를 실패로 오판하지 않도록 핸들을 대기 전에 확보한다. + $null = $build.Handle if (-not $build.WaitForExit($WebBuildTimeoutSeconds * 1000)) { $null = @( Stop-ProcessTreeBounded ` @@ -1584,6 +1588,9 @@ if (!$SkipWebRestart) { throw "Web build timed out after $WebBuildTimeoutSeconds seconds" } $build.Refresh() + if ($null -eq $build.ExitCode) { + throw "Web build exit code is unavailable" + } if ($build.ExitCode -ne 0) { throw "Web build failed with exit code $($build.ExitCode)" } diff --git a/scripts/test_start_public_runtime_contract.py b/scripts/test_start_public_runtime_contract.py index 81fef78..ba562b4 100644 --- a/scripts/test_start_public_runtime_contract.py +++ b/scripts/test_start_public_runtime_contract.py @@ -101,6 +101,70 @@ exit 0 with self.subTest(expected=expected): self.assertIn(expected, PUBLIC_RUNTIME) + def test_web_build_keeps_process_handle_before_timed_wait(self) -> None: + build_start = PUBLIC_RUNTIME.index( + '$build = Start-Process -FilePath "cmd.exe"' + ) + handle = PUBLIC_RUNTIME.index("$null = $build.Handle", build_start) + timed_wait = PUBLIC_RUNTIME.index( + "$build.WaitForExit($WebBuildTimeoutSeconds * 1000)", handle + ) + null_guard = PUBLIC_RUNTIME.index( + "if ($null -eq $build.ExitCode)", timed_wait + ) + failure_check = PUBLIC_RUNTIME.index( + "if ($build.ExitCode -ne 0)", null_guard + ) + self.assertLess(build_start, handle) + self.assertLess(handle, timed_wait) + self.assertLess(timed_wait, null_guard) + self.assertLess(null_guard, failure_check) + + def test_web_build_exit_code_is_available_in_windows_powershell(self) -> None: + powershell = shutil.which("powershell.exe") + if powershell is None: + self.skipTest("Windows PowerShell 5.1 is not available") + + with tempfile.TemporaryDirectory() as temporary_directory: + harness = Path(temporary_directory) / "process-exit-code.ps1" + harness.write_text( + """ +$process = Start-Process -FilePath 'cmd.exe' ` + -ArgumentList @('/c', 'exit 0') ` + -NoNewWindow ` + -PassThru +$null = $process.Handle +if (-not $process.WaitForExit(10000)) { exit 2 } +$process.Refresh() +if ($null -eq $process.ExitCode) { exit 3 } +exit $process.ExitCode +""", + encoding="utf-8-sig", + ) + completed = subprocess.run( + [ + powershell, + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + str(harness), + ], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=30, + check=False, + ) + self.assertEqual( + completed.returncode, + 0, + msg=f"stdout={completed.stdout}\nstderr={completed.stderr}", + ) + def test_web_build_timeout_stops_the_owned_process_tree(self) -> None: powershell = shutil.which("powershell.exe") if powershell is None: @@ -129,6 +193,7 @@ $root = Start-Process -FilePath 'cmd.exe' ` -ArgumentList @('/c', 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{quoted_child}"') ` -WindowStyle Hidden ` -PassThru +$childProcessId = $null try {{ $deadline = (Get-Date).AddSeconds(10) while (-not (Test-Path -LiteralPath '{quoted_child_pid_for_harness}') -and (Get-Date) -lt $deadline) {{ @@ -137,11 +202,21 @@ try {{ if (-not (Test-Path -LiteralPath '{quoted_child_pid_for_harness}')) {{ exit 2 }} $childProcessId = [int](Get-Content -LiteralPath '{quoted_child_pid_for_harness}' -Raw) $stopped = @(Stop-ProcessTreeBounded -RootProcessId $root.Id -TimeoutSec 10 -Role 'test tree') - if ($null -ne (Get-Process -Id $root.Id -ErrorAction SilentlyContinue)) {{ exit 3 }} - if ($null -ne (Get-Process -Id $childProcessId -ErrorAction SilentlyContinue)) {{ exit 4 }} + $exitDeadline = (Get-Date).AddSeconds(10) + do {{ + $rootAlive = $null -ne (Get-Process -Id $root.Id -ErrorAction SilentlyContinue) + $childAlive = $null -ne (Get-Process -Id $childProcessId -ErrorAction SilentlyContinue) + if (-not $rootAlive -and -not $childAlive) {{ break }} + Start-Sleep -Milliseconds 200 + }} while ((Get-Date) -lt $exitDeadline) + if ($rootAlive) {{ exit 3 }} + if ($childAlive) {{ exit 4 }} exit 0 }} finally {{ Stop-Process -Id $root.Id -Force -ErrorAction SilentlyContinue + if ($null -ne $childProcessId) {{ + Stop-Process -Id $childProcessId -Force -ErrorAction SilentlyContinue + }} }} ''', encoding="utf-8-sig",