diff --git a/tests/exit-flush-test-script.ps1 b/tests/exit-flush-test-script.ps1 new file mode 100644 index 0000000..d1eca0c --- /dev/null +++ b/tests/exit-flush-test-script.ps1 @@ -0,0 +1,40 @@ +# Regression script for https://github.com/getsentry/sentry-powershell/issues/38 +# +# Starts Sentry, captures a message, and exits WITHOUT calling Stop-Sentry to verify that +# events are still flushed/delivered automatically on process exit (and that the process +# exits cleanly without hanging). +# +# A file-writing transport (FileTransport, defined in utils.ps1) is used instead of a network +# call so that delivery can be observed from the parent process after this one exits, without +# relying on networking/open ports in CI. +# +# Usage: pwsh -File exit-flush-test-script.ps1 +param( + [Parameter(Mandatory)] + [string] $OutputFile +) + +Set-StrictMode -Version latest +$ErrorActionPreference = 'Stop' + +# When this script is launched cross-edition - e.g. powershell.exe (Windows PowerShell) spawned +# from a pwsh (PowerShell Core) host - the inherited $env:PSModulePath points only at the launching +# edition's module directories. That prevents Windows PowerShell from autoloading its built-in +# modules (notably Microsoft.PowerShell.Utility, which provides Import-PowerShellDataFile used while +# importing the Sentry module). Reset to the machine default so built-in modules are discoverable. +if ($PSVersionTable.PSEdition -eq 'Desktop') { + $env:PSModulePath = [System.Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') +} + +Import-Module "$PSScriptRoot/../modules/Sentry/Sentry.psd1" +. "$PSScriptRoot/utils.ps1" + +Start-Sentry { + $_.Dsn = 'https://key@127.0.0.1/1' + $_.Transport = [FileTransport]::new($OutputFile) +} + +$null = [Sentry.SentrySdk]::CaptureMessage('hello-from-exit-flush-test') + +# Intentionally NO Stop-Sentry call here - delivery must happen automatically on exit. +Write-Host 'CHILD_DONE' diff --git a/tests/exit-flush.tests.ps1 b/tests/exit-flush.tests.ps1 new file mode 100644 index 0000000..363c7ff --- /dev/null +++ b/tests/exit-flush.tests.ps1 @@ -0,0 +1,85 @@ +# Regression tests for https://github.com/getsentry/sentry-powershell/issues/38 +# +# Verifies that a script which captures an event and exits WITHOUT calling Stop-Sentry: +# * exits cleanly (exit code 0) within a timeout (i.e. does not hang on exit), and +# * still delivers the captured event. +# +# Historically the .NET SDK's automatic flush-on-exit hook (AppDomain.ProcessExit) was disabled +# in this module because it could hang/crash the process (sentry-dotnet#3141). That was fixed and +# the workaround was removed in #85, so omitting Stop-Sentry must now Just Work. + +BeforeAll { + $script:childScript = "$PSScriptRoot$([IO.Path]::DirectorySeparatorChar)exit-flush-test-script.ps1" + + # Runs the exit-flush child script in a separate process and returns whether it exited cleanly + # within the timeout, its exit code, and the delivered envelope content. + function Invoke-ExitFlushChild { + param( + [Parameter(Mandatory)] [string] $Executable, + [int] $TimeoutSeconds = 60 + ) + + $outputFile = [IO.Path]::GetTempFileName() + # FileTransport appends; start from an empty file. + Remove-Item $outputFile -ErrorAction SilentlyContinue + + # Use System.Diagnostics.Process directly rather than Start-Process: on Windows PowerShell 5.1 + # the object returned by `Start-Process -PassThru` does not reliably populate .ExitCode after + # WaitForExit(timeout), whereas reading it from a Process we started ourselves works on both + # editions. + $psi = [System.Diagnostics.ProcessStartInfo]::new() + $psi.FileName = $Executable + # ArgumentList isn't available on .NET Framework (WinPS 5.1), so build the argument string. + $psi.Arguments = '-NoProfile -File "{0}" "{1}"' -f $script:childScript, $outputFile + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.CreateNoWindow = $true + + $proc = [System.Diagnostics.Process]::Start($psi) + # Read the streams asynchronously to avoid deadlocking if a pipe buffer fills. + $stdoutTask = $proc.StandardOutput.ReadToEndAsync() + $stderrTask = $proc.StandardError.ReadToEndAsync() + + try { + $exited = $proc.WaitForExit($TimeoutSeconds * 1000) + if (-not $exited) { + try { $proc.Kill() } catch {} + return [PSCustomObject]@{ + Exited = $false + ExitCode = $null + Envelope = '' + StdOut = $stdoutTask.Result + StdErr = $stderrTask.Result + } + } + + return [PSCustomObject]@{ + Exited = $true + ExitCode = $proc.ExitCode + Envelope = (Get-Content -Raw $outputFile -ErrorAction SilentlyContinue) + StdOut = $stdoutTask.Result + StdErr = $stderrTask.Result + } + } finally { + $proc.Dispose() + Remove-Item $outputFile -ErrorAction SilentlyContinue + } + } +} + +Describe 'Automatic flush on exit (without Stop-Sentry)' { + It 'Windows PowerShell' -Skip:($env:OS -ne 'Windows_NT') { + $result = Invoke-ExitFlushChild -Executable 'powershell.exe' + $result.Exited | Should -BeTrue -Because "the process must not hang on exit. StdErr: $($result.StdErr)" + $result.ExitCode | Should -Be 0 -Because "the process must exit cleanly. StdErr: $($result.StdErr)" + $result.Envelope | Should -Match 'hello-from-exit-flush-test' -Because 'the captured event must be delivered even without Stop-Sentry' + } + + It 'PowerShell' { + $result = Invoke-ExitFlushChild -Executable 'pwsh' + $result.Exited | Should -BeTrue -Because "the process must not hang on exit. StdErr: $($result.StdErr)" + $result.ExitCode | Should -Be 0 -Because "the process must exit cleanly. StdErr: $($result.StdErr)" + $result.Envelope | Should -Match 'hello-from-exit-flush-test' -Because 'the captured event must be delivered even without Stop-Sentry' + } +} diff --git a/tests/utils.ps1 b/tests/utils.ps1 index da0da42..b8c7701 100644 --- a/tests/utils.ps1 +++ b/tests/utils.ps1 @@ -11,6 +11,26 @@ class RecordingTransport:Sentry.Extensibility.ITransport { } } +class FileTransport:Sentry.Extensibility.ITransport { + [string] $path + + FileTransport([string] $path) { + $this.path = $path + } + + # Serializes every envelope it's asked to send to a file on disk so that delivery can be + # observed from a parent process after the sending process exits (used by the exit-flush test). + [System.Threading.Tasks.Task]SendEnvelopeAsync([Sentry.Protocol.Envelopes.Envelope] $envelope, [System.Threading.CancellationToken] $cancellationToken) { + $stream = [System.IO.File]::Open($this.path, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite) + try { + $envelope.Serialize($stream, $null) + } finally { + $stream.Dispose() + } + return [System.Threading.Tasks.Task]::CompletedTask + } +} + class TestLogger:Sentry.Infrastructure.DiagnosticLogger { TestLogger([Sentry.SentryLevel]$level) : base($level) {}