diff --git a/.github/scripts/run-with-timeout.ps1 b/.github/scripts/run-with-timeout.ps1 new file mode 100644 index 000000000..cfbf13e33 --- /dev/null +++ b/.github/scripts/run-with-timeout.ps1 @@ -0,0 +1,253 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [ValidateRange(1, 86400)] + [int]$TimeoutSeconds = 600, + + [ValidateRange(1, 16)] + [int]$ProcessCount = 1, + + [ValidateNotNullOrEmpty()] + [string]$DiagnosticsDirectory = "test-diagnostics", + + [ValidateNotNullOrEmpty()] + [string]$Label = [System.IO.Path]::GetFileNameWithoutExtension($FilePath), + + [string]$ProcessArguments = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if (-not ("RunWithTimeout.NativeMethods" -as [type])) { + Add-Type -TypeDefinition @" +namespace RunWithTimeout +{ + using System; + using System.Runtime.InteropServices; + + public static class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWow64Process( + IntPtr processHandle, + [MarshalAs(UnmanagedType.Bool)] out bool wow64Process); + } +} +"@ +} + +function Stop-RunningProcess { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process]$Process + ) + + if (-not $Process.HasExited) { + try { + Stop-Process -Id $Process.Id + } + catch { + $Process.Refresh() + if (-not $Process.HasExited) { + throw + } + } + $Process.WaitForExit() + } +} + +function Get-DumpSystemDirectory { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process]$Process + ) + + if (-not [Environment]::Is64BitOperatingSystem) { + return (Join-Path $env:WINDIR "System32") + } + + $isWow64 = $false + if (-not [RunWithTimeout.NativeMethods]::IsWow64Process($Process.Handle, [ref]$isWow64)) { + $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Unable to determine the architecture of process $($Process.Id) (Win32 error $errorCode)." + } + + if ($isWow64) { + return (Join-Path $env:WINDIR "SysWOW64") + } + + if (-not [Environment]::Is64BitProcess) { + return (Join-Path $env:WINDIR "Sysnative") + } + + return (Join-Path $env:WINDIR "System32") +} + +function Save-ProcessDump { + param( + [Parameter(Mandatory = $true)] + [System.Diagnostics.Process]$Process, + + [Parameter(Mandatory = $true)] + [string]$DumpPath + ) + + $dumpProcess = $null + $procdump = Get-Command procdump.exe -ErrorAction SilentlyContinue + if ($null -ne $procdump) { + # A minidump contains the thread stacks and module list needed for a + # deadlock diagnosis without copying arbitrary process memory into CI + # artifacts. + $arguments = "-accepteula -mm $($Process.Id) `"$DumpPath`"" + $dumpProcess = Start-Process -FilePath $procdump.Source -ArgumentList $arguments -PassThru -NoNewWindow + } + else { + # The dump writer must match the target process architecture. A + # 64-bit helper cannot reliably capture Win32 thread context, and a + # 32-bit helper cannot inspect a 64-bit target. + $systemDirectory = Get-DumpSystemDirectory -Process $Process + $powershell = Join-Path $systemDirectory "WindowsPowerShell\v1.0\powershell.exe" + $dumpScript = Join-Path $PSScriptRoot "write-minidump.ps1" + $arguments = "-NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$dumpScript`" -ProcessId $($Process.Id) -DumpPath `"$DumpPath`"" + $dumpProcess = Start-Process -FilePath $powershell -ArgumentList $arguments -PassThru -NoNewWindow + } + + if (-not $dumpProcess.WaitForExit(30000)) { + Stop-RunningProcess -Process $dumpProcess + throw "Timed out while capturing dump for process $($Process.Id)." + } + $dumpProcess.WaitForExit() + $dumpProcess.Refresh() + + if ($dumpProcess.ExitCode -ne 0) { + throw "Dump capture for process $($Process.Id) exited with code $($dumpProcess.ExitCode)." + } + + if (-not (Test-Path -LiteralPath $DumpPath -PathType Leaf)) { + throw "Dump capture for process $($Process.Id) did not create $DumpPath." + } + + $dumpFile = Get-Item -LiteralPath $DumpPath -ErrorAction SilentlyContinue + if ($null -eq $dumpFile -or $dumpFile.Length -eq 0) { + throw "Dump capture for process $($Process.Id) created an empty dump." + } +} + +$resolvedFilePath = (Resolve-Path -LiteralPath $FilePath).Path +$resolvedDiagnosticsDirectory = [System.IO.Path]::GetFullPath($DiagnosticsDirectory) +New-Item -ItemType Directory -Path $resolvedDiagnosticsDirectory -Force | Out-Null + +$safeLabel = $Label -replace '[^A-Za-z0-9_.-]', '_' +$statusPath = Join-Path $resolvedDiagnosticsDirectory "$safeLabel-status.txt" +$startedAt = Get-Date +@( + "Command: $resolvedFilePath" + "Arguments: $ProcessArguments" + "Process count: $ProcessCount" + "Timeout seconds: $TimeoutSeconds" + "Started: $($startedAt.ToString('o'))" +) | Set-Content -LiteralPath $statusPath + +$processes = @() +try { + for ($index = 0; $index -lt $ProcessCount; $index++) { + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.FileName = $resolvedFilePath + $startInfo.Arguments = $ProcessArguments + $startInfo.UseShellExecute = $false + + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + if (-not $process.Start()) { + throw "Failed to start $resolvedFilePath." + } + $processes += $process + } +} +catch { + foreach ($process in $processes) { + Stop-RunningProcess -Process $process + } + throw +} + +$deadline = $startedAt.AddSeconds($TimeoutSeconds) +while ($true) { + $failedProcess = $null + $failedExitCode = 0 + $running = @() + foreach ($process in $processes) { + if ($process.HasExited) { + # WaitForExit() populates ExitCode reliably for processes that can + # finish before the first polling iteration. + $process.WaitForExit() + $process.Refresh() + if ($process.ExitCode -ne 0 -and $null -eq $failedProcess) { + $failedProcess = $process + $failedExitCode = $process.ExitCode + } + } + else { + $running += $process + } + } + + if ($null -ne $failedProcess) { + foreach ($process in $processes) { + Stop-RunningProcess -Process $process + } + Add-Content -LiteralPath $statusPath -Value @( + "Completed: $((Get-Date).ToString('o'))" + "Result: failed" + "Exit code: $failedExitCode" + ) + exit $failedExitCode + } + + if ($running.Count -eq 0) { + Add-Content -LiteralPath $statusPath -Value @( + "Completed: $((Get-Date).ToString('o'))" + "Result: passed" + "Exit code: 0" + ) + exit 0 + } + + if ((Get-Date) -ge $deadline) { + Write-Host "::error::$Label exceeded its $TimeoutSeconds-second timeout." + Add-Content -LiteralPath $statusPath -Value @( + "Completed: $((Get-Date).ToString('o'))" + "Result: timed out" + "Exit code: 124" + ) + + foreach ($process in $running) { + try { + if (-not $process.HasExited) { + $detailsPath = Join-Path $resolvedDiagnosticsDirectory "$safeLabel-$($process.Id).txt" + Get-Process -Id $process.Id | + Format-List Id, ProcessName, StartTime, TotalProcessorTime, Threads, HandleCount | + Out-File -LiteralPath $detailsPath + + $dumpPath = Join-Path $resolvedDiagnosticsDirectory "$safeLabel-$($process.Id).dmp" + Save-ProcessDump -Process $process -DumpPath $dumpPath + Write-Host "Captured $dumpPath" + } + } + catch { + Write-Warning $_ + } + finally { + Stop-RunningProcess -Process $process + } + } + exit 124 + } + + Start-Sleep -Milliseconds 200 +} diff --git a/.github/scripts/write-minidump.ps1 b/.github/scripts/write-minidump.ps1 new file mode 100644 index 000000000..9ff613062 --- /dev/null +++ b/.github/scripts/write-minidump.ps1 @@ -0,0 +1,63 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateRange(1, [int]::MaxValue)] + [int]$ProcessId, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$DumpPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +Add-Type -TypeDefinition @" +namespace WriteMiniDump +{ + using System; + using System.Runtime.InteropServices; + using Microsoft.Win32.SafeHandles; + + public static class NativeMethods + { + [DllImport("dbghelp.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool MiniDumpWriteDump( + IntPtr processHandle, + uint processId, + SafeFileHandle fileHandle, + uint dumpType, + IntPtr exceptionParameters, + IntPtr userStreamParameters, + IntPtr callbackParameters); + } +} +"@ + +$process = Get-Process -Id $ProcessId +$resolvedDumpPath = [System.IO.Path]::GetFullPath($DumpPath) +$dumpStream = [System.IO.File]::Open( + $resolvedDumpPath, + [System.IO.FileMode]::Create, + [System.IO.FileAccess]::Write, + [System.IO.FileShare]::None) + +try { + $created = [WriteMiniDump.NativeMethods]::MiniDumpWriteDump( + $process.Handle, + [uint32]$process.Id, + $dumpStream.SafeFileHandle, + 0, + [IntPtr]::Zero, + [IntPtr]::Zero, + [IntPtr]::Zero) + if (-not $created) { + $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "MiniDumpWriteDump failed for process $ProcessId (Win32 error $errorCode)." + } +} +finally { + $dumpStream.Dispose() + $process.Dispose() +} diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 576eeb1ce..96cb76c80 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -50,6 +50,8 @@ jobs: java-version: '17' - name: Setup Android SDK uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: platform-tools - name: Install NDK run: | java -version diff --git a/.github/workflows/build-windows-vs2022.yaml b/.github/workflows/build-windows-vs2022.yaml index e109575e8..c6efdb426 100644 --- a/.github/workflows/build-windows-vs2022.yaml +++ b/.github/workflows/build-windows-vs2022.yaml @@ -34,7 +34,6 @@ jobs: env: SKIP_ARM_BUILD: 1 SKIP_ARM64_BUILD: 1 - SKIP_NET40_BUILD: 1 PlatformToolset: v143 VSTOOLS_VERSION: vs2022 shell: cmd diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a47773036..bad283b5f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -132,6 +132,8 @@ jobs: java-version: '17' - name: Setup Android SDK uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2 + with: + packages: platform-tools - name: Install NDK run: | java -version diff --git a/.github/workflows/test-vcpkg.yml b/.github/workflows/test-vcpkg.yml index 59961ce53..98ef86429 100644 --- a/.github/workflows/test-vcpkg.yml +++ b/.github/workflows/test-vcpkg.yml @@ -24,7 +24,11 @@ concurrency: jobs: windows: runs-on: windows-latest - name: Windows (x64-windows-static) + name: Windows (x64-windows-static, ${{ matrix.transport }}) + strategy: + fail-fast: false + matrix: + transport: [WinHTTP, WinInet] steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -35,7 +39,12 @@ jobs: shell: pwsh - name: Run vcpkg port test - run: .\tests\vcpkg\test-vcpkg-windows.ps1 -VcpkgRoot "${{ runner.temp }}\vcpkg" + run: | + $arguments = @{ VcpkgRoot = "${{ runner.temp }}\vcpkg" } + if ("${{ matrix.transport }}" -eq "WinInet") { + $arguments.WinInet = $true + } + .\tests\vcpkg\test-vcpkg-windows.ps1 @arguments shell: pwsh linux: diff --git a/.github/workflows/test-win-latest.yml b/.github/workflows/test-win-latest.yml index 2a77d5e2a..66261d1e6 100644 --- a/.github/workflows/test-win-latest.yml +++ b/.github/workflows/test-win-latest.yml @@ -32,28 +32,47 @@ concurrency: jobs: test: - name: Test on Windows ${{ matrix.arch }}-${{ matrix.build }} + name: Test on Windows ${{ matrix.arch }}-${{ matrix.build }}${{ matrix.transport == 'WinInet' && ' (WinInet)' || '' }} runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: + fail-fast: false matrix: arch: [Win32, x64] build: [Release, Debug] + transport: [WinHTTP, WinInet] os: [windows-2022] + exclude: + - build: Debug + transport: WinInet steps: - name: Checkout uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - continue-on-error: true - name: setup-msbuild uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 with: vs-version: '[17,)' - - name: Test ${{ matrix.arch }} ${{ matrix.build }} + - name: Test ${{ matrix.transport }} ${{ matrix.arch }} ${{ matrix.build }} shell: cmd - run: build-tests.cmd ${{ matrix.arch }} ${{ matrix.build }} + run: build-tests.cmd ${{ matrix.arch }} ${{ matrix.build }} "" ${{ matrix.transport }} + + - name: Upload test failure diagnostics + if: failure() || cancelled() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: windows-test-failure-${{ matrix.transport }}-${{ matrix.arch }}-${{ matrix.build }} + path: | + test-diagnostics + Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/UnitTests/*.pdb + Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/UnitTests/*.map + Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/FuncTests/*.pdb + Solutions/out/${{ matrix.build }}/${{ matrix.arch }}/FuncTests/*.map + if-no-files-found: ignore + retention-days: 7 public-headers: name: Public header gate (MSVC) diff --git a/Solutions/Clienttelemetry/Clienttelemetry.vcxitems b/Solutions/Clienttelemetry/Clienttelemetry.vcxitems index 065ca3118..ec398399b 100644 --- a/Solutions/Clienttelemetry/Clienttelemetry.vcxitems +++ b/Solutions/Clienttelemetry/Clienttelemetry.vcxitems @@ -105,7 +105,7 @@ - + @@ -161,6 +161,7 @@ + diff --git a/Solutions/Clienttelemetry/Clienttelemetry.vcxitems.filters b/Solutions/Clienttelemetry/Clienttelemetry.vcxitems.filters index 376e1dfba..3f2ed6822 100644 --- a/Solutions/Clienttelemetry/Clienttelemetry.vcxitems.filters +++ b/Solutions/Clienttelemetry/Clienttelemetry.vcxitems.filters @@ -90,7 +90,7 @@ - + @@ -147,6 +147,9 @@ + + Header Files + diff --git a/Solutions/MSTelemetrySDK.sln b/Solutions/MSTelemetrySDK.sln index 8e83ec54a..0f531a2ea 100644 --- a/Solutions/MSTelemetrySDK.sln +++ b/Solutions/MSTelemetrySDK.sln @@ -19,7 +19,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "win10-cs", "win10-cs\win10- {8FD826F8-3739-44E6-8CC8-997122E53B8D} = {8FD826F8-3739-44E6-8CC8-997122E53B8D} EndProjectSection EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "net40", "net40\net40.vcxproj", "{DC91621E-A203-42DF-8E03-3A23DD0602B1}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "net48", "net48\net48.vcxproj", "{DC91621E-A203-42DF-8E03-3A23DD0602B1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{250EFB82-2F0E-4781-94BB-8313201ABDF0}" EndProject @@ -87,7 +87,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleCpp", "..\examples\cp EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SampleCppUWP", "..\examples\cpp\SampleCppUWP\SampleCppUWP.vcxproj", "{39DBD601-4D79-49F9-AD18-065404DBA273}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleCsNet40", "..\examples\cs\SampleCsNet40\SampleCsNet40.csproj", "{65AFA0E2-F9A2-4309-87E7-E419D59583C1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleCsNet48", "..\examples\cs\SampleCsNet48\SampleCsNet48.csproj", "{65AFA0E2-F9A2-4309-87E7-E419D59583C1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleCsUWP", "..\examples\cs\SampleCsUWP\SampleCsUWP.csproj", "{F797B22C-A1C4-4136-9DCC-0682A183A4DA}" EndProject diff --git a/Solutions/before.targets b/Solutions/before.targets index 43e18d434..1be68446e 100644 --- a/Solutions/before.targets +++ b/Solutions/before.targets @@ -2,6 +2,14 @@ $(SolutionDir)\..\third_party\krabsetw\krabs;$(CustomIncludePath) + + + + _SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS;%(PreprocessorDefinitions) + $(SolutionDir)..\zlib;$(SolutionDir)..\sqlite;$(SolutionDir)..\lib\pal\universal;%(AdditionalIncludeDirectories) + + diff --git a/Solutions/build.net40.props b/Solutions/build.net48.props similarity index 83% rename from Solutions/build.net40.props rename to Solutions/build.net48.props index 6bb7ab851..7c6843ee2 100644 --- a/Solutions/build.net40.props +++ b/Solutions/build.net48.props @@ -2,7 +2,7 @@ - %(PreprocessorDefinitions);CONFIG_CUSTOM_H="config-net40.h" + %(PreprocessorDefinitions);CONFIG_CUSTOM_H="config-net48.h" diff --git a/Solutions/conformance.props b/Solutions/conformance.props index 79e1ae779..6a2b1efdb 100644 --- a/Solutions/conformance.props +++ b/Solutions/conformance.props @@ -2,7 +2,7 @@ + diff --git a/Solutions/win32-cs/deploy-dll.cmd b/Solutions/win32-cs/deploy-dll.cmd index 5feac91bc..8adccab60 100644 --- a/Solutions/win32-cs/deploy-dll.cmd +++ b/Solutions/win32-cs/deploy-dll.cmd @@ -3,17 +3,16 @@ cd /d %~dp0 set OUTDIR=%CD%\..\..\out cd %OUTDIR% -xcopy /Y /D Debug\Win32\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Debug\x86\win32-cs\bin\ -xcopy /Y /D Debug\x64\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Debug\x64\win32-cs\bin\ +xcopy /Y /D Debug\Win32\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Debug\x86\win32-cs\bin\ +xcopy /Y /D Debug\x64\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Debug\x64\win32-cs\bin\ -xcopy /Y /D Debug.vs2013\Win32\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Debug.vs2013\x86\win32-cs\bin\ -xcopy /Y /D Debug.vs2013\x64\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Debug.vs2013\x64\win32-cs\bin\ +xcopy /Y /D Debug.vs2013\Win32\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Debug.vs2013\x86\win32-cs\bin\ +xcopy /Y /D Debug.vs2013\x64\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Debug.vs2013\x64\win32-cs\bin\ -xcopy /Y /D Release\Win32\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Release\x86\win32-cs\bin\ -xcopy /Y /D Release\x64\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Release\x64\win32-cs\bin\ +xcopy /Y /D Release\Win32\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Release\x86\win32-cs\bin\ +xcopy /Y /D Release\x64\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Release\x64\win32-cs\bin\ -xcopy /Y /D Release.vs2013\Win32\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Release.vs2013\x86\win32-cs\bin\ -xcopy /Y /D Release.vs2013\x64\net40\bin\Microsoft.Applications.Telemetry.Windows.dll Release.vs2013\x64\win32-cs\bin\ +xcopy /Y /D Release.vs2013\Win32\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Release.vs2013\x86\win32-cs\bin\ +xcopy /Y /D Release.vs2013\x64\net48\bin\Microsoft.Applications.Telemetry.Windows.dll Release.vs2013\x64\win32-cs\bin\ exit /b 0 - diff --git a/Solutions/win32-cs/packages.config b/Solutions/win32-cs/packages.config index a751695d0..c358ed79c 100644 --- a/Solutions/win32-cs/packages.config +++ b/Solutions/win32-cs/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/Solutions/win32-cs/win32-cs.csproj b/Solutions/win32-cs/win32-cs.csproj index a93a15fa7..53c3361b7 100644 --- a/Solutions/win32-cs/win32-cs.csproj +++ b/Solutions/win32-cs/win32-cs.csproj @@ -10,7 +10,7 @@ Properties CLI win32-cs - v4.0 + v4.8 512 false @@ -39,7 +39,7 @@ prompt MinimumRecommendedRules.ruleset true - v4.0 + v4.8 true ..\..\out\Debug\x86\win32-cs\bin\ true @@ -52,7 +52,7 @@ prompt MinimumRecommendedRules.ruleset true - v4.0 + v4.8 true @@ -62,7 +62,7 @@ prompt MinimumRecommendedRules.ruleset false - v4.0 + v4.8 TRACE @@ -73,7 +73,7 @@ MinimumRecommendedRules.ruleset false true - v4.0 + v4.8 CLI.Program @@ -117,9 +117,9 @@ - + False - Microsoft .NET Framework 4 %28x86 and x64%29 + Microsoft .NET Framework 4.8 %28x86 and x64%29 true @@ -134,9 +134,9 @@ - + {dc91621e-a203-42df-8e03-3a23dd0602b1} - net40 + net48 diff --git a/Solutions/win32-dll/win32-dll.vcxproj b/Solutions/win32-dll/win32-dll.vcxproj index b01b9e690..968f36990 100644 --- a/Solutions/win32-dll/win32-dll.vcxproj +++ b/Solutions/win32-dll/win32-dll.vcxproj @@ -28,10 +28,10 @@ - + - + @@ -211,7 +211,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;version.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;version.lib;%(AdditionalDependencies) runtimeobject.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -233,7 +233,7 @@ true - wininet.lib;user32.lib;shell32.lib;Advapi32.lib;Ole32.lib + user32.lib;shell32.lib;Advapi32.lib;Ole32.lib %(AdditionalLibraryDirectories) @@ -297,7 +297,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;version.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;version.lib;%(AdditionalDependencies) runtimeobject.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -322,7 +322,7 @@ true - wininet.lib;user32.lib;shell32.lib;Advapi32.lib;Ole32.lib + user32.lib;shell32.lib;Advapi32.lib;Ole32.lib %(AdditionalLibraryDirectories) @@ -336,6 +336,22 @@ {2ebc7b3c-2af1-442c-9285-cab39bbb8c00} + + + wininet.lib;%(AdditionalDependencies) + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/Solutions/win32-dll/win32-dll.vcxproj.filters b/Solutions/win32-dll/win32-dll.vcxproj.filters index 302a6f598..ec89c6c46 100644 --- a/Solutions/win32-dll/win32-dll.vcxproj.filters +++ b/Solutions/win32-dll/win32-dll.vcxproj.filters @@ -1,7 +1,7 @@  - + @@ -9,7 +9,7 @@ - + diff --git a/Solutions/win32-lib/win32-lib.vcxproj b/Solutions/win32-lib/win32-lib.vcxproj index 1b9fb6a7c..d088b06c7 100644 --- a/Solutions/win32-lib/win32-lib.vcxproj +++ b/Solutions/win32-lib/win32-lib.vcxproj @@ -279,7 +279,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll false @@ -347,7 +347,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll false @@ -425,7 +425,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -501,7 +501,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -536,6 +536,22 @@ true + + + wininet.lib;%(AdditionalDependencies) + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/Solutions/win32-mini-dll/win32-mini-dll.vcxproj b/Solutions/win32-mini-dll/win32-mini-dll.vcxproj index fe923aee2..99d21d1ba 100644 --- a/Solutions/win32-mini-dll/win32-mini-dll.vcxproj +++ b/Solutions/win32-mini-dll/win32-mini-dll.vcxproj @@ -28,10 +28,10 @@ - + - + @@ -240,7 +240,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;version.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;version.lib;%(AdditionalDependencies) runtimeobject.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -268,7 +268,7 @@ true - wininet.lib;user32.lib;shell32.lib;Advapi32.lib;Ole32.lib + user32.lib;shell32.lib;Advapi32.lib;Ole32.lib %(AdditionalLibraryDirectories) @@ -357,7 +357,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;version.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;version.lib;%(AdditionalDependencies) runtimeobject.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -385,10 +385,26 @@ true - wininet.lib;user32.lib;shell32.lib;Advapi32.lib;Ole32.lib + user32.lib;shell32.lib;Advapi32.lib;Ole32.lib %(AdditionalLibraryDirectories) + + + wininet.lib;%(AdditionalDependencies) + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/Solutions/win32-mini-dll/win32-mini-dll.vcxproj.filters b/Solutions/win32-mini-dll/win32-mini-dll.vcxproj.filters index 302a6f598..ec89c6c46 100644 --- a/Solutions/win32-mini-dll/win32-mini-dll.vcxproj.filters +++ b/Solutions/win32-mini-dll/win32-mini-dll.vcxproj.filters @@ -1,7 +1,7 @@  - + @@ -9,7 +9,7 @@ - + diff --git a/Solutions/win32-mini-lib/win32-mini-lib.vcxproj b/Solutions/win32-mini-lib/win32-mini-lib.vcxproj index 700623d89..aba9e8999 100644 --- a/Solutions/win32-mini-lib/win32-mini-lib.vcxproj +++ b/Solutions/win32-mini-lib/win32-mini-lib.vcxproj @@ -321,7 +321,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll false @@ -427,7 +427,7 @@ Windows true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll false @@ -534,7 +534,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -642,7 +642,7 @@ true true true - uuid.lib;wininet.lib;crypt32.lib;%(AdditionalDependencies) + uuid.lib;crypt32.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) api-ms-win-core-winrt-l1-1-0.dll;api-ms-win-core-winrt-string-l1-1-0.dll @@ -677,6 +677,22 @@ true + + + wininet.lib;%(AdditionalDependencies) + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/build-Win32Debug.bat b/build-Win32Debug.bat index 890bddc16..6b5cdf9f5 100644 --- a/build-Win32Debug.bat +++ b/build-Win32Debug.bat @@ -3,5 +3,5 @@ cd %~dp0 call tools\gen-version.cmd @setlocal ENABLEEXTENSIONS -call tools\RunMsBuild.bat Win32 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net40:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" +call tools\RunMsBuild.bat Win32 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net48:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" call tools\RunTests.bat Win32 Debug \ No newline at end of file diff --git a/build-Win32Release.bat b/build-Win32Release.bat index a9f8ee040..ed987543c 100644 --- a/build-Win32Release.bat +++ b/build-Win32Release.bat @@ -3,5 +3,5 @@ cd %~dp0 call tools\gen-version.cmd @setlocal ENABLEEXTENSIONS -call tools\RunMsBuild.bat Win32 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net40:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" +call tools\RunMsBuild.bat Win32 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net48:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" call tools\RunTests.bat Win32 Release \ No newline at end of file diff --git a/build-all-v143.bat b/build-all-v143.bat index 8d5ebbfa9..1e67178ab 100644 --- a/build-all-v143.bat +++ b/build-all-v143.bat @@ -2,5 +2,4 @@ set VSTOOLS_VERSION=vs2022 set PlatformToolset=v143 -set SKIP_NET40_BUILD=1 call "%~dp0build-all-windows.bat" %* diff --git a/build-all-v145.bat b/build-all-v145.bat index 54f6b4e9a..6bc704bc9 100644 --- a/build-all-v145.bat +++ b/build-all-v145.bat @@ -2,5 +2,4 @@ set VSTOOLS_VERSION=vs2026 set PlatformToolset=v145 -set SKIP_NET40_BUILD=1 call "%~dp0build-all-windows.bat" %* diff --git a/build-all-windows.bat b/build-all-windows.bat index 4ea3808e9..d5b3c7214 100644 --- a/build-all-windows.bat +++ b/build-all-windows.bat @@ -33,12 +33,12 @@ exit /b 1 :after_custom_props_validation call tools\gen-version.cmd -set NET40_MD_TARGETS=,net40:Rebuild -set NET40_SAMPLE_TARGETS=,Samples\cs\SampleCsNet40:Rebuild -if DEFINED SKIP_NET40_BUILD ( - echo Skipping legacy .NET Framework 4.0 targets. - set NET40_MD_TARGETS= - set NET40_SAMPLE_TARGETS= +set NET48_MD_TARGETS=,net48:Rebuild +set NET48_SAMPLE_TARGETS=,Samples\cs\SampleCsNet48:Rebuild +if DEFINED SKIP_NET48_BUILD ( + echo Skipping .NET Framework 4.8 targets. + set NET48_MD_TARGETS= + set NET48_SAMPLE_TARGETS= ) echo Update all public submodules... @@ -57,15 +57,15 @@ if NOT EXIST %GTEST_PATH%\CMakeLists.txt ( if NOT DEFINED SKIP_MD_BUILD ( REM DLL and static /MD build REM Release - call tools\RunMsBuild.bat Win32 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET40_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild%NET40_SAMPLE_TARGETS%" %CUSTOM_PROPS% + call tools\RunMsBuild.bat Win32 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET48_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild%NET48_SAMPLE_TARGETS%" %CUSTOM_PROPS% if errorlevel 1 exit /b 1 - call tools\RunMsBuild.bat x64 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET40_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild%NET40_SAMPLE_TARGETS%" %CUSTOM_PROPS% + call tools\RunMsBuild.bat x64 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET48_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild%NET48_SAMPLE_TARGETS%" %CUSTOM_PROPS% if errorlevel 1 exit /b 1 REM Debug if NOT DEFINED SKIP_DEBUG_BUILD ( - call tools\RunMsBuild.bat Win32 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET40_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" %CUSTOM_PROPS% + call tools\RunMsBuild.bat Win32 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET48_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" %CUSTOM_PROPS% if errorlevel 1 exit /b 1 - call tools\RunMsBuild.bat x64 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET40_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" %CUSTOM_PROPS% + call tools\RunMsBuild.bat x64 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild%NET48_MD_TARGETS%,win10-cs:Rebuild,win10-dll:Rebuild,win10-lib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" %CUSTOM_PROPS% if errorlevel 1 exit /b 1 ) ) diff --git a/build-tests.cmd b/build-tests.cmd index 7f3d0a0ba..12b74e59a 100644 --- a/build-tests.cmd +++ b/build-tests.cmd @@ -2,6 +2,18 @@ cd %~dp0 @setlocal ENABLEEXTENSIONS +set TRANSPORT=%~4 +if not defined TRANSPORT set TRANSPORT=WinHTTP +if /I "%TRANSPORT%"=="WinInet" ( + set TRANSPORT_PROPERTY=/p:MATSDK_USE_WININET=true +) else if /I "%TRANSPORT%"=="WinHTTP" ( + set TRANSPORT_PROPERTY=/p:MATSDK_USE_WININET=false +) else ( + echo ERROR: Unknown HTTP transport "%TRANSPORT%". Expected WinHTTP or WinInet. + exit /b 2 +) +echo HTTP transport: %TRANSPORT% + set CUSTOM_PROPS= if not "%~3"=="" ( if not exist "%~f3" ( @@ -52,11 +64,11 @@ set CONFIGURATION=%2 set MAXCPUCOUNT=%NUMBER_OF_PROCESSORS% set SOLUTION=Solutions\MSTelemetrySDK.sln -msbuild %SOLUTION% /target:sqlite:Rebuild,zlib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild /p:BuildProjectReferences=true /maxcpucount:%MAXCPUCOUNT% /detailedsummary /p:Configuration=%CONFIGURATION% /p:Platform=%PLAT% %CUSTOM_PROPS% +msbuild %SOLUTION% /target:sqlite:Rebuild,zlib:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild /p:BuildProjectReferences=true /maxcpucount:%MAXCPUCOUNT% /detailedsummary /p:Configuration=%CONFIGURATION% /p:Platform=%PLAT% %TRANSPORT_PROPERTY% %CUSTOM_PROPS% if not "%ERRORLEVEL%"=="0" exit /b %ERRORLEVEL% -Solutions\out\%CONFIGURATION%\%PLAT%\UnitTests\UnitTests.exe +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .github\scripts\run-with-timeout.ps1 -FilePath Solutions\out\%CONFIGURATION%\%PLAT%\UnitTests\UnitTests.exe -TimeoutSeconds 600 -Label UnitTests-%CONFIGURATION%-%PLAT%-%TRANSPORT% if not "%ERRORLEVEL%"=="0" exit /b %ERRORLEVEL% -Solutions\out\%CONFIGURATION%\%PLAT%\FuncTests\FuncTests.exe +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .github\scripts\run-with-timeout.ps1 -FilePath Solutions\out\%CONFIGURATION%\%PLAT%\FuncTests\FuncTests.exe -TimeoutSeconds 600 -Label FuncTests-%CONFIGURATION%-%PLAT%-%TRANSPORT% if not "%ERRORLEVEL%"=="0" exit /b %ERRORLEVEL% -powershell -NoProfile -ExecutionPolicy Bypass -Command "$path = Join-Path (Get-Location) 'Solutions\out\%CONFIGURATION%\%PLAT%\FuncTests\FuncTests.exe'; $args = '--gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager'; $p1 = Start-Process -FilePath $path -ArgumentList $args -PassThru; $p2 = Start-Process -FilePath $path -ArgumentList $args -PassThru; $p1.WaitForExit(); $p2.WaitForExit(); if ($p1.ExitCode -ne 0 -or $p2.ExitCode -ne 0) { exit 1 }" +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .github\scripts\run-with-timeout.ps1 -FilePath Solutions\out\%CONFIGURATION%\%PLAT%\FuncTests\FuncTests.exe -ProcessArguments "--gtest_filter=MultipleLogManagersTests.MultiProcessesLogManager" -ProcessCount 2 -TimeoutSeconds 600 -Label FuncTests-concurrent-%CONFIGURATION%-%PLAT%-%TRANSPORT% if not "%ERRORLEVEL%"=="0" exit /b %ERRORLEVEL% diff --git a/build-x64Debug.bat b/build-x64Debug.bat index 1567e1aa7..a5485c2da 100644 --- a/build-x64Debug.bat +++ b/build-x64Debug.bat @@ -3,5 +3,5 @@ cd %~dp0 call tools\gen-version.cmd @setlocal ENABLEEXTENSIONS -call tools\RunMsBuild.bat x64 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net40:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" +call tools\RunMsBuild.bat x64 Debug "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net48:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" call tools\RunTests.bat x64 Debug \ No newline at end of file diff --git a/build-x64Release.bat b/build-x64Release.bat index 5b3d6619a..ee9ee7166 100644 --- a/build-x64Release.bat +++ b/build-x64Release.bat @@ -3,5 +3,5 @@ cd %~dp0 call tools\gen-version.cmd @setlocal ENABLEEXTENSIONS -call tools\RunMsBuild.bat x64 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net40:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" +call tools\RunMsBuild.bat x64 Release "sqlite:Rebuild,zlib:Rebuild,sqlite-uwp:Rebuild,win32-dll:Rebuild,win32-lib:Rebuild,net48:Rebuild,win10-cs:Rebuild,win10-dll:Rebuild,Tests\gmock:Rebuild,Tests\gtest:Rebuild,Tests\UnitTests:Rebuild,Tests\FuncTests:Rebuild" call tools\RunTests.bat x64 Release diff --git a/cmake/MSTelemetryConfig.cmake.in b/cmake/MSTelemetryConfig.cmake.in index a46d7d94c..60a3579c2 100644 --- a/cmake/MSTelemetryConfig.cmake.in +++ b/cmake/MSTelemetryConfig.cmake.in @@ -17,6 +17,7 @@ if(@MATSDK_CONFIG_STATIC_PACKAGE@) SQLite3::SQLite3 "@MATSDK_SQLITE_PROVIDER_RESOLVED@" SQLite3 + LEGACY_TARGET SQLite::SQLite3 ${_matsdk_package_sqlite_args}) matsdk_add_package_system_dependency( MSTelemetry::zlib_dependency diff --git a/cmake/MatsdkDependencyTargets.cmake b/cmake/MatsdkDependencyTargets.cmake index f5f320043..47ddf21a4 100644 --- a/cmake/MatsdkDependencyTargets.cmake +++ b/cmake/MatsdkDependencyTargets.cmake @@ -20,7 +20,7 @@ function(matsdk_add_package_system_dependency dependency_target canonical_target endif() set(options APPLE_SYSTEM) - set(one_value_args APPLE_LIBRARY) + set(one_value_args APPLE_LIBRARY LEGACY_TARGET) cmake_parse_arguments(MATSDK_PACKAGE_DEP "${options}" "${one_value_args}" "" ${ARGN}) if(MATSDK_PACKAGE_DEP_APPLE_SYSTEM) @@ -35,6 +35,12 @@ function(matsdk_add_package_system_dependency dependency_target canonical_target elseif(NOT TARGET "${canonical_target}") find_dependency(${package_name}) endif() + if(NOT TARGET "${canonical_target}" + AND DEFINED MATSDK_PACKAGE_DEP_LEGACY_TARGET + AND TARGET "${MATSDK_PACKAGE_DEP_LEGACY_TARGET}") + matsdk_add_interface_dependency( + "${canonical_target}" "${MATSDK_PACKAGE_DEP_LEGACY_TARGET}") + endif() if(NOT TARGET "${canonical_target}") message(FATAL_ERROR "${package_name} did not create the required ${canonical_target} target.") diff --git a/cmake/MatsdkOptions.cmake b/cmake/MatsdkOptions.cmake index c90487350..ed95ec805 100644 --- a/cmake/MatsdkOptions.cmake +++ b/cmake/MatsdkOptions.cmake @@ -42,6 +42,8 @@ option(MATSDK_BUILD_AZMON "Build Azure Monitor / Application Insights support" ON) option(MATSDK_BUILD_APPLE_HTTP "Build the Apple-native HTTP client" "${APPLE}") +option(MATSDK_USE_WININET + "Use WinInet instead of WinHTTP as the Win32 desktop HTTP client" OFF) option(MATSDK_DISABLE_LOGGING "Compile internal SDK logging out" OFF) diff --git a/docs/Offline-storage-settings.md b/docs/Offline-storage-settings.md index 137cb5c34..a3f2fd60f 100644 --- a/docs/Offline-storage-settings.md +++ b/docs/Offline-storage-settings.md @@ -21,6 +21,8 @@ Set `skipSqliteInitAndShutdown` to `"true"` only when your application already o When this option is enabled, the application is responsible for calling `sqlite3_initialize()` before creating a `LogManager` that uses offline storage and for delaying `sqlite3_shutdown()` until all SDK offline storage instances have been released. +This also applies when multiple libraries in one process each embed 1DS but link to the same system or shared SQLite runtime. Configure every 1DS copy to skip SQLite initialization and shutdown, and let the host own that shared runtime. No coordination is required when each library contains a genuinely private bundled SQLite copy; the bundled CMake target hides its SQLite symbols to preserve that isolation. + ## Deprecated configurations | Configuration | diff --git a/docs/building-with-vcpkg.md b/docs/building-with-vcpkg.md index 8cdb27c69..a4aa85a3c 100644 --- a/docs/building-with-vcpkg.md +++ b/docs/building-with-vcpkg.md @@ -220,18 +220,27 @@ On Linux, libcurl is provided by the default `curl-openssl` feature; `curl-mbedtls` swaps in the mbedTLS backend — see [Choose the Linux HTTP client / TLS backend](#choose-the-linux-http-client--tls-backend-largest-lever-on-linux). -Windows and macOS/iOS use platform-native HTTP clients (WinInet and +Windows and macOS/iOS use platform-native HTTP clients (WinHTTP and NSURLSession respectively). Android defaults to the platform Java/JNI HTTP bridge; native curl is available only through explicit `android-curl-*` features. > **Note (Windows):** The port targets the MSVC/`WIN32` PAL on Windows, which -> uses WinInet, so the default `curl` dependency is declared for Linux only +> uses WinHTTP, so the default `curl` dependency is declared for Linux only > (Android has separate explicit `android-curl-*` features). A MinGW / > non-MSVC Windows triplet — or forcing `-DPAL_IMPLEMENTATION=CPP11` on Windows — > selects the curl HTTP client, which the port does not provision on Windows > (broadening `curl` to `windows` would pull an unused curl into every MSVC > build, since vcpkg platform expressions can't key off the PAL). Use a standard > MSVC triplet such as `x64-windows-static` for Windows vcpkg builds. +> +> Consumers that require WinInet's IE-integrated proxy or cookie behavior can +> opt in with the `wininet` feature, for example +> `"features": ["wininet", "system-sqlite"]`. +> WinHTTP uses automatic or machine-level proxy configuration rather than the +> logged-on user's Internet Explorer settings, does not answer authentication +> challenges with ambient user credentials, and reports WinHTTP error codes. +> Consumers that depend on the prior WinInet behavior should select the feature +> explicitly before updating. ## Optional: SIMD-Optimized zlib with zlib-ng @@ -303,7 +312,7 @@ export table pins its symbols and defeats `/OPT:REF`. ### Choose the Linux HTTP client / TLS backend (largest lever on Linux) On Linux the built-in HTTP client is libcurl, and curl's TLS backend dominates -the SDK's footprint. (Windows uses WinInet, Apple uses NSURLSession, and Android +the SDK's footprint. (Windows uses WinHTTP by default, Apple uses NSURLSession, and Android uses the Java/JNI bridge by default, so this section does not apply there.) The port exposes the Linux TLS backend as two mutually-exclusive features; pick the one that matches what your application already has: diff --git a/docs/cpp-start-windows.md b/docs/cpp-start-windows.md index 6f6189056..2a6877d17 100644 --- a/docs/cpp-start-windows.md +++ b/docs/cpp-start-windows.md @@ -29,7 +29,7 @@ If your project requires the Universal Telemetry Client (a.k.a. UTC) to send tel The version-specific scripts set `VSTOOLS_VERSION` and `PlatformToolset` before calling `build-all-windows.bat`, which builds the Windows Visual Studio solution matrix. `build-all.bat` remains as a compatibility wrapper for existing automation; if you call either script directly, set both values yourself so `tools\vcvars.cmd` selects the same Visual Studio installation as your requested toolset. -Visual Studio 2022 and newer may report the legacy .NET Framework 4.0 projects (`net40` and `SampleCsNet40`) as unsupported. They are only needed for the legacy .NET Framework wrapper; the VS2022 and VS2026 command-line wrappers skip those projects, and you can unload them in the IDE when building the native SDK. +The Windows solution includes the .NET Framework 4.8 wrapper (`net48`) and C# sample (`SampleCsNet48`). Install the .NET Framework 4.8 SDK and targeting pack through the Visual Studio Installer to build these projects. Set `SKIP_NET48_BUILD=1` before running a command-line build only when you want to build the native SDK without the managed wrapper and sample. If your build fails, then you most likely missing the following optional Visual Studio components: diff --git a/examples/c/SampleC-Guest/CMakeLists.txt b/examples/c/SampleC-Guest/CMakeLists.txt index 06a7d85b5..f64d0bcd6 100644 --- a/examples/c/SampleC-Guest/CMakeLists.txt +++ b/examples/c/SampleC-Guest/CMakeLists.txt @@ -6,8 +6,10 @@ project(SampleC-Guest) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/MSTelemetrySample.cmake) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +if(NOT MSVC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +endif() find_package (Threads) @@ -20,4 +22,4 @@ source_group(" " REGULAR_EXPRESSION "") # The 1DS SDK's required Apple frameworks are provided by MATSDK_SAMPLE_PLATFORM_LIBS. -target_link_libraries(SampleC-Guest ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} dl) +target_link_libraries(SampleC-Guest ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS}) diff --git a/examples/c/SampleC/SampleC.vcxproj b/examples/c/SampleC/SampleC.vcxproj index d307939cf..8dc8948f1 100644 --- a/examples/c/SampleC/SampleC.vcxproj +++ b/examples/c/SampleC/SampleC.vcxproj @@ -1,4 +1,4 @@ - + @@ -43,7 +43,7 @@ false - $(MSBuildProjectDirectory)\lib\$(Configuration)\$(Platform);$(VCInstallDir)lib;$(VCInstallDir)atlmfc\lib;$(WindowsSdkDir)lib;$(FrameworkSDKDir)\lib + $(LibraryPath) $(VCInstallDir)include;$(VCInstallDir)atlmfc\include;$(WindowsSdkDir)include;$(FrameworkSDKDir)\include;$(MSBuildProjectDirectory)\include @@ -53,7 +53,7 @@ Level3 Disabled HAVE_DYNAMIC_C_LIB;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - $(SolutionDir)\..\lib\include\public + $(ProjectDir)\..\..\..\lib\include\public Console @@ -80,7 +80,7 @@ - $(SolutionDir)\..\lib\include\public + $(ProjectDir)\..\..\..\lib\include\public Console @@ -102,10 +102,10 @@ - - - - + + + + diff --git a/examples/c/SampleC/SampleC.vcxproj.filters b/examples/c/SampleC/SampleC.vcxproj.filters index ec99270d2..bc6cb1d50 100644 --- a/examples/c/SampleC/SampleC.vcxproj.filters +++ b/examples/c/SampleC/SampleC.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -20,16 +20,16 @@ - + Header Files - + Header Files - + Header Files - + Header Files diff --git a/examples/cmake/MSTelemetrySample.cmake b/examples/cmake/MSTelemetrySample.cmake index 5a7eca8ab..de4d636cf 100644 --- a/examples/cmake/MSTelemetrySample.cmake +++ b/examples/cmake/MSTelemetrySample.cmake @@ -13,9 +13,24 @@ if(NOT EXISTS "${MATSDK_LIB_DIR}/libmat.a" set(MATSDK_LIB_DIR "${MATSDK_LIB_DIR}/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu" CACHE PATH "MSTelemetry library directory" FORCE) endif() -find_library(MATSDK_LIBRARY NAMES mat HINTS "${MATSDK_LIB_DIR}" NO_DEFAULT_PATH) -if(NOT MATSDK_LIBRARY) - message(FATAL_ERROR "Could not find libmat under ${MATSDK_LIB_DIR}. Set MATSDK_INSTALL_DIR or MATSDK_LIB_DIR.") +find_package(MSTelemetry CONFIG QUIET + PATHS "${MATSDK_INSTALL_DIR}/lib/cmake/MSTelemetry" + NO_DEFAULT_PATH) +if(TARGET MSTelemetry::mat) + set(MATSDK_LIBRARY MSTelemetry::mat) + set(MATSDK_SAMPLE_DEPENDENCY_LIBS "") +else() + find_library(MATSDK_LIBRARY NAMES mat HINTS "${MATSDK_LIB_DIR}" NO_DEFAULT_PATH) + if(NOT MATSDK_LIBRARY) + message(FATAL_ERROR "Could not find libmat under ${MATSDK_LIB_DIR}. Set MATSDK_INSTALL_DIR or MATSDK_LIB_DIR.") + endif() + find_package(ZLIB REQUIRED) + set(MATSDK_SAMPLE_DEPENDENCY_LIBS ZLIB::ZLIB) + if(NOT WIN32 AND NOT APPLE + AND NOT CMAKE_SYSTEM_NAME STREQUAL "Android") + find_package(CURL REQUIRED) + list(APPEND MATSDK_SAMPLE_DEPENDENCY_LIBS CURL::libcurl) + endif() endif() if(NOT EXISTS "${MATSDK_INCLUDE_DIR}") @@ -40,9 +55,13 @@ if(APPLE) endif() endif() -find_library(MATSDK_SQLITE3_LIB NAMES sqlite3 HINTS "${MATSDK_INSTALL_DIR}/lib" NO_DEFAULT_PATH) -if(NOT MATSDK_SQLITE3_LIB) - set(MATSDK_SQLITE3_LIB sqlite3) +if(TARGET MSTelemetry::mat) + set(MATSDK_SQLITE3_LIB "") +else() + find_library(MATSDK_SQLITE3_LIB NAMES sqlite3 sqlite3_bundled HINTS "${MATSDK_INSTALL_DIR}/lib" NO_DEFAULT_PATH) + if(NOT MATSDK_SQLITE3_LIB) + set(MATSDK_SQLITE3_LIB sqlite3) + endif() endif() mark_as_advanced(MATSDK_INSTALL_DIR MATSDK_INCLUDE_DIR MATSDK_LIB_DIR MATSDK_LIBRARY MATSDK_SQLITE3_LIB) diff --git a/examples/cpp/EventSender/CMakeLists.txt b/examples/cpp/EventSender/CMakeLists.txt index 223ebb785..76a86982b 100644 --- a/examples/cpp/EventSender/CMakeLists.txt +++ b/examples/cpp/EventSender/CMakeLists.txt @@ -23,4 +23,4 @@ source_group(" " REGULAR_EXPRESSION "") #tcmalloc turned off by default #target_link_libraries(EventSender ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} dl tcmalloc) -target_link_libraries(EventSender ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} dl) +target_link_libraries(EventSender ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS}) diff --git a/examples/cpp/MacProxy/CMakeLists.txt b/examples/cpp/MacProxy/CMakeLists.txt index 04dfa5d01..082ee5fed 100644 --- a/examples/cpp/MacProxy/CMakeLists.txt +++ b/examples/cpp/MacProxy/CMakeLists.txt @@ -26,4 +26,4 @@ if (CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7l") set (PLATFORM_LIBS "atomic") endif() -target_link_libraries(MacProxy ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS} dl) +target_link_libraries(MacProxy ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS}) diff --git a/examples/cpp/SampleCpp/CMakeLists.txt b/examples/cpp/SampleCpp/CMakeLists.txt index bfa90995e..4cc763ceb 100644 --- a/examples/cpp/SampleCpp/CMakeLists.txt +++ b/examples/cpp/SampleCpp/CMakeLists.txt @@ -6,8 +6,10 @@ project(SampleCpp) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/MSTelemetrySample.cmake) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +if(NOT MSVC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +endif() find_package (Threads) @@ -30,4 +32,4 @@ endif() #target_link_libraries(SampleCpp ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS} dl tcmalloc) # TODO: use add_library to allow linking against a proper exported SDK target -target_link_libraries(SampleCpp ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS} dl) +target_link_libraries(SampleCpp ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS}) diff --git a/examples/cpp/SampleCpp/SampleCpp.vcxproj b/examples/cpp/SampleCpp/SampleCpp.vcxproj index a8548808f..39bf28e1c 100644 --- a/examples/cpp/SampleCpp/SampleCpp.vcxproj +++ b/examples/cpp/SampleCpp/SampleCpp.vcxproj @@ -244,7 +244,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public @@ -252,37 +252,37 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public @@ -290,19 +290,19 @@ true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public @@ -310,37 +310,37 @@ true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public @@ -348,13 +348,13 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public @@ -461,7 +461,7 @@ true true true - wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -546,7 +546,7 @@ true true true - wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;%(AdditionalDependencies) @@ -666,7 +666,7 @@ Console true - wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -818,7 +818,7 @@ Console true - wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -894,7 +894,7 @@ Console true - wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;%(AdditionalDependencies) @@ -1013,7 +1013,7 @@ true true true - wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) @@ -1107,6 +1107,16 @@ {216a8e97-21f7-4bef-9e52-7f772c177c32} + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/examples/cpp/SampleCppLogManagers/SampleCppLogManagers.vcxproj b/examples/cpp/SampleCppLogManagers/SampleCppLogManagers.vcxproj index 7f6b47434..def42ce22 100644 --- a/examples/cpp/SampleCppLogManagers/SampleCppLogManagers.vcxproj +++ b/examples/cpp/SampleCppLogManagers/SampleCppLogManagers.vcxproj @@ -67,7 +67,7 @@ true - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public;$(SolutionDir)\..\lib\pal\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public;$(SolutionDir)\..\lib\pal\ $(ProjectDir) $(Configuration)\ $(LibraryPath) @@ -75,16 +75,16 @@ true $(ProjectDir) - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public;$(SolutionDir)\..\lib\pal\ + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public;$(SolutionDir)\..\lib\pal\ $(LibraryPath) false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)\..\..\..\lib\include\public diff --git a/examples/cpp/SampleCppMini/CMakeLists.txt b/examples/cpp/SampleCppMini/CMakeLists.txt index a2c33224f..181aff4ce 100644 --- a/examples/cpp/SampleCppMini/CMakeLists.txt +++ b/examples/cpp/SampleCppMini/CMakeLists.txt @@ -6,8 +6,10 @@ project(SampleCppMini) include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/MSTelemetrySample.cmake) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +if(NOT MSVC) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -ggdb -gdwarf-2 -std=c11") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -ggdb -gdwarf-2 -std=c++11") +endif() find_package (Threads) @@ -23,4 +25,4 @@ source_group(" " REGULAR_EXPRESSION "") #tcmalloc turned off by default #target_link_libraries(SampleCppMini ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} dl tcmalloc) -target_link_libraries(SampleCppMini ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} dl) +target_link_libraries(SampleCppMini ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS}) diff --git a/examples/cpp/SampleCppMini/SampleCppMini.vcxproj b/examples/cpp/SampleCppMini/SampleCppMini.vcxproj index 424394f7e..82f12a487 100644 --- a/examples/cpp/SampleCppMini/SampleCppMini.vcxproj +++ b/examples/cpp/SampleCppMini/SampleCppMini.vcxproj @@ -262,7 +262,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public @@ -272,7 +272,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -280,7 +280,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -288,7 +288,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -296,7 +296,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -304,7 +304,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -312,7 +312,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public @@ -322,7 +322,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -330,7 +330,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -338,7 +338,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public @@ -348,7 +348,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -356,7 +356,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -364,7 +364,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -372,7 +372,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -380,7 +380,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -388,7 +388,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public @@ -398,7 +398,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -406,7 +406,7 @@ false - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(SolutionDir)\..\lib\include\public + $(VC_IncludePath);$(WindowsSDK_IncludePath);$(ProjectDir)..\..\..\lib\include\public true @@ -430,7 +430,7 @@ false false false - false + Sync false Disabled Size @@ -453,7 +453,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -486,7 +486,7 @@ false false false - false + Sync false Disabled Size @@ -509,7 +509,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -549,7 +549,7 @@ false false false - false + Sync false Disabled Size @@ -563,7 +563,7 @@ true true true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -611,7 +611,7 @@ false false false - false + Sync false Disabled Size @@ -626,7 +626,7 @@ true true true - wininet.lib;Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true false true @@ -675,7 +675,7 @@ false false false - false + Sync false Disabled Size @@ -689,7 +689,7 @@ true true true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) false true true @@ -737,7 +737,7 @@ false false false - false + Sync false Disabled Size @@ -752,7 +752,7 @@ true true true - wininet.lib;Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) true false true @@ -802,7 +802,7 @@ Default false false - false + Sync Disabled Size false @@ -824,7 +824,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -866,7 +866,7 @@ Default false false - false + Sync Disabled Size false @@ -877,7 +877,7 @@ Console true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -930,7 +930,7 @@ Default false false - false + Sync Disabled Size false @@ -941,7 +941,7 @@ Console true - wininet.lib;Crypt32.lib;libucrtd.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;libucrtd.lib;Version.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -990,7 +990,7 @@ Default false false - false + Sync false Disabled Size @@ -1014,7 +1014,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -1054,7 +1054,7 @@ Default false false - false + Sync false Disabled Size @@ -1078,7 +1078,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -1119,7 +1119,7 @@ Default false false - false + Sync false Disabled Size @@ -1132,7 +1132,7 @@ Console true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -1183,7 +1183,7 @@ Default false false - false + Sync false Disabled Size @@ -1196,7 +1196,7 @@ Console true - wininet.lib;Crypt32.lib;libucrtd.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;libucrtd.lib;Version.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -1246,7 +1246,7 @@ Default false false - false + Sync false Disabled Size @@ -1259,7 +1259,7 @@ Console true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) false true true @@ -1310,7 +1310,7 @@ Default false false - false + Sync false Disabled Size @@ -1323,7 +1323,7 @@ Console true - wininet.lib;Crypt32.lib;libucrtd.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) + Crypt32.lib;libucrtd.lib;Version.lib;kernel32.lib;user32.lib;%(AdditionalDependencies) false true true @@ -1370,7 +1370,7 @@ false false false - false + Sync false Disabled Size @@ -1394,7 +1394,7 @@ /merge:.rdata=.text false API-MS-WIN-CORE-WINRT-STRING-L1-1-0;API-MS-WIN-CORE-WINRT-L1-1-0 - wininet.lib;Crypt32.lib; + Crypt32.lib; $(ProjectDir)\deploy-dll.cmd $(PlatformTarget) $(Configuration) $(OutDir) @@ -1433,7 +1433,7 @@ false false false - false + Sync false Disabled Size @@ -1448,7 +1448,7 @@ true true true - wininet.lib;Crypt32.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) false true true @@ -1496,7 +1496,7 @@ false false false - false + Sync false Disabled Size @@ -1512,7 +1512,7 @@ true true true - wininet.lib;Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;wininet.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + Crypt32.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;Version.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) true false true @@ -1548,16 +1548,26 @@ - + - + {1dc6b38a-b390-34ce-907f-4958807a3d43} + + + wininet.lib;%(AdditionalDependencies) + + + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/examples/cpp/SampleCppUWP/SampleCppUWP.vcxproj b/examples/cpp/SampleCppUWP/SampleCppUWP.vcxproj index a55fbf088..39c8649fe 100644 --- a/examples/cpp/SampleCppUWP/SampleCppUWP.vcxproj +++ b/examples/cpp/SampleCppUWP/SampleCppUWP.vcxproj @@ -134,7 +134,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) Cdecl true @@ -146,7 +146,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) Cdecl true @@ -159,7 +159,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) MinSpace Size @@ -173,7 +173,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _ARM_WINAPI_PARTITION_DESKTOP_SDK_AVAILABLE=1;%(ClCompile.PreprocessorDefinitions) MinSpace Size @@ -188,7 +188,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) ProgramDatabase Cdecl @@ -204,7 +204,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) MinSpace Size @@ -217,7 +217,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) ProgramDatabase Cdecl @@ -229,7 +229,7 @@ /bigobj %(AdditionalOptions) 4453;28204 - $(SolutionDir)\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\lib\include\public;$(ProjectDir);$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories) _UNICODE;UNICODE;%(PreprocessorDefinitions) MinSpace Size diff --git a/examples/cs/SampleCsNet40/deploy-dll.cmd b/examples/cs/SampleCsNet40/deploy-dll.cmd deleted file mode 100644 index 305a494a8..000000000 --- a/examples/cs/SampleCsNet40/deploy-dll.cmd +++ /dev/null @@ -1,2 +0,0 @@ -copy %3\..\net40\*.dll %3 -exit /b 0 diff --git a/examples/cs/SampleCsNet40/.gitignore b/examples/cs/SampleCsNet48/.gitignore similarity index 100% rename from examples/cs/SampleCsNet40/.gitignore rename to examples/cs/SampleCsNet48/.gitignore diff --git a/examples/cs/SampleCsNet40/App.config b/examples/cs/SampleCsNet48/App.config similarity index 89% rename from examples/cs/SampleCsNet40/App.config rename to examples/cs/SampleCsNet48/App.config index 357d2c97a..9dba31111 100644 --- a/examples/cs/SampleCsNet40/App.config +++ b/examples/cs/SampleCsNet48/App.config @@ -1,6 +1,6 @@ - + diff --git a/examples/cs/SampleCsNet40/Program.cs b/examples/cs/SampleCsNet48/Program.cs similarity index 98% rename from examples/cs/SampleCsNet40/Program.cs rename to examples/cs/SampleCsNet48/Program.cs index 12a5c38e0..9472ca10c 100644 --- a/examples/cs/SampleCsNet40/Program.cs +++ b/examples/cs/SampleCsNet48/Program.cs @@ -55,7 +55,7 @@ static void Main(string[] args) for (int i = 0; i < 999; i++) { EventProperties props2 = new EventProperties("EventSimpleFromCSharpApp"); - props.SetProperty("EventSeqNum", Convert.ToString(i)); + props2.SetProperty("EventSeqNum", Convert.ToString(i)); logger.LogEvent(props2); } diff --git a/examples/cs/SampleCsNet40/Properties/AssemblyInfo.cs b/examples/cs/SampleCsNet48/Properties/AssemblyInfo.cs similarity index 93% rename from examples/cs/SampleCsNet40/Properties/AssemblyInfo.cs rename to examples/cs/SampleCsNet48/Properties/AssemblyInfo.cs index e01acda76..65c23771c 100644 --- a/examples/cs/SampleCsNet40/Properties/AssemblyInfo.cs +++ b/examples/cs/SampleCsNet48/Properties/AssemblyInfo.cs @@ -5,11 +5,11 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("SampleCsNet40")] +[assembly: AssemblyTitle("SampleCsNet48")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Microsoft Corporation")] -[assembly: AssemblyProduct("SampleCsNet40 Testapp")] +[assembly: AssemblyProduct("SampleCsNet48 Testapp")] [assembly: AssemblyCopyright("Copyright (c) Microsoft Corporation. All rights reserved.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/examples/cs/SampleCsNet40/Properties/Resources.Designer.cs b/examples/cs/SampleCsNet48/Properties/Resources.Designer.cs similarity index 100% rename from examples/cs/SampleCsNet40/Properties/Resources.Designer.cs rename to examples/cs/SampleCsNet48/Properties/Resources.Designer.cs diff --git a/examples/cs/SampleCsNet40/Properties/Resources.resx b/examples/cs/SampleCsNet48/Properties/Resources.resx similarity index 100% rename from examples/cs/SampleCsNet40/Properties/Resources.resx rename to examples/cs/SampleCsNet48/Properties/Resources.resx diff --git a/examples/cs/SampleCsNet40/Properties/Settings.Designer.cs b/examples/cs/SampleCsNet48/Properties/Settings.Designer.cs similarity index 100% rename from examples/cs/SampleCsNet40/Properties/Settings.Designer.cs rename to examples/cs/SampleCsNet48/Properties/Settings.Designer.cs diff --git a/examples/cs/SampleCsNet40/Properties/Settings.settings b/examples/cs/SampleCsNet48/Properties/Settings.settings similarity index 100% rename from examples/cs/SampleCsNet40/Properties/Settings.settings rename to examples/cs/SampleCsNet48/Properties/Settings.settings diff --git a/examples/cs/SampleCsNet40/SampleCsNet40.csproj b/examples/cs/SampleCsNet48/SampleCsNet48.csproj similarity index 90% rename from examples/cs/SampleCsNet40/SampleCsNet40.csproj rename to examples/cs/SampleCsNet48/SampleCsNet48.csproj index 5d30599df..ae7203c18 100644 --- a/examples/cs/SampleCsNet40/SampleCsNet40.csproj +++ b/examples/cs/SampleCsNet48/SampleCsNet48.csproj @@ -10,8 +10,8 @@ Exe Properties CLI - SampleCsNet40 - v4.0 + SampleCsNet48 + v4.8 512 false @@ -43,7 +43,7 @@ prompt MinimumRecommendedRules.ruleset true - v4.0 + v4.8 true true .\ @@ -56,7 +56,7 @@ prompt MinimumRecommendedRules.ruleset true - v4.0 + v4.8 .\ @@ -67,7 +67,7 @@ prompt MinimumRecommendedRules.ruleset false - v4.0 + v4.8 bin\ @@ -79,7 +79,7 @@ MinimumRecommendedRules.ruleset false true - v4.0 + v4.8 bin\ @@ -114,9 +114,9 @@ - + False - Microsoft .NET Framework 4 %28x86 and x64%29 + Microsoft .NET Framework 4.8 %28x86 and x64%29 true @@ -132,15 +132,15 @@ - C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Microsoft.CSharp.dll + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Microsoft.CSharp.dll - - + + {dc91621e-a203-42df-8e03-3a23dd0602b1} - net40 + net48 diff --git a/examples/cs/SampleCsNet48/deploy-dll.cmd b/examples/cs/SampleCsNet48/deploy-dll.cmd new file mode 100644 index 000000000..a5a02644a --- /dev/null +++ b/examples/cs/SampleCsNet48/deploy-dll.cmd @@ -0,0 +1,2 @@ +copy %3\..\net48\*.dll %3 +exit /b 0 diff --git a/examples/cs/SampleCsNet40/packages.config b/examples/cs/SampleCsNet48/packages.config similarity index 100% rename from examples/cs/SampleCsNet40/packages.config rename to examples/cs/SampleCsNet48/packages.config diff --git a/examples/cs/SampleCsUWP/SampleCsUWP.csproj b/examples/cs/SampleCsUWP/SampleCsUWP.csproj index 4c73a88f9..c488e2bdc 100644 --- a/examples/cs/SampleCsUWP/SampleCsUWP.csproj +++ b/examples/cs/SampleCsUWP/SampleCsUWP.csproj @@ -11,7 +11,7 @@ SampleCsUWP en-US UAP - 10.0.17763.0 + 10.0.22621.0 10.0.10240.0 14 512 diff --git a/examples/objc/cocoa-app/CMakeLists.txt b/examples/objc/cocoa-app/CMakeLists.txt index 353098e92..70285039a 100644 --- a/examples/objc/cocoa-app/CMakeLists.txt +++ b/examples/objc/cocoa-app/CMakeLists.txt @@ -42,4 +42,4 @@ set_target_properties( ${CMAKE_CURRENT_LIST_DIR}/plist.in ) -target_link_libraries(foo ${MATSDK_LIBRARY} curl z ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS} dl) +target_link_libraries(foo ${MATSDK_LIBRARY} ${MATSDK_SAMPLE_DEPENDENCY_LIBS} ${CMAKE_THREAD_LIBS_INIT} ${MATSDK_SQLITE3_LIB} ${MATSDK_SAMPLE_PLATFORM_LIBS} ${PLATFORM_LIBS}) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 29d56105e..321ed6b8c 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -85,6 +85,8 @@ endif() # Support for Azure Monitor / Application Insights if(MATSDK_BUILD_AZMON) include(modules/azmon/CMakeLists.txt OPTIONAL) +else() + target_compile_definitions(matsdk_internal_config INTERFACE MATSDK_NO_AZMON) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/exp/") @@ -299,9 +301,24 @@ target_compile_definitions(matsdk_internal_config INTERFACE _USRDLL WINVER=_WIN32_WINNT_WIN7) target_compile_options(matsdk_internal_config INTERFACE /U_MBCS) +if(MATSDK_USE_WININET) + target_compile_definitions(matsdk_internal_config INTERFACE HAVE_MAT_WININET_HTTP_CLIENT) +else() + target_compile_definitions(matsdk_internal_config INTERFACE HAVE_MAT_WINHTTP_HTTP_CLIENT) +endif() +if(MATSDK_USE_WININET) list(APPEND SRCS http/HttpClient_WinInet.cpp http/HttpClient_WinInet.hpp + ) +else() + list(APPEND SRCS + http/HttpClient_WinHttp.cpp + http/HttpClient_WinHttp.hpp + http/IBoundedHttpClientCancel.hpp + ) +endif() + list(APPEND SRCS pal/desktop/WindowsDesktopDeviceInformationImpl.cpp pal/desktop/WindowsDesktopNetworkInformationImpl.cpp pal/desktop/WindowsDesktopSystemInformationImpl.cpp @@ -666,7 +683,12 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Android") target_link_libraries(mat PUBLIC log) endif() elseif(PAL_IMPLEMENTATION STREQUAL "WIN32") - target_link_libraries(mat PUBLIC wininet crypt32 ws2_32) + if(MATSDK_USE_WININET) + target_link_libraries(mat PRIVATE wininet) + else() + target_link_libraries(mat PRIVATE winhttp) + endif() + target_link_libraries(mat PRIVATE crypt32) elseif(APPLE) target_link_libraries(mat PUBLIC "-framework CoreFoundation" diff --git a/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/SDKUnitNativeTest.java b/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/SDKUnitNativeTest.java index 95118242d..648b66eff 100644 --- a/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/SDKUnitNativeTest.java +++ b/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/SDKUnitNativeTest.java @@ -260,7 +260,8 @@ public void runNativeTests() { OfflineRoom.connectContext(appContext); TestStub stub = new TestStub(); - int result = stub.runNativeTests(this); + int result = + stub.runNativeTests(this, client, appContext, System.getProperty("java.io.tmpdir")); assertEquals(0, result); Log.i("MAE", "Test finished"); } diff --git a/lib/android_build/app/src/main/cpp/native-lib.cpp b/lib/android_build/app/src/main/cpp/native-lib.cpp index 1cffaa805..407a32d3f 100644 --- a/lib/android_build/app/src/main/cpp/native-lib.cpp +++ b/lib/android_build/app/src/main/cpp/native-lib.cpp @@ -10,6 +10,10 @@ #include "LogManager.hpp" #include "api/LogManagerImpl.hpp" +#include "config/RuntimeConfig_Default.hpp" +#include "http/HttpClient_Android.hpp" +#include "offline/OfflineStorage_Room.hpp" +#include "pal/PAL.hpp" LOGMANAGER_INSTANCE @@ -114,13 +118,14 @@ int RunTests::run_all_tests(JNIEnv* env, jobject java_logger) { int argc = 2; char command_name[] = "maesdk-test"; - char filter[] = "--gtest_filter=*"; + // Java HTTP callbacks target the AAR's shared SDK, not this test binary's + // private static SDK copy. Exercise that transport through instrumentation. + char filter[] = "--gtest_filter=-HttpClientTests.*"; char* argv[] = {command_name, filter}; ::testing::InitGoogleTest(&argc, argv); ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); listeners.Append(new AndroidLogger(env, java_logger)); - auto logger = Microsoft::Applications::Events::LogManager::Initialize("0123456789abcdef0123456789abcdef-01234567-0123-0123-0123-0123456789ab-0123"); return RUN_ALL_TESTS(); } @@ -129,9 +134,27 @@ extern "C" JNIEXPORT jint JNICALL Java_com_microsoft_applications_events_maesdktest_TestStub_runNativeTests( JNIEnv* env, jobject /* stub */, - jobject logger) + jobject logger, + jobject http_client, + jobject app_context, + jstring cache_file_path) { - return RunTests::run_all_tests(env, logger); + auto path = env->GetStringUTFChars(cache_file_path, nullptr); + Microsoft::Applications::Events::HttpClient_Android::SetCacheFilePath(path); + env->ReleaseStringUTFChars(cache_file_path, path); + Microsoft::Applications::Events::HttpClient_Android::CreateClientInstance(env, http_client); + Microsoft::Applications::Events::OfflineStorage_Room::ConnectJVM(env, app_context); + + JavaVM* java_vm = nullptr; + env->GetJavaVM(&java_vm); + Microsoft::Applications::Events::ILogConfiguration pal_config; + pal_config[CFG_PTR_ANDROID_JVM] = static_cast(java_vm); + pal_config[CFG_JOBJECT_ANDROID_ACTIVITY] = reinterpret_cast(app_context); + Microsoft::Applications::Events::RuntimeConfig_Default runtime_config(pal_config); + PAL::GetPAL().initialize(runtime_config); + const int result = RunTests::run_all_tests(env, logger); + PAL::GetPAL().shutdown(); + return result; } @@ -154,4 +177,3 @@ Java_com_microsoft_applications_events_maesdktest_SDKUnitNativeTest_nativeGetDat auto property = GetEventProperty(env, jProperty); return static_cast(property.dataCategory); } - diff --git a/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/MainActivity.java b/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/MainActivity.java index 7159f10a6..73cbf59e7 100644 --- a/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/MainActivity.java +++ b/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/MainActivity.java @@ -44,7 +44,8 @@ protected void onCreate(Bundle savedInstanceState) { // Example of a call to a native method TextView tv = findViewById(R.id.sample_text); try { - Integer result = testStub.executorRun(dummyLogger); + Integer result = + testStub.executorRun(dummyLogger, m_client, getApplicationContext()); tv.setText(String.format(Locale.ROOT, "Tests returned %d", result)); } catch (ExecutionException e) { tv.setText("Woopsy"); @@ -53,4 +54,3 @@ protected void onCreate(Bundle savedInstanceState) { } } } - diff --git a/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/TestStub.java b/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/TestStub.java index f224933b8..c25f1896e 100644 --- a/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/TestStub.java +++ b/lib/android_build/app/src/main/java/com/microsoft/applications/events/maesdktest/TestStub.java @@ -4,6 +4,8 @@ // package com.microsoft.applications.events.maesdktest; +import android.content.Context; +import com.microsoft.applications.events.HttpClient; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -13,9 +15,13 @@ public class TestStub { class CallTests implements Callable { MaeUnitLogger logger; + HttpClient httpClient; + Context appContext; - CallTests(MaeUnitLogger logger) { + CallTests(MaeUnitLogger logger, HttpClient httpClient, Context appContext) { this.logger = logger; + this.httpClient = httpClient; + this.appContext = appContext; } /** @@ -26,17 +32,21 @@ class CallTests implements Callable { */ @Override public Integer call() throws Exception { - return Integer.valueOf(runNativeTests(logger)); + return Integer.valueOf( + runNativeTests(logger, httpClient, appContext, System.getProperty("java.io.tmpdir"))); } } - public Integer executorRun(MaeUnitLogger logger) throws ExecutionException, InterruptedException { + public Integer executorRun(MaeUnitLogger logger, HttpClient httpClient, Context appContext) + throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); - FutureTask tests = new FutureTask(new CallTests(logger)); + FutureTask tests = + new FutureTask(new CallTests(logger, httpClient, appContext)); executorService.execute(tests); return tests.get(); } - public native int runNativeTests(MaeUnitLogger logger); + public native int runNativeTests( + MaeUnitLogger logger, HttpClient httpClient, Context appContext, String cacheFilePath); } diff --git a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java index 0ce2881ef..b7abb002d 100644 --- a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java +++ b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/LogConfigurationKey.java @@ -23,6 +23,9 @@ public enum LogConfigurationKey { /** Enable database compression. */ CFG_BOOL_ENABLE_DB_COMPRESS("enableDBCompression", Boolean.class), + /** Batch records when flushing the RAM queue to disk storage. */ + CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH("enableBatchedStorageFlush", Boolean.class), + /** Enable WAL journal. */ CFG_BOOL_ENABLE_WAL_JOURNAL("enableWALJournal", Boolean.class), @@ -187,4 +190,3 @@ public Class getValueType() { return valueType; } } - diff --git a/lib/api/LogConfiguration.cpp b/lib/api/LogConfiguration.cpp index 23a7e53cd..0eb6581b2 100644 --- a/lib/api/LogConfiguration.cpp +++ b/lib/api/LogConfiguration.cpp @@ -19,6 +19,7 @@ namespace MAT_NS_BEGIN { { CFG_BOOL_ENABLE_ANALYTICS, false }, { CFG_INT_CACHE_FILE_SIZE, 3145728 }, { CFG_INT_RAM_QUEUE_SIZE, 524288 }, + { CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH, true }, { CFG_BOOL_ENABLE_MULTITENANT, true }, { CFG_BOOL_ENABLE_DB_DROP_IF_FULL, false }, { CFG_INT_MAX_TEARDOWN_TIME, 0 }, @@ -51,6 +52,7 @@ namespace MAT_NS_BEGIN { { CFG_BOOL_ENABLE_ANALYTICS, src.enableLifecycleSession }, { CFG_INT_CACHE_FILE_SIZE, src.cacheFileSizeLimitInBytes }, { CFG_INT_RAM_QUEUE_SIZE, src.cacheMemorySizeLimitInBytes }, + { CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH, true }, { CFG_BOOL_ENABLE_MULTITENANT, src.multiTenantEnabled }, { CFG_INT_MAX_TEARDOWN_TIME, src.maxTeardownUploadTimeInSec }, { CFG_INT_MAX_PENDING_REQ, src.maxPendingHTTPRequests }, @@ -128,4 +130,3 @@ namespace MAT_NS_BEGIN { } } MAT_NS_END - diff --git a/lib/api/LogManagerFactory.hpp b/lib/api/LogManagerFactory.hpp index 5e26267d8..63adfb646 100644 --- a/lib/api/LogManagerFactory.hpp +++ b/lib/api/LogManagerFactory.hpp @@ -67,7 +67,15 @@ namespace MAT_NS_BEGIN { // C++11 Magic Statics (N2660) static LogManagerFactory& instance() { - static LogManagerFactory impl; + // Deliberately never destroyed. LogManagerProvider::Release() must be + // able to walk this factory's registries during process teardown, but + // a normal function-local static's destruction order relative to that + // teardown call is unspecified -- if this were destroyed first, + // Release() would walk already-freed std::map nodes (a downstream + // consumer observed this as EXC_BAD_ACCESS in release() at process + // exit). Leaking one small, fixed-size object avoids the ordering + // hazard entirely; the OS reclaims it when the process exits. + static LogManagerFactory& impl = *new LogManagerFactory(); return impl; } diff --git a/lib/api/LogManagerImpl.cpp b/lib/api/LogManagerImpl.cpp index 24215c0cd..dd943fd6c 100644 --- a/lib/api/LogManagerImpl.cpp +++ b/lib/api/LogManagerImpl.cpp @@ -2,11 +2,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // -#ifdef _MSC_VER -// evntprov.h(838) : warning C4459 : declaration of 'Version' hides global declaration -#pragma warning(disable : 4459) +#ifdef _WIN32 +// Include the SDK declaration before the telemetry Version symbol enters scope. +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include #endif #include "LogManagerImpl.hpp" +#include +#include "ctmacros.hpp" #include "mat/config.h" #include "offline/LogSessionDataProvider.hpp" @@ -289,6 +295,11 @@ namespace MAT_NS_BEGIN if (m_httpClient == nullptr) { m_httpClient = HttpClientFactory::Create(); + if (m_httpClient == nullptr) + { + LOG_ERROR("The default HTTP client has not been initialized."); + MATSDK_THROW(std::invalid_argument("configuration")); + } m_httpClient->ApplySettings(m_logConfiguration); } else @@ -368,9 +379,31 @@ namespace MAT_NS_BEGIN LogManagerImpl::~LogManagerImpl() noexcept { - FlushAndTeardown(); - LOCKGUARD(ILogManagerInternal::managers_lock); - ILogManagerInternal::managers.erase(this); + MATSDK_TRY + { + FlushAndTeardown(); + } +#if HAVE_EXCEPTIONS + MATSDK_CATCH(const std::exception& e) + { + std::fprintf(stderr, "Log manager teardown failed: %s\n", e.what()); + } + MATSDK_CATCH(...) + { + std::fputs("Log manager teardown failed with an unknown exception\n", stderr); + } +#endif + MATSDK_TRY + { + LOCKGUARD(ILogManagerInternal::managers_lock); + ILogManagerInternal::managers.erase(this); + } +#if HAVE_EXCEPTIONS + MATSDK_CATCH(...) + { + std::fputs("Log manager registry cleanup failed\n", stderr); + } +#endif } size_t LogManagerImpl::GetDeadLoggerCount() @@ -959,20 +992,33 @@ namespace MAT_NS_BEGIN return true; } - void LogManagerImpl::EndActivity() + void LogManagerImpl::EndActivity() noexcept { - std::unique_lock lock(m_pause_mutex); - if (m_pause_active_count == 0) { - return; + MATSDK_TRY + { + std::unique_lock lock(m_pause_mutex); + if (m_pause_active_count == 0) { + return; + } + m_pause_active_count -= 1; + if (m_pause_active_count > 0) { + return; + } + if (m_pause_state == PauseState::Pausing) { + m_pause_state = PauseState::Paused; + m_pause_cv.notify_all(); + } } - m_pause_active_count -= 1; - if (m_pause_active_count > 0) { - return; +#if HAVE_EXCEPTIONS + MATSDK_CATCH(const std::exception& e) + { + std::fprintf(stderr, "Failed to end telemetry activity: %s\n", e.what()); } - if (m_pause_state == PauseState::Pausing) { - m_pause_state = PauseState::Paused; - m_pause_cv.notify_all(); + MATSDK_CATCH(...) + { + std::fputs("Failed to end telemetry activity\n", stderr); } +#endif } } MAT_NS_END diff --git a/lib/api/LogManagerImpl.hpp b/lib/api/LogManagerImpl.hpp index 7dd7f7442..75e062868 100644 --- a/lib/api/LogManagerImpl.hpp +++ b/lib/api/LogManagerImpl.hpp @@ -306,7 +306,7 @@ namespace MAT_NS_BEGIN virtual void ResumeActivity() override; virtual void WaitPause() override; virtual bool StartActivity() override; - virtual void EndActivity() override; + virtual void EndActivity() noexcept override; protected: std::unique_ptr& GetSystem(); diff --git a/lib/api/Logger.cpp b/lib/api/Logger.cpp index aec4b9e52..75d3bd292 100644 --- a/lib/api/Logger.cpp +++ b/lib/api/Logger.cpp @@ -127,7 +127,8 @@ namespace MAT_NS_BEGIN Logger::~Logger() noexcept { - LOG_TRACE("%p: Destroyed", this); + // Intentionally empty — logging here triggers a static-destruction-order + // crash on iOS simulator (recursive_mutex used after teardown). } ISemanticContext* Logger::GetSemanticContext() const diff --git a/lib/config/RuntimeConfig_Default.hpp b/lib/config/RuntimeConfig_Default.hpp index 504aeefe3..ecd6e079a 100644 --- a/lib/config/RuntimeConfig_Default.hpp +++ b/lib/config/RuntimeConfig_Default.hpp @@ -16,6 +16,7 @@ namespace MAT_NS_BEGIN {CFG_BOOL_ENABLE_ANALYTICS, false}, {CFG_INT_CACHE_FILE_SIZE, 3145728}, {CFG_INT_RAM_QUEUE_SIZE, 524288}, + {CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH, true}, {CFG_BOOL_ENABLE_MULTITENANT, true}, {CFG_BOOL_ENABLE_DB_DROP_IF_FULL, false}, {CFG_INT_MAX_TEARDOWN_TIME, 1}, @@ -61,7 +62,7 @@ namespace MAT_NS_BEGIN {"contentEncoding", "deflate"}, /* Optional parameter to require Microsoft Root CA */ {CFG_BOOL_HTTP_MS_ROOT_CHECK, false}, - /* Optional parameter for SSL certificate verification (curl) */ + /* Compatibility parameter; curl verification cannot be disabled */ {CFG_BOOL_HTTP_SSL_VERIFY, true}, /* Optional CA bundle path for OpenSSL-backed curl */ {CFG_STR_HTTP_SSL_CAINFO, ""}}}, @@ -233,4 +234,3 @@ namespace MAT_NS_BEGIN } MAT_NS_END - diff --git a/lib/filter/EventFilterCollection.cpp b/lib/filter/EventFilterCollection.cpp index f6387e99e..092f7ae5b 100644 --- a/lib/filter/EventFilterCollection.cpp +++ b/lib/filter/EventFilterCollection.cpp @@ -18,9 +18,17 @@ namespace MAT_NS_BEGIN if (filter == nullptr) MATSDK_THROW(std::invalid_argument("filter")); - std::lock_guard lock(m_filterLock); - m_filters.emplace_back(std::move(filter)); - m_size = m_filters.size(); + std::shared_ptr sharedFilter(std::move(filter)); + { + std::lock_guard lock(m_filterLock); + auto current = std::atomic_load(&m_filters); + auto updated = std::make_shared( + current == nullptr ? FilterList{} : *current); + updated->emplace_back(std::move(sharedFilter)); + std::atomic_store( + &m_filters, + std::shared_ptr(std::move(updated))); + } } void EventFilterCollection::UnregisterEventFilter(const char* filterName) @@ -28,33 +36,52 @@ namespace MAT_NS_BEGIN if (filterName == nullptr) MATSDK_THROW(std::invalid_argument("filterName")); - std::lock_guard lock(m_filterLock); - m_filters.erase( - std::remove_if(m_filters.begin(), m_filters.end(), - [filterName](const std::unique_ptr& filter) noexcept - { - return strcmp(filter->GetName(), filterName) == 0; - }), - m_filters.end()); - m_size = m_filters.size(); + std::shared_ptr removedFilters; + { + std::lock_guard lock(m_filterLock); + auto current = std::atomic_load(&m_filters); + if (current == nullptr) + { + return; + } + + auto updated = std::make_shared(*current); + updated->erase( + std::remove_if(updated->begin(), updated->end(), + [filterName](const std::shared_ptr& filter) noexcept + { + return strcmp(filter->GetName(), filterName) == 0; + }), + updated->end()); + if (updated->size() == current->size()) + { + return; + } + + removedFilters = std::move(current); + std::atomic_store( + &m_filters, + updated->empty() + ? std::shared_ptr{} + : std::shared_ptr(std::move(updated))); + } } void EventFilterCollection::UnregisterAllFilters() noexcept { - std::lock_guard lock(m_filterLock); - std::vector>{}.swap(m_filters); - m_size = 0; + std::shared_ptr removedFilters; + { + std::lock_guard lock(m_filterLock); + removedFilters = std::atomic_exchange( + &m_filters, std::shared_ptr{}); + } } bool EventFilterCollection::CanEventPropertiesBeSent(const EventProperties& properties) const noexcept { - if (Empty()) - { - return true; - } - std::lock_guard lock(m_filterLock); - return std::all_of(m_filters.cbegin(), m_filters.cend(), - [&properties](const std::unique_ptr& filter) + auto filters = std::atomic_load(&m_filters); + return filters == nullptr || std::all_of(filters->cbegin(), filters->cend(), + [&properties](const std::shared_ptr& filter) { return filter->CanEventPropertiesBeSent(properties); }); @@ -62,12 +89,13 @@ namespace MAT_NS_BEGIN size_t EventFilterCollection::Size() const noexcept { - return m_size.load(); + auto filters = std::atomic_load(&m_filters); + return filters == nullptr ? 0 : filters->size(); } bool EventFilterCollection::Empty() const noexcept { - return (Size() == 0); + return Size() == 0; } } MAT_NS_END diff --git a/lib/filter/EventFilterCollection.hpp b/lib/filter/EventFilterCollection.hpp index 3c3efcebc..dc59e7409 100644 --- a/lib/filter/EventFilterCollection.hpp +++ b/lib/filter/EventFilterCollection.hpp @@ -11,7 +11,6 @@ #include #include #include -#include namespace MAT_NS_BEGIN { @@ -26,9 +25,10 @@ namespace MAT_NS_BEGIN virtual bool Empty() const noexcept override; protected: - std::atomic m_size { 0 }; + using FilterList = std::vector>; + mutable std::mutex m_filterLock; - std::vector> m_filters; + std::shared_ptr m_filters; }; } MAT_NS_END diff --git a/lib/http/HttpClientFactory.cpp b/lib/http/HttpClientFactory.cpp index 5419f161d..b58175e1a 100644 --- a/lib/http/HttpClientFactory.cpp +++ b/lib/http/HttpClientFactory.cpp @@ -18,6 +18,8 @@ #include "http/HttpClient_WinRt.hpp" #elif defined(HAVE_MAT_WININET_HTTP_CLIENT) #include "http/HttpClient_WinInet.hpp" + #elif defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + #include "http/HttpClient_WinHttp.hpp" #endif #elif defined(MATSDK_PAL_CPP11) #if TARGET_OS_IPHONE || (defined(__APPLE__) && defined(APPLE_HTTP)) @@ -49,6 +51,13 @@ namespace MAT_NS_BEGIN { return std::make_shared(); } +#elif defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + /* Win32 WinHTTP client (default) */ + std::shared_ptr HttpClientFactory::Create() { + LOG_TRACE("Creating HttpClient_WinHttp"); + return std::make_shared(); + } + #endif #elif defined(HAVE_MAT_CURL_HTTP_CLIENT) std::shared_ptr HttpClientFactory::Create() { diff --git a/lib/http/HttpClientFactory.hpp b/lib/http/HttpClientFactory.hpp index c96bc2ab0..ae1fb9681 100644 --- a/lib/http/HttpClientFactory.hpp +++ b/lib/http/HttpClientFactory.hpp @@ -25,8 +25,22 @@ class HttpClientFactory // TODO: [maxgolov] - remove this once there is a better way to pass HTTP client configuration #if defined(MATSDK_PAL_WIN32) && !defined(_WINRT_DLL) -#define HAVE_MAT_WININET_HTTP_CLIENT -#include "http/HttpClient_WinInet.hpp" + #if defined(HAVE_MAT_WININET_HTTP_CLIENT) && defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + #error WinInet and WinHTTP cannot both be selected. + #endif + #if defined(HAVE_MAT_WININET_HTTP_CLIENT) + #include "http/HttpClient_WinInet.hpp" + #else + // WinHTTP is the default Win32 desktop transport: unlike WinInet, it does + // not depend on a logged-on interactive user or that user's Internet + // Explorer settings, so it works in services and other non-interactive + // processes without extra configuration. Define HAVE_MAT_WININET_HTTP_CLIENT + // to opt back into WinInet (e.g. for IE-integrated proxy/cookie behavior). + #ifndef HAVE_MAT_WINHTTP_HTTP_CLIENT + #define HAVE_MAT_WINHTTP_HTTP_CLIENT + #endif + #include "http/HttpClient_WinHttp.hpp" + #endif #endif #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT diff --git a/lib/http/HttpClientManager.cpp b/lib/http/HttpClientManager.cpp index 3c7d1f809..c449d2aad 100644 --- a/lib/http/HttpClientManager.cpp +++ b/lib/http/HttpClientManager.cpp @@ -10,8 +10,14 @@ #include #include +#include #include +#include +#include +#include +#include #include +#include #include #ifdef linux @@ -35,15 +41,49 @@ namespace MAT_NS_BEGIN { class HttpClientManager::HttpCallback : public IHttpResponseCallback { public: + struct CompletionState + { + explicit CompletionState(std::string id) + : requestId(std::move(id)) + { + } + + bool TryStartTerminal() noexcept + { + bool expected = false; + return terminalStarted.compare_exchange_strong(expected, true); + } + + std::atomic terminalStarted{false}; + std::string const requestId; + }; HttpCallback(HttpClientManager& hcm, EventsUploadContextPtr const& ctx) : m_hcm(hcm), m_ctx(ctx), - m_startTime(PAL::getMonotonicTimeMs()) + m_startTime(PAL::getMonotonicTimeMs()), + m_completion(std::make_shared( + !ctx->httpRequestId.empty() + ? ctx->httpRequestId + : (ctx->httpRequest != nullptr + ? ctx->httpRequest->GetId() + : std::string()))) { } virtual void OnHttpResponse(IHttpResponse* response) override + { + std::unique_ptr ownedResponse(response); + if (!m_completion->TryStartTerminal()) + { + LOG_ERROR("Ignoring duplicate terminal HTTP callback for request %s", + m_completion->requestId.c_str()); + return; + } + CompleteClaimed(ownedResponse.release()); + } + + void CompleteClaimed(IHttpResponse* response) { m_ctx->durationMs = static_cast(PAL::getMonotonicTimeMs() - m_startTime); m_ctx->httpResponse = response; @@ -77,6 +117,7 @@ namespace MAT_NS_BEGIN { HttpClientManager& m_hcm; EventsUploadContextPtr m_ctx; int64_t m_startTime; + std::shared_ptr m_completion; }; //--- @@ -86,16 +127,39 @@ namespace MAT_NS_BEGIN { m_httpClient(httpClient), m_taskDispatcher(taskDispatcher) { + int64_t configuredSeconds = + logManager.GetLogConfiguration()[CFG_INT_MAX_TEARDOWN_TIME]; + if (configuredSeconds > 0) + { + int64_t const maxSeconds = + std::chrono::milliseconds::max().count() / 1000; + m_cancelDrainTimeout = std::chrono::seconds( + std::min(configuredSeconds, maxSeconds)); + } } HttpClientManager::~HttpClientManager() noexcept { - cancelAllRequestsAsync(); + // HttpCallback and scheduled response tasks retain a reference to this + // manager, so non-reentrant destruction must be a full callback lifetime + // barrier. Reentrant destruction is unsupported because the active + // callback itself must still unwind through this object. +#ifndef NDEBUG + { + std::lock_guard lock(m_httpCallbacksMtx); + for (auto const& active : m_activeHttpCallbacks) + { + assert(active.second != std::this_thread::get_id()); + } + } +#endif + cancelAllRequests(); } void HttpClientManager::handleSendRequest(EventsUploadContextPtr const& ctx) { HttpCallback *callback = new HttpCallback(*this, ctx); + auto completion = callback->m_completion; { LOCKGUARD(m_httpCallbacksMtx); m_httpCallbacks.push_back(callback); @@ -105,47 +169,185 @@ namespace MAT_NS_BEGIN { static_cast(ctx->recordIdsAndTenantIds.size()), ctx->latency, latencyToStr(ctx->latency), static_cast(ctx->packageIds.size()), ctx->httpRequest->GetId().c_str(), static_cast(ctx->httpRequest->GetSizeEstimate())); +#if HAVE_EXCEPTIONS + try + { + m_httpClient.SendRequestAsync(ctx->httpRequest, callback); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("HTTP client rejected request %s with an exception: %s", + completion->requestId.c_str(), ex.what()); + if (completion->TryStartTerminal()) + { + callback->CompleteClaimed( + new SimpleHttpResponse(completion->requestId)); + } + } + catch (...) + { + LOG_ERROR("HTTP client rejected request %s with a non-standard exception", + completion->requestId.c_str()); + if (completion->TryStartTerminal()) + { + callback->CompleteClaimed( + new SimpleHttpResponse(completion->requestId)); + } + } +#else m_httpClient.SendRequestAsync(ctx->httpRequest, callback); +#endif } void HttpClientManager::scheduleOnHttpResponse(HttpCallback* callback) { - PAL::scheduleTask(&m_taskDispatcher, 0, this, &HttpClientManager::onHttpResponse, callback); + auto started = std::make_shared>(false); +#if HAVE_EXCEPTIONS + try + { +#endif + auto task = PAL::scheduleTask( + &m_taskDispatcher, 0, this, + &HttpClientManager::runScheduledHttpResponse, started, callback); + if (task.GetTask() != nullptr || + started->load(std::memory_order_acquire)) + { + return; + } +#if HAVE_EXCEPTIONS + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("Failed to schedule HTTP response callback: %s", ex.what()); + if (started->load(std::memory_order_acquire)) + { + return; + } + } + catch (...) + { + LOG_ERROR("Failed to schedule HTTP response callback with a non-standard exception"); + if (started->load(std::memory_order_acquire)) + { + return; + } + } +#endif + // Some supported dispatchers synchronously destroy tasks they cannot + // accept. Complete inline so the claimed callback cannot remain tracked. + onHttpResponse(callback); + } + + void HttpClientManager::runScheduledHttpResponse( + std::shared_ptr> const& started, + HttpCallback* callback) + { + started->store(true, std::memory_order_release); + onHttpResponse(callback); } /* This method may get executed synchronously on Windows from handleSendRequest in case of connection failure */ void HttpClientManager::onHttpResponse(HttpCallback* callback) { - EventsUploadContextPtr &ctx = callback->m_ctx; { - LOCKGUARD(m_httpCallbacksMtx); + std::lock_guard lock(m_httpCallbacksMtx); auto z = std::find(m_httpCallbacks.cbegin(), m_httpCallbacks.cend(), callback); if (z == m_httpCallbacks.end()) { - assert(false); + LOG_ERROR("Ignoring untracked HTTP callback=%p", callback); + return; } + m_activeHttpCallbacks[callback] = std::this_thread::get_id(); + m_httpCallbacksCV.notify_all(); + } + + EventsUploadContextPtr &ctx = callback->m_ctx; #if !defined(NDEBUG) && defined(HAVE_MAT_LOGGING) - // Response may be null if request got aborted - if (ctx->httpResponse != nullptr) - { - IHttpResponse const& response = (*ctx->httpResponse); - LOG_TRACE("HTTP response %s: result=%u, status=%u, body=%u bytes", - response.GetId().c_str(), response.GetResult(), response.GetStatusCode(), static_cast(response.GetBody().size())); - } + // Response may be null if request got aborted + if (ctx->httpResponse != nullptr) + { + IHttpResponse const& response = (*ctx->httpResponse); + LOG_TRACE("HTTP response %s: result=%u, status=%u, body=%u bytes", + response.GetId().c_str(), response.GetResult(), response.GetStatusCode(), static_cast(response.GetBody().size())); + } #endif + // Never hold m_httpCallbacksMtx while calling the transport or + // dispatching requestDone(): either path may synchronously re-enter this + // manager. Reentrant cancellation recognizes this callback as active + // and does not wait for its own stack to unwind. +#if HAVE_EXCEPTIONS + try + { requestDone(ctx); - // request done should be handled by now + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("Unhandled exception in HTTP response callback: %s", ex.what()); + notifyRequestFailure(ctx); + } + catch (...) + { + LOG_ERROR("Unhandled non-standard exception in HTTP response callback"); + notifyRequestFailure(ctx); + } +#else + requestDone(ctx); +#endif + // request done should be handled by now + { + std::lock_guard lock(m_httpCallbacksMtx); LOG_TRACE("HTTP remove callback=%p", callback); m_httpCallbacks.remove(callback); - // Wake cancelAllRequests() waiting for the list to drain. + m_activeHttpCallbacks.erase(callback); + // Wake cancelAllRequests() waiting for the list to drain while the + // condition variable is still guaranteed to be alive. m_httpCallbacksCV.notify_all(); } delete callback; } + void HttpClientManager::notifyRequestFailure(EventsUploadContextPtr const& ctx) noexcept + { +#if HAVE_EXCEPTIONS + try + { + requestFailed(ctx); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("Unhandled exception while releasing failed HTTP request: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("Unhandled non-standard exception while releasing failed HTTP request"); + } + + try + { + requestFailureComplete(ctx); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("Unhandled exception while completing failed HTTP request: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("Unhandled non-standard exception while completing failed HTTP request"); + } +#else + requestFailed(ctx); + requestFailureComplete(ctx); +#endif + } + void HttpClientManager::cancelAllRequestsAsync(std::chrono::milliseconds bestEffortTimeout) { if (bestEffortTimeout > std::chrono::milliseconds::zero()) @@ -154,8 +356,25 @@ namespace MAT_NS_BEGIN { auto boundedCancel = dynamic_cast(&m_httpClient); if (boundedCancel != nullptr) { +#if HAVE_EXCEPTIONS + try + { + boundedCancel->CancelAllRequests(bestEffortTimeout); + return; + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("HTTP client bounded cancellation failed: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("HTTP client bounded cancellation failed with a non-standard exception"); + } +#else boundedCancel->CancelAllRequests(bestEffortTimeout); return; +#endif } #endif @@ -163,7 +382,25 @@ namespace MAT_NS_BEGIN { return; } +#if HAVE_EXCEPTIONS + try + { + m_httpClient.CancelAllRequests(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("HTTP client cancellation failed: %s", ex.what()); + cancelTrackedRequestsAsync(); + } + catch (...) + { + LOG_ERROR("HTTP client cancellation failed with a non-standard exception"); + cancelTrackedRequestsAsync(); + } +#else m_httpClient.CancelAllRequests(); +#endif } void HttpClientManager::cancelTrackedRequestsAsync() @@ -192,28 +429,72 @@ namespace MAT_NS_BEGIN { for (const auto& id : requestIds) { +#if HAVE_EXCEPTIONS + try + { + m_httpClient.CancelRequestAsync(id); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("HTTP client failed to cancel request %s: %s", + id.c_str(), ex.what()); + } + catch (...) + { + LOG_ERROR("HTTP client failed to cancel request %s with a non-standard exception", + id.c_str()); + } +#else m_httpClient.CancelRequestAsync(id); +#endif } } void HttpClientManager::cancelAllRequests(bool bestEffort) { - // Use the transport-specific bounded path when available; older clients - // fall back to cancelling tracked requests individually. + if (bestEffort && + m_cancelDrainTimeout <= std::chrono::milliseconds::zero()) + { + // A zero budget means "do not wait", not "leave requests running". + // Snapshot IDs and initiate asynchronous cancellation before returning. + cancelTrackedRequestsAsync(); + return; + } + // Quiesce the transport before taking m_httpCallbacksMtx. Moving this + // call under the mutex deadlocks when a synchronous transport completion + // re-enters onHttpResponse(). const auto cancelStart = std::chrono::steady_clock::now(); cancelAllRequestsAsync(bestEffort ? m_cancelDrainTimeout : std::chrono::milliseconds::zero()); // Drain callbacks through the condition variable signaled by onHttpResponse. - std::unique_lock lock(m_httpCallbacksMtx); + std::unique_lock lock(m_httpCallbacksMtx); + std::thread::id const callerThread = std::this_thread::get_id(); + auto callbacksDrainedForCaller = [this, callerThread] { + for (auto const& active : m_activeHttpCallbacks) + { + if (active.second == callerThread) + { + // A completion running on a single-thread dispatcher cannot + // wait for peer completions queued behind itself. Returning + // from reentrant cancellation lets this callback unwind and + // the dispatcher drain the remaining work. + return true; + } + } + return m_httpCallbacks.empty(); + }; if (bestEffort) { - // Keep pause bounded, including time spent in the transport cancel. + // Keep pause within the configured soft cap, including time spent + // in transport cancellation. A synchronous native handle close + // already in progress can finish after the deadline. const auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - cancelStart); const auto remaining = (elapsed < m_cancelDrainTimeout) ? (m_cancelDrainTimeout - elapsed) : std::chrono::milliseconds::zero(); - if (!m_httpCallbacksCV.wait_for(lock, remaining, - [this] { return m_httpCallbacks.empty(); })) + if (!m_httpCallbacksCV.wait_for( + lock, remaining, callbacksDrainedForCaller)) { LOG_WARN("cancelAllRequests: %zu callback(s) still draining after %lld ms (best-effort)", m_httpCallbacks.size(), static_cast(m_cancelDrainTimeout.count())); @@ -221,8 +502,10 @@ namespace MAT_NS_BEGIN { } else { - // Shutdown/cleanup is the lifetime barrier for callback state, so drain fully. - m_httpCallbacksCV.wait(lock, [this] { return m_httpCallbacks.empty(); }); + // Non-reentrant shutdown/cleanup is the lifetime barrier for callback + // state. A callback re-entering cancellation must return so its own + // stack can unwind; destroying the manager from that stack is unsupported. + m_httpCallbacksCV.wait(lock, callbacksDrainedForCaller); } } diff --git a/lib/http/HttpClientManager.hpp b/lib/http/HttpClientManager.hpp index 4f350e37f..e2717cc6f 100644 --- a/lib/http/HttpClientManager.hpp +++ b/lib/http/HttpClientManager.hpp @@ -10,10 +10,14 @@ #include "system/Route.hpp" #include "ILogManager.hpp" -#include -#include +#include #include #include +#include +#include +#include +#include +#include namespace MAT_NS_BEGIN { @@ -46,6 +50,8 @@ class HttpClientManager } RouteSource requestDone; + RouteSource requestFailed; + RouteSource requestFailureComplete; RouteSink sendRequest { @@ -54,27 +60,31 @@ class HttpClientManager protected: class HttpCallback; - friend class HttpCallback; void handleSendRequest(EventsUploadContextPtr const& ctx); virtual void scheduleOnHttpResponse(HttpCallback* callback); + void runScheduledHttpResponse( + std::shared_ptr> const& started, + HttpCallback* callback); void onHttpResponse(HttpCallback* callback); + void notifyRequestFailure(EventsUploadContextPtr const& ctx) noexcept; void cancelAllRequestsAsync(std::chrono::milliseconds bestEffortTimeout = std::chrono::milliseconds::zero()); void cancelTrackedRequestsAsync(); ILogManager& m_logManager; IHttpClient& m_httpClient; ITaskDispatcher& m_taskDispatcher; - mutable std::recursive_mutex m_httpCallbacksMtx; + mutable std::mutex m_httpCallbacksMtx; std::list m_httpCallbacks; + std::map m_activeHttpCallbacks; // Signaled from onHttpResponse when a callback is removed, so cancelAllRequests // can drain via a condition variable instead of a poll loop. - std::condition_variable_any m_httpCallbacksCV; - // Upper bound on how long cancelAllRequests waits for callbacks to drain. A - // last-resort safety valve so a stalled dispatcher/HTTP stack can never make - // the drain spin or block forever. Adjustable so tests can - // exercise the timeout path without a long wait. - std::chrono::milliseconds m_cancelDrainTimeout{std::chrono::seconds(30)}; + std::condition_variable m_httpCallbacksCV; + // Configured soft cap on the best-effort pause drain. One native handle + // close already in progress may finish after it. Non-reentrant full + // shutdown remains a lifetime barrier and waits for every accepted + // request's terminal callback. + std::chrono::milliseconds m_cancelDrainTimeout{std::chrono::milliseconds::zero()}; }; } MAT_NS_END diff --git a/lib/http/HttpClient_Android.cpp b/lib/http/HttpClient_Android.cpp index c41a09d19..9b93ea451 100644 --- a/lib/http/HttpClient_Android.cpp +++ b/lib/http/HttpClient_Android.cpp @@ -283,6 +283,7 @@ namespace MAT_NS_BEGIN if (request->m_callback) { auto failure = new HttpResponse(request->m_id); + failure->SetResult(HttpResult_Aborted); request->m_callback->OnHttpResponse(failure); } } @@ -441,9 +442,10 @@ namespace MAT_NS_BEGIN jobject java_client) { auto client = std::make_shared(); - s_client = client; - client->SetClient(env, java_client); + + std::lock_guard lock(s_clientMutex); + s_client = std::move(client); } void HttpClient_Android::SetJavaVM(JavaVM* vm) @@ -451,9 +453,16 @@ namespace MAT_NS_BEGIN HttpClient_Android::s_java_vm = vm; } - void HttpClient_Android::DeleteClientInstance(JNIEnv* env) + void HttpClient_Android::DeleteClientInstance(JNIEnv* env, jobject java_client) { - s_client.reset(); + std::shared_ptr client; + { + std::lock_guard lock(s_clientMutex); + if (s_client && env->IsSameObject(s_client->m_client, java_client)) + { + client = std::move(s_client); + } + } } void HttpClient_Android::SetCacheFilePath(std::string&& path) @@ -471,7 +480,8 @@ namespace MAT_NS_BEGIN std::shared_ptr HttpClient_Android::GetClientInstance() { - return std::shared_ptr(s_client); + std::lock_guard lock(s_clientMutex); + return s_client; } bool HttpClient_Android::CheckException(JNIEnv* env, HttpRequest* request) @@ -486,6 +496,7 @@ namespace MAT_NS_BEGIN return true; } + std::mutex HttpClient_Android::s_clientMutex; std::shared_ptr HttpClient_Android::s_client; std::string HttpClient_Android::s_cache_file_path; @@ -504,9 +515,10 @@ extern "C" JNIEXPORT void extern "C" JNIEXPORT void JNICALL - Java_com_microsoft_applications_events_HttpClient_deleteClientInstance(JNIEnv* env) + Java_com_microsoft_applications_events_HttpClient_deleteClientInstance(JNIEnv* env, + jobject java_client) { - Microsoft::Applications::Events::HttpClient_Android::DeleteClientInstance(env); + Microsoft::Applications::Events::HttpClient_Android::DeleteClientInstance(env, java_client); } extern "C" JNIEXPORT void diff --git a/lib/http/HttpClient_Android.hpp b/lib/http/HttpClient_Android.hpp index ad7905eca..65d4af7a5 100644 --- a/lib/http/HttpClient_Android.hpp +++ b/lib/http/HttpClient_Android.hpp @@ -38,15 +38,7 @@ namespace MAT_NS_BEGIN HttpResult GetResult() const override { - switch (m_response) - { - case 0: - return HttpResult_LocalFailure; - case -1: - return HttpResult_NetworkFailure; - default: - return HttpResult_OK; - } + return m_result; } unsigned int GetStatusCode() const override @@ -67,6 +59,14 @@ namespace MAT_NS_BEGIN void SetResponse(int response) { m_response = response; + m_result = response == 0 + ? HttpResult_LocalFailure + : response == -1 ? HttpResult_NetworkFailure : HttpResult_OK; + } + + void SetResult(HttpResult result) + { + m_result = result; } void AddHeader(std::string&& key, std::string&& value) @@ -84,6 +84,7 @@ namespace MAT_NS_BEGIN HttpHeaders m_headers; std::vector> m_body; int m_response = 0; + HttpResult m_result = HttpResult_LocalFailure; }; public: @@ -186,7 +187,7 @@ namespace MAT_NS_BEGIN static void CreateClientInstance(JNIEnv* env, jobject java_client); - static void DeleteClientInstance(JNIEnv* env); + static void DeleteClientInstance(JNIEnv* env, jobject java_client); static void SetCacheFilePath(std::string&& path); static const std::string& GetCacheFilePath(); static std::shared_ptr GetClientInstance(); @@ -204,6 +205,7 @@ namespace MAT_NS_BEGIN jmethodID m_execute_id = nullptr; static JavaVM* s_java_vm; std::atomic m_id; + static std::mutex s_clientMutex; static std::shared_ptr s_client; static std::string s_cache_file_path; diff --git a/lib/http/HttpClient_Apple.mm b/lib/http/HttpClient_Apple.mm index 1a047f5d6..92b0ac2e8 100644 --- a/lib/http/HttpClient_Apple.mm +++ b/lib/http/HttpClient_Apple.mm @@ -15,6 +15,32 @@ #include "utils/StringUtils.hpp" #include "utils/Utils.hpp" +#include +#include + +namespace +{ + thread_local bool isAppleDelegateCallback = false; + + class AppleDelegateCallbackScope final + { + public: + AppleDelegateCallbackScope() : + m_previous(isAppleDelegateCallback) + { + isAppleDelegateCallback = true; + } + + ~AppleDelegateCallbackScope() noexcept + { + isAppleDelegateCallback = m_previous; + } + + private: + bool m_previous; + }; +} + // Streams the response body in bounded chunks and enforces MAX_HTTP_RESPONSE_SIZE. // The completionHandler-based NSURLSession APIs fully materialize the response body // as an NSData before handing it over, so an attacker-controlled collector could force @@ -23,7 +49,7 @@ // more than the cap is ever buffered. Delegate callbacks may arrive on the session's // delegate queue while a request thread registers a task, so shared state is guarded. @interface MATStreamingSessionDelegate : NSObject -- (void)registerTask:(NSURLSessionTask*)task +- (BOOL)registerTask:(NSURLSessionTask*)task handler:(void (^)(NSData* data, NSURLResponse* response, NSError* error))handler; @end @@ -45,14 +71,31 @@ - (instancetype)init return self; } -- (void)registerTask:(NSURLSessionTask*)task +- (BOOL)registerTask:(NSURLSessionTask*)task handler:(void (^)(NSData*, NSURLResponse*, NSError*))handler { NSNumber* key = @(task.taskIdentifier); + NSMutableData* buffer = [NSMutableData new]; + id copiedHandler = [handler copy]; + if (buffer == nil || copiedHandler == nil) + { + return NO; + } @synchronized(self) { - _buffers[key] = [NSMutableData new]; - _handlers[key] = [handler copy]; + @try + { + _buffers[key] = buffer; + _handlers[key] = copiedHandler; + return YES; + } + @catch (NSException* exception) + { + (void)exception; + [_buffers removeObjectForKey:key]; + [_handlers removeObjectForKey:key]; + return NO; + } } } @@ -104,6 +147,7 @@ - (void)URLSession:(NSURLSession*)session { return; } + AppleDelegateCallbackScope callbackScope; if (overCap) { // Surface a non-cancellation error so the request maps to NetworkFailure @@ -128,12 +172,6 @@ - (void)URLSession:(NSURLSession*)session return std::string("REQ-") + std::to_string(seq.fetch_add(1)); } -static std::string NextRespId() -{ - static std::atomic seq; - return std::string("RESP-") + std::to_string(seq.fetch_add(1)); -} - static dispatch_once_t once; static NSURLSession* session; static MATStreamingSessionDelegate* sessionDelegate; @@ -162,68 +200,194 @@ - (void)URLSession:(NSURLSession*)session void SendAsync(IHttpResponseCallback* callback) { - @autoreleasepool + bool cancelledBeforeSend = false; + bool registered = false; + NSURLSessionDataTask* task = nil; { + std::lock_guard lock(m_mutex); m_callback = callback; - NSString* url = [[NSString alloc] initWithUTF8String:m_url.c_str()]; - m_urlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]]; + cancelledBeforeSend = m_cancelRequested; + } + if (cancelledBeforeSend) + { + // A Cancel() raced ahead of SendAsync and only set the flag (it never + // completes on its own because there was no callback yet). Now that the + // callback is published we own the single terminal Aborted. + Complete(HttpResult_Aborted); + return; + } - for(const auto& header : m_headers) + @try + { + @autoreleasepool { - NSString* name = [[NSString alloc] initWithUTF8String:header.first.c_str()]; - NSString* value = [[NSString alloc] initWithUTF8String:header.second.c_str()]; - [m_urlRequest setValue:value forHTTPHeaderField:name]; - } + NSString* url = [[NSString alloc] initWithUTF8String:m_url.c_str()]; + NSURL* nsUrl = (url != nil) ? [NSURL URLWithString:url] : nil; + if (nsUrl == nil || nsUrl.scheme == nil) + { + Complete(HttpResult_LocalFailure); + return; + } + + NSMutableURLRequest* urlRequest = [[NSMutableURLRequest alloc] initWithURL:nsUrl]; + if (urlRequest == nil) + { + Complete(HttpResult_LocalFailure); + return; + } + + for(const auto& header : m_headers) + { + NSString* name = [[NSString alloc] initWithUTF8String:header.first.c_str()]; + NSString* value = [[NSString alloc] initWithUTF8String:header.second.c_str()]; + if (name == nil || value == nil) + { + Complete(HttpResult_LocalFailure); + return; + } + [urlRequest setValue:value forHTTPHeaderField:name]; + } + + m_completionMethod = + ^(NSData *data, NSURLResponse *response, NSError *error) + { + HandleResponse(data, response, error); + }; + + if (session == nil || sessionDelegate == nil) + { + Complete(HttpResult_NetworkFailure); + return; + } + + if(equalsIgnoreCase(m_method, "get")) + { + [urlRequest setHTTPMethod:@"GET"]; + task = [session dataTaskWithRequest:urlRequest]; + } + else + { + [urlRequest setHTTPMethod:@"POST"]; + NSData* postData = [NSData dataWithBytes:m_body.data() length:m_body.size()]; + task = [session uploadTaskWithRequest:urlRequest fromData:postData]; + } + + if (task == nil || m_completionMethod == nil) + { + Complete(HttpResult_LocalFailure); + return; + } - m_completionMethod = - ^(NSData *data, NSURLResponse *response, NSError *error) + m_urlRequest = urlRequest; + + // Register before exposing the task to Cancel() so cancellation cannot + // deliver the task's only terminal callback before its handler exists. + registered = [sessionDelegate registerTask:task handler:m_completionMethod]; + if (!registered) { - HandleResponse(data, response, error); - }; + [task cancel]; + Complete(HttpResult_LocalFailure); + return; + } - if(equalsIgnoreCase(m_method, "get")) + { + std::lock_guard lock(m_mutex); + m_dataTask = task; + if (m_cancelRequested) + { + // The registered delegate remains the sole terminal producer. + [task cancel]; + } + else + { + [task resume]; + } + } + } + } + @catch (NSException* exception) + { + LOG_WARN("HTTP request setup failed: %s", [[exception reason] UTF8String]); + if (registered) { - [m_urlRequest setHTTPMethod:@"GET"]; - m_dataTask = [session dataTaskWithRequest:m_urlRequest]; + [task cancel]; + return; } - else + if (task != nil) { - [m_urlRequest setHTTPMethod:@"POST"]; - NSData* postData = [NSData dataWithBytes:m_body.data() length:m_body.size()]; - m_dataTask = [session uploadTaskWithRequest:m_urlRequest fromData:postData]; + [task cancel]; } - - // Register before resume so the streaming delegate has the buffer and - // completion handler in place before any response data arrives. - [sessionDelegate registerTask:m_dataTask handler:m_completionMethod]; - [m_dataTask resume]; + Complete(HttpResult_LocalFailure); } } void HandleResponse(NSData* data, NSURLResponse* response, NSError* error) { + IHttpResponseCallback* callback = nullptr; + bool cancelRequested = false; + HttpClient_Apple* parent = m_parent; + IHttpRequest* self = static_cast(this); + const std::string requestId = GetId(); + { + std::lock_guard lock(m_mutex); + if (m_terminal) + { + return; + } + m_terminal = true; + callback = m_callback; + cancelRequested = m_cancelRequested; + } + @autoreleasepool { - NSHTTPURLResponse *httpResp = static_cast(response); - auto simpleResponse = new SimpleHttpResponse { NextRespId() }; + NSHTTPURLResponse *httpResp = + [response isKindOfClass:[NSHTTPURLResponse class]] + ? static_cast(response) + : nil; + auto simpleResponse = new SimpleHttpResponse { requestId }; - simpleResponse->m_statusCode = static_cast(httpResp.statusCode); + simpleResponse->m_statusCode = + (httpResp != nil) ? static_cast(httpResp.statusCode) : 0; - NSDictionary *responseHeaders = [httpResp allHeaderFields]; - for (id key in responseHeaders) + if (httpResp != nil) { - simpleResponse->m_headers.add([key UTF8String], [responseHeaders[key] UTF8String]); + NSDictionary *responseHeaders = [httpResp allHeaderFields]; + for (id key in responseHeaders) + { + const char* keyString = [key UTF8String]; + const char* valueString = [responseHeaders[key] UTF8String]; + if (keyString != nullptr && valueString != nullptr) + { + simpleResponse->m_headers.add(keyString, valueString); + } + } } - if (error) + if (cancelRequested) + { + simpleResponse->m_result = HttpResult_Aborted; + } + else if (error) { NSString* errorDomain = [error domain]; long errorCode = [error code]; - if ([errorDomain isEqualToString:@"NSURLErrorDomain"] && (errorCode == NSURLErrorCancelled)) + if ([errorDomain isEqualToString:@"NSURLErrorDomain"] && + errorCode == NSURLErrorCancelled) { simpleResponse->m_result = HttpResult_Aborted; } + else if ([errorDomain isEqualToString:@"NSURLErrorDomain"] && + (errorCode == NSURLErrorBadURL || + errorCode == NSURLErrorUnsupportedURL)) + { + simpleResponse->m_result = HttpResult_LocalFailure; + } + else if (httpResp == nil) + { + simpleResponse->m_result = HttpResult_NetworkFailure; + } else { LOG_TRACE("HTTP response error code: %li", errorCode); @@ -245,21 +409,94 @@ void HandleResponse(NSData* data, NSURLResponse* response, NSError* error) std::copy(body, body + length, std::back_inserter(simpleResponse->m_body)); } } - m_callback->OnHttpResponse(simpleResponse); + if (parent != nullptr) + { + // Remove the request from the parent map before the callback runs. + // A concurrent CancelRequestAsync that already holds the parent mutex + // must finish first, keeping this raw request alive while it calls + // Cancel(); later cancels will not find the request at all. The + // callback may delete the request, so this erase must happen first. + parent->Erase(self); + } + if (callback != nullptr) + { + callback->OnHttpResponse(simpleResponse); + } + else + { + delete simpleResponse; + } } + // Do not touch `this` after invoking the callback: it may delete the request. } - void Cancel() + bool Cancel() { - [m_dataTask cancel]; + // Only set the flag and cancel the in-flight task; never invoke the callback + // here. A cancel before SendAsync has no callback yet, so completing from + // Cancel would claim the terminal transition with no one to notify. SendAsync + // (or the task's own delegate completion) delivers the single Aborted. + std::lock_guard lock(m_mutex); + m_cancelRequested = true; + if (m_dataTask != nil) + { + [m_dataTask cancel]; + } + return m_callback == nullptr && m_dataTask == nil; } private: + void Complete(HttpResult result) + { + IHttpResponseCallback* callback = nullptr; + HttpClient_Apple* parent = m_parent; + IHttpRequest* self = static_cast(this); + const std::string requestId = GetId(); + { + std::lock_guard lock(m_mutex); + if (m_terminal) + { + return; + } + if (m_cancelRequested) + { + result = HttpResult_Aborted; + } + m_terminal = true; + callback = m_callback; + } + + auto response = new SimpleHttpResponse { requestId }; + response->m_statusCode = 0; + response->m_result = result; + if (parent != nullptr) + { + // Same ordering rule as HandleResponse(): deregister before invoking + // the callback because the callback may delete the request. + parent->Erase(self); + } + if (callback != nullptr) + { + callback->OnHttpResponse(response); + } + else + { + delete response; + } + // Do not touch `this` after invoking the callback: it may delete the request. + } + HttpClient_Apple* m_parent = nullptr; IHttpResponseCallback* m_callback = nullptr; NSURLSessionDataTask* m_dataTask = nullptr; NSMutableURLRequest* m_urlRequest = nullptr; void (^m_completionMethod)(NSData* data, NSURLResponse* response, NSError* error); + // Guards m_callback, m_cancelRequested, m_dataTask and m_terminal so setup, + // cancellation and the single terminal completion observe a consistent view. + // The callback is always invoked outside this lock. + std::mutex m_mutex; + bool m_cancelRequested = false; + bool m_terminal = false; }; HttpClient_Apple::HttpClient_Apple() @@ -288,43 +525,54 @@ void Cancel() void HttpClient_Apple::CancelRequestAsync(const std::string& id) { - HttpRequestApple* request = nullptr; + // Hold the requests mutex across Cancel(): Cancel() only flips the per-request + // flag and cancels the NSURLSession task, and never completes synchronously. + // That lets the mutex pin the raw request lifetime while we touch it. A request + // that has never started has no callback capable of removing it, so retire it + // here; a later SendAsync still observes its cancel flag and delivers Aborted. + std::lock_guard lock(m_requestsMtx); + auto it = m_requests.find(id); + if (it != m_requests.cend()) { - std::lock_guard lock(m_requestsMtx); - if (m_requests.find(id) != m_requests.cend()) + auto* request = static_cast(it->second); + if (request != nullptr) { - request = static_cast(m_requests[id]); - if (request != nullptr) + LOG_TRACE("HTTP request=%p id=%s being aborted...", request, id.c_str()); + if (request->Cancel()) { - LOG_TRACE("HTTP request=%p id=%s being aborted...", request, id.c_str()); - request->Cancel(); + m_requests.erase(it); } - m_requests.erase(id); } } } void HttpClient_Apple::CancelAllRequests() { - std::vector ids; - { - std::lock_guard lock(m_requestsMtx); - for (auto const& item : m_requests) { - ids.push_back(item.first); - } - } - - for (const auto &id : ids) - CancelRequestAsync(id); - + // NSURLSession serializes delegate callbacks when delegateQueue is nil. A + // callback may cancel its peers, but it cannot wait for their callbacks to + // drain without blocking the only queue that can deliver them. + const bool waitForDrain = !isAppleDelegateCallback; for (;;) { + std::vector ids; { std::lock_guard lock(m_requestsMtx); if (m_requests.empty()) { return; } + for (auto const& item : m_requests) + { + ids.push_back(item.first); + } + } + for (const auto& id : ids) + { + CancelRequestAsync(id); + } + if (!waitForDrain) + { + return; } PAL::sleep(100); } diff --git a/lib/http/HttpClient_CAPI.cpp b/lib/http/HttpClient_CAPI.cpp index 5f344b366..a55f79441 100644 --- a/lib/http/HttpClient_CAPI.cpp +++ b/lib/http/HttpClient_CAPI.cpp @@ -11,16 +11,33 @@ namespace MAT_NS_BEGIN { + class HttpClient_CAPI_State + { + public: + explicit HttpClient_CAPI_State(uint64_t id) + : ownerId(id) + { + } + + uint64_t const ownerId; + std::mutex requestsMutex; + }; + // Represents a single in-flight, cancellable HTTP operation class HttpClient_Operation { public: - HttpClient_Operation(SimpleHttpRequest* request, IHttpResponseCallback* callback, http_cancel_fn_t cancelFn) - : m_request(request), + HttpClient_Operation( + uint64_t ownerId, + std::string requestId, + IHttpResponseCallback* callback, + http_cancel_fn_t cancelFn) + : m_requestId(std::move(requestId)), + m_ownerId(ownerId), m_callback(callback), m_cancelFn(cancelFn) { - if ((m_request == nullptr) || (callback == nullptr) || (cancelFn == nullptr)) + if (m_requestId.empty() || (callback == nullptr) || (cancelFn == nullptr)) { MATSDK_THROW(std::invalid_argument("Created HttpClient_Operation with invalid parameters")); } @@ -28,24 +45,70 @@ namespace MAT_NS_BEGIN { void Cancel() { - m_cancelFn(m_request->m_id.c_str()); + m_cancelFn(m_requestId.c_str()); + } + + void CompleteAborted() + { + auto response = std::unique_ptr( + new SimpleHttpResponse(m_requestId)); + response->m_result = HttpResult_Aborted; + OnResponse(response.release()); + } + + void BeginSendHandoff() + { + std::lock_guard lock(m_completionMutex); + m_sendInProgress = true; + } + + void FinishSendHandoff() + { + std::unique_ptr deferredResponse; + { + std::lock_guard lock(m_completionMutex); + m_sendInProgress = false; + deferredResponse = std::move(m_deferredResponse); + } + if (deferredResponse != nullptr) + { + m_callback->OnHttpResponse(deferredResponse.release()); + } } void OnResponse(IHttpResponse* response) { - m_callback->OnHttpResponse(response); + std::unique_ptr ownedResponse(response); + { + std::lock_guard lock(m_completionMutex); + if (m_sendInProgress) + { + m_deferredResponse = std::move(ownedResponse); + return; + } + } + m_callback->OnHttpResponse(ownedResponse.release()); + } + + uint64_t OwnerId() const noexcept + { + return m_ownerId; } private: - SimpleHttpRequest* m_request; - + std::string m_requestId; + uint64_t const m_ownerId; IHttpResponseCallback* m_callback; http_cancel_fn_t m_cancelFn; + std::mutex m_completionMutex; + bool m_sendInProgress {false}; + std::unique_ptr m_deferredResponse; }; // Manage tracking of in-flight operations static std::mutex s_operationsLock; + static std::atomic s_nextOwnerId{0}; std::map>& GetPendingOperations() { @@ -61,12 +124,15 @@ namespace MAT_NS_BEGIN { } // An operation is removed when a response has been received or the operation has been cancelled - std::shared_ptr RemovePendingOperation(const std::string& requestId) + std::shared_ptr RemovePendingOperation( + const std::string& requestId, + uint64_t ownerId = 0) { LOCKGUARD(s_operationsLock); std::shared_ptr operation; auto itOperation = GetPendingOperations().find(requestId); - if (itOperation != GetPendingOperations().end()) + if (itOperation != GetPendingOperations().end() && + (ownerId == 0 || itOperation->second->OwnerId() == ownerId)) { operation = itOperation->second; GetPendingOperations().erase(itOperation); @@ -75,6 +141,27 @@ namespace MAT_NS_BEGIN { return operation; } + std::vector> + RemovePendingOperations(uint64_t ownerId) + { + std::vector> operations; + LOCKGUARD(s_operationsLock); + for (auto it = GetPendingOperations().begin(); + it != GetPendingOperations().end();) + { + if (it->second->OwnerId() == ownerId) + { + operations.push_back(it->second); + it = GetPendingOperations().erase(it); + } + else + { + ++it; + } + } + return operations; + } + // Callback invoked when a response is ready. The ID of the response will match the ID of the corresponding request. void EVTSDK_LIBABI_CDECL OnHttpResponse(const char* requestId, http_result_t result, http_response_t* capiResponse) { @@ -127,7 +214,8 @@ namespace MAT_NS_BEGIN { HttpClient_CAPI::HttpClient_CAPI(http_send_fn_t sendFn, http_cancel_fn_t cancelFn) : m_sendFn(sendFn), - m_cancelFn(cancelFn) + m_cancelFn(cancelFn), + m_state(std::make_shared(++s_nextOwnerId)) { if ((sendFn == nullptr) || (cancelFn == nullptr)) { @@ -135,6 +223,27 @@ namespace MAT_NS_BEGIN { } } + HttpClient_CAPI::~HttpClient_CAPI() noexcept + { +#if HAVE_EXCEPTIONS + try + { + CancelAllRequests(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("CAPI HTTP client teardown failed: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("CAPI HTTP client teardown failed with a non-standard exception"); + } +#else + CancelAllRequests(); +#endif + } + IHttpRequest* HttpClient_CAPI::CreateRequest() { // Generate a unique request ID @@ -148,7 +257,17 @@ namespace MAT_NS_BEGIN { void HttpClient_CAPI::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) { - // Note: 'request' is never owned by IHttpClient and gets deleted in EventsUploadContext.clear() + auto state = m_state; + auto sendFn = m_sendFn; + auto cancelFn = m_cancelFn; + // The external hook borrows pointers into the caller's request until it + // returns. Serialize this short handoff with cancellation so cancellation + // cannot terminally complete the request while the hook still copies them. + // Shared state pins the lock and owner identity if a synchronous callback + // destroys the HttpClient_CAPI facade before this method returns. + std::unique_lock requestLock(state->requestsMutex); + + // SendRequestAsync borrows the request; the caller retains ownership. auto simpleRequest = static_cast(request); auto requestId = simpleRequest->m_id; @@ -177,48 +296,161 @@ namespace MAT_NS_BEGIN { capiRequest.headersCount = static_cast(capiHeaders.size()); capiRequest.headers = capiHeaders.data(); - auto operation = std::make_shared(simpleRequest, callback, m_cancelFn); + auto operation = std::make_shared( + state->ownerId, requestId, callback, cancelFn); AddPendingOperation(requestId, operation); - m_sendFn(&capiRequest, &OnHttpResponse); + operation->BeginSendHandoff(); +#if HAVE_EXCEPTIONS + std::exception_ptr sendException; + try + { + sendFn(&capiRequest, &OnHttpResponse); + } + catch (...) + { + sendException = std::current_exception(); + } + requestLock.unlock(); + operation->FinishSendHandoff(); + + if (sendException != nullptr) + { + // A throwing send rejected the request. Retire the operation so a + // misbehaving hook cannot later call into a callback the manager has + // already completed synthetically. + auto rejectedOperation = RemovePendingOperation( + requestId, state->ownerId); + if (rejectedOperation == nullptr) + { + // The hook completed (or cancellation completed) the request + // synchronously before throwing. The terminal callback is the + // authoritative outcome; do not expose both completion and an + // exception to a direct CAPI client. + LOG_ERROR("CAPI HTTP send hook threw after completing request %s", + requestId.c_str()); + return; + } + std::rethrow_exception(sendException); + } +#else + sendFn(&capiRequest, &OnHttpResponse); + requestLock.unlock(); + operation->FinishSendHandoff(); +#endif } void HttpClient_CAPI::CancelRequestAsync(const std::string& id) { + auto state = m_state; LOG_TRACE("Cancelling CAPI HTTP request '%s'", id.c_str()); - std::shared_ptr operation(nullptr); + std::shared_ptr operation; { - // Only lock mutex while actually reading/writing pending operations collection to prevent potential recursive deadlock - LOCKGUARD(s_operationsLock); - auto itOperation = GetPendingOperations().find(id); - if (itOperation != GetPendingOperations().end()) - { - operation = itOperation->second; - } + // Wait for the external send hook to release request-backed + // pointers, then retire the operation before dropping the lock. + std::lock_guard requestLock( + state->requestsMutex); + operation = RemovePendingOperation(id, state->ownerId); } if (operation != nullptr) { - operation->Cancel();// CodeQL [cpp/uninitializedptrfield] operation is explicitly constructed with nullptr so it will never hold garbage value +#if HAVE_EXCEPTIONS + try + { + operation->Cancel(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("CAPI HTTP cancellation failed for request %s: %s", + id.c_str(), ex.what()); + } + catch (...) + { + LOG_ERROR("CAPI HTTP cancellation failed for request %s", + id.c_str()); + } +#else + operation->Cancel(); +#endif + // Cancellation is terminal from the adapter's perspective. The + // operation was removed first, so synchronous or late external + // completions are ignored and cannot double-complete the callback. +#if HAVE_EXCEPTIONS + try + { + operation->CompleteAborted(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("CAPI HTTP cancellation callback failed for request %s: %s", + id.c_str(), ex.what()); + } + catch (...) + { + LOG_ERROR("CAPI HTTP cancellation callback failed for request %s", + id.c_str()); + } +#else + operation->CompleteAborted(); +#endif } } void HttpClient_CAPI::CancelAllRequests() { + auto state = m_state; LOG_TRACE("Cancelling all CAPI HTTP requests"); + // Retire this client's full snapshot before invoking external + // cancellation. Other CAPI clients keep their independent operations. std::vector> operations; { - // Only lock mutex while actually reading/writing pending operations collection to prevent potential recursive deadlock - LOCKGUARD(s_operationsLock); - for (const auto& operation : GetPendingOperations()) - { - operations.push_back(operation.second); - } + // Wait until any external send hook has released request-backed + // pointers. Do not hold this member lock across terminal callbacks: + // a direct callback is allowed to destroy the client. + std::lock_guard requestLock( + state->requestsMutex); + operations = RemovePendingOperations(state->ownerId); } for (const auto& operation : operations) { +#if HAVE_EXCEPTIONS + try + { + operation->Cancel(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("CAPI HTTP cancellation failed: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("CAPI HTTP cancellation failed with a non-standard exception"); + } +#else operation->Cancel(); +#endif +#if HAVE_EXCEPTIONS + try + { + operation->CompleteAborted(); + } + catch (const std::exception& ex) + { + (void)ex; + LOG_ERROR("CAPI HTTP cancellation callback failed: %s", ex.what()); + } + catch (...) + { + LOG_ERROR("CAPI HTTP cancellation callback failed with a non-standard exception"); + } +#else + operation->CompleteAborted(); +#endif } } diff --git a/lib/http/HttpClient_CAPI.hpp b/lib/http/HttpClient_CAPI.hpp index 5fb3cc088..5271e7769 100644 --- a/lib/http/HttpClient_CAPI.hpp +++ b/lib/http/HttpClient_CAPI.hpp @@ -9,13 +9,19 @@ #include "pal/PAL.hpp" #include "mat.h" +#include +#include +#include #include namespace MAT_NS_BEGIN { + class HttpClient_CAPI_State; + class HttpClient_CAPI : public IHttpClient { public: HttpClient_CAPI(http_send_fn_t sendFn, http_cancel_fn_t cancelFn); + ~HttpClient_CAPI() noexcept override; virtual IHttpRequest* CreateRequest() override; virtual void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) override; @@ -25,7 +31,7 @@ namespace MAT_NS_BEGIN { private: http_send_fn_t m_sendFn; http_cancel_fn_t m_cancelFn; - std::mutex m_requestsMutex; + std::shared_ptr m_state; }; } MAT_NS_END diff --git a/lib/http/HttpClient_Curl.cpp b/lib/http/HttpClient_Curl.cpp index b910cdf28..cf04603cf 100644 --- a/lib/http/HttpClient_Curl.cpp +++ b/lib/http/HttpClient_Curl.cpp @@ -10,51 +10,293 @@ #include "ctmacros.hpp" +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include #include "utils/Utils.hpp" #include "HttpClient_Curl.hpp" #include "ILogConfiguration.hpp" +// The SDK must never tear down libcurl's process-wide state; see +// EnsureCurlGlobalInit() for why teardown is unknowable from inside an embedded +// library. Poisoning the identifier after the libcurl headers have been +// included turns any future call from this translation unit into a build error +// instead of a rare crash in an unrelated component of the host process. +#if defined(__GNUC__) +#pragma GCC poison curl_global_cleanup +#endif + namespace MAT_NS_BEGIN { + static bool IsLocalRequestError(CURLcode error) noexcept + { + return error == CURLE_UNSUPPORTED_PROTOCOL || + error == CURLE_URL_MALFORMAT || + error == CURLE_NOT_BUILT_IN; + } + static std::string NextReqId() { static std::atomic seq(0); return std::string("REQ-") + std::to_string(seq.fetch_add(1)); } + // The request carries request data and an id and nothing else. It owns no + // transport object and holds no cancellation handle. The current Curl + // implementation copies request data into operation-owned storage, but the + // public IHttpClient contract still requires the caller to retain a request + // until its terminal callback begins. class CurlHttpRequest : public SimpleHttpRequest { public: CurlHttpRequest() : SimpleHttpRequest(NextReqId()) { } + }; + + /** + * Per-client shared state. + * + * Held by the facade and captured by every completion, so it outlives the + * HttpClient_Curl object. Completions never capture the client itself. + * + * No user callback, libcurl call, or operation-local lock is ever taken + * while this mutex is held. + */ + struct CurlClientState + { + std::mutex mutex; + std::condition_variable cv; + + // Owning registry. The operation outlives both the caller's IHttpRequest + // and the client facade, so cancellation and completion never + // dereference storage owned by somebody else. + std::map> operations; + + bool accepting {true}; + size_t cancelAllDepth {0}; + size_t registryGeneration {0}; + size_t callbackGeneration {0}; + size_t callbacksInFlight {0}; + // Incremented before an operation is constructed and decremented by the + // operation's shared_ptr deleter, i.e. only after ~CurlHttpOperation has + // joined or detached its worker and run curl_easy_cleanup(). A full + // drain that observes zero here knows no curl handle is still live. + size_t liveOperationCount {0}; + std::map callbacksByThread; + + std::atomic sslVerify {true}; + std::string sslCaInfo; // guarded by mutex + + // Returns true when the caller should start the worker. A false return + // means the operation must complete as Aborted without touching the + // network: either admission has stopped, or a cancellation epoch is in + // progress and must not be starved by late sends. + bool registerOperation(std::string const& id, std::shared_ptr operation) + { + bool shouldSend; + { + std::lock_guard lock(mutex); + if (!accepting) + { + return false; + } + operations[id] = std::move(operation); + ++registryGeneration; + shouldSend = (cancelAllDepth == 0); + } + cv.notify_all(); + return shouldSend; + } + + // Re-evaluated after the deferred creation event has run: the worker may + // only start if admission is still open and no cancellation epoch is in + // progress. Mirrors registerOperation's send decision so a creation + // callback that stopped admission or opened an epoch cannot be raced. + bool stillAcceptingSend() + { + std::lock_guard lock(mutex); + return accepting && cancelAllDepth == 0; + } + + void eraseOperation(std::string const& id) + { + { + std::lock_guard lock(mutex); + operations.erase(id); + ++registryGeneration; + } + cv.notify_all(); + } + + void stopAccepting() + { + std::lock_guard lock(mutex); + accepting = false; + } + + void beginCallback() + { + { + std::lock_guard lock(mutex); + ++callbacksInFlight; + ++callbacksByThread[std::this_thread::get_id()]; + ++callbackGeneration; + } + cv.notify_all(); + } + + void endCallback() + { + { + std::lock_guard lock(mutex); + if (callbacksInFlight == 0) + { + LOG_ERROR("curl callback accounting underflow"); + } + else + { + --callbacksInFlight; + auto it = callbacksByThread.find(std::this_thread::get_id()); + if (it == callbacksByThread.end() || it->second == 0) + { + LOG_ERROR("curl callback thread was not registered"); + } + else if (--it->second == 0) + { + callbacksByThread.erase(it); + } + } + ++callbackGeneration; + } + cv.notify_all(); + } - void SetOperation(const std::shared_ptr& curlOperation) + void noteOperationCreated() { - m_curlOperation = curlOperation; + std::lock_guard lock(mutex); + ++liveOperationCount; } - void Cancel() + void noteOperationDestroyed() { - if (m_curlOperation != nullptr) { - m_curlOperation->Abort(); + { + std::lock_guard lock(mutex); + if (liveOperationCount == 0) + { + LOG_ERROR("curl operation accounting underflow"); + } + else + { + --liveOperationCount; + } } + cv.notify_all(); + } + }; + + // RAII accounting for a user-visible callback. A drain that starts while a + // callback is running must see it, and must still be able to tell that + // callback apart from a peer on another thread. + class CurlCallbackScope + { + public: + explicit CurlCallbackScope(std::shared_ptr state) + : m_state(std::move(state)) + { + m_state->beginCallback(); } + ~CurlCallbackScope() + { + m_state->endCallback(); + } + + CurlCallbackScope(CurlCallbackScope const&) = delete; + CurlCallbackScope& operator=(CurlCallbackScope const&) = delete; + private: - std::shared_ptr m_curlOperation; + std::shared_ptr m_state; }; - HttpClient_Curl::HttpClient_Curl() + namespace + { + // Ties liveOperationCount to the operation's destructor mechanically: the + // count is released by the deleter, after ~CurlHttpOperation has joined + // or detached the worker and released the curl handle. No caller can + // forget to decrement it, and no drain can observe zero while a curl + // handle is still alive. + std::shared_ptr MakeTrackedOperation( + std::shared_ptr const& state, + std::string method, + std::string url, + IHttpResponseCallback* callback, + std::map requestHeaders, + std::vector requestBody, + size_t httpConnTimeout, + bool sslVerify, + std::string sslCaInfo) + { + state->noteOperationCreated(); + CurlHttpOperation* raw = nullptr; + try + { + raw = new CurlHttpOperation( + std::move(method), std::move(url), callback, + std::move(requestHeaders), std::move(requestBody), + false, httpConnTimeout, sslVerify, std::move(sslCaInfo), + CurlHttpOperation::CallbackHooks { + [state]() { state->beginCallback(); }, + [state]() { state->endCallback(); } + }, + // Tracked operations defer OnCreated/OnCreateFailed until + // after registration so a reentrant cancel can find them. + true); + } + catch (...) + { + state->noteOperationDestroyed(); + throw; + } + + // If control-block allocation fails, shared_ptr invokes this + // deleter before propagating the exception. + return std::shared_ptr( + raw, [state](CurlHttpOperation* operation) noexcept { + delete operation; + state->noteOperationDestroyed(); + }); + } + } + + HttpClient_Curl::HttpClient_Curl() : + m_state(std::make_shared()) { - /* In windows, this will init the winsock stuff */ TRACE("Initializing HttpClient_Curl...\n"); - curl_global_init(CURL_GLOBAL_ALL); + EnsureCurlGlobalInit(); TRACE("libcurl version = %s\n", curl_version_info(CURLVERSION_NOW)->version); } HttpClient_Curl::~HttpClient_Curl() { - curl_global_cleanup(); + // Stop admitting work before draining, so the drain below cannot be + // starved by a concurrent SendRequestAsync. + m_state->stopAccepting(); + CancelAllRequests(); + // Deliberately no curl_global_cleanup(); see EnsureCurlGlobalInit(). + // + // Reentrant destruction (a caller deleting this client from inside one + // of its own callbacks) is safe: CancelAllRequests() recognizes that + // caller and returns without waiting for it, and the shared state, the + // running operation and the completion that owns them are all kept alive + // by the callback's own captures. The client object itself must not be + // touched after this returns. TRACE("Destroyed HttpClient_Curl.\n"); }; @@ -65,100 +307,342 @@ namespace MAT_NS_BEGIN { void HttpClient_Curl::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) { - // Note: 'request' is never owned by IHttpClient and gets deleted in EventsUploadContext.clear() - AddRequest(request); + // Keep shared state locally: the deferred OnCreated / OnCreateFailed + // event dispatched below (or the terminal callback) may destroy this + // facade, so nothing after construction may touch m_state. The request + // is borrowed under the public IHttpClient contract, while this Curl + // implementation copies its fields and never touches it after this + // initial extraction. + auto state = m_state; auto curlRequest = static_cast(request); - std::string requestId = curlRequest->GetId(); + const std::string requestId = curlRequest->GetId(); + std::string method = curlRequest->m_method; + std::string url = curlRequest->m_url; + std::vector body = curlRequest->m_body; std::map requestHeaders; for (const auto& header : curlRequest->m_headers) { requestHeaders[header.first] = header.second; } + bool sslVerify; std::string sslCaInfo; { - std::lock_guard lock(m_requestsMtx); - sslCaInfo = m_sslCaInfo; + std::lock_guard lock(state->mutex); + sslVerify = state->sslVerify.load(std::memory_order_acquire); + sslCaInfo = state->sslCaInfo; } - auto curlOperation = std::make_shared(curlRequest->m_method, curlRequest->m_url, callback, requestHeaders, curlRequest->m_body, false, HTTP_CONN_TIMEOUT, m_sslVerify, sslCaInfo); - curlRequest->SetOperation(curlOperation); - - // The lifetime of curlOperation is guarnteed by the call to result.wait() in the d'tor. - curlOperation->SendAsync([this, callback, requestId](CurlHttpOperation& operation) { - this->EraseRequest(requestId); + std::shared_ptr operation; + try + { + operation = MakeTrackedOperation( + state, std::move(method), std::move(url), callback, + std::move(requestHeaders), std::move(body), + HTTP_CONN_TIMEOUT, sslVerify, std::move(sslCaInfo)); + } + catch (const std::exception&) + { + CurlCallbackScope callbackScope(state); + auto response = std::unique_ptr( + new SimpleHttpResponse(requestId)); + response->m_result = HttpResult_LocalFailure; + callback->OnHttpResponse(response.release()); + return; + } + + auto completion = [state, operation, callback, requestId](CurlHttpOperation& op) { + // Account for this callback before anything else, so a drain that + // starts now waits for it (or recognizes itself in it). + CurlCallbackScope callbackScope(state); + + // Release the registry identity before the user callback runs: the + // id is then free for reuse and a concurrent CancelRequestAsync() + // can no longer pick up an operation that is already completing. + // The 'operation' capture keeps the object alive across the response + // build and the callback itself. + state->eraseOperation(requestId); auto response = std::unique_ptr(new SimpleHttpResponse(requestId)); response->m_result = HttpResult_OK; - response->m_statusCode = operation.GetResponseCode(); - if (response->m_statusCode == CURLE_FAILED_INIT) { - // There was an error in CURL stack while trying to create request + response->m_statusCode = op.GetHttpStatusCode(); + if (op.WasAborted()) { + // Cancellation wins even when libcurl finishes the transfer + // successfully after the caller has requested an abort. + response->m_result = HttpResult_Aborted; + } else if (op.GetSetupError() != CURLE_OK || + IsLocalRequestError(op.GetTransportError())) { + // There was an error configuring the CURL request. response->m_result = HttpResult_LocalFailure; - } else if ((CURLE_OK < response->m_statusCode) && (response->m_statusCode <= CURL_LAST)) { - if (operation.WasAborted()) { - // Operation was manually aborted - response->m_result = HttpResult_Aborted; - } else { - // There was an error in CURL stack while trying to connect - response->m_result = HttpResult_NetworkFailure; - } + } else if (op.GetTransportError() != CURLE_OK) { + // There was an error in CURL stack while trying to connect. + response->m_result = HttpResult_NetworkFailure; } - auto responseHeaders = operation.GetResponseHeaders(); + auto responseHeaders = op.GetResponseHeaders(); response->m_headers.insert(responseHeaders.begin(), responseHeaders.end()); - response->m_body = operation.GetResponseBody(); - + response->m_body = op.GetResponseBody(); + // 'response' is no longer owned by IHttpClient and gets deleted in EventsUploadContext.clear() callback->OnHttpResponse(response.release()); - }); + }; + + // Register before dispatching the creation event. A cancellation that + // arrives from that event (or between here and the first byte on the + // wire) must not be able to miss the operation. + const bool shouldSend = state->registerOperation(requestId, operation); + + // Now that the operation is discoverable, replay the OnCreated / + // OnCreateFailed state event that construction deferred. A reentrant + // CancelRequestAsync/CancelAllRequests fired from it will find and abort + // this operation, and it is accounted as a callback via the operation + // hooks so a concurrent drain observes it. + bool startWorker = false; + try + { + operation->DispatchDeferredCreationEvent(); + + // Re-evaluate the send decision after the creation event. A fast + // constructor/setup failure never touches the network. Otherwise + // the worker starts only if registration admitted it, the creation + // callback did not cancel it, and admission is still open with no + // cancellation epoch in progress. + const bool creationFailed = operation->GetSetupError() != CURLE_OK; + startWorker = shouldSend && !creationFailed && + !operation->WasAborted() && state->stillAcceptingSend(); + if (!startWorker && !creationFailed) + { + // Canceled, client destroyed, or landed in a cancellation epoch: + // complete exactly one Aborted terminal, no worker, no socket. + operation->Abort(); + } + } + catch (...) + { + // A state observer must not strand the operation without a terminal. + operation->Abort(); + startWorker = false; + } + + if (!startWorker) + { + // Destroy-before-terminal, no-send path. Exactly one terminal here, + // on this thread: OnCreateFailed/OnCreated already fired, OnDestroy + // and the response callback follow in order. + operation->CompleteWithoutSend(completion); + return; + } + + operation->SendAsync(completion); } void HttpClient_Curl::CancelRequestAsync(std::string const& id) { - CurlHttpRequest* request = nullptr; + // Snapshot the shared operation under the lock, then abort outside it. + // The entry is never erased here: only the operation's own completion + // retires its identity, so cancellation can never race a caller into + // dropping the last owner of a running transfer. + std::shared_ptr operation; { - // Hold the lock only while iterating over the list of requests - std::lock_guard lock(m_requestsMtx); - if (m_requests.find(id) != m_requests.cend()) { - request = static_cast(m_requests[id]); - LOG_TRACE("HTTP request=%p id=%s being aborted...", request, id.c_str()); - m_requests.erase(id); + std::lock_guard lock(m_state->mutex); + auto it = m_state->operations.find(id); + if (it != m_state->operations.end()) { + LOG_TRACE("HTTP request id=%s being aborted...", id.c_str()); + operation = it->second; } } - if (request != nullptr) { - request->Cancel(); + if (operation != nullptr) { + operation->Abort(); } } - void HttpClient_Curl::ApplySettings(ILogConfiguration& config) + void HttpClient_Curl::CancelAllRequests() { - SetSslVerification( - config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY], - (const char *)config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]); + CancelAllRequests(std::chrono::milliseconds::zero()); } - void HttpClient_Curl::SetSslVerification(bool sslVerify, const std::string& caInfo) + void HttpClient_Curl::CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) { - m_sslVerify = sslVerify; - std::lock_guard lock(m_requestsMtx); - m_sslCaInfo = caInfo; + auto state = m_state; + + // The epoch is open for as long as this call runs. Sends that register + // inside it complete as Aborted without starting work, which is what + // stops late arrivals from starving the drain; conversely the epoch + // never rejects them silently, so every send still gets exactly one + // terminal callback. + class CancelAllScope + { + public: + explicit CancelAllScope(std::shared_ptr state) + : m_state(std::move(state)) + { + std::lock_guard lock(m_state->mutex); + ++m_state->cancelAllDepth; + } + + ~CancelAllScope() + { + if (m_active) + { + std::lock_guard lock(m_state->mutex); + if (m_state->cancelAllDepth == 0) + { + LOG_ERROR("curl cancel epoch accounting underflow"); + } + else + { + --m_state->cancelAllDepth; + } + m_state->cv.notify_all(); + } + } + + void finishLocked() + { + if (m_state->cancelAllDepth == 0) + { + LOG_ERROR("curl cancel epoch accounting underflow"); + } + else + { + --m_state->cancelAllDepth; + } + m_active = false; + m_state->cv.notify_all(); + } + + private: + std::shared_ptr m_state; + bool m_active {true}; + } cancelAllScope(state); + + const bool hasTimeout = bestEffortTimeout > std::chrono::milliseconds::zero(); + const auto deadline = std::chrono::steady_clock::now() + bestEffortTimeout; + const std::thread::id callerThread = std::this_thread::get_id(); + + std::vector> initialOperations; + bool callerIsInsideTrackedCallback = false; + { + std::lock_guard lock(state->mutex); + for (auto const& item : state->operations) + { + initialOperations.push_back(item.second); + } + callerIsInsideTrackedCallback = + state->callbacksByThread.find(callerThread) != state->callbacksByThread.end(); + } + + // A reentrant cancellation must still abort all peers observed at entry. + // It then ends its epoch and returns rather than waiting for its own + // callback (or another simultaneously cancelling callback). + for (auto const& operation : initialOperations) + { + operation->Abort(); + } + initialOperations.clear(); + + if (callerIsInsideTrackedCallback) + { + std::lock_guard lock(state->mutex); + cancelAllScope.finishLocked(); + return; + } + + auto drained = [&state]() { + return state->operations.empty() && + state->callbacksInFlight == 0 && + state->liveOperationCount == 0; + }; + + for (;;) + { + size_t registryGeneration = 0; + size_t callbackGeneration = 0; + { + // Scoped so the snapshot's shared_ptr references are gone before + // the wait below: otherwise this call would hold operations + // alive and liveOperationCount could never reach zero. + std::vector> operations; + { + std::lock_guard lock(state->mutex); + if (drained()) + { + // Completing the epoch under the registry lock makes + // this the linearization point: anything registered + // later is new work, not work this drain missed. + cancelAllScope.finishLocked(); + return; + } + + registryGeneration = state->registryGeneration; + callbackGeneration = state->callbackGeneration; + for (auto const& item : state->operations) + { + operations.push_back(item.second); + } + } + + for (auto const& operation : operations) + { + operation->Abort(); + } + } + + std::unique_lock lock(state->mutex); + if (drained()) + { + cancelAllScope.finishLocked(); + return; + } + if (hasTimeout && std::chrono::steady_clock::now() >= deadline) + { + cancelAllScope.finishLocked(); + return; + } + auto stateChangedOrDrained = [&]() { + return state->registryGeneration != registryGeneration || + state->callbackGeneration != callbackGeneration || + drained(); + }; + if (hasTimeout) + { + // Soft cap. Returning here may leave the shared state and one + // operation alive; both are owned by the completion that is + // still running, and the manager drains its own HttpCallbacks + // separately. + if (!state->cv.wait_until(lock, deadline, stateChangedOrDrained)) + { + cancelAllScope.finishLocked(); + return; + } + } + else + { + state->cv.wait(lock, stateChangedOrDrained); + } + } } - void HttpClient_Curl::EraseRequest(std::string const& id) + void HttpClient_Curl::ApplySettings(ILogConfiguration& config) { - std::lock_guard lock(m_requestsMtx); - m_requests.erase(id); + SetSslVerification( + config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY], + (const char *)config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO]); } - void HttpClient_Curl::AddRequest(IHttpRequest* request) + void HttpClient_Curl::SetSslVerification(bool sslVerify, const std::string& caInfo) { - std::lock_guard lock(m_requestsMtx); - m_requests[request->GetId()] = request; + std::lock_guard lock(m_state->mutex); + if (!sslVerify) + { + LOG_WARN("Ignoring sslVerify=false: curl TLS certificate and hostname verification cannot be disabled"); + } + m_state->sslVerify.store(true, std::memory_order_release); + m_state->sslCaInfo = caInfo; } } MAT_NS_END #endif - diff --git a/lib/http/HttpClient_Curl.hpp b/lib/http/HttpClient_Curl.hpp index 7d599dec9..227e316b5 100644 --- a/lib/http/HttpClient_Curl.hpp +++ b/lib/http/HttpClient_Curl.hpp @@ -17,11 +17,19 @@ #include #include #include +#include #include #include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -29,6 +37,7 @@ #include #include "IHttpClient.hpp" +#include "IBoundedHttpClientCancel.hpp" #include "pal/PAL.hpp" #ifdef HAVE_ONEDS_BOUNDCHECK_METHODS @@ -44,10 +53,43 @@ namespace MAT_NS_BEGIN { +/** + * Perform libcurl's process-wide initialization exactly once. + * + * curl_global_init() is not thread-safe on the libcurl versions this SDK + * supports, and it must run before any other libcurl entry point. Every code + * path that can be the process's first libcurl user -- the HttpClient_Curl + * facade and a directly constructed CurlHttpOperation -- funnels through this + * function. The C++11 function-local static guarantees the initializer runs + * exactly once per process and that concurrent first callers block until it + * has completed, so overlapping client construction cannot race. + * + * There is deliberately no matching curl_global_cleanup() anywhere in the SDK. + * libcurl's global state is process-wide and shared with every other static + * libcurl user in the host process: the application itself, other SDKs, and + * plugins that may be loaded after this library. This SDK cannot observe those + * users, so it cannot know when the last one is finished, which makes teardown + * unknowable from here. Releasing the global state when a telemetry client is + * destroyed would pull it out from under an unrelated component (and, worse, + * out from under this SDK's own in-flight transfers). Leaving it initialized + * for the life of the process is the only correct choice for an embedded + * library; the host may still call curl_global_cleanup() itself at exit. + */ +inline void EnsureCurlGlobalInit() noexcept +{ + static const CURLcode initResult = curl_global_init(CURL_GLOBAL_ALL); + (void)initResult; +} + +// Private per-client shared state. Defined in HttpClient_Curl.cpp: it owns the +// operation registry, the drain bookkeeping and the SSL settings, and it +// outlives the facade because every completion captures it by shared_ptr. +struct CurlClientState; + /** * Curl-based HTTP client */ -class HttpClient_Curl : public IHttpClient { +class HttpClient_Curl : public IHttpClient, public IBoundedHttpClientCancel { public: HttpClient_Curl(); virtual ~HttpClient_Curl(); @@ -56,39 +98,97 @@ class HttpClient_Curl : public IHttpClient { virtual void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) override; virtual void CancelRequestAsync(std::string const& id) override; + // Full drain: returns once every tracked operation has delivered its + // terminal callback and has been destroyed, unless the caller is itself + // running inside one of this client's callbacks (see the implementation). + virtual void CancelAllRequests() override; + // Soft-bounded drain: stops initiating further cancellations at the + // deadline and may return while an operation and the shared state are + // still alive. + virtual void CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) override; + virtual void ApplySettings(ILogConfiguration& config) override; + // sslVerify is retained for source compatibility, but false is ignored: + // production transports always verify the peer certificate and hostname. void SetSslVerification(bool sslVerify, const std::string& caInfo = ""); private: - void EraseRequest(std::string const& id); - void AddRequest(IHttpRequest* request); - - std::mutex m_requestsMtx; - std::map m_requests; - std::atomic m_sslVerify { true }; - std::string m_sslCaInfo; + std::shared_ptr m_state; }; class CurlHttpOperation { public: - static long GetPreferredHttpVersion() + struct CallbackHooks { - const curl_version_info_data* versionInfo = curl_version_info(CURLVERSION_NOW); - return (versionInfo != nullptr && (versionInfo->features & CURL_VERSION_HTTP2) != 0) - ? CURL_HTTP_VERSION_2_0 - : CURL_HTTP_VERSION_1_1; - } + std::function begin; + std::function end; + }; + +private: + class HookScope + { + public: + explicit HookScope(CallbackHooks const& hooks) + : m_hooks(hooks) + { + if (m_hooks.begin != nullptr) + { + m_hooks.begin(); + m_started = true; + } + } + ~HookScope() noexcept + { + if (m_started && m_hooks.end != nullptr) + { + try + { + m_hooks.end(); + } + catch (...) + { + } + } + } + + HookScope(HookScope const&) = delete; + HookScope& operator=(HookScope const&) = delete; + + private: + CallbackHooks const& m_hooks; + bool m_started {false}; + }; + +public: void DispatchEvent(HttpStateEvent type) { if (m_callback != nullptr) { + HookScope callbackScope(m_callbackHooks); m_callback->OnHttpStateEvent(type, static_cast(curl), 0); } } - std::atomic isAborted { false }; // Set to 'true' when async callback is aborted + // Replays the creation state event (OnCreated / OnCreateFailed) that + // construction deferred (see the deferCreationEvent constructor parameter). + // A no-op for a directly constructed operation, which dispatches its + // creation event during construction. Dispatching here -- after the caller + // has registered the operation -- is what lets a reentrant + // CancelRequestAsync/CancelAllRequests fired from the creation callback find + // and abort this operation before any network work starts. The dispatch is + // accounted through the operation's callback hooks, exactly like every other + // state event, so a concurrent drain sees it. + void DispatchDeferredCreationEvent() + { + if (m_hasPendingCreationEvent) + { + m_hasPendingCreationEvent = false; + DispatchEvent(m_pendingCreationEvent); + } + } + std::atomic isAborted { false }; // Set to 'true' when async callback is aborted /** * Create local CURL instance for url and body * @@ -97,79 +197,116 @@ class CurlHttpOperation { * @param httpConnTimeout HTTP connection timeout in seconds * @param httpReadTimeout HTTP read timeout in seconds */ + // Selects HTTP/2 only when the libcurl we are actually linked against was + // built with HTTP/2 support. Setting CURLOPT_HTTP_VERSION to + // CURL_HTTP_VERSION_2_0 against a libcurl without HTTP/2 does not silently + // downgrade -- it fails the transfer with CURLE_UNSUPPORTED_PROTOCOL -- so + // the version has to be probed at runtime rather than assumed. + static long GetPreferredHttpVersion() noexcept + { + const curl_version_info_data* versionInfo = curl_version_info(CURLVERSION_NOW); + if (versionInfo != nullptr && (versionInfo->features & CURL_VERSION_HTTP2) != 0) + { + return CURL_HTTP_VERSION_2_0; + } + return CURL_HTTP_VERSION_1_1; + } + + static long ClampConnectionTimeout(size_t timeout) noexcept + { + const long maxSeconds = std::numeric_limits::max() / 1000L; + return static_cast(std::min( + timeout, static_cast(maxSeconds))); + } + CurlHttpOperation( std::string method, std::string url, IHttpResponseCallback* callback, - // requestHeaders is copied into the curl_slist during construction - // and need not outlive this operation. requestBody is stored by - // reference and read by Send(), so it must outlive this operation. - const std::map& requestHeaders, - const std::vector& requestBody, + // Request data is copied or moved into operation-owned storage so + // the worker does not depend on the caller retaining the request. + std::map requestHeaders, + std::vector requestBody, // Default connectivity and response size options bool rawResponse = false, size_t httpConnTimeout = HTTP_CONN_TIMEOUT, // SSL certificate verification options bool sslVerify = true, - const std::string& sslCaInfo = "") : + std::string sslCaInfo = "", + CallbackHooks callbackHooks = CallbackHooks(), + // When true (client-created, tracked operations), the OnCreated / + // OnCreateFailed state event is not dispatched during construction. + // It is recorded and replayed later by DispatchDeferredCreationEvent() + // once the operation has been registered, so a reentrant + // CancelRequestAsync/CancelAllRequests fired from that event can find + // the operation. A directly constructed operation keeps the historical + // immediate-dispatch behavior. + bool deferCreationEvent = false) : // Optional connection params rawResponse(rawResponse), - httpConnTimeout(httpConnTimeout), + httpConnTimeout(ClampConnectionTimeout(httpConnTimeout)), m_callback(callback), - m_method(method), - m_url(url), - m_sslCaInfo(sslCaInfo), + m_method(std::move(method)), + m_url(std::move(url)), + m_sslCaInfo(std::move(sslCaInfo)), + m_callbackHooks(std::move(callbackHooks)), + m_deferCreationEvent(deferCreationEvent), // Local vars - requestBody(requestBody) + m_requestBody(std::move(requestBody)) { + // sslVerify is retained for source compatibility. Disabling TLS + // authentication is never permitted by the production transport. + (void)sslVerify; + TRACE("--------------------------------------------------------------------------------------------------\n"); response.memory = nullptr; response.size = 0; + // A directly constructed operation may be the process's first libcurl + // user, so it shares the client's init-once rather than assuming an + // HttpClient_Curl was built first. + EnsureCurlGlobalInit(); + /* get a curl handle */ curl = curl_easy_init(); if(!curl) { TRACE("libcurl failed to init!\n"); - res = CURLE_FAILED_INIT; - DispatchEvent(OnCreateFailed); - return; - } - -#if 0 - // Be verbose - if (!SetOption(CURLOPT_VERBOSE, 1L)) -#else - if (!SetOption(CURLOPT_VERBOSE, 0L)) -#endif - { - DispatchEvent(OnCreateFailed); - return; - } - - // Specify target URL - if (!SetOption(CURLOPT_URL, m_url.c_str()) - || !SetOption(CURLOPT_SSL_VERIFYPEER, sslVerify ? 1L : 0L) - || !SetOption(CURLOPT_SSL_VERIFYHOST, sslVerify ? 2L : 0L)) - { - DispatchEvent(OnCreateFailed); + m_transportError = CURLE_FAILED_INIT; + m_setupError = CURLE_FAILED_INIT; + EmitCreationEvent(OnCreateFailed); return; } - if (!m_sslCaInfo.empty() && !SetOption(CURLOPT_CAINFO, m_sslCaInfo.c_str())) + if (!SetOption(CURLOPT_VERBOSE, 0L) || + !SetOption(CURLOPT_URL, m_url.c_str()) || + !SetOption(CURLOPT_SSL_VERIFYPEER, 1L) || + !SetOption(CURLOPT_SSL_VERIFYHOST, 2L) || + (!m_sslCaInfo.empty() && !SetOption(CURLOPT_CAINFO, m_sslCaInfo.c_str())) || + // The worker is one thread of a host process this SDK does not own: + // never let libcurl install process-wide signal handlers or use + // SIGALRM-based timeouts. + !SetOption(CURLOPT_NOSIGNAL, 1L) || + // Bound DNS, TCP, proxy, and TLS connection establishment before + // curl_easy_perform() returns the connected socket. + !SetOption(CURLOPT_CONNECTTIMEOUT, httpConnTimeout) || + // The progress callback is the only cancellation channel that is + // safe to trigger from another thread: it runs on the worker, + // inside libcurl, and aborts the transfer in an orderly way. + !SetOption(CURLOPT_NOPROGRESS, 0L) || + !SetAbortProgressOption() || + // HTTP/2 when the linked libcurl supports it, otherwise HTTP/1.1 + !SetOption(CURLOPT_HTTP_VERSION, GetPreferredHttpVersion())) { - DispatchEvent(OnCreateFailed); + EmitCreationEvent(OnCreateFailed); return; } - if (!SetOption(CURLOPT_HTTP_VERSION, GetPreferredHttpVersion())) - { - DispatchEvent(OnCreateFailed); - return; - } + // With NOSIGNAL, a synchronous resolver may still prevent libcurl from + // enforcing a strict deadline until the resolver call returns. // Headers are copied into m_headersChunk during construction and the // curl_slist is kept alive until destruction, so the original map does @@ -177,25 +314,25 @@ class CurlHttpOperation { for (const auto& kv : requestHeaders) { std::string header = kv.first + ": " + kv.second; - curl_slist* appended = curl_slist_append(m_headersChunk, header.c_str()); - if (appended == nullptr) + curl_slist* appendedHeaders = curl_slist_append(m_headersChunk, header.c_str()); + if (appendedHeaders == nullptr) { - res = CURLE_OUT_OF_MEMORY; - DispatchEvent(OnCreateFailed); + m_transportError = CURLE_OUT_OF_MEMORY; + m_setupError = CURLE_OUT_OF_MEMORY; + EmitCreationEvent(OnCreateFailed); return; } - m_headersChunk = appended; + m_headersChunk = appendedHeaders; } - if(m_headersChunk != nullptr && !SetOption(CURLOPT_HTTPHEADER, m_headersChunk)) + if (m_headersChunk != nullptr && !SetOption(CURLOPT_HTTPHEADER, m_headersChunk)) { - DispatchEvent(OnCreateFailed); + EmitCreationEvent(OnCreateFailed); return; } TRACE("method=%s, url=%s\n", this->m_method.c_str(), this->m_url.c_str()); - m_isConfigured = true; - DispatchEvent(OnCreated); + EmitCreationEvent(OnCreated); } /** @@ -203,44 +340,66 @@ class CurlHttpOperation { */ virtual ~CurlHttpOperation() { - // Given the request has not been aborted we should wait for completion here - // This guarantees the lifetime of this request. - if (result.valid()) + if (m_worker.joinable()) { - result.wait(); + if (m_worker.get_id() == std::this_thread::get_id()) + { + // The completion callback can release the owning request on this + // worker. Detach rather than joining the current thread; Send() has + // finished and the worker does not touch this operation afterward. + m_worker.detach(); + } + else + { + m_worker.join(); + } } - DispatchEvent(OnDestroy); - res = CURLE_OK; + + DispatchDestroyEvent(); + m_transportError = CURLE_OK; if (curl != nullptr) { curl_easy_cleanup(curl); } - curl_slist_free_all(m_headersChunk); + if (m_headersChunk != nullptr) + { + curl_slist_free_all(m_headersChunk); + } ReleaseResponse(); } /** * Send request synchronously */ - long Send() + void Send() { TRACE("method=%s\n", this->m_method.c_str()); ReleaseResponse(); // Request buffer - const void *request = requestBody.empty() ? nullptr : requestBody.data(); - const size_t reqSize = requestBody.size(); - int socketWaitResult = 0; + const void *request = m_requestBody.empty() ? nullptr : m_requestBody.data(); + const size_t reqSize = m_requestBody.size(); + long httpStatusCode = 0; + CURLcode infoResult = CURLE_OK; - if(!curl || !m_isConfigured) + if(!curl) { - if (res == CURLE_OK) - { - res = CURLE_FAILED_INIT; - } + m_transportError = CURLE_FAILED_INIT; DispatchEvent(OnSendFailed); goto cleanup; } + if (m_setupError != CURLE_OK) + { + DispatchEvent(OnSendFailed); + goto cleanup; + } + if (isAborted) + { + // Cancelled before the worker reached the network. Do not open a + // connection; the terminal result is Aborted either way. + m_transportError = CURLE_ABORTED_BY_CALLBACK; + goto cleanup; + } // TODO: should we control what local source port we use? // curl_easy_setopt(curl, CURLOPT_LOCALPORT, dcf_port); @@ -252,46 +411,52 @@ class CurlHttpOperation { goto cleanup; } DispatchEvent(OnConnecting); + m_transportError = curl_easy_perform(curl); + if(CURLE_OK != m_transportError) { - const CURLcode curlResult = curl_easy_perform(curl); - res = static_cast(curlResult); - if(CURLE_OK != curlResult) - { - DispatchEvent(OnConnectFailed); // couldn't connect - stage 1 - TRACE("Error #1: %s\n", curl_easy_strerror(curlResult)); - goto cleanup; - } + DispatchEvent(OnConnectFailed); // couldn't connect - stage 1 + TRACE("Error #1: %s\n", curl_easy_strerror(m_transportError)); + goto cleanup; } - { - CURLcode infoResult; + /* Extract the socket from the curl handle - we'll need it for waiting. + * Note that this API takes a pointer to a 'long' while we use + * curl_socket_t for sockets otherwise. + */ + #if LIBCURL_VERSION_NUM >= 0x072D00 // Version 7.45.00 - infoResult = curl_easy_getinfo(curl, CURLINFO_ACTIVESOCKET, &sockextr); + m_transportError = curl_easy_getinfo(curl, CURLINFO_ACTIVESOCKET, &sockextr); #else + { long lastSocket = -1; - infoResult = curl_easy_getinfo(curl, CURLINFO_LASTSOCKET, &lastSocket); - if (infoResult == CURLE_OK) + m_transportError = curl_easy_getinfo(curl, CURLINFO_LASTSOCKET, &lastSocket); + if (m_transportError == CURLE_OK) { sockextr = static_cast(lastSocket); } + } #endif - if(CURLE_OK != infoResult || sockextr == CURL_SOCKET_BAD) - { - res = static_cast( - infoResult != CURLE_OK ? infoResult : CURLE_COULDNT_CONNECT); - DispatchEvent(OnConnectFailed); // couldn't connect - stage 2 - TRACE("Error #2: %s\n", curl_easy_strerror(static_cast(res))); - goto cleanup; - } + + if(CURLE_OK != m_transportError) + { + DispatchEvent(OnConnectFailed); // couldn't connect - stage 2 + TRACE("Error #2: %s\n", curl_easy_strerror(m_transportError)); + goto cleanup; + } + if (sockextr == CURL_SOCKET_BAD) + { + m_transportError = CURLE_FAILED_INIT; + DispatchEvent(OnConnectFailed); // couldn't connect - no socket + TRACE("Error #2: curl returned an invalid socket\n"); + goto cleanup; } /* wait for the socket to become ready for sending */ sockfd = sockextr; - socketWaitResult = WaitOnSocket(sockfd, 0, HTTP_CONN_TIMEOUT * 1000L); - if(socketWaitResult <= 0 || isAborted) + if (WaitOnSocket(sockfd, 0, static_cast(httpConnTimeout) * 1000L) <= 0 || isAborted) { TRACE("Error #3: timeout, aborted=%u\n", isAborted.load() ); - res = CURLE_OPERATION_TIMEDOUT; + m_transportError = CURLE_OPERATION_TIMEDOUT; DispatchEvent(OnConnectFailed); // couldn't connect - stage 3 goto cleanup; } @@ -306,33 +471,31 @@ class CurlHttpOperation { // send all data to our callback function if (rawResponse) { - if (!SetOption(CURLOPT_HEADER, 1L) - || !SetOption(CURLOPT_WRITEFUNCTION, - static_cast(&WriteMemoryCallback)) - || !SetOption(CURLOPT_WRITEDATA, static_cast(&response))) + if (!SetOption(CURLOPT_HEADER, 1L) || + !SetOption(CURLOPT_WRITEFUNCTION, &WriteMemoryCallback) || + !SetOption(CURLOPT_WRITEDATA, static_cast(&response))) + { + DispatchEvent(OnSendFailed); + goto cleanup; + } + } else { + if (!SetOption(CURLOPT_HEADERFUNCTION, &WriteVectorCallback) || + !SetOption(CURLOPT_HEADERDATA, static_cast(&respHeaders)) || + !SetOption(CURLOPT_WRITEFUNCTION, &WriteVectorCallback) || + !SetOption(CURLOPT_WRITEDATA, static_cast(&respBody))) { DispatchEvent(OnSendFailed); goto cleanup; } - } - else if (!SetOption(CURLOPT_WRITEFUNCTION, - static_cast(&WriteVectorCallback)) - || !SetOption(CURLOPT_HEADERFUNCTION, - static_cast(&WriteVectorCallback)) - || !SetOption(CURLOPT_HEADERDATA, static_cast(&respHeaders)) - || !SetOption(CURLOPT_WRITEDATA, static_cast(&respBody))) - { - DispatchEvent(OnSendFailed); - goto cleanup; } // TODO: only two methods supported for now - POST and GET if (m_method.compare("POST") == 0) { // POST - if (!SetOption(CURLOPT_POST, 1L) - || !SetOption(CURLOPT_POSTFIELDS, static_cast(request)) - || !SetOption(CURLOPT_POSTFIELDSIZE_LARGE, static_cast(reqSize))) + if (!SetOption(CURLOPT_POST, 1L) || + !SetOption(CURLOPT_POSTFIELDS, static_cast(request)) || + !SetOption(CURLOPT_POSTFIELDSIZE_LARGE, static_cast(reqSize))) { DispatchEvent(OnSendFailed); goto cleanup; @@ -344,26 +507,23 @@ class CurlHttpOperation { } else { TRACE("Error #4: unsupported method %s\n", m_method.c_str()); - res = CURLE_UNSUPPORTED_PROTOCOL; + m_transportError = CURLE_UNSUPPORTED_PROTOCOL; goto cleanup; } - if (!SetOption(CURLOPT_LOW_SPEED_TIME, 30L) - || !SetOption(CURLOPT_LOW_SPEED_LIMIT, 4096L)) + if (!SetOption(CURLOPT_LOW_SPEED_TIME, 30L) || + !SetOption(CURLOPT_LOW_SPEED_LIMIT, 4096L)) { DispatchEvent(OnSendFailed); goto cleanup; } DispatchEvent(OnSending); + m_transportError = curl_easy_perform(curl); + if(CURLE_OK != m_transportError) { - const CURLcode curlResult = curl_easy_perform(curl); - res = static_cast(curlResult); - if(CURLE_OK != curlResult) - { - DispatchEvent(OnSendFailed); - TRACE("Error: %s\n", curl_easy_strerror(curlResult)); - goto cleanup; - } + DispatchEvent(OnSendFailed); + TRACE("Error: %s\n", curl_easy_strerror(m_transportError)); + goto cleanup; } /* Code snippet to parse raw HTTP response. This might come in handy @@ -378,56 +538,108 @@ class CurlHttpOperation { */ /* libcurl is nice enough to parse the response code itself: */ + infoResult = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatusCode); + if (infoResult != CURLE_OK) { - long responseCode = 0; - const CURLcode infoResult = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode); - if (infoResult != CURLE_OK) - { - res = static_cast(infoResult); - DispatchEvent(OnSendFailed); - goto cleanup; - } - res = responseCode; + m_transportError = infoResult; + DispatchEvent(OnSendFailed); + TRACE("Error getting HTTP response code: %s\n", curl_easy_strerror(m_transportError)); + goto cleanup; } + m_httpStatusCode = httpStatusCode; // We got some response from server. Dump the contents. - TRACE("HTTP response code %d\n", res); + TRACE("HTTP response code %ld\n", httpStatusCode); DispatchEvent(OnResponse); cleanup: + return; + } + + void SendAsync(std::function callback = nullptr) { + // A newly created std::thread may run before it is assigned to m_worker. + // Hold this gate until the assignment completes so a fast failure cannot + // destroy the operation from its callback while SendAsync still uses it. + { + std::lock_guard startGuard(m_workerStartMtx); + if (m_sendAttempted) + { + throw std::logic_error("CurlHttpOperation is single-use"); + } + m_sendAttempted = true; + + try + { + m_worker = std::thread([this, callback]() { + { + std::lock_guard startGuard(m_workerStartMtx); + } + try + { + Send(); + } + catch (...) + { + // std::async stored worker exceptions in its unobserved + // future. A raw thread must contain them. + m_transportError = CURLE_FAILED_INIT; + m_setupError = CURLE_FAILED_INIT; + } + Complete(callback); + }); + return; + } + catch (...) + { + // Callable allocation/copy or std::thread creation failed. + } + } - // This function returns: - // - on success: HTTP status code. - // - on failure: CURL error code. - // The two sets of enums (CURLE, HTTP codes) - do not intersect, so we collapse them in one set. - return res; + m_transportError = CURLE_FAILED_INIT; + m_setupError = CURLE_FAILED_INIT; + CompleteWithoutSend(callback); } - std::future & SendAsync(std::function callback = nullptr) { - result = std::async(std::launch::async, [this, callback] { - long result = Send(); - if (callback!=nullptr) - callback(*this); - return result; - }); - return result; + void CompleteWithoutSend(const std::function& callback) noexcept + { + Complete(callback); } - /** - * Get HTTP response code. This function returns CURL error code if HTTP response code is invalid. - */ - long GetResponseCode() + CURLcode GetTransportError() const { - return res; + return m_transportError; + } + + long GetHttpStatusCode() const + { + return m_httpStatusCode; } /** - * Get whether or not response was programmatically aborted + * Get whether or not response was programmatically aborted. + * + * Once the outcome has been frozen (at the start of Complete, before the + * OnDestroy state event runs; see FreezeOutcome) this returns the latched + * classification rather than the live flag. That is what stops an Abort() + * triggered from an OnDestroy observer -- which is legitimately allowed to + * cancel *peers* -- from retroactively turning this operation's already + * finished, successful transfer into an Aborted one. A cancellation that + * won before the freeze is captured by the latch and still reported as + * Aborted. */ bool WasAborted() { + if (m_outcomeFrozen.load(std::memory_order_acquire)) + { + return m_frozenAborted.load(std::memory_order_relaxed); + } return isAborted.load(); } + CURLcode GetSetupError() const + { + return m_setupError; + } + /** * Return a copy of response headers * @@ -496,19 +708,21 @@ class CurlHttpOperation { } /** - * Abort request in connecting or reading state. + * Request cancellation of a request that is connecting or transferring. + * + * This raises a flag and nothing else. It deliberately does not close the + * socket: the descriptor is owned by the worker thread and by libcurl, and + * closing it from another thread races with libcurl's own close. After that + * race the descriptor number can be handed straight back out by the kernel, + * so a late close tears down an unrelated connection somewhere else in the + * host process. The worker observes the flag from libcurl's progress + * callback and from its poll loop and unwinds the transfer on the thread + * that owns it. The terminal result stays Aborted because WasAborted() + * wins over whatever CURLcode the unwind produces. */ void Abort() { - isAborted = true; - if (curl!=nullptr) - { - // Simply close the socket - connection reset by peer.. Ha-ha-ha-ha-ha! - if (sockfd) { - ::close(sockfd); - sockfd = 0; - } - } + isAborted.store(true, std::memory_order_release); } CURL *GetHandle() @@ -518,23 +732,29 @@ class CurlHttpOperation { protected: const bool rawResponse; // Do not split response headers from response body - const size_t httpConnTimeout; // Timeout for connect. Default: 5s + const long httpConnTimeout; // Timeout for connect. Default: 5s CURL *curl; // Local curl instance - long res = CURLE_OK; // Curl result OR HTTP status code if successful - + CURLcode m_transportError = CURLE_OK; + CURLcode m_setupError = CURLE_OK; + long m_httpStatusCode = 0; + IHttpResponseCallback* m_callback = nullptr; // Request values std::string m_method; std::string m_url; std::string m_sslCaInfo; - bool m_isConfigured = false; - // The SDK upload path keeps the owning IHttpRequest alive through the - // callback context until Send() completes; copying this body would duplicate - // every upload payload. Unlike CURLOPT_CAINFO, the body pointer is set and - // consumed during Send(), not retained from construction. - const std::vector& requestBody; + CallbackHooks m_callbackHooks; + // Deferred creation-event bookkeeping (see the deferCreationEvent ctor arg + // and DispatchDeferredCreationEvent). m_deferCreationEvent is fixed at + // construction; the pending fields are only touched on the caller thread + // before the worker exists, so they need no synchronization. + bool m_deferCreationEvent; + bool m_hasPendingCreationEvent {false}; + HttpStateEvent m_pendingCreationEvent {OnCreated}; + // Own the payload so operation lifetime is independent of CurlHttpRequest. + std::vector m_requestBody; struct curl_slist *m_headersChunk = nullptr; // Processed response headers and body @@ -542,7 +762,9 @@ class CurlHttpOperation { std::vector respBody; // Socket parameters - curl_socket_t sockfd = 0; + // Owned exclusively by the worker thread; CURL_SOCKET_BAD is the "no + // socket" sentinel (0 is a valid descriptor number). + curl_socket_t sockfd = CURL_SOCKET_BAD; curl_socket_t sockextr = CURL_SOCKET_BAD; @@ -550,40 +772,184 @@ class CurlHttpOperation { size_t sendlen = 0; // # bytes sent by client size_t acklen = 0; // # bytes ack by server - std::future result; + std::mutex m_workerStartMtx; + bool m_sendAttempted = false; + std::thread m_worker; + std::atomic m_destroyEventDispatched { false }; + + // Latched cancellation classification. Frozen once, at the very start of + // completion, before the OnDestroy state event can run. Only the + // cancellation outcome is latched -- transport/setup/status fields stay + // live -- because those are already final by completion, while isAborted is + // the one input an OnDestroy observer can still legally flip (when it + // cancels peers) after this transfer has already succeeded. + std::atomic m_outcomeFrozen { false }; + std::atomic m_frozenAborted { false }; + + // Snapshot the abort classification exactly once. After this returns, + // WasAborted() reports the latched value regardless of any later Abort(). + void FreezeOutcome() noexcept + { + if (!m_outcomeFrozen.load(std::memory_order_acquire)) + { + m_frozenAborted.store(isAborted.load(std::memory_order_acquire), std::memory_order_relaxed); + m_outcomeFrozen.store(true, std::memory_order_release); + } + } - template - bool SetOption(CURLoption option, TValue value) + // Dispatch the creation event immediately, or record it for later replay + // when the operation was constructed in deferred mode. + void EmitCreationEvent(HttpStateEvent type) { - const CURLcode optionResult = curl_easy_setopt(curl, option, value); - if (optionResult != CURLE_OK) + if (m_deferCreationEvent) + { + m_pendingCreationEvent = type; + m_hasPendingCreationEvent = true; + return; + } + DispatchEvent(type); + } + + void DispatchDestroyEvent() noexcept + { + if (!m_destroyEventDispatched.exchange(true, std::memory_order_acq_rel)) { - res = static_cast(optionResult); - TRACE("curl_easy_setopt(%d) failed: %s\n", - static_cast(option), curl_easy_strerror(optionResult)); + try + { + DispatchEvent(OnDestroy); + } + catch (...) + { + // State observers must not terminate the worker or destructor. + } + } + } + + void Complete(const std::function& callback) noexcept + { + // Latch the cancellation outcome before the OnDestroy event fires. The + // operation is still in the registry here, so an OnDestroy observer may + // reenter CancelAllRequests/CancelRequestAsync and Abort() this object; + // freezing first guarantees response mapping sees the outcome as it was + // when the transfer actually finished, not as a late cancel rewrote it. + FreezeOutcome(); + // Preserve the documented state event while m_callback is still valid. + // The completion callback can release the last owner, so this must remain + // the worker's final access to the operation. + DispatchDestroyEvent(); + try + { + if (callback != nullptr) + { + callback(*this); + } + } + catch (...) + { + // Match the old unobserved-future behavior at the thread boundary. + } + } + + template + bool SetOption(CURLoption option, T value) + { + if (curl == nullptr) + { + m_transportError = CURLE_FAILED_INIT; + m_setupError = CURLE_FAILED_INIT; return false; } - return true; + + const CURLcode optionResult = curl_easy_setopt(curl, option, value); + if (optionResult == CURLE_OK) + { + return true; + } + + LOG_WARN("curl_easy_setopt(%d) failed: %s", static_cast(option), curl_easy_strerror(optionResult)); + m_transportError = optionResult; + m_setupError = optionResult; + return false; } /** - * Helper routine to wait for data on socket + * Helper routine to wait for data on socket. + * + * Polls in short slices instead of one long sleep so a cancellation flagged + * on another thread is observed within a bounded delay, without anybody + * closing the descriptor the worker owns. * - * @param sockfd + * @param socket * @param for_recv * @param timeout_ms - * @return + * @return >0 when the socket is ready, 0 on timeout or cancellation, <0 on error */ - static int WaitOnSocket(curl_socket_t sockfd, int for_recv, long timeout_ms) + int WaitOnSocket(curl_socket_t socket, int for_recv, long timeout_ms) { - struct pollfd pfd; - pfd.fd = sockfd; - pfd.events = for_recv ? POLLIN : POLLOUT; // Cap timeout to max int value to avoid overflow in poll() - auto timeout = std::min(timeout_ms, static_cast(std::numeric_limits::max())); - return poll(&pfd, 1, static_cast(timeout)); + long remaining = std::min(std::max(timeout_ms, 0L), static_cast(std::numeric_limits::max())); + constexpr long sliceMs = 100; + for (;;) + { + if (isAborted.load(std::memory_order_acquire)) + { + return 0; + } + + const long slice = std::min(remaining, sliceMs); + struct pollfd pfd; + pfd.fd = socket; + pfd.events = for_recv ? POLLIN : POLLOUT; + pfd.revents = 0; + const int pollResult = poll(&pfd, 1, static_cast(slice)); + if (pollResult != 0) + { + // Ready, or a poll() error. Both are terminal, exactly as the + // single-shot poll() this replaced. + return pollResult; + } + if (remaining <= slice) + { + return 0; // timed out + } + remaining -= slice; + } } + /** + * Install the libcurl progress callback used to abort a transfer. + * + * XFERINFO supersedes PROGRESSFUNCTION in libcurl 7.32.0; keep the old + * option for builds pinned to an older libcurl. + */ + bool SetAbortProgressOption() + { +#if LIBCURL_VERSION_NUM >= 0x072000 // Version 7.32.0 + return SetOption(CURLOPT_XFERINFOFUNCTION, &XferInfoAbortCallback) && + SetOption(CURLOPT_XFERINFODATA, static_cast(this)); +#else + return SetOption(CURLOPT_PROGRESSFUNCTION, &ProgressAbortCallback) && + SetOption(CURLOPT_PROGRESSDATA, static_cast(this)); +#endif + } + +#if LIBCURL_VERSION_NUM >= 0x072000 // Version 7.32.0 + static int XferInfoAbortCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) noexcept + { + const auto* operation = static_cast(clientp); + // Returning non-zero makes libcurl fail the transfer with + // CURLE_ABORTED_BY_CALLBACK, on the worker thread, with the socket and + // the easy handle still owned by their owner. + return (operation != nullptr && operation->isAborted.load(std::memory_order_acquire)) ? 1 : 0; + } +#else + static int ProgressAbortCallback(void* clientp, double, double, double, double) noexcept + { + const auto* operation = static_cast(clientp); + return (operation != nullptr && operation->isAborted.load(std::memory_order_acquire)) ? 1 : 0; + } +#endif + // SECURITY: upper bound on the collector response the client will buffer. The // OneCollector protocol responses (status, kill-switch tokens, retry-after, small // config) are tiny, so this generous cap never rejects a legitimate response but @@ -607,14 +973,14 @@ class CurlHttpOperation { * @param userp * @return */ - static size_t WriteMemoryCallback(char *contents, size_t size, size_t nmemb, void *userp) + static size_t WriteMemoryCallback(char* contents, size_t size, size_t nmemb, void* userp) { // Guard the size * nmemb product against size_t overflow before using it. if (nmemb != 0 && size > static_cast(-1) / nmemb) { return 0; } size_t realsize = size * nmemb; - struct MemoryStruct *mem = (struct MemoryStruct *)userp; + auto* mem = static_cast(userp); // SECURITY: bound the buffered response (see kMaxResponseBytes). Compare // overflow-safely (mem->size is always <= kMaxResponseBytes here). Returning a @@ -651,7 +1017,7 @@ class CurlHttpOperation { * @param data * @return */ - static size_t WriteVectorCallback(char *ptr, size_t size, size_t nmemb, void* userp) + static size_t WriteVectorCallback(char* ptr, size_t size, size_t nmemb, void* userp) { // Guard the size * nmemb product against size_t overflow before using it. if (nmemb != 0 && size > static_cast(-1) / nmemb) { diff --git a/lib/http/HttpClient_WinHttp.cpp b/lib/http/HttpClient_WinHttp.cpp new file mode 100644 index 000000000..f5383a808 --- /dev/null +++ b/lib/http/HttpClient_WinHttp.cpp @@ -0,0 +1,1651 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#include "mat/config.h" + +#ifdef HAVE_MAT_DEFAULT_HTTP_CLIENT +#include "HttpClient_WinHttp.hpp" +#include "utils/StringConversion.hpp" +#include "utils/StringUtils.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "crypt32.lib") +#pragma comment(lib, "winhttp.lib") + +namespace MAT_NS_BEGIN { + +namespace { + +constexpr DWORD DEFAULT_MAX_CONNECTIONS_PER_SERVER = 4; + +void setConnectionLimits(HINTERNET session, DWORD maxConnections) noexcept +{ + if (session == nullptr) + { + return; + } + + if (!::WinHttpSetOption(session, WINHTTP_OPTION_MAX_CONNS_PER_SERVER, + &maxConnections, sizeof(maxConnections))) + { + LOG_WARN("WinHttpSetOption(MAX_CONNS_PER_SERVER) failed: %d", ::GetLastError()); + } + if (!::WinHttpSetOption(session, WINHTTP_OPTION_MAX_CONNS_PER_1_0_SERVER, + &maxConnections, sizeof(maxConnections))) + { + LOG_WARN("WinHttpSetOption(MAX_CONNS_PER_1_0_SERVER) failed: %d", ::GetLastError()); + } +} + +} // namespace + +class WinHttpRequestWrapper; + +struct WinHttpClientState +{ + explicit WinHttpClientState(HINTERNET sessionHandle); + ~WinHttpClientState(); + + bool registerRequest( + std::string const& id, + std::shared_ptr request); + void eraseRequest(std::string const& id); + void stopAcceptingRequests(); + void beginCallback(); + void beginCallbackLocked(); + void endCallback(); + + HINTERNET session; + std::mutex requestsMutex; + std::map> requests; + std::condition_variable requestsCv; + std::atomic msRootCheck {false}; + bool acceptingRequests {true}; + size_t cancelAllDepth {0}; + size_t registryGeneration {0}; + size_t callbackGeneration {0}; + size_t callbacksInFlight {0}; + std::map callbacksByThread; +}; + +struct WinHttpCallbackAlreadyStarted +{ +}; + +class WinHttpCallbackScope +{ + public: + explicit WinHttpCallbackScope(std::shared_ptr state) + : m_state(std::move(state)) + { + m_state->beginCallback(); + } + + WinHttpCallbackScope( + std::shared_ptr state, + WinHttpCallbackAlreadyStarted) + : m_state(std::move(state)) + { + } + + ~WinHttpCallbackScope() + { + m_state->endCallback(); + } + + WinHttpCallbackScope(WinHttpCallbackScope const&) = delete; + WinHttpCallbackScope& operator=(WinHttpCallbackScope const&) = delete; + + private: + std::shared_ptr m_state; +}; + +// Ownership of the WinHTTP status-callback context. +// +// WinHTTP keeps the context value associated with a request handle until that +// handle is torn down, and documents WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING as +// the final callback for the handle ("There will be no more callbacks for this +// handle"). The context therefore holds a *strong* reference to the wrapper: +// every buffer WinHTTP was handed lives inside (or is kept alive by) that +// wrapper, so it stays valid for exactly as long as WinHTTP can still touch it. +// The reference is released only from the HANDLE_CLOSING callback, which also +// deletes the context. +struct WinHttpCallbackContext +{ + explicit WinHttpCallbackContext(std::shared_ptr request) + : request(std::move(request)) + { + } + + std::shared_ptr request; +}; + +class WinHttpRequestWrapper : public std::enable_shared_from_this +{ + protected: + // The step the WinHTTP state machine should take next. Operations are never + // issued directly from a completion callback; see schedule()/runPump(). + enum class NextOperation + { + None, + WriteBody, + ReceiveResponse, + QueryDataAvailable, + ReadData, + Complete + }; + + std::shared_ptr m_clientState; + std::string m_id; + IHttpResponseCallback* m_appCallback {nullptr}; + HINTERNET m_hConnect {nullptr}; + HINTERNET m_hRequest {nullptr}; + SimpleHttpRequest* m_request; + std::vector m_bodyBuffer; + // Fixed response read buffer. WinHttpReadData keeps the pointer until the + // read completes, so the buffer must never move for the life of the + // request; sizing it once up front also keeps the number of read + // completions needed to drain a response low (see MAX_HTTP_RESPONSE_SIZE, + // which still bounds the total that is buffered). + uint8_t m_readBuffer[8192] {0}; + size_t m_bodyWritten {0}; + std::atomic isCallbackCalled {false}; + bool isAborted {false}; + bool m_isHttps {false}; + bool m_msRootCheckRequired {false}; + std::atomic m_msRootCheckCompleted {false}; + bool m_contextInstalled {false}; + bool m_sendIssued {false}; + bool m_handleCallInProgress {false}; + bool m_closeRequestAfterCall {false}; + unsigned m_stateCallbackDepth {0}; + std::map m_stateCallbacksByThread; + bool m_stateCompletionPending {false}; + DWORD m_stateCompletionError {ERROR_SUCCESS}; + // Reason recorded by an abort that must let WinHTTP report the terminal + // callback itself instead of completing inline. + std::atomic m_deferredError {ERROR_SUCCESS}; + + // requestsMutex may nest this mutex only while the initial send claims or + // releases the pump. Code holding m_pumpMutex must release it before any + // operation that acquires requestsMutex. + std::mutex m_pumpMutex; + bool m_pumpActive {false}; + NextOperation m_nextOperation {NextOperation::None}; + DWORD m_completionError {ERROR_SUCCESS}; + + public: + WinHttpRequestWrapper( + std::shared_ptr clientState, + SimpleHttpRequest* request) + : m_clientState(std::move(clientState)), + m_id(request->GetId()), + m_request(request) + { + LOG_TRACE("%p WinHttpRequestWrapper()", this); + } + + WinHttpRequestWrapper(WinHttpRequestWrapper const&) = delete; + WinHttpRequestWrapper& operator=(WinHttpRequestWrapper const&) = delete; + + // The caller must hold m_clientState->requestsMutex. + bool hasStateCallbackOnThreadLocked(std::thread::id threadId) const + { + return m_stateCallbacksByThread.find(threadId) != + m_stateCallbacksByThread.end(); + } + + // The caller must hold m_clientState->requestsMutex. + bool hasActiveStateCallbackLocked() const + { + return m_stateCallbackDepth != 0; + } + + ~WinHttpRequestWrapper() noexcept + { + LOG_TRACE("%p ~WinHttpRequestWrapper()", this); + // Both completion and cancellation close the request handle explicitly: + // while WinHTTP owns the callback context it also owns a strong + // reference to this object, so the destructor can never be what closes + // that handle. Anything still open here belongs to a request that + // failed before WinHTTP took ownership of the context. + if (m_hRequest != nullptr) + { + ::WinHttpCloseHandle(m_hRequest); + } + if (m_hConnect != nullptr) + { + ::WinHttpCloseHandle(m_hConnect); + } + } + + /// + /// Asynchronously cancel pending request. + /// + /// Unlike WinInet's InternetCloseHandle, WinHttpCloseHandle on a request + /// with a pending async operation blocks the calling thread until that + /// operation's completion callback has finished running -- and that + /// callback runs on a *different* WinHTTP-internal thread. Holding + /// m_clientState->requestsMutex across the call (WinInet's pattern, safe there + /// because its callback runs synchronously on the calling thread) would + /// deadlock here: this thread would block inside WinHttpCloseHandle holding + /// the lock, while the completion callback blocks on the same thread's + /// erase() needing that same lock. So the handle is captured and closed + /// without holding the lock. This wrapper is only reachable through a + /// shared_ptr (see WinHttpClientState::requests / CancelRequestAsync), so + /// releasing the lock here cannot race with the object being freed -- + /// the caller already holds its own shared_ptr keeping *this* alive. + /// + void cancel() + { + abortRequest(ERROR_WINHTTP_OPERATION_CANCELLED); + } + + /// + /// Tears the request down and records why, without delivering the terminal + /// response from this call. + /// + /// WinHttpSendRequest documents that buffers handed to WinHTTP must stay + /// valid until an aborted operation reports + /// WINHTTP_CALLBACK_STATUS_REQUEST_ERROR with ERROR_WINHTTP_OPERATION_CANCELLED, + /// and invoking OnHttpResponse() is precisely what lets the caller destroy + /// the request object those buffers live in. Synthesizing the response as + /// soon as WinHttpCloseHandle returns would assume a teardown ordering + /// WinHTTP does not guarantee, so instead the handle is closed and the + /// response is delivered from the resulting REQUEST_ERROR callback -- or + /// from HANDLE_CLOSING, which WinHTTP always delivers last. + /// + void abortRequest(DWORD dwError, bool calledFromWinHttpCallback = false) + { + HINTERNET hRequestToClose = nullptr; + bool completeHere = false; + { + std::lock_guard lock(m_clientState->requestsMutex); + if (isCallbackCalled) + { + return; + } + isAborted = true; + DWORD noError = ERROR_SUCCESS; + m_deferredError.compare_exchange_strong(noError, dwError); + if (m_handleCallInProgress && !calledFromWinHttpCallback) + { + // WinHTTP forbids another thread from closing an asynchronous + // handle while this thread is inside WinHttpSendRequest or + // WinHttpWriteData. Record the cancellation and let that API + // frame close the handle as soon as its call returns. + m_closeRequestAfterCall = true; + return; + } + hRequestToClose = m_hRequest; + m_hRequest = nullptr; + // Without an installed callback context WinHTTP has no way to + // report HANDLE_CLOSING back to this object, so nothing else would + // ever complete the request. And until WinHttpSendRequest has been + // issued WinHTTP holds none of this request's buffers, so there is + // nothing to wait for. Both states may be completed inline. + completeHere = !m_contextInstalled || !m_sendIssued; + } + if (hRequestToClose != nullptr) + { + ::WinHttpCloseHandle(hRequestToClose); + } + if (completeHere) + { + onRequestComplete(dwError); + } + } + + /// + /// Verify that the server end-point certificate is MS-Rooted. + /// Unlike WinInet's INTERNET_OPTION_SERVER_CERT_CHAIN_CONTEXT (which hands + /// back a ready-made chain), WinHttpQueryOption only returns the leaf server + /// certificate context, so the chain must be built explicitly before running + /// the same CERT_CHAIN_POLICY_MICROSOFT_ROOT policy check WinInet performs. + /// + bool isMsRootCert(HINTERNET hRequest) + { + PCCERT_CONTEXT pCertContext = nullptr; + DWORD dwSize = sizeof(pCertContext); + if (!::WinHttpQueryOption(hRequest, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &pCertContext, &dwSize)) + { + LOG_WARN("WinHttpQueryOption(SERVER_CERT_CONTEXT) failed: %d", ::GetLastError()); + return false; + } + + bool result = true; + PCCERT_CHAIN_CONTEXT pChainCtx = nullptr; + CERT_CHAIN_PARA chainPara = { sizeof(chainPara) }; + if (::CertGetCertificateChain(NULL, pCertContext, NULL, pCertContext->hCertStore, &chainPara, 0, NULL, &pChainCtx)) + { + CERT_CHAIN_POLICY_STATUS pps = { 0, 0, 0, 0, nullptr }; + pps.cbSize = sizeof(pps); + // Verify that the cert chain roots up to the Microsoft application root at top level + CERT_CHAIN_POLICY_PARA policyPara = { 0, 0, nullptr }; + policyPara.cbSize = sizeof(policyPara); + policyPara.dwFlags = MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; + policyPara.pvExtraPolicyPara = nullptr; + + BOOL policyChecked = ::CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_MICROSOFT_ROOT, pChainCtx, &policyPara, &pps); + if (!policyChecked) + { + LOG_WARN("CertVerifyCertificateChainPolicy() failed: unable to verify"); + result = false; + } + else if (pps.dwError != ERROR_SUCCESS) + { + LOG_WARN("CertVerifyCertificateChainPolicy() failed: invalid root CA - %d", pps.dwError); + result = false; + } + ::CertFreeCertificateChain(pChainCtx); + } + else + { + LOG_WARN("CertGetCertificateChain() failed: %d", ::GetLastError()); + result = false; + } + ::CertFreeCertificateContext(pCertContext); + return result; + } + + HINTERNET getRequestHandle() + { + std::lock_guard lock(m_clientState->requestsMutex); + return m_hRequest; + } + + // Keep each WinHTTP operation and the handle check under the same lock as + // cancellation. WinHttpCloseHandle remains outside the lock because it + // waits for callbacks that may need this mutex. + DWORD receiveResponse() + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_hRequest == nullptr) + { + return ERROR_WINHTTP_OPERATION_CANCELLED; + } + if (!::WinHttpReceiveResponse(m_hRequest, NULL)) + { + return ::GetLastError(); + } + return ERROR_SUCCESS; + } + + DWORD queryDataAvailable() + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_hRequest == nullptr) + { + return ERROR_WINHTTP_OPERATION_CANCELLED; + } + if (!::WinHttpQueryDataAvailable(m_hRequest, NULL)) + { + return ::GetLastError(); + } + return ERROR_SUCCESS; + } + + DWORD readData() + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_hRequest == nullptr) + { + return ERROR_WINHTTP_OPERATION_CANCELLED; + } + if (!::WinHttpReadData(m_hRequest, m_readBuffer, + static_cast(sizeof(m_readBuffer)), NULL)) + { + return ::GetLastError(); + } + return ERROR_SUCCESS; + } + + // Hands the remaining request body to WinHTTP. The body is deliberately not + // passed as WinHttpSendRequest's lpOptional: that buffer belongs to the + // caller's request object and WinHTTP may hold it until the request handle + // is closed, whereas WinHttpWriteData releases it at WRITE_COMPLETE. + DWORD writeBody() + { + HINTERNET request = nullptr; + const void* body = nullptr; + DWORD bodySize = 0; + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_hRequest == nullptr) + { + return ERROR_WINHTTP_OPERATION_CANCELLED; + } + size_t remaining = m_request->m_body.size() - m_bodyWritten; + request = m_hRequest; + body = m_request->m_body.data() + m_bodyWritten; + bodySize = static_cast(remaining); + m_handleCallInProgress = true; + } + + BOOL result = ::WinHttpWriteData(request, body, bodySize, NULL); + DWORD error = result ? ERROR_SUCCESS : ::GetLastError(); + + HINTERNET cancelledRequest = nullptr; + { + std::lock_guard lock(m_clientState->requestsMutex); + m_handleCallInProgress = false; + if (m_closeRequestAfterCall) + { + m_closeRequestAfterCall = false; + cancelledRequest = m_hRequest; + m_hRequest = nullptr; + } + } + if (cancelledRequest != nullptr) + { + ::WinHttpCloseHandle(cancelledRequest); + } + return error; + } + + DWORD validateCurrentRequestMsRootCert() + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_hRequest == nullptr) + { + return ERROR_WINHTTP_OPERATION_CANCELLED; + } + return isMsRootCert(m_hRequest) ? ERROR_SUCCESS : ERROR_WINHTTP_SECURE_INVALID_CERT; + } + + // Detaches and closes the request handle. WinHttpCloseHandle can block + // until an in-flight callback returns, and that callback may need + // m_clientState->requestsMutex, so the handle is detached under the lock and + // closed without it. + void closeRequestHandle() + { + HINTERNET hRequestToClose = nullptr; + { + std::lock_guard lock(m_clientState->requestsMutex); + hRequestToClose = m_hRequest; + m_hRequest = nullptr; + } + if (hRequestToClose != nullptr) + { + ::WinHttpCloseHandle(hRequestToClose); + } + } + + // Queues the next step of the WinHTTP state machine. + // + // WinHTTP is explicitly allowed to complete an operation synchronously and + // re-enter this object's status callback on the calling thread ("reentered + // on the same thread for the current request"). Issuing the next WinHTTP + // call straight from a completion would then nest a pair of stack frames + // per response chunk -- unbounded for a large response -- and would also + // re-enter m_clientState->requestsMutex, which is not recursive. So only the + // outermost frame ever issues operations: a nested completion records what + // should happen next and returns, and runPump() picks it up once the + // WinHTTP call it was nested inside has returned. + void schedule(NextOperation next, DWORD completionError = ERROR_SUCCESS) + { + { + std::lock_guard lock(m_pumpMutex); + if (m_nextOperation == NextOperation::Complete && next != NextOperation::Complete) + { + // A terminal result is already queued; nothing may displace it. + return; + } + m_nextOperation = next; + m_completionError = completionError; + if (m_pumpActive) + { + return; + } + m_pumpActive = true; + } + runPump(); + } + + // Issues queued operations until WinHTTP takes one asynchronously. The + // caller must already own the pump (m_pumpActive set) and must not hold + // m_clientState->requestsMutex. + void runPump() + { + for (;;) + { + NextOperation current = NextOperation::None; + DWORD completionError = ERROR_SUCCESS; + { + std::lock_guard lock(m_pumpMutex); + current = m_nextOperation; + completionError = m_completionError; + m_nextOperation = NextOperation::None; + if (current == NextOperation::None || isCallbackCalled) + { + m_pumpActive = false; + return; + } + if (current == NextOperation::Complete) + { + m_pumpActive = false; + } + } + + if (current == NextOperation::Complete) + { + onRequestComplete(completionError); + return; + } + + DWORD dwError = issueOperation(current); + if (dwError == ERROR_SUCCESS) + { + continue; + } + + { + std::lock_guard lock(m_pumpMutex); + m_nextOperation = NextOperation::None; + m_pumpActive = false; + } + if (current == NextOperation::WriteBody) + { + // A synchronous WinHttpWriteData failure leaves no documented + // way to prove WinHTTP has let go of the caller's body buffer, + // so let the handle's final callback deliver the response. + abortRequest(dwError); + } + else + { + onRequestComplete(dwError); + } + return; + } + } + + DWORD issueOperation(NextOperation operation) + { + switch (operation) + { + case NextOperation::WriteBody: + return writeBody(); + + case NextOperation::ReceiveResponse: + return receiveResponse(); + + case NextOperation::QueryDataAvailable: + return queryDataAvailable(); + + case NextOperation::ReadData: + return readData(); + + default: + return ERROR_SUCCESS; + } + } + + void DispatchEvent(std::unique_lock& lock, HttpStateEvent type) + { + if (m_appCallback != nullptr && !isCallbackCalled) + { + void* handle = static_cast(m_hRequest); + IHttpResponseCallback* callback = m_appCallback; + auto state = m_clientState; + ++m_stateCallbackDepth; + ++m_stateCallbacksByThread[std::this_thread::get_id()]; + state->beginCallbackLocked(); + lock.unlock(); + { + WinHttpCallbackScope callbackScope( + state, WinHttpCallbackAlreadyStarted {}); + callback->OnHttpStateEvent(type, handle, 0); + } + + bool complete = false; + DWORD completionError = ERROR_SUCCESS; + { + lock.lock(); + assert(m_stateCallbackDepth != 0); + --m_stateCallbackDepth; + auto stateCallback = m_stateCallbacksByThread.find( + std::this_thread::get_id()); + assert(stateCallback != m_stateCallbacksByThread.end()); + if (stateCallback != m_stateCallbacksByThread.end() && + --stateCallback->second == 0) + { + m_stateCallbacksByThread.erase(stateCallback); + } + if (m_stateCallbackDepth == 0 && m_stateCompletionPending) + { + complete = true; + completionError = m_stateCompletionError; + m_stateCompletionPending = false; + m_stateCompletionError = ERROR_SUCCESS; + } + } + if (complete) + { + // Terminal delivery may free the application callback. Leave the + // setup lock released, matching the existing DispatchEvent + // contract when a state callback synchronously completes. + lock.unlock(); + onRequestComplete(completionError); + } + } + } + + // Asynchronously send HTTP request and invoke response callback. + // Ownership semantics: send(...) method self-destroys *this* upon + // reaching the terminal WinHTTP callback. There must be absolutely no + // methods that attempt to use the object after triggering send on it. + // Send operation on request may be issued no more than once. + // + // Handle setup runs under m_clientState->requestsMutex. State callbacks are the + // deliberate exception: DispatchEvent releases the lock while invoking + // application code, then setup checks cancellation before continuing. + // + // DEADLOCK NOTE: the lock must NOT still be held when a synchronous + // failure completes the request. onRequestComplete() invokes the + // application callback, which is documented (below) to be able to tear the + // client down synchronously -- that reaches CancelAllRequests(), which + // waits on the shared state's condition variable. DispatchEvent releases this lock + // around application state callbacks. If a callback completes the request, + // it leaves the lock released and sendLocked() returns without touching the + // client again; otherwise it reacquires the lock before setup continues. + void send(IHttpResponseCallback* callback) + { + m_appCallback = callback; + std::shared_ptr keepAlive = shared_from_this(); + if (!m_clientState->registerRequest(m_id, keepAlive)) + { + onRequestComplete(ERROR_WINHTTP_OPERATION_CANCELLED); + return; + } + + bool failed = false; + DWORD dwError = ERROR_SUCCESS; + { + std::unique_lock lock(m_clientState->requestsMutex); + failed = !sendLocked(lock, dwError); + } + if (failed) + { + onRequestComplete(dwError); + return; + } + // sendLocked() claimed the pump before calling WinHttpSendRequest, so a + // completion WinHTTP delivered synchronously on this thread could only + // park the next step instead of issuing it while the setup lock was + // still held. Run whatever it parked now that the lock is gone. + runPump(); + } + + // Returns true if the request was handed off to WinHTTP asynchronously. + // Returns false on synchronous failure, setting dwError to the result the + // caller must complete the request with (once the lock has been dropped). + bool sendLocked(std::unique_lock& lock, DWORD& dwErrorOut) + { + if (isCallbackCalled || isAborted) + { + // Request force-aborted before creating a WinHTTP handle. + if (!isCallbackCalled) + { + DispatchEvent(lock, OnConnectFailed); + } + dwErrorOut = ERROR_WINHTTP_OPERATION_CANCELLED; + return false; + } + + DispatchEvent(lock, OnConnecting); + if (isCallbackCalled || isAborted) + { + dwErrorOut = ERROR_WINHTTP_OPERATION_CANCELLED; + return false; + } + + std::wstring wUrl = to_utf16_string(m_request->m_url); + URL_COMPONENTS urlc; + memset(&urlc, 0, sizeof(urlc)); + urlc.dwStructSize = sizeof(urlc); + urlc.dwHostNameLength = static_cast(-1); + urlc.dwUrlPathLength = static_cast(-1); + urlc.dwExtraInfoLength = static_cast(-1); + if (!::WinHttpCrackUrl(wUrl.c_str(), static_cast(wUrl.size()), 0, &urlc)) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpCrackUrl() failed: dwError=%d url=%s", dwError, m_request->m_url.c_str()); + // Invalid URL passed to WinHTTP API + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + std::wstring hostname(urlc.lpszHostName, urlc.dwHostNameLength); + std::wstring objectName(urlc.lpszUrlPath, urlc.dwUrlPathLength); + if (urlc.lpszExtraInfo != nullptr) + { + std::wstring extraInfo(urlc.lpszExtraInfo, urlc.dwExtraInfoLength); + const auto fragment = extraInfo.find(L'#'); + objectName.append(extraInfo, 0, fragment); + } + + if (m_clientState->session == nullptr) + { + LOG_WARN("WinHttpOpen() did not produce a usable session handle"); + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = ERROR_WINHTTP_CANNOT_CONNECT; + return false; + } + + // TODO: connect handle for the same target should be cached across + // requests to enable keep-alive (same pre-existing opportunity noted + // in HttpClient_WinInet.cpp; out of scope for this transport swap). + m_hConnect = ::WinHttpConnect(m_clientState->session, hostname.c_str(), urlc.nPort, 0); + if (m_hConnect == nullptr) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpConnect() failed: %d", dwError); + // Cannot connect to host + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + + std::wstring wMethod = to_utf16_string(m_request->m_method); + m_isHttps = (urlc.nScheme == INTERNET_SCHEME_HTTPS); + // Latch the policy for this request: the callbacks that enforce it run + // long after send() returns, and the setting can be changed at any time. + m_msRootCheckRequired = + m_clientState->msRootCheck.load(std::memory_order_acquire); + m_hRequest = ::WinHttpOpenRequest( + m_hConnect, wMethod.c_str(), objectName.c_str(), NULL, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_REFRESH | (m_isHttps ? WINHTTP_FLAG_SECURE : 0)); + if (m_hRequest == nullptr) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpOpenRequest() failed: %d", dwError); + // Request cannot be opened to given URL because of some connectivity issue + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + + // Match the WinInet transport's INTERNET_FLAG_NO_AUTH and + // INTERNET_FLAG_NO_COOKIES behavior. Telemetry requests must not answer + // authentication challenges with ambient credentials or retain + // collector-controlled cookies across requests in the shared session. + DWORD disableFeatures = + WINHTTP_DISABLE_AUTHENTICATION | WINHTTP_DISABLE_COOKIES; + if (m_msRootCheckRequired) + { + // Automatic redirects would move the request to a new TLS peer + // after the original certificate check, potentially forwarding + // telemetry credentials to a non-Microsoft-root endpoint. + disableFeatures |= WINHTTP_DISABLE_REDIRECTS; + } + if (!::WinHttpSetOption( + m_hRequest, WINHTTP_OPTION_DISABLE_FEATURE, &disableFeatures, sizeof(disableFeatures))) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpSetOption(DISABLE_FEATURE) failed: %d", dwError); + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + + // WinHTTP never shows UI, so INTERNET_FLAG_NO_UI has no equivalent. + + // WinHttpSetStatusCallback returns the PREVIOUS callback function + // pointer (typically NULL here, since this is the first registration + // on a freshly opened request handle) -- not a BOOL -- and signals + // failure only via the distinct WINHTTP_INVALID_STATUS_CALLBACK + // sentinel. Treating a null "previous callback" as failure would + // reject every request immediately after this call. + if (::WinHttpSetStatusCallback(m_hRequest, &WinHttpRequestWrapper::winHttpCallback, + WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | + WINHTTP_CALLBACK_FLAG_HANDLES | + WINHTTP_CALLBACK_FLAG_SEND_REQUEST, + 0) == WINHTTP_INVALID_STATUS_CALLBACK) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpSetStatusCallback() failed: %d", dwError); + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + + // Install the callback context explicitly, before anything else can + // fail. Relying on WinHttpSendRequest's dwContext instead would strand + // the context (and the strong reference it holds) whenever the send + // fails before WinHTTP records it -- WinHTTP would then report + // HANDLE_CLOSING with a zero context and nothing would free it. Once + // the option is set, the handle owns the context and HANDLE_CLOSING is + // guaranteed to hand it back. Until then unique_ptr owns it, so no path + // out of this function can leak it. + std::unique_ptr context(new WinHttpCallbackContext(shared_from_this())); + DWORD_PTR contextValue = reinterpret_cast(context.get()); + if (!::WinHttpSetOption( + m_hRequest, WINHTTP_OPTION_CONTEXT_VALUE, &contextValue, sizeof(contextValue))) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpSetOption(CONTEXT_VALUE) failed: %d", dwError); + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + context.release(); + m_contextInstalled = true; + + std::ostringstream os; + for (auto const& header : m_request->m_headers) { + os << header.first << ": " << header.second << "\r\n"; + } + std::wstring wHeaders = to_utf16_string(os.str()); + + if (!wHeaders.empty() && + wHeaders.size() > static_cast(std::numeric_limits::max())) + { + LOG_WARN("Request headers exceed WinHTTP's maximum size"); + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = ERROR_INVALID_PARAMETER; + return false; + } + if (!wHeaders.empty() && + !::WinHttpAddRequestHeaders(m_hRequest, wHeaders.c_str(), static_cast(wHeaders.size()), + WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE)) + { + DWORD dwError = ::GetLastError(); + LOG_WARN("WinHttpAddRequestHeaders() failed: %d", dwError); + // Unable to add request headers. There's no point in proceeding with upload because + // our server is expecting those custom request headers to always be there. + DispatchEvent(lock, OnConnectFailed); + dwErrorOut = dwError; + return false; + } + + // Try to send headers and request body to server + DispatchEvent(lock, OnSending); + if (isCallbackCalled || isAborted) + { + dwErrorOut = ERROR_WINHTTP_OPERATION_CANCELLED; + return false; + } + if (m_request->m_body.size() > static_cast(std::numeric_limits::max())) + { + LOG_WARN("Request body exceeds WinHTTP's maximum size"); + DispatchEvent(lock, OnSendFailed); + dwErrorOut = ERROR_INVALID_PARAMETER; + return false; + } + if (m_hRequest == nullptr) + { + dwErrorOut = ERROR_WINHTTP_OPERATION_CANCELLED; + return false; + } + // Send the headers only. dwTotalLength still declares Content-Length, so + // the server sees the same request; the body follows via + // WinHttpWriteData. The SENDING_REQUEST callback validates the negotiated + // certificate before WinHTTP commits these headers to the wire. + DWORD totalLength = static_cast(m_request->m_body.size()); + // Claim the pump so that a completion WinHTTP may deliver synchronously + // on this thread parks its next step instead of issuing a WinHTTP call + // (and re-entering the shared-state mutex) while setup still holds the lock. + // send() releases the pump once the lock is gone. + { + std::lock_guard pumpLock(m_pumpMutex); + m_pumpActive = true; + m_nextOperation = NextOperation::None; + } + m_sendIssued = true; + m_handleCallInProgress = true; + HINTERNET hRequest = m_hRequest; + // SENDING_REQUEST may run synchronously from WinHttpSendRequest and must + // acquire requestsMutex to enforce the certificate policy. Keep the + // wrapper alive, but release the registry lock across the WinHTTP call. + lock.unlock(); + BOOL bResult = ::WinHttpSendRequest( + hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, totalLength, contextValue); + DWORD dwSendError = bResult ? ERROR_SUCCESS : ::GetLastError(); + lock.lock(); + m_handleCallInProgress = false; + HINTERNET cancelledRequest = nullptr; + if (m_closeRequestAfterCall) + { + m_closeRequestAfterCall = false; + cancelledRequest = m_hRequest; + m_hRequest = nullptr; + } + if (cancelledRequest != nullptr) + { + // Closing the handle may synchronously invoke a terminal callback, + // which acquires requestsMutex through onRequestComplete(). + lock.unlock(); + ::WinHttpCloseHandle(cancelledRequest); + lock.lock(); + } + if (!bResult) + { + DWORD dwError = m_deferredError.load(std::memory_order_acquire); + if (dwError == ERROR_SUCCESS) + { + dwError = dwSendError; + } + { + std::lock_guard pumpLock(m_pumpMutex); + m_pumpActive = false; + m_nextOperation = NextOperation::None; + } + // The send never started, so WinHTTP holds none of this request's + // buffers and cancellation may still complete inline. It does keep + // the context on the request handle and delivers HANDLE_CLOSING once + // onRequestComplete() closes that handle, which is what frees it. + m_sendIssued = false; + LOG_WARN("WinHttpSendRequest() failed: %d", dwError); + // Unable to send request + DispatchEvent(lock, OnSendFailed); + dwErrorOut = dwError; + return false; + } + // Async request has been queued; completion arrives via winHttpCallback. + return true; + } + + // Drives the WinHTTP async state machine: SendRequest -> (certificate + // policy) -> WriteData -> ReceiveResponse -> (QueryDataAvailable -> + // ReadData)* -> onRequestComplete. Unlike WinInet (whose async completions + // all report through the single INTERNET_STATUS_REQUEST_COMPLETE code, and + // whose synchronous API calls signal a pending async op via a FALSE return + // + GetLastError()==ERROR_IO_PENDING), WinHTTP has one distinct callback + // status per stage, and a FALSE return from any of these calls on an async + // handle is always a genuine synchronous failure -- never "pending". + // + // No stage issues the next WinHTTP call directly: everything goes through + // schedule(), so a completion WinHTTP delivers synchronously on the calling + // thread cannot nest another operation inside the one it is reporting. + static void CALLBACK winHttpCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) + { + UNREFERENCED_PARAMETER(hInternet); + + WinHttpCallbackContext* context = reinterpret_cast(dwContext); + if (context == nullptr) + { + return; + } + + if (dwInternetStatus == WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING) + { + // Documented as the final callback for this handle, so WinHTTP no + // longer references anything this request handed it. Release the + // context -- and with it the strong reference that has been keeping + // the wrapper (and its read buffer) alive -- but only after using it + // as the backstop that guarantees every request produces exactly one + // terminal response, including the cancellation paths that + // deliberately do not complete inline. + std::shared_ptr self = context->request; + delete context; + if (self != nullptr && !self->isCallbackCalled) + { + self->onRequestComplete(self->m_deferredError.exchange(ERROR_SUCCESS)); + } + return; + } + + std::shared_ptr self = context->request; + if (self == nullptr || self->isCallbackCalled) + { + // The terminal response has already been delivered; the request is + // no longer tracked by the client, which may since have been torn + // down. Nothing here may touch it again. + return; + } + + LOG_TRACE("winHttpCallback: hInternet %p, self %p, dwInternetStatus %u", hInternet, self.get(), dwInternetStatus); + + switch (dwInternetStatus) + { + case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: + // TLS is negotiated, but the request headers have not left the + // process. Enforce the configured Microsoft-root policy here so + // API keys and auth tickets are never disclosed to a server that + // only passes the platform's broader certificate policy. + if (self->m_isHttps && self->m_msRootCheckRequired && + !self->m_msRootCheckCompleted.exchange(true)) + { + DWORD dwError = self->validateCurrentRequestMsRootCert(); + if (dwError != ERROR_SUCCESS) + { + // WinHTTP permits closing a handle from its own status + // callback even while WinHttpSendRequest is active. Do + // that here so rejected credentials never leave the + // process; external cancellation uses the deferred path. + self->abortRequest(dwError, true); + } + } + return; + + case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: + self->schedule(self->m_request->m_body.empty() + ? NextOperation::ReceiveResponse + : NextOperation::WriteBody); + return; + + case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE: + { + // WinHTTP has released the caller's body buffer for the bytes it + // reports here. Short writes are not expected, but honour them + // rather than truncating the payload. + DWORD written = (lpvStatusInformation != nullptr) + ? *static_cast(lpvStatusInformation) : 0; + self->m_bodyWritten += written; + if (self->m_bodyWritten < self->m_request->m_body.size()) + { + if (written == 0) + { + self->schedule(NextOperation::Complete, ERROR_WINHTTP_CONNECTION_ERROR); + return; + } + self->schedule(NextOperation::WriteBody); + return; + } + self->schedule(NextOperation::ReceiveResponse); + return; + } + + case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: + // The certificate policy was already enforced before the + // request headers were transmitted. + self->schedule(NextOperation::QueryDataAvailable); + return; + + case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: + { + DWORD bytesAvailable = (lpvStatusInformation != nullptr) + ? *static_cast(lpvStatusInformation) : 0; + if (bytesAvailable == 0) + { + // No more data: response is complete. + self->schedule(NextOperation::Complete, ERROR_SUCCESS); + return; + } + // SECURITY: refuse an over-large response instead of buffering it + // (see MAX_HTTP_RESPONSE_SIZE) so a hostile/MITM'd collector cannot + // exhaust process memory. Checked before every read so the buffer + // never exceeds the cap; reported as an invalid server response -> + // NetworkFailure (retried). + if (self->m_bodyBuffer.size() > MAX_HTTP_RESPONSE_SIZE || + bytesAvailable > MAX_HTTP_RESPONSE_SIZE - self->m_bodyBuffer.size()) + { + LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE); + self->schedule(NextOperation::Complete, ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + return; + } + // readData() takes whatever fits in the fixed buffer; anything + // beyond that is reported again by the next QueryDataAvailable. + self->schedule(NextOperation::ReadData); + return; + } + + case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: + // dwStatusInformationLength is the number of bytes actually placed + // into the buffer passed to WinHttpReadData (may be less than the + // buffer size that was offered). + if (dwStatusInformationLength > sizeof(self->m_readBuffer) || + self->m_bodyBuffer.size() > MAX_HTTP_RESPONSE_SIZE || + dwStatusInformationLength > MAX_HTTP_RESPONSE_SIZE - self->m_bodyBuffer.size()) + { + self->schedule(NextOperation::Complete, ERROR_WINHTTP_INVALID_SERVER_RESPONSE); + return; + } + self->m_bodyBuffer.insert(self->m_bodyBuffer.end(), + self->m_readBuffer, self->m_readBuffer + dwStatusInformationLength); + self->schedule(NextOperation::QueryDataAvailable); + return; + + case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: + { + DWORD dwError = ERROR_WINHTTP_INTERNAL_ERROR; + if (lpvStatusInformation != nullptr && + dwStatusInformationLength >= sizeof(WINHTTP_ASYNC_RESULT)) + { + dwError = static_cast(lpvStatusInformation)->dwError; + } + // The operation that owned the buffers WinHTTP was given has + // finished failing, so the response may be handed back now. A + // locally recorded abort reason wins over WinHTTP's generic + // "operation cancelled". + DWORD deferred = self->m_deferredError.exchange(ERROR_SUCCESS); + self->schedule(NextOperation::Complete, (deferred != ERROR_SUCCESS) ? deferred : dwError); + return; + } + + default: + return; + } + } + + void onRequestComplete(DWORD dwError) + { + { + std::lock_guard lock(m_clientState->requestsMutex); + if (m_stateCallbackDepth != 0) + { + m_stateCompletionPending = true; + m_stateCompletionError = dwError; + return; + } + if (isCallbackCalled.exchange(true)) + { + return; + } + } + + std::unique_ptr response(new SimpleHttpResponse(m_id)); + // Closing the request handle below releases WinHTTP's callback context, + // and that context holds the strong reference that has been keeping + // this object alive. Hold one here so the rest of this method -- and + // the application callback it invokes -- cannot run on a freed object. + auto keepAlive = shared_from_this(); + HINTERNET request = getRequestHandle(); + if (dwError == ERROR_SUCCESS && request == nullptr) + { + dwError = ERROR_WINHTTP_OPERATION_CANCELLED; + } + bool const receivedResponse = dwError == ERROR_SUCCESS; + + if (dwError == ERROR_SUCCESS) { + response->m_body = std::move(m_bodyBuffer); + response->m_result = HttpResult_OK; + + DWORD statusCode = 0; + DWORD dwSize = sizeof(statusCode); + if (!::WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &dwSize, WINHTTP_NO_HEADER_INDEX)) + { + LOG_WARN("WinHttpQueryHeaders(STATUS_CODE) failed: %d", ::GetLastError()); + response->m_result = HttpResult_NetworkFailure; + } + response->m_statusCode = statusCode; + + // Raw headers, as "Name: Value\r\n..." pairs -- the same shape WinInet + // hands back via HTTP_QUERY_RAW_HEADERS_CRLF. + DWORD headerBytes = 0; + BOOL headersQueried = ::WinHttpQueryHeaders( + request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, + WINHTTP_NO_HEADER_INDEX); + DWORD headerErr = headersQueried ? ERROR_SUCCESS : ::GetLastError(); + if (!headersQueried && headerErr == ERROR_INSUFFICIENT_BUFFER && headerBytes > 0) + { + if (headerBytes % sizeof(wchar_t) != 0) + { + LOG_WARN("WinHttpQueryHeaders(RAW_HEADERS_CRLF) returned an invalid byte count: %lu", headerBytes); + } + else + { + std::wstring wHeaders(headerBytes / sizeof(wchar_t), L'\0'); + DWORD bufferBytes = headerBytes; + if (::WinHttpQueryHeaders( + request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, &wHeaders[0], &bufferBytes, + WINHTTP_NO_HEADER_INDEX)) + { + // WinHttpQueryHeaders includes the buffer's trailing NUL(s) in + // the byte count; trim at the first one before converting. + size_t nul = wHeaders.find(L'\0'); + if (nul != std::wstring::npos) + { + wHeaders.resize(nul); + } + parseHeaders(to_utf8_string(wHeaders), *response); + } + else + { + LOG_WARN("WinHttpQueryHeaders(RAW_HEADERS_CRLF) failed twice: %d", ::GetLastError()); + } + } + } + else if (!headersQueried) + { + LOG_WARN("WinHttpQueryHeaders(RAW_HEADERS_CRLF) failed: %d", headerErr); + } + } else { + switch (dwError) { + case ERROR_WINHTTP_OPERATION_CANCELLED: + response->m_result = HttpResult_Aborted; + break; + + case ERROR_WINHTTP_TIMEOUT: + case ERROR_WINHTTP_NAME_NOT_RESOLVED: + case ERROR_WINHTTP_CANNOT_CONNECT: + case ERROR_WINHTTP_CONNECTION_ERROR: + case ERROR_WINHTTP_RESEND_REQUEST: + case ERROR_WINHTTP_SECURE_CERT_DATE_INVALID: + case ERROR_WINHTTP_SECURE_CERT_CN_INVALID: + case ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED: + case ERROR_WINHTTP_SECURE_INVALID_CA: + case ERROR_WINHTTP_SECURE_CERT_REV_FAILED: + case ERROR_WINHTTP_SECURE_CHANNEL_ERROR: + case ERROR_WINHTTP_SECURE_INVALID_CERT: + case ERROR_WINHTTP_SECURE_CERT_REVOKED: + case ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE: + case ERROR_WINHTTP_SECURE_FAILURE: + case ERROR_WINHTTP_REDIRECT_FAILED: + case ERROR_WINHTTP_INVALID_SERVER_RESPONSE: + case ERROR_WINHTTP_RESPONSE_DRAIN_OVERFLOW: + response->m_result = HttpResult_NetworkFailure; + break; + + default: + response->m_result = HttpResult_LocalFailure; + break; + } + } + + { + auto state = m_clientState; + WinHttpCallbackScope callbackScope(state); + auto callback = m_appCallback; + auto requestId = m_id; + // Let go of the request handle before entering application code: + // OnHttpResponse() is what allows the caller to destroy the request + // object whose body buffer WinHTTP was given, so WinHTTP must be + // done with this request first. Closing it is also what triggers + // HANDLE_CLOSING, which releases the callback context. + closeRequestHandle(); + // Remove the request before entering application code. The callback + // can synchronously tear down the client and destroy this wrapper. + state->eraseRequest(requestId); + if (callback != nullptr) + { + // The implementation-specific handle is no longer valid once + // terminal delivery begins, so do not expose a stale handle. + if (receivedResponse) + { + callback->OnHttpStateEvent(OnResponse, nullptr, 0); + } + callback->OnHttpResponse(response.release()); + } + } + } + + private: + // Parses "Name: Value\r\n"-formatted raw headers (as returned by + // WINHTTP_QUERY_RAW_HEADERS_CRLF / HTTP_QUERY_RAW_HEADERS_CRLF) into an + // HttpHeaders map. Shared shape with HttpClient_WinInet's inline parser. + static void parseHeaders(std::string const& raw, SimpleHttpResponse& response) + { + size_t lineStart = 0; + while (lineStart < raw.size()) { + size_t lineEnd = raw.find("\r\n", lineStart); + if (lineEnd == std::string::npos) { + lineEnd = raw.size(); + } + + const std::string line = raw.substr(lineStart, lineEnd - lineStart); + const size_t colon = line.find(':'); + if (colon != std::string::npos) { + size_t valueStart = colon + 1; + while (valueStart < line.size() && line[valueStart] == ' ') { + ++valueStart; + } + response.m_headers.add(line.substr(0, colon), line.substr(valueStart)); + } + + if (lineEnd == raw.size()) { + break; + } + lineStart = lineEnd + 2; + } + } +}; + +//--- + +WinHttpClientState::WinHttpClientState(HINTERNET sessionHandle) : + session(sessionHandle) +{ +} + +WinHttpClientState::~WinHttpClientState() +{ + if (session != nullptr) + { + ::WinHttpCloseHandle(session); + } +} + +bool WinHttpClientState::registerRequest( + std::string const& id, + std::shared_ptr request) +{ + std::lock_guard lock(requestsMutex); + if (!acceptingRequests) + { + return false; + } + requests[id] = std::move(request); + ++registryGeneration; + bool const shouldSend = cancelAllDepth == 0; + requestsCv.notify_all(); + return shouldSend; +} + +void WinHttpClientState::eraseRequest(std::string const& id) +{ + std::lock_guard lock(requestsMutex); + requests.erase(id); + ++registryGeneration; + requestsCv.notify_all(); +} + +void WinHttpClientState::stopAcceptingRequests() +{ + std::lock_guard lock(requestsMutex); + acceptingRequests = false; +} + +void WinHttpClientState::beginCallback() +{ + std::lock_guard lock(requestsMutex); + beginCallbackLocked(); + requestsCv.notify_all(); +} + +void WinHttpClientState::beginCallbackLocked() +{ + ++callbacksInFlight; + ++callbacksByThread[std::this_thread::get_id()]; + ++callbackGeneration; +} + +void WinHttpClientState::endCallback() +{ + std::lock_guard lock(requestsMutex); + auto it = callbacksByThread.find(std::this_thread::get_id()); + if (callbacksInFlight == 0) + { + LOG_ERROR("WinHTTP callback accounting underflow"); + requestsCv.notify_all(); + return; + } + + --callbacksInFlight; + if (it == callbacksByThread.end() || it->second == 0) + { + LOG_ERROR("WinHTTP callback thread was not registered"); + } + else if (--it->second == 0) + { + callbacksByThread.erase(it); + } + ++callbackGeneration; + requestsCv.notify_all(); +} + +unsigned HttpClient_WinHttp::s_nextRequestId = 0; + +HttpClient_WinHttp::HttpClient_WinHttp() +{ + // WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY (Windows 8.1+) resolves the proxy + // without depending on a logged-on interactive user or that user's + // Internet Explorer settings -- unlike WinInet's + // INTERNET_OPEN_TYPE_PRECONFIG, which requires one. This is why WinHTTP, + // not WinInet, is Microsoft's documented recommendation for services and + // other non-interactive processes. On an older OS that rejects this access + // type, fall back to the machine-wide WinHTTP proxy configuration. This is + // the documented pre-Windows-8.1 behavior and avoids bypassing enterprise + // proxies entirely. Only fall back for the compatibility error; other + // failures should not be hidden by a second, unrelated WinHttpOpen call. + HINTERNET session = ::WinHttpOpen( + NULL, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); + if (session == nullptr) + { + DWORD dwError = ::GetLastError(); + if (dwError == ERROR_INVALID_PARAMETER) + { + LOG_WARN("WinHttpOpen(AUTOMATIC_PROXY) is unsupported; retrying with default proxy"); + session = ::WinHttpOpen( + NULL, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC); + } + else + { + LOG_WARN("WinHttpOpen(AUTOMATIC_PROXY) failed: %lu", dwError); + } + } + // WinHTTP otherwise permits an unlimited number of connections per origin. + // Keep transport concurrency aligned with the SDK's default pending-upload + // limit until ApplySettings supplies the configured value. + setConnectionLimits(session, DEFAULT_MAX_CONNECTIONS_PER_SERVER); + m_state = std::make_shared(session); +} + +HttpClient_WinHttp::~HttpClient_WinHttp() +{ + m_state->stopAcceptingRequests(); + CancelAllRequests(); + m_state.reset(); +} + +IHttpRequest* HttpClient_WinHttp::CreateRequest() +{ + std::string id = "WH-" + toString(::InterlockedIncrement(&s_nextRequestId)); + return new SimpleHttpRequest(id); +} + +void HttpClient_WinHttp::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) +{ + // SendRequestAsync borrows the request; the caller retains ownership. + auto state = m_state; + auto wrapper = std::make_shared( + std::move(state), static_cast(request)); + wrapper->send(callback); +} + +void HttpClient_WinHttp::CancelRequestAsync(std::string const& id) +{ + auto state = m_state; + // Copy the shared_ptr out of the map while holding the lock only for the + // lookup, then call cancel() without the lock held (cancel() blocks in + // WinHttpCloseHandle waiting for a completion callback on another thread + // that needs this same lock -- see cancel()'s comment). The local copy + // keeps the wrapper alive for the duration of this call even if erase() + // concurrently removes the map's own reference. + std::shared_ptr request; + { + std::lock_guard lock(state->requestsMutex); + auto it = state->requests.find(id); + if (it != state->requests.end()) { + request = it->second; + } + } + if (request) { + request->cancel(); + } +} + +void HttpClient_WinHttp::CancelAllRequests() +{ + CancelAllRequests(std::chrono::milliseconds::zero()); +} + +void HttpClient_WinHttp::CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) +{ + auto state = m_state; + class CancelAllScope + { + public: + explicit CancelAllScope(std::shared_ptr state) + : m_state(std::move(state)) + { + std::lock_guard lock(m_state->requestsMutex); + ++m_state->cancelAllDepth; + } + + ~CancelAllScope() + { + if (m_active) + { + std::lock_guard lock(m_state->requestsMutex); + --m_state->cancelAllDepth; + } + } + + void finishLocked() + { + --m_state->cancelAllDepth; + m_active = false; + } + + private: + std::shared_ptr m_state; + bool m_active {true}; + } cancelAllScope(state); + + bool const hasTimeout = + bestEffortTimeout > std::chrono::milliseconds::zero(); + auto const deadline = + std::chrono::steady_clock::now() + bestEffortTimeout; + std::thread::id const callerThread = std::this_thread::get_id(); + auto callbacksDrainedForCaller = [&state, callerThread]() { + // Application callbacks cannot wait for peer callbacks: simultaneous + // callbacks doing so would wait on one another. Each callback scope + // retains the shared client state independently. + return state->callbacksByThread.find(callerThread) != + state->callbacksByThread.end() || + state->callbacksInFlight == 0; + }; + auto requestsDrainedForCaller = [&state, callerThread]() { + if (state->requests.empty()) + { + return true; + } + + bool callerIsInStateCallback = false; + for (auto const& item : state->requests) + { + if (item.second->hasStateCallbackOnThreadLocked(callerThread)) + { + callerIsInStateCallback = true; + break; + } + } + for (auto const& item : state->requests) + { + if (!callerIsInStateCallback || + !item.second->hasActiveStateCallbackLocked()) + { + return false; + } + } + return true; + }; + + for (;;) + { + std::vector> requests; + size_t registryGeneration; + size_t callbackGeneration; + { + std::lock_guard lock(state->requestsMutex); + if (state->requests.empty() && callbacksDrainedForCaller()) + { + // Holding the registry lock makes completion of this cancellation + // epoch the linearization point: later registrations are new work. + cancelAllScope.finishLocked(); + return; + } + + registryGeneration = state->registryGeneration; + callbackGeneration = state->callbackGeneration; + for (auto const& item : state->requests) + { + requests.push_back(item.second); + } + } + + for (auto const& request : requests) + { + if (hasTimeout && std::chrono::steady_clock::now() >= deadline) + { + break; + } + request->cancel(); + } + + std::unique_lock lock(state->requestsMutex); + if (requestsDrainedForCaller() && callbacksDrainedForCaller()) + { + cancelAllScope.finishLocked(); + return; + } + auto stateChangedOrDrained = [&]() { + return state->registryGeneration != registryGeneration || + state->callbackGeneration != callbackGeneration || + (requestsDrainedForCaller() && callbacksDrainedForCaller()); + }; + if (hasTimeout) + { + if (!state->requestsCv.wait_until( + lock, deadline, stateChangedOrDrained)) + { + return; + } + } + else + { + state->requestsCv.wait(lock, stateChangedOrDrained); + } + } +} + +/// +/// Enforces MS-root server certificate check. +/// +/// if set to true [enforce verification that server cert is MS-Rooted]. +void HttpClient_WinHttp::ApplySettings(ILogConfiguration& config) +{ + int64_t configuredMaxConnections = config[CFG_INT_MAX_PENDING_REQ]; + DWORD maxConnections = DEFAULT_MAX_CONNECTIONS_PER_SERVER; + if (configuredMaxConnections > 0) + { + auto const largestFiniteLimit = + static_cast(std::numeric_limits::max() - 1); + maxConnections = static_cast( + configuredMaxConnections > largestFiniteLimit + ? largestFiniteLimit + : configuredMaxConnections); + } + setConnectionLimits(m_state->session, maxConnections); + SetMsRootCheck(config[CFG_MAP_HTTP][CFG_BOOL_HTTP_MS_ROOT_CHECK]); +} + +void HttpClient_WinHttp::SetMsRootCheck(bool enforceMsRoot) +{ + m_state->msRootCheck.store(enforceMsRoot, std::memory_order_release); +} + +/// +/// Determines whether MS-Rooted server cert check required. +/// +/// +/// true if [MS-Rooted server cert check required]; otherwise, false. +/// +bool HttpClient_WinHttp::IsMsRootCheckRequired() +{ + return m_state->msRootCheck.load(std::memory_order_acquire); +} + +} MAT_NS_END +#endif // HAVE_MAT_DEFAULT_HTTP_CLIENT +// clang-format on diff --git a/lib/http/HttpClient_WinHttp.hpp b/lib/http/HttpClient_WinHttp.hpp new file mode 100644 index 000000000..d4fa0219a --- /dev/null +++ b/lib/http/HttpClient_WinHttp.hpp @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef HTTPCLIENT_WINHTTP_HPP +#define HTTPCLIENT_WINHTTP_HPP + +#ifdef HAVE_MAT_DEFAULT_HTTP_CLIENT + +#include "IHttpClient.hpp" +#include "IBoundedHttpClientCancel.hpp" +#include "pal/PAL.hpp" + +#include "ILogManager.hpp" + +#include +#include +#include +#include + +namespace MAT_NS_BEGIN { + +#ifndef _WINHTTPX_ +typedef void* HINTERNET; +#endif + +class WinHttpRequestWrapper; +struct WinHttpClientState; + +// WinHTTP-based HTTP client. Unlike WinInet, WinHTTP does not depend on a +// logged-on interactive user or that user's Internet Explorer settings, so +// it is Microsoft's recommended transport for services and other +// non-interactive processes (see +// https://learn.microsoft.com/windows/win32/winhttp/porting-wininet-applications-to-winhttp). +// This is the default Win32 desktop transport; HttpClient_WinInet remains +// available as an explicit opt-in for callers that need IE-integrated proxy +// or cookie behavior. +class HttpClient_WinHttp : public IHttpClient, public IBoundedHttpClientCancel { + public: + // Common IHttpClient methods + HttpClient_WinHttp(); + virtual ~HttpClient_WinHttp(); + virtual IHttpRequest* CreateRequest() final; + virtual void SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) final; + virtual void CancelRequestAsync(std::string const& id) final; + virtual void CancelAllRequests() final; + virtual void CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) final; + + virtual void ApplySettings(ILogConfiguration& config) override; + + // Methods unique to WinHttp implementation. + void SetMsRootCheck(bool enforceMsRoot); + bool IsMsRootCheckRequired(); + + protected: + std::shared_ptr m_state; + static unsigned s_nextRequestId; +}; + +} MAT_NS_END + +#endif // HAVE_MAT_DEFAULT_HTTP_CLIENT + +#endif // HTTPCLIENT_WINHTTP_HPP diff --git a/lib/http/HttpClient_WinInet.cpp b/lib/http/HttpClient_WinInet.cpp index 2ec8be9b0..4a7620f21 100644 --- a/lib/http/HttpClient_WinInet.cpp +++ b/lib/http/HttpClient_WinInet.cpp @@ -6,40 +6,422 @@ #include "mat/config.h" #ifdef HAVE_MAT_DEFAULT_HTTP_CLIENT -#pragma warning(push) -#pragma warning(disable:4189) /* Turn off Level 4: local variable is initialized but not referenced. dwError unused in Release without printing it. */ #include "HttpClient_WinInet.hpp" +#include "detail/MsRootCertPolicy.hpp" #include "utils/StringUtils.hpp" #include #include -#include +#include +#include +#include #include #include +#include +#include #include #include +#pragma comment(lib, "crypt32.lib") +#pragma comment(lib, "wininet.lib") + namespace MAT_NS_BEGIN { -class WinInetRequestWrapper +class WinInetRequestWrapper; + +struct WinInetCallbackContext +{ + explicit WinInetCallbackContext(std::shared_ptr request) + : request(std::move(request)) + { + } + + std::shared_ptr request; +}; + +struct WinInetClientState +{ + explicit WinInetClientState(HINTERNET internetHandle); + ~WinInetClientState(); + + bool registerRequest( + std::string const& id, + std::shared_ptr request); + void eraseRequest(std::string const& id); + void stopAcceptingRequests(); + void beginCallback(); + void endCallback(); + + HINTERNET internet; + std::mutex requestsMutex; + std::map> requests; + std::condition_variable requestsCv; + std::atomic msRootCheck {false}; + bool acceptingRequests {true}; + size_t cancelAllDepth {0}; + size_t registryGeneration {0}; + size_t callbackGeneration {0}; + size_t callbacksInFlight {0}; + std::map callbacksByThread; +}; + +class WinInetCallbackScope +{ + public: + explicit WinInetCallbackScope( + std::shared_ptr state) + : m_state(std::move(state)) + { + m_state->beginCallback(); + } + + ~WinInetCallbackScope() + { + m_state->endCallback(); + } + + WinInetCallbackScope(WinInetCallbackScope const&) = delete; + WinInetCallbackScope& operator=(WinInetCallbackScope const&) = delete; + + private: + std::shared_ptr m_state; +}; + +class WinInetRequestWrapper : public std::enable_shared_from_this { protected: - HttpClient_WinInet& m_parent; + enum class PendingApi + { + None, + StagedHeaders, + StagedBody, + StagedEnd + }; + + std::shared_ptr m_clientState; std::string m_id; IHttpResponseCallback* m_appCallback {nullptr}; + // WinInet may deliver completion callbacks synchronously from an async API. + // This per-request recursive mutex permits only that narrow re-entry. It is + // never nested with the parent request-map mutex; cancellation snapshots + // the registry before touching request handles or invoking application code. + std::recursive_mutex m_handleMutex; HINTERNET m_hWinInetSession {nullptr}; HINTERNET m_hWinInetRequest {nullptr}; + WinInetCallbackContext* m_callbackContext {nullptr}; SimpleHttpRequest* m_request; BYTE m_buffer[1024] {0}; DWORD m_bufferUsed {0}; std::vector m_bodyBuffer; bool m_readingData {false}; - bool isCallbackCalled {false}; - bool isAborted {false}; + std::atomic m_terminalCallbackStarted {false}; + std::atomic m_isAborted {false}; + std::atomic m_deferredError {ERROR_SUCCESS}; + bool m_msRootCheckRequired {false}; + // HTTPS is latched before the request handle exists so the staged send can + // distinguish requests that require certificate-policy enforcement. + bool m_isHttps {false}; + // The MS-root check runs at most once per request handle, on the first + // staged-send completion after the TLS handshake completes. + std::atomic m_msRootChecked {false}; + bool m_contextInstalled {false}; + bool m_sendIssued {false}; + bool m_setupActive {false}; + unsigned m_stateCallbackDepth {0}; + std::map m_stateCallbacksByThread; + bool m_setupCompletionPending {false}; + DWORD m_setupCompletionError {ERROR_SUCCESS}; + unsigned m_asyncApiDepth {0}; + bool m_apiCompletionPending {false}; + DWORD m_apiCompletionError {ERROR_SUCCESS}; + PendingApi m_pendingApi {PendingApi::None}; + size_t m_stagedBodyOffset {0}; + DWORD m_stagedBytesWritten {0}; + + class SetupGuard + { + public: + explicit SetupGuard(WinInetRequestWrapper& owner) noexcept + : m_owner(owner) + { + std::lock_guard lock(m_owner.m_handleMutex); + m_owner.m_setupActive = true; + } + + ~SetupGuard() noexcept(false) + { + m_owner.finishSetup(); + } + + SetupGuard(SetupGuard const&) = delete; + SetupGuard& operator=(SetupGuard const&) = delete; + + private: + WinInetRequestWrapper& m_owner; + }; + + void finishSetup() + { + bool complete = false; + DWORD completionError = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + m_setupActive = false; + complete = m_setupCompletionPending; + completionError = m_setupCompletionError; + m_setupCompletionPending = false; + m_setupCompletionError = ERROR_SUCCESS; + } + if (complete) + { + onRequestComplete(completionError); + } + } + + HINTERNET detachRequestHandle() + { + std::lock_guard lock(m_handleMutex); + HINTERNET request = m_hWinInetRequest; + m_hWinInetRequest = nullptr; + return request; + } + + HINTERNET detachSessionHandle() + { + std::lock_guard lock(m_handleMutex); + HINTERNET session = m_hWinInetSession; + m_hWinInetSession = nullptr; + return session; + } + + void closeRequestHandle() + { + HINTERNET request = detachRequestHandle(); + if (request != nullptr) + { + // InternetCloseHandle may synchronously deliver HANDLE_CLOSING. + // Never hold either mutex while closing. + ::InternetCloseHandle(request); + } + } + + void closeSessionHandle() + { + HINTERNET session = detachSessionHandle(); + if (session != nullptr) + { + ::InternetCloseHandle(session); + } + } + + bool shouldStopSetup() const noexcept + { + return m_isAborted.load(std::memory_order_acquire) || + m_terminalCallbackStarted.load(std::memory_order_acquire); + } + + void handleWinInetCompletion(DWORD dwError) + { + PendingApi completedApi = PendingApi::None; + { + std::lock_guard lock(m_handleMutex); + if (m_asyncApiDepth != 0) + { + m_apiCompletionPending = true; + m_apiCompletionError = dwError; + return; + } + completedApi = m_pendingApi; + m_pendingApi = PendingApi::None; + } + + if (completedApi == PendingApi::None) + { + onRequestComplete(dwError); + return; + } + continueStagedSend(completedApi, dwError); + } + + void completeIssuedApi(BOOL result, DWORD error) + { + bool completionPending = false; + DWORD completionError = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + completionPending = m_apiCompletionPending; + completionError = m_apiCompletionError; + m_apiCompletionPending = false; + m_apiCompletionError = ERROR_SUCCESS; + } + + if (completionPending) + { + handleWinInetCompletion(completionError); + } + else if (result) + { + handleWinInetCompletion(ERROR_SUCCESS); + } + else if (error != ERROR_IO_PENDING) + { + handleWinInetCompletion(error); + } + } + + void issueStagedEnd() + { + BOOL result = FALSE; + DWORD error = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr || shouldStopSetup()) + { + error = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + m_pendingApi = PendingApi::StagedEnd; + ++m_asyncApiDepth; + result = ::HttpEndRequestA(m_hWinInetRequest, nullptr, 0, 0); + error = result ? ERROR_SUCCESS : ::GetLastError(); + --m_asyncApiDepth; + } + } + if (error == ERROR_INTERNET_OPERATION_CANCELLED) + { + onRequestComplete(error); + return; + } + completeIssuedApi(result, error); + } + + void issueStagedBody() + { + size_t const bodySize = m_request->m_body.size(); + if (m_stagedBodyOffset == bodySize) + { + issueStagedEnd(); + return; + } + + BOOL result = FALSE; + DWORD error = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr || shouldStopSetup()) + { + error = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + size_t const remaining = bodySize - m_stagedBodyOffset; + m_stagedBytesWritten = 0; + m_pendingApi = PendingApi::StagedBody; + ++m_asyncApiDepth; + result = ::InternetWriteFile( + m_hWinInetRequest, + m_request->m_body.data() + m_stagedBodyOffset, + static_cast(remaining), + &m_stagedBytesWritten); + error = result ? ERROR_SUCCESS : ::GetLastError(); + --m_asyncApiDepth; + } + } + if (error == ERROR_INTERNET_OPERATION_CANCELLED) + { + onRequestComplete(error); + return; + } + completeIssuedApi(result, error); + } + + void continueStagedSend(PendingApi completedApi, DWORD dwError) + { + if (dwError != ERROR_SUCCESS) + { + DispatchEvent(OnSendFailed); + onRequestComplete(dwError); + return; + } + + switch (completedApi) + { + case PendingApi::StagedHeaders: + runMsRootCheckOnce(); + dwError = m_deferredError.load(std::memory_order_acquire); + if (dwError != ERROR_SUCCESS) + { + onRequestComplete(dwError); + return; + } + issueStagedBody(); + return; + + case PendingApi::StagedBody: + if (m_stagedBytesWritten == 0 || + m_stagedBytesWritten > + m_request->m_body.size() - m_stagedBodyOffset) + { + LOG_ERROR("InternetWriteFile() returned an invalid byte count"); + DispatchEvent(OnSendFailed); + onRequestComplete(ERROR_INTERNET_INTERNAL_ERROR); + return; + } + m_stagedBodyOffset += m_stagedBytesWritten; + issueStagedBody(); + return; + + case PendingApi::StagedEnd: + onRequestComplete(ERROR_SUCCESS); + return; + + case PendingApi::None: + onRequestComplete(ERROR_INTERNET_INTERNAL_ERROR); + return; + } + } + + void issueStagedHeaders() + { + INTERNET_BUFFERSA buffers {}; + buffers.dwStructSize = sizeof(buffers); + buffers.dwBufferTotal = static_cast(m_request->m_body.size()); + + BOOL result = FALSE; + DWORD error = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr || shouldStopSetup()) + { + error = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + m_sendIssued = true; + m_pendingApi = PendingApi::StagedHeaders; + ++m_asyncApiDepth; + result = ::HttpSendRequestExA( + m_hWinInetRequest, &buffers, nullptr, 0, + reinterpret_cast(m_callbackContext)); + error = result ? ERROR_SUCCESS : ::GetLastError(); + --m_asyncApiDepth; + } + } + if (error == ERROR_INTERNET_OPERATION_CANCELLED) + { + onRequestComplete(error); + return; + } + completeIssuedApi(result, error); + } + public: - WinInetRequestWrapper(HttpClient_WinInet& parent, SimpleHttpRequest* request) - : m_parent(parent), + WinInetRequestWrapper( + std::shared_ptr clientState, + SimpleHttpRequest* request) + : m_clientState(std::move(clientState)), m_id(request->GetId()), m_request(request) { @@ -49,14 +431,24 @@ class WinInetRequestWrapper WinInetRequestWrapper(WinInetRequestWrapper const&) = delete; WinInetRequestWrapper& operator=(WinInetRequestWrapper const&) = delete; + bool hasStateCallbackOnThread(std::thread::id threadId) + { + std::lock_guard lock(m_handleMutex); + return m_stateCallbacksByThread.find(threadId) != + m_stateCallbacksByThread.end(); + } + + bool hasActiveStateCallback() + { + std::lock_guard lock(m_handleMutex); + return m_stateCallbackDepth != 0; + } + ~WinInetRequestWrapper() noexcept { LOG_TRACE("%p ~WinInetRequestWrapper()", this); - if (m_hWinInetRequest != nullptr) - { - ::InternetCloseHandle(m_hWinInetRequest); - ::InternetCloseHandle(m_hWinInetSession); - } + closeRequestHandle(); + closeSessionHandle(); } /// @@ -64,14 +456,10 @@ class WinInetRequestWrapper /// the object destructor, but rather hints the implementation to speed-up the /// destruction. /// - /// Two possible outcomes:. - //// - /// - set isAborted to true: cancel request without sending to WinInet stack, - /// in case if request has not been sent to WinInet stack yet. - //// - /// - close m_hWinInetRequest handle: WinInet fails all subsequent attempts to - /// use invalidated handle and aborts all pending WinInet worker threads on it. - /// In that case we complete with ERROR_INTERNET_OPERATION_CANCELLED. + /// Cancellation marks setup as aborted and closes an existing request handle. + /// Before the asynchronous send starts, completion can be delivered directly. + /// After it starts, completion is deferred until REQUEST_COMPLETE or + /// HANDLE_CLOSING proves that WinInet has released the caller's body buffer. /// /// It may happen that we get some feedback from WinInet, i.e. we are canceling /// at that same moment when the request is complete. In that case we process @@ -79,53 +467,87 @@ class WinInetRequestWrapper /// void cancel() { - LOCKGUARD(m_parent.m_requestsMutex); - isAborted = true; - if (m_hWinInetRequest != nullptr) + HINTERNET request = nullptr; + bool completeHere = false; { - ::InternetCloseHandle(m_hWinInetRequest); - // async request callback destroys the object + std::lock_guard lock(m_handleMutex); + if (m_terminalCallbackStarted.load(std::memory_order_acquire)) + { + return; + } + m_isAborted.store(true, std::memory_order_release); + DWORD noError = ERROR_SUCCESS; + m_deferredError.compare_exchange_strong( + noError, ERROR_INTERNET_OPERATION_CANCELLED, std::memory_order_acq_rel); + request = m_hWinInetRequest; + m_hWinInetRequest = nullptr; + // Before an async send is issued, WinInet owns none of the request + // body's storage and no REQUEST_COMPLETE callback is guaranteed. + completeHere = + m_stateCallbackDepth == 0 && + !m_setupActive && + (!m_contextInstalled || !m_sendIssued); + } + if (request != nullptr) + { + // WinInet may invoke callbacks here. The callback context retains + // this wrapper until HANDLE_CLOSING. + ::InternetCloseHandle(request); + } + if (completeHere) + { + onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); } } /** - * Verify that the server end-point certificate is MS-Rooted + * Gather the server certificate chain facts for the current request handle + * and reduce them to a pure policy decision. This is the only place that + * touches WinInet/Wincrypt; the Allow/Reject/Unable logic lives in the + * platform-independent detail::EvaluateMsRootPolicy helper so it can be + * reasoned about and unit-tested without a live connection. + * + * Called after HttpSendRequestEx completes and before InternetWriteFile, so + * cancellation and terminal completion cannot close the request handle + * during the query, policy evaluation, or chain release. */ - bool isMsRootCert() + detail::MsRootPolicyDecision evaluateServerCertificatePolicyLocked() { + detail::MsRootCertQuery query; + query.httpsScheme = m_isHttps; + + if (m_hWinInetRequest == nullptr) + { + // Cancellation or terminal completion won before evaluation began. + return detail::EvaluateMsRootPolicy(query); + } + // Pointer to certificate chain obtained via InternetQueryOption : // Ref. https://blogs.msdn.microsoft.com/alejacma/2012/01/18/how-to-use-internet_option_server_cert_chain_context-with-internetqueryoption-in-c/ PCCERT_CHAIN_CONTEXT pCertCtx = nullptr; DWORD dwCertChainContextSize = sizeof(PCCERT_CHAIN_CONTEXT); - // Proceed to process the result if API call succeeds. That option is available in MSIE 8.x+ since Windows 7.1 and Win Server 2008 R2. - // In case if API call fails, then proceed without cert validation. This behavior is identical to default old behavior to avoid - // regressions for downlevel OS. + // That option is available in MSIE 8.x+ since Windows 7.1 and Win Server + // 2008 R2. If the chain cannot be obtained, the optional policy fails closed. if (::InternetQueryOption(m_hWinInetRequest, INTERNET_OPTION_SERVER_CERT_CHAIN_CONTEXT, (LPVOID)&pCertCtx, &dwCertChainContextSize)) { - CERT_CHAIN_POLICY_STATUS pps = { 0, 0, 0, 0, nullptr }; - pps.cbSize = sizeof(pps); - // Verify that the cert chain roots up to the Microsoft application root at top level - CERT_CHAIN_POLICY_PARA policyPara = {0, 0, nullptr }; - policyPara.cbSize = sizeof(policyPara); - policyPara.dwFlags = MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; - policyPara.pvExtraPolicyPara = nullptr; - - BOOL policyChecked = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_MICROSOFT_ROOT, pCertCtx, &policyPara, &pps); + query.chainQuerySucceeded = true; + query.chainContextPresent = (pCertCtx != nullptr); if (pCertCtx != nullptr) { + CERT_CHAIN_POLICY_STATUS pps = { sizeof(pps), 0, 0, 0, nullptr }; + // Verify that the cert chain roots up to the Microsoft application root at top level + CERT_CHAIN_POLICY_PARA policyPara = { sizeof(policyPara), 0, nullptr }; + policyPara.dwFlags = MICROSOFT_ROOT_CERT_CHAIN_POLICY_CHECK_APPLICATION_ROOT_FLAG; + policyPara.pvExtraPolicyPara = nullptr; + + BOOL policyChecked = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_MICROSOFT_ROOT, pCertCtx, &policyPara, &pps); + query.policyCheckPerformed = (policyChecked == TRUE); + query.policyStatusError = static_cast(pps.dwError); CertFreeCertificateChain(pCertCtx); } - // Unable to verify the chain - if (!policyChecked) - { - LOG_WARN("CertVerifyCertificateChainPolicy() failed: unable to verify"); - return false; - } - // Non-MS rooted cert chain - if (pps.dwError != ERROR_SUCCESS) + else { - LOG_WARN("CertVerifyCertificateChainPolicy() failed: invalid root CA - %d", pps.dwError); - return false; + LOG_TRACE("InternetQueryOption() returned no server cert chain"); } } else @@ -133,48 +555,108 @@ class WinInetRequestWrapper // Downlevel OS prior to Win 7 and Win 2008 Server R2 do not support cert chain retrieval LOG_TRACE("InternetQueryOption() failed to obtain cert chain"); } - return true; + + return detail::EvaluateMsRootPolicy(query); + } + + /** + * Run the MS-root certificate policy exactly once per request handle after + * the staged headers establish TLS but before any request body is written. + * Rejection and inability to evaluate both fail closed. + */ + void runMsRootCheckOnce() + { + if (!m_msRootCheckRequired || !m_isHttps) + { + return; + } + if (m_msRootChecked.exchange(true, std::memory_order_acq_rel)) + { + return; // atomic latch: at most once per handle + } + + HINTERNET requestToClose = nullptr; + detail::MsRootPolicyDecision decision; + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr) + { + return; + } + decision = evaluateServerCertificatePolicyLocked(); + if (!detail::ShouldProceed(decision)) + { + // We still own a live handle under this lock, so this evaluated + // rejection takes precedence over a cancellation that has not yet + // acquired the lock. A prior cancellation removes the handle and + // therefore evaluates as Unable above. + m_deferredError.store( + ERROR_INTERNET_SEC_INVALID_CERT, std::memory_order_release); + m_isAborted.store(true, std::memory_order_release); + requestToClose = m_hWinInetRequest; + m_hWinInetRequest = nullptr; + } + } + + switch (decision) + { + case detail::MsRootPolicyDecision::Allow: + return; + + case detail::MsRootPolicyDecision::Unable: + LOG_ERROR("MS-root certificate policy could not be evaluated; aborting request"); + break; + + case detail::MsRootPolicyDecision::Reject: + LOG_ERROR("Server certificate chain is not MS-rooted; aborting request"); + break; + } + if (requestToClose != nullptr) + { + // InternetCloseHandle may synchronously deliver HANDLE_CLOSING. + // The callback holds its own shared_ptr before this call. + ::InternetCloseHandle(requestToClose); + } } // Asynchronously send HTTP request and invoke response callback. - // Ownership semantics: send(...) method self-destroys *this* upon - // receiving WinInet callback. There must be absolutely no methods - // that attempt to use the object after triggering send on it. - // Send operation on request may be issued no more than once. - // - // Implementation details: - // - // lockguard around m_requestsMutex covers the following stages: - // - request added to map - // - URL parsed - // - DNS lookup performed, socket opened, SSL handshake - // - MS-Root SSL cert validation (if requested) - // - populating HTTP request headers - // - scheduling async(!) upload of HTTP post body - // - // Note that if any of the stages above fails, we invoke onRequestComplete(...). - // That method destroys "this" request object and in order to avoid - // any corruption we immediately return after invoking onRequestComplete(...). - // + // The request map owns the wrapper during setup, and the callback context + // retains it after a WinInet request handle is created. Send may be issued + // only once. void send(IHttpResponseCallback* callback) { - LOCKGUARD(m_parent.m_requestsMutex); - // Register app callback and request in HttpClient map + SetupGuard setupGuard(*this); m_appCallback = callback; - m_parent.m_requests[m_id] = this; + m_msRootCheckRequired = + m_clientState->msRootCheck.load(std::memory_order_acquire); + if (!m_clientState->registerRequest(m_id, shared_from_this())) + { + onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + return; + } - // If outside code asked us to abort that request before we could proceed with - // creating a WinInet handle, then clean it right away before proceeding with - // any async WinInet API calls. - if (isAborted) + if (shouldStopSetup()) { - // Request force-aborted before creating a WinInet handle. DispatchEvent(OnConnectFailed); onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); return; } DispatchEvent(OnConnecting); + if (shouldStopSetup()) + { + onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + return; + } + + if (m_request->m_url.size() > static_cast(std::numeric_limits::max())) + { + LOG_WARN("Request URL exceeds WinInet's maximum size"); + DispatchEvent(OnConnectFailed); + onRequestComplete(ERROR_INVALID_PARAMETER); + return; + } + URL_COMPONENTSA urlc; memset(&urlc, 0, sizeof(urlc)); urlc.dwStructSize = sizeof(urlc); @@ -184,123 +666,262 @@ class WinInetRequestWrapper char path[1024] = { 0 }; urlc.lpszUrlPath = path; urlc.dwUrlPathLength = sizeof(path); - if (!::InternetCrackUrlA(m_request->m_url.data(), (DWORD)m_request->m_url.size(), 0, &urlc)) + if (!::InternetCrackUrlA( + m_request->m_url.c_str(), static_cast(m_request->m_url.size()), 0, &urlc)) { DWORD dwError = ::GetLastError(); - LOG_WARN("InternetCrackUrl() failed: dwError=%d url=%s", dwError, m_request->m_url.data()); - // Invalid URL passed to WinInet API + LOG_WARN("InternetCrackUrl() failed: dwError=%d url=%s", dwError, m_request->m_url.c_str()); DispatchEvent(OnConnectFailed); - onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + onRequestComplete(dwError); return; } - m_hWinInetSession = ::InternetConnectA(m_parent.m_hInternet, hostname, urlc.nPort, - NULL, NULL, INTERNET_SERVICE_HTTP, 0, reinterpret_cast(this)); - if (m_hWinInetSession == NULL) { - DWORD dwError = ::GetLastError(); + // Latch the scheme before the request handle exists: the SENDING_REQUEST + // callback uses this to apply the MS-root policy to HTTPS only. + m_isHttps = (urlc.nScheme == INTERNET_SCHEME_HTTPS); + + DWORD dwError = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + if (shouldStopSetup()) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + m_hWinInetSession = ::InternetConnectA( + m_clientState->internet, hostname, urlc.nPort, + NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); + if (m_hWinInetSession == nullptr) + { + dwError = ::GetLastError(); + } + } + } + if (dwError != ERROR_SUCCESS) + { LOG_WARN("InternetConnect() failed: %d", dwError); - // Cannot connect to host DispatchEvent(OnConnectFailed); - onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + onRequestComplete(dwError); return; } // TODO: Session handle for the same target should be cached across requests to enable keep-alive. PCSTR szAcceptTypes[] = {"*/*", NULL}; - m_hWinInetRequest = ::HttpOpenRequestA( - m_hWinInetSession, m_request->m_method.c_str(), path, NULL, NULL, szAcceptTypes, - INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_CACHE_WRITE | - INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | - INTERNET_FLAG_RELOAD | (urlc.nScheme == INTERNET_SCHEME_HTTPS ? INTERNET_FLAG_SECURE : 0), - reinterpret_cast(this)); - if (m_hWinInetRequest == NULL) { - DWORD dwError = ::GetLastError(); + { + std::unique_ptr context( + new WinInetCallbackContext(shared_from_this())); + std::lock_guard lock(m_handleMutex); + if (shouldStopSetup()) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + m_hWinInetRequest = ::HttpOpenRequestA( + m_hWinInetSession, m_request->m_method.c_str(), path, NULL, NULL, szAcceptTypes, + INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_NO_CACHE_WRITE | + INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI | INTERNET_FLAG_PRAGMA_NOCACHE | + INTERNET_FLAG_RELOAD | + (m_msRootCheckRequired ? INTERNET_FLAG_NO_AUTO_REDIRECT : 0) | + (urlc.nScheme == INTERNET_SCHEME_HTTPS ? INTERNET_FLAG_SECURE : 0), + reinterpret_cast(context.get())); + if (m_hWinInetRequest == nullptr) + { + dwError = ::GetLastError(); + } + else if (::InternetSetStatusCallback( + m_hWinInetRequest, &WinInetRequestWrapper::winInetCallback) == + INTERNET_INVALID_STATUS_CALLBACK) + { + dwError = ::GetLastError(); + } + else + { + m_callbackContext = context.get(); + context.release(); + m_contextInstalled = true; + } + } + } + if (dwError != ERROR_SUCCESS) + { LOG_WARN("HttpOpenRequest() failed: %d", dwError); - // Request cannot be opened to given URL because of some connectivity issue DispatchEvent(OnConnectFailed); - onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + onRequestComplete(dwError); return; } - - /* Perform optional MS Root certificate check for certain end-point URLs */ - if (m_parent.IsMsRootCheckRequired()) + if (shouldStopSetup()) { - if (!isMsRootCert()) - { - // Request cannot be completed: end-point certificate is not MS-Rooted - DispatchEvent(OnConnectFailed); - onRequestComplete(ERROR_INTERNET_SEC_INVALID_CERT); - return; - } + onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + return; } - ::InternetSetStatusCallback(m_hWinInetRequest, &WinInetRequestWrapper::winInetCallback); + // HTTPS requests with the optional policy use a staged send below: + // establish TLS and send headers, validate the negotiated chain, then + // write the body only after the policy allows the connection. std::ostringstream os; for (auto const& header : m_request->m_headers) { os << header.first << ": " << header.second << "\r\n"; } + std::string headers = os.str(); - if (!::HttpAddRequestHeadersA(m_hWinInetRequest, os.str().data(), static_cast(os.tellp()), HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE)) + if (headers.size() > static_cast(std::numeric_limits::max())) + { + LOG_WARN("Request headers exceed WinInet's maximum size"); + DispatchEvent(OnConnectFailed); + onRequestComplete(ERROR_INVALID_PARAMETER); + return; + } + + if (!headers.empty()) + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr || shouldStopSetup()) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + else if (!::HttpAddRequestHeadersA( + m_hWinInetRequest, headers.c_str(), static_cast(headers.size()), + HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE)) + { + dwError = ::GetLastError(); + } + } + if (dwError != ERROR_SUCCESS) { - DWORD dwError = ::GetLastError(); LOG_WARN("HttpAddRequestHeadersA() failed: %d", dwError); - // Unable to add request headers. There's no point in proceeding with upload because - // our server is expecting those custom request headers to always be there. DispatchEvent(OnConnectFailed); + onRequestComplete(dwError); + return; + } + if (shouldStopSetup()) + { onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); return; } - // Try to send headers and request body to server DispatchEvent(OnSending); - void *data = static_cast(m_request->m_body.data()); - DWORD size = static_cast(m_request->m_body.size()); - BOOL bResult = ::HttpSendRequest(m_hWinInetRequest, NULL, 0, data, (DWORD)size); - DWORD dwError = GetLastError(); + if (shouldStopSetup()) + { + onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + return; + } + if (m_request->m_body.size() > static_cast(std::numeric_limits::max())) + { + LOG_WARN("Request body exceeds WinInet's maximum size"); + DispatchEvent(OnSendFailed); + onRequestComplete(ERROR_INVALID_PARAMETER); + return; + } + + if (m_msRootCheckRequired && m_isHttps) + { + issueStagedHeaders(); + return; + } + + BOOL sendResult = FALSE; + bool completionPending = false; + DWORD completionError = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + if (m_hWinInetRequest == nullptr || shouldStopSetup()) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + void* data = m_request->m_body.empty() + ? nullptr + : static_cast(m_request->m_body.data()); + m_sendIssued = true; + ++m_asyncApiDepth; + sendResult = ::HttpSendRequestA( + m_hWinInetRequest, nullptr, 0, data, + static_cast(m_request->m_body.size())); + dwError = sendResult ? ERROR_SUCCESS : ::GetLastError(); + --m_asyncApiDepth; + completionPending = m_apiCompletionPending; + completionError = m_apiCompletionError; + m_apiCompletionPending = false; + m_apiCompletionError = ERROR_SUCCESS; + } + } - if (bResult == TRUE && dwError != ERROR_IO_PENDING) { - dwError = ::GetLastError(); + if (completionPending) + { + onRequestComplete(completionError); + return; + } + if (sendResult) + { + // WinInet is permitted to finish an asynchronous-session request + // synchronously. A TRUE return is success, not an error. + onRequestComplete(ERROR_SUCCESS); + return; + } + if (dwError != ERROR_IO_PENDING) + { LOG_WARN("HttpSendRequest() failed: %d", dwError); - // Unable to send requerst DispatchEvent(OnSendFailed); - onRequestComplete(ERROR_INTERNET_OPERATION_CANCELLED); + onRequestComplete(dwError); return; } - // Async request has been queued in WinInet thread pool } static void CALLBACK winInetCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) { - UNREFERENCED_PARAMETER(dwStatusInformationLength); // Only used inside an assertion - UNREFERENCED_PARAMETER(hInternet); // Only used in debug printout OACR_USE_PTR(hInternet); - WinInetRequestWrapper* self = reinterpret_cast(dwContext); + WinInetCallbackContext* context = reinterpret_cast(dwContext); + if (context == nullptr) + { + return; + } LOG_TRACE("winInetCallback: hInternet %p, dwContext %p, dwInternetStatus %u", hInternet, dwContext, dwInternetStatus); // Are you looking at logs and need to decode dwInternetStatus values? // Go To Definition (F12) on INTERNET_STATUS_REQUEST_COMPLETE below to get to the right place of WinInet.h. switch (dwInternetStatus) { - case INTERNET_STATUS_REQUEST_SENT: { - assert(hInternet == self->m_hWinInetRequest); + case INTERNET_STATUS_SENDING_REQUEST: return; - } - case INTERNET_STATUS_HANDLE_CLOSING: - // HANDLE_CLOSING should always come after REQUEST_COMPLETE. When (and if) - // it (ever) happens, WinInetRequestWrapper* self pointer may point to object - // that has been already destroyed. We do not perform any actions on it. + case INTERNET_STATUS_REQUEST_SENT: return; + case INTERNET_STATUS_HANDLE_CLOSING: { + // The request handle owns the callback context after callback + // registration. HANDLE_CLOSING is its final notification. + std::unique_ptr contextOwner(context); + auto self = contextOwner->request; + { + std::lock_guard lock(self->m_handleMutex); + self->m_callbackContext = nullptr; + } + DWORD deferredError = self->m_deferredError.load(std::memory_order_acquire); + if (deferredError != ERROR_SUCCESS && + !self->m_terminalCallbackStarted.load(std::memory_order_acquire)) + { + self->onRequestComplete(deferredError); + } + return; + } + case INTERNET_STATUS_REQUEST_COMPLETE: { - assert(dwStatusInformationLength >= sizeof(INTERNET_ASYNC_RESULT)); - INTERNET_ASYNC_RESULT& result = *static_cast(lpvStatusInformation); - assert(hInternet == self->m_hWinInetRequest); - if ((self != nullptr) && (self->m_hWinInetRequest != nullptr)) { - self->onRequestComplete(result.dwError); + auto self = context->request; + if (lpvStatusInformation == nullptr || + dwStatusInformationLength < sizeof(INTERNET_ASYNC_RESULT)) + { + LOG_WARN("WinInet REQUEST_COMPLETE callback returned invalid status data"); + self->onRequestComplete(ERROR_INTERNET_INTERNAL_ERROR); + return; } + INTERNET_ASYNC_RESULT const& result = + *static_cast(lpvStatusInformation); + self->handleWinInetCompletion(result.dwError); return; } @@ -311,118 +932,231 @@ class WinInetRequestWrapper void DispatchEvent(HttpStateEvent type) { - if (m_appCallback != nullptr) + IHttpResponseCallback* callback = nullptr; + HINTERNET request = nullptr; + std::thread::id const callbackThread = std::this_thread::get_id(); + { + std::lock_guard lock(m_handleMutex); + if (m_appCallback == nullptr || + m_terminalCallbackStarted.load(std::memory_order_acquire)) + { + return; + } + callback = m_appCallback; + request = m_hWinInetRequest; + ++m_stateCallbackDepth; + ++m_stateCallbacksByThread[callbackThread]; + } + callback->OnHttpStateEvent(type, static_cast(request), 0); { - m_appCallback->OnHttpStateEvent(type, static_cast(m_hWinInetRequest), 0); + std::lock_guard lock(m_handleMutex); + --m_stateCallbackDepth; + auto it = m_stateCallbacksByThread.find(callbackThread); + if (it != m_stateCallbacksByThread.end() && --it->second == 0) + { + m_stateCallbacksByThread.erase(it); + } } } void onRequestComplete(DWORD dwError) { - if (dwError == ERROR_SUCCESS) { - // If looking good so far, try to fetch the response body first. - // It might potentially be another async operation which will - // trigger INTERNET_STATUS_REQUEST_COMPLETE again. - - // SECURITY: refuse an over-large response instead of buffering it (see - // MAX_HTTP_RESPONSE_SIZE) so a hostile/MITM'd collector cannot exhaust - // process memory. Checked before every append so the buffer never exceeds - // the cap; reported as an invalid server response -> NetworkFailure (retried). - if (m_bodyBuffer.size() + m_bufferUsed > MAX_HTTP_RESPONSE_SIZE) { - LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE); - dwError = ERROR_HTTP_INVALID_SERVER_RESPONSE; - } else { - m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed); - while (!m_readingData || m_bufferUsed != 0) { - BOOL bResult = ::InternetReadFile(m_hWinInetRequest, m_buffer, sizeof(m_buffer), &m_bufferUsed); + { + std::lock_guard lock(m_handleMutex); + if (m_stateCallbackDepth != 0 || m_setupActive) + { + m_setupCompletionPending = true; + m_setupCompletionError = dwError; + return; + } + if (m_asyncApiDepth != 0) + { + // WinInet can invoke REQUEST_COMPLETE before an asynchronous + // API returns. Let the issuing frame consume that completion + // after it has restored its local state. + m_apiCompletionPending = true; + m_apiCompletionError = dwError; + return; + } + if (m_terminalCallbackStarted.load(std::memory_order_acquire)) + { + return; + } + DWORD deferredError = m_deferredError.load(std::memory_order_acquire); + if (deferredError != ERROR_SUCCESS) + { + dwError = deferredError; + } + } + + if (dwError == ERROR_SUCCESS) + { + std::lock_guard lock(m_handleMutex); + DWORD deferredError = m_deferredError.load(std::memory_order_acquire); + if (deferredError != ERROR_SUCCESS) + { + dwError = deferredError; + } + else if (m_hWinInetRequest == nullptr) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + else + { + auto appendReadBuffer = [this]() -> bool { + if (m_bodyBuffer.size() > MAX_HTTP_RESPONSE_SIZE || + m_bufferUsed > MAX_HTTP_RESPONSE_SIZE - m_bodyBuffer.size()) + { + return false; + } + m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed); + return true; + }; + + bool shouldRead = !m_readingData || m_bufferUsed != 0; + if (m_readingData && !appendReadBuffer()) + { + dwError = ERROR_HTTP_INVALID_SERVER_RESPONSE; + } + + while (dwError == ERROR_SUCCESS && shouldRead) + { + ++m_asyncApiDepth; + BOOL readResult = ::InternetReadFile( + m_hWinInetRequest, m_buffer, sizeof(m_buffer), &m_bufferUsed); + DWORD readError = readResult ? ERROR_SUCCESS : ::GetLastError(); + --m_asyncApiDepth; m_readingData = true; - if (!bResult) { - dwError = GetLastError(); - if (dwError == ERROR_IO_PENDING) { - // Do not touch anything from this thread anymore. - // The buffer passed to InternetReadFile() and the - // read count will be filled asynchronously, so they - // must stay valid and writable until the next - // INTERNET_STATUS_REQUEST_COMPLETE callback comes - // (that's why those are member variables). - LOG_TRACE("InternetReadFile() failed: ERROR_IO_PENDING. Waiting for INTERNET_STATUS_REQUEST_COMPLETE to be called again"); + + bool completionPending = m_apiCompletionPending; + DWORD completionError = m_apiCompletionError; + m_apiCompletionPending = false; + m_apiCompletionError = ERROR_SUCCESS; + + if (completionPending) + { + if (completionError != ERROR_SUCCESS) + { + dwError = completionError; + break; + } + } + else if (!readResult) + { + if (readError == ERROR_IO_PENDING) + { + LOG_TRACE("InternetReadFile() is pending; waiting for REQUEST_COMPLETE"); return; } - LOG_WARN("InternetReadFile() failed: %d", dwError); + dwError = readError; break; } - if (m_bodyBuffer.size() + m_bufferUsed > MAX_HTTP_RESPONSE_SIZE) { - LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE); + if (!appendReadBuffer()) + { dwError = ERROR_HTTP_INVALID_SERVER_RESPONSE; break; } - m_bodyBuffer.insert(m_bodyBuffer.end(), m_buffer, m_buffer + m_bufferUsed); + shouldRead = m_bufferUsed != 0; } } } + if (dwError == ERROR_HTTP_INVALID_SERVER_RESPONSE) + { + LOG_WARN("HTTP response exceeds max buffered size (%zu bytes); aborting", MAX_HTTP_RESPONSE_SIZE); + } + else if (dwError != ERROR_SUCCESS && + dwError != ERROR_INTERNET_OPERATION_CANCELLED) + { + LOG_WARN("WinInet request failed: %d", dwError); + } + + HINTERNET request = nullptr; + { + std::lock_guard lock(m_handleMutex); + DWORD deferredError = m_deferredError.load(std::memory_order_acquire); + if (deferredError != ERROR_SUCCESS) + { + dwError = deferredError; + } + if (m_terminalCallbackStarted.exchange(true, std::memory_order_acq_rel)) + { + return; + } + request = m_hWinInetRequest; + if (dwError == ERROR_SUCCESS && request == nullptr) + { + dwError = ERROR_INTERNET_OPERATION_CANCELLED; + } + } + std::unique_ptr response(new SimpleHttpResponse(m_id)); + if (dwError == ERROR_SUCCESS) + { + response->m_body = std::move(m_bodyBuffer); - // SUCCESS with no IO_PENDING means we're done with the response body: try to parse the response headers. - if (dwError == ERROR_SUCCESS) { - response->m_body = m_bodyBuffer; - response->m_result = HttpResult_OK; - - uint32_t value = 0; - DWORD dwSize = sizeof(value); - BOOL bResult = ::HttpQueryInfoA(m_hWinInetRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &value, &dwSize, NULL); - if (!bResult) { - LOG_WARN("HttpQueryInfo(STATUS_CODE) failed: %d", GetLastError()); - } - response->m_statusCode = value; - - char* pBuffer = reinterpret_cast(m_buffer); - dwSize = sizeof(m_buffer) - 1; - if (!HttpQueryInfoA(m_hWinInetRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pBuffer, &dwSize, NULL)) { - dwError = GetLastError(); - if (dwError != ERROR_INSUFFICIENT_BUFFER) { - LOG_WARN("HttpQueryInfo(RAW_HEADERS) failed: %d", dwError); - dwSize = 0; - } else { - m_bodyBuffer.resize(dwSize + 1); - pBuffer = reinterpret_cast(m_bodyBuffer.data()); - if (!HttpQueryInfoA(m_hWinInetRequest, HTTP_QUERY_RAW_HEADERS_CRLF, pBuffer, &dwSize, NULL)) { - LOG_WARN("HttpQueryInfo(RAW_HEADERS) failed twice: %d", dwError); - dwSize = 0; - } + uint32_t statusCode = 0; + DWORD statusBytes = sizeof(statusCode); + { + std::lock_guard lock(m_handleMutex); + if (!::HttpQueryInfoA( + request, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, + &statusCode, &statusBytes, nullptr)) + { + dwError = ::GetLastError(); + LOG_WARN("HttpQueryInfo(STATUS_CODE) failed: %d", dwError); } } - pBuffer[dwSize] = '\0'; + response->m_statusCode = statusCode; - char const* ptr = pBuffer; - while (*ptr) { - char const* colon = strchr(ptr, ':'); - if (!colon) { - break; - } - std::string name(ptr, colon); + if (dwError == ERROR_SUCCESS) + { + response->m_result = HttpResult_OK; - ptr = colon + 1; - while (*ptr == ' ') { - ptr++; + DWORD headerBytes = 0; + BOOL headersQueried = FALSE; + DWORD headerError = ERROR_SUCCESS; + { + std::lock_guard lock(m_handleMutex); + headersQueried = ::HttpQueryInfoA( + request, HTTP_QUERY_RAW_HEADERS_CRLF, nullptr, + &headerBytes, nullptr); + headerError = headersQueried ? ERROR_SUCCESS : ::GetLastError(); } - - char const* eol = strstr(ptr, "\r\n"); - if (!eol) { - break; + if (!headersQueried && + headerError == ERROR_INSUFFICIENT_BUFFER && + headerBytes > 0 && + headerBytes < std::numeric_limits::max()) + { + std::vector headers(static_cast(headerBytes) + 1, '\0'); + DWORD bufferBytes = headerBytes; + { + std::lock_guard lock(m_handleMutex); + headersQueried = ::HttpQueryInfoA( + request, HTTP_QUERY_RAW_HEADERS_CRLF, headers.data(), + &bufferBytes, nullptr); + headerError = headersQueried ? ERROR_SUCCESS : ::GetLastError(); + } + if (headersQueried) + { + headers.back() = '\0'; + parseHeaders(std::string(headers.data()), *response); + } + else + { + LOG_WARN("HttpQueryInfo(RAW_HEADERS) failed twice: %d", headerError); + } + } + else if (!headersQueried && headerError != ERROR_SUCCESS) + { + LOG_WARN("HttpQueryInfo(RAW_HEADERS) failed: %d", headerError); } - std::string value1(ptr, eol); - - response->m_headers.add(name, value1); - ptr = eol + 2; } - // This event handler covers the only positive case when we actually got some server response. - // We may still invoke OnHttpResponse(...) below for this positive as well as other negative - // cases where there was a short-read, connection failuire or timeout on reading the response. - DispatchEvent(OnResponse); + } - } else { + if (dwError != ERROR_SUCCESS) + { switch (dwError) { case ERROR_INTERNET_OPERATION_CANCELLED: response->m_result = HttpResult_Aborted; @@ -461,53 +1195,164 @@ class WinInetRequestWrapper } } - assert(isCallbackCalled == false); - if (!isCallbackCalled) + auto keepAlive = shared_from_this(); + auto callback = m_appCallback; + auto requestId = m_id; + + // Closing first guarantees WinInet no longer owns the caller's request + // body before OnHttpResponse allows that request to be destroyed. + closeRequestHandle(); + closeSessionHandle(); + WinInetCallbackScope callbackScope(m_clientState); + // Remove the request before application code so a callback may safely + // cancel all requests or tear the client down synchronously. + m_clientState->eraseRequest(requestId); + + if (callback != nullptr) { - // Only one WinInet worker thread may invoke async callback for a given request at any given moment of time. - // That ensures that isCallbackCalled does not require a lock around it. We unregister the callback here - // to ensure that no more callbacks are coming for that m_hWinInetRequest. - ::InternetSetStatusCallback(m_hWinInetRequest, NULL); - isCallbackCalled = true; - m_appCallback->OnHttpResponse(response.release()); - // HttpClient parent is destroying this HttpRequest object by id - m_parent.erase(m_id); + if (dwError == ERROR_SUCCESS) + { + // The implementation-specific handle is no longer valid once + // terminal delivery begins, so do not expose a stale handle. + callback->OnHttpStateEvent(OnResponse, nullptr, 0); + } + callback->OnHttpResponse(response.release()); + } + } + + static void parseHeaders(std::string const& raw, SimpleHttpResponse& response) + { + size_t lineStart = 0; + while (lineStart < raw.size()) + { + size_t lineEnd = raw.find("\r\n", lineStart); + if (lineEnd == std::string::npos) + { + lineEnd = raw.size(); + } + + std::string const line = raw.substr(lineStart, lineEnd - lineStart); + size_t const colon = line.find(':'); + if (colon != std::string::npos) + { + size_t valueStart = colon + 1; + while (valueStart < line.size() && line[valueStart] == ' ') + { + ++valueStart; + } + response.m_headers.add( + line.substr(0, colon), line.substr(valueStart)); + } + + if (lineEnd == raw.size()) + { + break; + } + lineStart = lineEnd + 2; } } }; //--- -unsigned HttpClient_WinInet::s_nextRequestId = 0; +WinInetClientState::WinInetClientState(HINTERNET internetHandle) : + internet(internetHandle) +{ +} -HttpClient_WinInet::HttpClient_WinInet() : - m_msRootCheck(false) +WinInetClientState::~WinInetClientState() { - m_hInternet = ::InternetOpen(NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC); + if (internet != nullptr) + { + ::InternetCloseHandle(internet); + } } -HttpClient_WinInet::~HttpClient_WinInet() +bool WinInetClientState::registerRequest( + std::string const& id, + std::shared_ptr request) { - CancelAllRequests(); - ::InternetCloseHandle(m_hInternet); + bool shouldSend; + { + std::lock_guard lock(requestsMutex); + if (!acceptingRequests) + { + return false; + } + requests[id] = std::move(request); + ++registryGeneration; + shouldSend = cancelAllDepth == 0; + } + requestsCv.notify_all(); + return shouldSend; +} + +void WinInetClientState::eraseRequest(std::string const& id) +{ + { + std::lock_guard lock(requestsMutex); + requests.erase(id); + ++registryGeneration; + } + requestsCv.notify_all(); } -/** - * This method is called exclusively from onRequestComplete . - * No other code paths that lead to request destruction. - */ -void HttpClient_WinInet::erase(std::string const& id) +void WinInetClientState::stopAcceptingRequests() { - LOCKGUARD(m_requestsMutex); - auto it = m_requests.find(id); - if (it != m_requests.end()) { - auto req = it->second; - m_requests.erase(it); - // Wake CancelAllRequests() waiting for the map to drain. - m_requestsCV.notify_all(); - // delete WinInetRequestWrapper - delete req; + std::lock_guard lock(requestsMutex); + acceptingRequests = false; +} + +void WinInetClientState::beginCallback() +{ + { + std::lock_guard lock(requestsMutex); + ++callbacksInFlight; + ++callbacksByThread[std::this_thread::get_id()]; + ++callbackGeneration; + } + requestsCv.notify_all(); +} + +void WinInetClientState::endCallback() +{ + { + std::lock_guard lock(requestsMutex); + if (callbacksInFlight == 0) + { + LOG_ERROR("WinInet callback accounting underflow"); + requestsCv.notify_all(); + return; + } + + --callbacksInFlight; + auto it = callbacksByThread.find(std::this_thread::get_id()); + if (it == callbacksByThread.end() || it->second == 0) + { + LOG_ERROR("WinInet callback thread was not registered"); + } + else if (--it->second == 0) + { + callbacksByThread.erase(it); + } + ++callbackGeneration; } + requestsCv.notify_all(); +} + +unsigned HttpClient_WinInet::s_nextRequestId = 0; + +HttpClient_WinInet::HttpClient_WinInet() +{ + auto internet = ::InternetOpen( + NULL, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, INTERNET_FLAG_ASYNC); + m_state = std::make_shared(internet); +} + +HttpClient_WinInet::~HttpClient_WinInet() +{ + m_state->stopAcceptingRequests(); + CancelAllRequests(); } IHttpRequest* HttpClient_WinInet::CreateRequest() @@ -518,21 +1363,25 @@ IHttpRequest* HttpClient_WinInet::CreateRequest() void HttpClient_WinInet::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) { - // Note: 'request' is never owned by IHttpClient and gets deleted in EventsUploadContext.clear() - WinInetRequestWrapper *wrapper = new WinInetRequestWrapper(*this, static_cast(request)); + // SendRequestAsync borrows the request; the caller retains ownership. + auto wrapper = std::make_shared( + m_state, static_cast(request)); wrapper->send(callback); } void HttpClient_WinInet::CancelRequestAsync(std::string const& id) { - LOCKGUARD(m_requestsMutex); - auto it = m_requests.find(id); - if (it != m_requests.end()) { - auto request = it->second; - if (request) { - request->cancel(); + std::shared_ptr request; + { + std::lock_guard lock(m_state->requestsMutex); + auto it = m_state->requests.find(id); + if (it != m_state->requests.end()) { + request = it->second; } } + if (request) { + request->cancel(); + } } @@ -543,38 +1392,131 @@ void HttpClient_WinInet::CancelAllRequests() void HttpClient_WinInet::CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) { - // vector of all request IDs - std::vector ids; + auto state = m_state; + class CancelAllScope { - LOCKGUARD(m_requestsMutex); - for (auto const& item : m_requests) { - ids.push_back(item.first); + public: + explicit CancelAllScope(std::shared_ptr state) + : m_state(std::move(state)) + { + std::lock_guard lock(m_state->requestsMutex); + ++m_state->cancelAllDepth; } - } - // cancel all requests one-by-one not holding the lock - for (const auto &id : ids) - CancelRequestAsync(id); - // Wait for all request destructors to run (erase() removes them on the WinInet - // callback thread). Use a condition variable signaled from erase() rather than a - // poll loop so this never spins at 100% CPU while draining. WinInet delivers the - // cancellation callbacks on its own threads, so the wait completes without - // depending on the SDK task dispatcher. - std::unique_lock lock(m_requestsMutex); - if (bestEffortTimeout > std::chrono::milliseconds::zero()) - { - // Best-effort (e.g. pause): the caller must not block indefinitely. The client - // is NOT being destroyed here, so a late callback that arrives after this - // returns still runs erase() on a live client -- returning early is safe. - m_requestsCV.wait_for(lock, bestEffortTimeout, [this] { return m_requests.empty(); }); - } - else + ~CancelAllScope() + { + if (m_active) + { + std::lock_guard lock(m_state->requestsMutex); + --m_state->cancelAllDepth; + } + } + + void finishLocked() + { + --m_state->cancelAllDepth; + m_active = false; + } + + private: + std::shared_ptr m_state; + bool m_active {true}; + } cancelAllScope(state); + + bool const hasTimeout = + bestEffortTimeout > std::chrono::milliseconds::zero(); + auto const deadline = + std::chrono::steady_clock::now() + bestEffortTimeout; + std::thread::id const callerThread = std::this_thread::get_id(); + auto requestsDrainedForCaller = [&state, callerThread]() { + if (state->requests.empty()) + { + return true; + } + + bool callerIsInStateCallback = false; + for (auto const& item : state->requests) + { + if (item.second->hasStateCallbackOnThread(callerThread)) + { + callerIsInStateCallback = true; + break; + } + } + for (auto const& item : state->requests) + { + if (!callerIsInStateCallback || + !item.second->hasActiveStateCallback()) + { + return false; + } + } + return true; + }; + auto callbacksDrainedForCaller = [&state, callerThread]() { + // A terminal callback cannot wait for peer callbacks: two callbacks + // doing so concurrently would wait on each other. Each callback scope + // retains the shared client state independently. + return state->callbacksByThread.find(callerThread) != + state->callbacksByThread.end() || + state->callbacksInFlight == 0; + }; + + for (;;) { - // Full drain barrier (the destructor calls this): returning early with - // requests still in flight would let a late WinInet callback invoke - // WinInetRequestWrapper::OnHttpResponse -> m_parent.erase() on a destroyed - // client, so wait for every request to drain. - m_requestsCV.wait(lock, [this] { return m_requests.empty(); }); + std::vector> requests; + size_t registryGeneration; + size_t callbackGeneration; + { + std::lock_guard lock(state->requestsMutex); + if (state->requests.empty() && callbacksDrainedForCaller()) + { + // Holding the registry lock makes completion of this cancellation + // epoch the linearization point: later registrations are new work. + cancelAllScope.finishLocked(); + return; + } + + registryGeneration = state->registryGeneration; + callbackGeneration = state->callbackGeneration; + for (auto const& item : state->requests) + { + requests.push_back(item.second); + } + } + + for (auto const& request : requests) + { + if (hasTimeout && std::chrono::steady_clock::now() >= deadline) + { + break; + } + request->cancel(); + } + + std::unique_lock lock(state->requestsMutex); + if (requestsDrainedForCaller() && callbacksDrainedForCaller()) + { + cancelAllScope.finishLocked(); + return; + } + auto stateChangedOrDrained = [&]() { + return state->registryGeneration != registryGeneration || + state->callbackGeneration != callbackGeneration || + (requestsDrainedForCaller() && callbacksDrainedForCaller()); + }; + if (hasTimeout) + { + if (!state->requestsCv.wait_until( + lock, deadline, stateChangedOrDrained)) + { + return; + } + } + else + { + state->requestsCv.wait(lock, stateChangedOrDrained); + } } } @@ -589,21 +1531,20 @@ void HttpClient_WinInet::ApplySettings(ILogConfiguration& config) void HttpClient_WinInet::SetMsRootCheck(bool enforceMsRoot) { - m_msRootCheck = enforceMsRoot; + m_state->msRootCheck.store(enforceMsRoot, std::memory_order_release); } /// -/// Determines whether MS-Roted server cert check required. +/// Determines whether an MS-Rooted server certificate check is required. /// /// /// true if [MS-Rooted server cert check required]; otherwise, false. /// bool HttpClient_WinInet::IsMsRootCheckRequired() { - return m_msRootCheck; + return m_state->msRootCheck.load(std::memory_order_acquire); } } MAT_NS_END -#pragma warning(pop) #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT // clang-format on diff --git a/lib/http/HttpClient_WinInet.hpp b/lib/http/HttpClient_WinInet.hpp index 42b256157..4f4864aec 100644 --- a/lib/http/HttpClient_WinInet.hpp +++ b/lib/http/HttpClient_WinInet.hpp @@ -14,6 +14,7 @@ #include "ILogManager.hpp" #include +#include #include namespace MAT_NS_BEGIN { @@ -23,6 +24,7 @@ typedef void* HINTERNET; #endif class WinInetRequestWrapper; +struct WinInetClientState; class HttpClient_WinInet : public IHttpClient, public IBoundedHttpClientCancel { public: @@ -42,18 +44,8 @@ class HttpClient_WinInet : public IHttpClient, public IBoundedHttpClientCancel { bool IsMsRootCheckRequired(); protected: - void erase(std::string const& id); - - protected: - HINTERNET m_hInternet; - std::recursive_mutex m_requestsMutex; - std::map m_requests; - // Signaled from erase() when a request is removed, so CancelAllRequests can drain - // via a condition variable instead of a poll loop (no 100% CPU spin). - std::condition_variable_any m_requestsCV; + std::shared_ptr m_state; static unsigned s_nextRequestId; - bool m_msRootCheck; - friend class WinInetRequestWrapper; }; } MAT_NS_END diff --git a/lib/http/HttpClient_WinRt.cpp b/lib/http/HttpClient_WinRt.cpp index 12ac6aa00..c689ecd8a 100644 --- a/lib/http/HttpClient_WinRt.cpp +++ b/lib/http/HttpClient_WinRt.cpp @@ -11,9 +11,7 @@ #include "http/HttpClient_WinRt.hpp" #include "utils/StringUtils.hpp" -#include #include -#include #include #include @@ -21,7 +19,6 @@ #include #include #include -#include using namespace Windows::Foundation; using namespace Windows::Foundation::Collections; @@ -371,7 +368,7 @@ namespace MAT_NS_BEGIN { void HttpClient_WinRt::SendRequestAsync(IHttpRequest* request, IHttpResponseCallback* callback) { - // Note: 'request' is never owned by IHttpClient and gets deleted in EventsUploadContext.clear() + // SendRequestAsync borrows the request; the caller retains ownership. if (request==nullptr) { LOG_ERROR("request is null!"); diff --git a/lib/http/HttpResponseDecoder.cpp b/lib/http/HttpResponseDecoder.cpp index 6014cec19..b46eeff98 100644 --- a/lib/http/HttpResponseDecoder.cpp +++ b/lib/http/HttpResponseDecoder.cpp @@ -67,13 +67,11 @@ namespace MAT_NS_BEGIN { break; case HttpResult_Aborted: - ctx->httpResponse = nullptr; outcome = Abort; break; case HttpResult_LocalFailure: case HttpResult_NetworkFailure: - ctx->httpResponse = nullptr; outcome = RetryNetwork; break; } @@ -132,7 +130,6 @@ namespace MAT_NS_BEGIN { evt.param2 = ctx->recordIdsAndTenantIds.size(); DispatchEvent(evt); } - ctx->httpResponse = nullptr; // eventsRejected(ctx); // FIXME: [MG] - investigate why ctx gets corrupt after eventsRejected requestAborted(ctx); break; diff --git a/lib/http/IBoundedHttpClientCancel.hpp b/lib/http/IBoundedHttpClientCancel.hpp index f832e4678..c0527d311 100644 --- a/lib/http/IBoundedHttpClientCancel.hpp +++ b/lib/http/IBoundedHttpClientCancel.hpp @@ -16,8 +16,10 @@ class IBoundedHttpClientCancel public: virtual ~IBoundedHttpClientCancel() noexcept = default; - // Positive timeout is a best-effort cap. Zero means the caller requires a - // full drain, matching IHttpClient::CancelAllRequests(). + // Positive timeout is a soft, best-effort cap. Implementations stop + // initiating additional cancellations at the deadline, but one synchronous + // native handle close already in progress may finish after it. Zero means + // the caller requires a full drain, matching IHttpClient::CancelAllRequests(). virtual void CancelAllRequests(std::chrono::milliseconds bestEffortTimeout) = 0; }; diff --git a/lib/http/detail/MsRootCertPolicy.hpp b/lib/http/detail/MsRootCertPolicy.hpp new file mode 100644 index 000000000..576f6d59b --- /dev/null +++ b/lib/http/detail/MsRootCertPolicy.hpp @@ -0,0 +1,126 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// PRIVATE, internal-only header. It is intentionally NOT part of the installed +// public SDK surface: it is not referenced by any public header, is not copied +// by the install rules, and exposes no ABI. It contains a single pure, +// platform-independent policy-decision function so the MS-root certificate +// decision can be reasoned about and unit-tested without a live TLS connection +// or any WinInet/Wincrypt dependency. The runtime transport (HttpClient_WinInet) +// gathers the raw query/build/policy facts from WinInet and feeds them here; it +// does not reach back into transport internals, so no friend/test hook is +// required. +// +#ifndef HTTP_DETAIL_MSROOTCERTPOLICY_HPP +#define HTTP_DETAIL_MSROOTCERTPOLICY_HPP + +#include "ctmacros.hpp" + +#include + +namespace MAT_NS_BEGIN +{ +namespace detail +{ + /// + /// Tri-state outcome of the Microsoft-root certificate policy evaluation. + /// + /// The distinction between Reject and Unable is the whole point + /// of this helper: the legacy transport collapsed both into a single "not + /// trusted" boolean, which conflated "the chain was evaluated and is not + /// MS-rooted" with "the chain could not be evaluated at all". The policy + /// fails closed whenever evaluation cannot establish that the server + /// certificate satisfies the Microsoft-root requirement. + /// + enum class MsRootPolicyDecision + { + /// The connection may proceed: policy is not applicable (non-HTTPS) or + /// the chain was evaluated and satisfies the Microsoft-root policy. + Allow, + + /// The chain was evaluated and confirmed NOT to be MS-rooted (or the + /// policy engine reported an explicit policy error). Reject the request. + Reject, + + /// The chain could not be queried, built, or verified. This is distinct + /// from an evaluated rejection for diagnostics, but both outcomes stop + /// the request. + Unable + }; + + /// + /// Raw, transport-gathered facts required to make the policy decision. All + /// fields are plain scalars so this header carries no platform dependency. + /// + struct MsRootCertQuery + { + /// True when the request scheme is HTTPS. The MS-root policy only + /// inspects HTTPS connections; anything else is Allow. + bool httpsScheme{false}; + + /// True when querying the server certificate chain context succeeded + /// (e.g. InternetQueryOption(INTERNET_OPTION_SERVER_CERT_CHAIN_CONTEXT)). + bool chainQuerySucceeded{false}; + + /// True when the query actually produced a non-null chain context to + /// evaluate. A successful query that yields no context is still "unable". + bool chainContextPresent{false}; + + /// True when the policy-verification API ran to completion (e.g. + /// CertVerifyCertificateChainPolicy returned TRUE). False means the + /// verification itself could not be performed. + bool policyCheckPerformed{false}; + + /// The policy status error reported by the verification API when + /// policyCheckPerformed is true (0 == success == MS-rooted). + std::uint32_t policyStatusError{0}; + }; + + /// + /// Deterministically maps the gathered facts to an Allow / Reject / Unable + /// decision. Pure function: no I/O, no globals, no platform calls. + /// + inline MsRootPolicyDecision EvaluateMsRootPolicy(const MsRootCertQuery& query) noexcept + { + // Policy only applies to HTTPS. HTTP (and anything non-HTTPS) proceeds. + if (!query.httpsScheme) + { + return MsRootPolicyDecision::Allow; + } + + // Could not obtain a chain to evaluate -> cannot establish trust. + if (!query.chainQuerySucceeded || !query.chainContextPresent) + { + return MsRootPolicyDecision::Unable; + } + + // Obtained a chain but the verification API itself did not run to + // completion -> cannot establish trust. + if (!query.policyCheckPerformed) + { + return MsRootPolicyDecision::Unable; + } + + // Verification ran: a non-success status is an evaluated rejection. + if (query.policyStatusError != 0u) + { + return MsRootPolicyDecision::Reject; + } + + return MsRootPolicyDecision::Allow; + } + + /// + /// Only a successfully evaluated Allow permits the request. + /// + inline bool ShouldProceed(MsRootPolicyDecision decision) noexcept + { + return decision == MsRootPolicyDecision::Allow; + } + +} // namespace detail +} +MAT_NS_END + +#endif // HTTP_DETAIL_MSROOTCERTPOLICY_HPP diff --git a/lib/include/mat/config-default-cs4.h b/lib/include/mat/config-default-cs4.h index 71a79c10f..1a267ac27 100644 --- a/lib/include/mat/config-default-cs4.h +++ b/lib/include/mat/config-default-cs4.h @@ -7,7 +7,7 @@ #define EVTSDK_VERSION_PREFIX "EVT" #if defined(_WIN32) #if defined __has_include -# if __has_include ("modules/azmon/AITelemetrySystem.hpp") +# if !defined(MATSDK_NO_AZMON) && !defined(HAVE_MAT_AI) && __has_include ("modules/azmon/AITelemetrySystem.hpp") # define HAVE_MAT_AI # endif # if __has_include ("modules/utc/UtcTelemetrySystem.hpp") @@ -45,4 +45,3 @@ #define HAVE_CS4 #define HAVE_CS4_FULL //#define HAVE_ONEDS_BOUNDCHECK_METHODS - diff --git a/lib/include/mat/config-default-exp.h b/lib/include/mat/config-default-exp.h index 256dfe615..55ab6301a 100644 --- a/lib/include/mat/config-default-exp.h +++ b/lib/include/mat/config-default-exp.h @@ -7,7 +7,7 @@ #define EVTSDK_VERSION_PREFIX "EVT" #if defined(_WIN32) #if defined __has_include -# if __has_include ("modules/azmon/AITelemetrySystem.hpp") +# if !defined(MATSDK_NO_AZMON) && !defined(HAVE_MAT_AI) && __has_include ("modules/azmon/AITelemetrySystem.hpp") # define HAVE_MAT_AI # endif # if __has_include ("modules/utc/UtcTelemetrySystem.hpp") @@ -43,4 +43,3 @@ //#define HAVE_CS4 //#define HAVE_CS4_FULL //#define HAVE_ONEDS_BOUNDCHECK_METHODS - diff --git a/lib/include/mat/config-default.h b/lib/include/mat/config-default.h index e5a094f1a..2f9101051 100644 --- a/lib/include/mat/config-default.h +++ b/lib/include/mat/config-default.h @@ -7,8 +7,10 @@ #define EVTSDK_VERSION_PREFIX "EVT" #if defined(_WIN32) #if defined __has_include -# if __has_include ("modules/azmon/AITelemetrySystem.hpp") +# if !defined(MATSDK_NO_AZMON) && __has_include ("modules/azmon/AITelemetrySystem.hpp") +# ifndef HAVE_MAT_AI # define HAVE_MAT_AI +# endif # endif # if __has_include ("modules/utc/UtcTelemetrySystem.hpp") # define HAVE_MAT_UTC diff --git a/lib/include/mat/config-net40.h b/lib/include/mat/config-net48.h similarity index 100% rename from lib/include/mat/config-net40.h rename to lib/include/mat/config-net48.h diff --git a/lib/include/public/DebugEvents.hpp b/lib/include/public/DebugEvents.hpp index 506611c04..65fde316e 100644 --- a/lib/include/public/DebugEvents.hpp +++ b/lib/include/public/DebugEvents.hpp @@ -167,8 +167,10 @@ namespace MAT_NS_BEGIN /// for debugging and unit testing (not recommended for use in a production environment). /// /// Customers can implement this abstract class to track when certain events - /// happen under the hood in the Microsoft Telemetry SDK. The callback is synchronously executed - /// within the context of the Microsoft Telemetry worker thread. + /// happen under the hood in the Microsoft Telemetry SDK. The callback is synchronously + /// executed within the context of an SDK-owned thread. A listener must not synchronously + /// destroy the LogManager or call FlushAndTeardown(); defer teardown to an + /// application-owned thread after the callback returns instead. /// class MATSDK_LIBABI DebugEventListener { @@ -247,4 +249,3 @@ namespace MAT_NS_BEGIN } MAT_NS_END #endif - diff --git a/lib/include/public/IHttpClient.hpp b/lib/include/public/IHttpClient.hpp index 0b2727803..29b57edc0 100644 --- a/lib/include/public/IHttpClient.hpp +++ b/lib/include/public/IHttpClient.hpp @@ -196,9 +196,9 @@ namespace MAT_NS_BEGIN virtual ~IHttpResponse() noexcept = default; /// - /// Gets the response ID. + /// Gets the ID of the request that produced this response. /// - /// A string that contains the response ID. + /// The same ID returned by the originating IHttpRequest::GetId(). virtual const std::string& GetId() const = 0; /// @@ -521,25 +521,37 @@ namespace MAT_NS_BEGIN /// /// Creates an empty HTTP request object. - /// The created request object has only its ID prepopulated. Other fields - /// must be set by the caller. The request object can then be sent - /// using SendRequestAsync(). If you are not going to use the request object, - /// then you can delete it safely using its virtual destructor. + /// The object has only its ID prepopulated; the caller must populate the + /// other fields before passing it to SendRequestAsync(). If the request is + /// never sent, delete it using its virtual destructor. Ownership after + /// SendRequestAsync() is implementation-specific for compatibility with + /// custom IHttpClient modules; see that implementation's contract. /// /// An HTTP request object for you to prepare. virtual IHttpRequest* CreateRequest() = 0; /// /// Begins an HTTP request. - /// The method takes ownership of the passed request, and can destroy it before - /// returning to the caller. Do not access the request object in any - /// way after this invocation, and do not delete it. - /// The callback object is always called, even if the request is - /// cancelled, or if an error occurs immediately during sending. In the - /// latter case, the OnHttpResponse() callback is called before this - /// method returns. You must keep the callback object alive until its - /// OnHttpResponse() callback is called. It will never be used twice, so - /// after you use it - you can safely delete it. + /// The SDK-provided transports borrow the request object; they do not take + /// ownership and do not delete it. For those transports, keep the request + /// alive and do not modify it from the start of this call until the + /// request's terminal OnHttpResponse() callback begins. They finish their + /// last request access before invoking OnHttpResponse(), so the caller may + /// delete the request during that callback or any time after it returns. + /// + /// Custom IHttpClient modules are a legacy extension point and may retain + /// their own documented ownership behavior, including taking ownership. + /// Callers using a custom module must follow that module's contract. + /// + /// Every request accepted by an implementation must produce exactly one + /// terminal OnHttpResponse() callback, including after cancellation. If + /// this method throws, the request was not accepted: the implementation + /// must not invoke the callback before throwing or at any later time. + /// + /// On synchronous setup or validation failure, OnHttpResponse() may be + /// invoked before this method returns. Keep the callback object alive until + /// OnHttpResponse() returns. For portability, delete request objects created + /// by a client before destroying that client. /// /// The filled request object returned earlier by /// CreateRequest() @@ -549,16 +561,20 @@ namespace MAT_NS_BEGIN /// /// Cancels an HTTP request. /// The caller must provide a string ID returned earlier by request->GetId(). - /// The request is cancelled asynchronously. The caller must still - /// wait for the relevant OnHttpResponse() callback (it can just come - /// earlier with some "aborted" error status). + /// Cancellation is asynchronous. The built-in SDK transports still report + /// completion through the request's terminal OnHttpResponse() callback, so + /// the caller must keep the request alive and unchanged until that callback + /// begins. /// /// A string that contains the ID of the request to cancel. virtual void CancelRequestAsync(std::string const& id) = 0; /// /// Cancels all pending requests, draining fully before returning when the - /// implementation owns a synchronous drain. + /// implementation owns a synchronous transport drain. This method is not a + /// universal terminal-callback barrier; callers must still observe the + /// relevant OnHttpResponse() callbacks unless their implementation documents + /// a stronger guarantee. /// virtual void CancelAllRequests() {} diff --git a/lib/include/public/ILogConfiguration.hpp b/lib/include/public/ILogConfiguration.hpp index af1bc44c2..f806f87a5 100644 --- a/lib/include/public/ILogConfiguration.hpp +++ b/lib/include/public/ILogConfiguration.hpp @@ -154,6 +154,12 @@ namespace MAT_NS_BEGIN /// static constexpr const char* const CFG_INT_RAM_QUEUE_BUFFERS = "maxDBFlushQueues"; + /// + /// Batch records when flushing the RAM queue to disk storage. + /// Set to false to use per-record disk stores during flush. + /// + static constexpr const char* const CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH = "enableBatchedStorageFlush"; + /// /// SQLite DB will be checkpointed when flushing. /// @@ -372,7 +378,9 @@ namespace MAT_NS_BEGIN static constexpr const char* const CFG_BOOL_HTTP_COMPRESSION = "compress"; /// - /// HTTP configuration: SSL certificate verification (peer + host) + /// HTTP configuration: SSL certificate verification (peer + host). + /// Retained for compatibility; the curl transport always verifies TLS and + /// ignores attempts to set this value to false. /// static constexpr const char* const CFG_BOOL_HTTP_SSL_VERIFY = "sslVerify"; @@ -481,4 +489,3 @@ namespace MAT_NS_BEGIN } MAT_NS_END #endif - diff --git a/lib/include/public/ITaskDispatcher.hpp b/lib/include/public/ITaskDispatcher.hpp index 070f054bc..943b29b03 100644 --- a/lib/include/public/ITaskDispatcher.hpp +++ b/lib/include/public/ITaskDispatcher.hpp @@ -115,12 +115,17 @@ namespace MAT_NS_BEGIN virtual void Queue(Task* task) = 0; /// - /// Cancel a previously queued tasks + /// Cancel a previously queued task /// - /// Task to be cancelled + /// + /// Opaque task identity to cancel. The task may complete concurrently; + /// implementations must not dereference this pointer outside their own + /// queue/execution synchronization. + /// /// Amount of time to wait for if the task is currently executing /// True if successfully cancelled, else false virtual bool Cancel(Task* task, uint64_t waitTime = 0) = 0; + }; /// @endcond @@ -128,4 +133,3 @@ namespace MAT_NS_BEGIN } MAT_NS_END #endif // ITASKDISPATCHER_HPP - diff --git a/lib/include/public/mat.h b/lib/include/public/mat.h index 315627ca7..0403e4124 100644 --- a/lib/include/public/mat.h +++ b/lib/include/public/mat.h @@ -257,7 +257,20 @@ extern "C" { int32_t headersCount; } http_response_t; - /* HTTP callback function signatures */ + /* + * HTTP callback function signatures. + * + * http_send_fn_t borrows every pointer in http_request_t only for the + * duration of the call. Implementations must copy data needed by asynchronous + * work, return promptly without waiting for completion, and invoke the + * supplied http_complete_fn_t exactly once for each accepted request. + * Completion may be synchronous. Exceptions must not cross this C ABI. + * + * http_cancel_fn_t requests cancellation of the identified operation. The + * SDK adapter terminally reports HTTP_RESULT_CANCELLED after invoking the + * hook; the hook must not retain request pointers, and any later completion + * it attempts is ignored. + */ typedef void (EVTSDK_LIBABI_CDECL *http_complete_fn_t)(const char* /*requestId*/, http_result_t, http_response_t*); typedef void (EVTSDK_LIBABI_CDECL *http_send_fn_t)(http_request_t*, http_complete_fn_t); typedef void (EVTSDK_LIBABI_CDECL *http_cancel_fn_t)(const char* /*requestId*/); diff --git a/lib/jni/LogManager_jni.cpp b/lib/jni/LogManager_jni.cpp index 22b4244ee..70cb5b1ec 100644 --- a/lib/jni/LogManager_jni.cpp +++ b/lib/jni/LogManager_jni.cpp @@ -547,7 +547,7 @@ namespace { auto element = env->GetObjectArrayElement(value, i); rethrow(env); - array.emplace_back(std::move(translateVariant(element))); + array.emplace_back(translateVariant(element)); } } diff --git a/lib/modules b/lib/modules index 7bd8b516e..cc4e64156 160000 --- a/lib/modules +++ b/lib/modules @@ -1 +1 @@ -Subproject commit 7bd8b516e2d93d1704834e0895733ae7bc2d1f43 +Subproject commit cc4e64156f452cba05bba1506301f039e34fc43c diff --git a/lib/offline/IOfflineStorageProvider.hpp b/lib/offline/IOfflineStorageProvider.hpp new file mode 100644 index 000000000..3883893b8 --- /dev/null +++ b/lib/offline/IOfflineStorageProvider.hpp @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +#ifndef IOFFLINESTORAGEPROVIDER_HPP +#define IOFFLINESTORAGEPROVIDER_HPP + +#include "IOfflineStorage.hpp" + +#include + +namespace MAT_NS_BEGIN +{ + class IOfflineStorageProvider + { + public: + virtual ~IOfflineStorageProvider() = default; + + // Implementations may be shared by multiple handlers and must be + // thread-safe when Initialize is called concurrently. + virtual std::shared_ptr CreateDiskStorage( + ILogManager& logManager, IRuntimeConfig& runtimeConfig) = 0; + + virtual std::shared_ptr CreateMemoryStorage( + ILogManager& logManager, IRuntimeConfig& runtimeConfig) = 0; + }; +} +MAT_NS_END + +#endif // IOFFLINESTORAGEPROVIDER_HPP diff --git a/lib/offline/LogSessionDataProvider.cpp b/lib/offline/LogSessionDataProvider.cpp index 68e152d0e..ea3112c72 100644 --- a/lib/offline/LogSessionDataProvider.cpp +++ b/lib/offline/LogSessionDataProvider.cpp @@ -97,7 +97,8 @@ namespace MAT_NS_BEGIN void LogSessionDataProvider::DeleteLogSessionDataFromFile() { - std::string sessionPath = m_cacheFilePath.empty() ? "" : (m_cacheFilePath + ".ses").c_str(); + std::string sessionPath = + (m_cacheFilePath.empty() || m_cacheFilePath == ":memory:") ? "" : m_cacheFilePath + ".ses"; if (!sessionPath.empty() && MAT::FileExists(sessionPath.c_str())) { MAT::FileDelete(sessionPath.c_str()); @@ -108,7 +109,8 @@ namespace MAT_NS_BEGIN { uint64_t sessionFirstTimeLaunch = 0; std::string sessionSDKUid; - std::string sessionPath = m_cacheFilePath.empty() ? "" : (m_cacheFilePath + ".ses").c_str(); + std::string sessionPath = + (m_cacheFilePath.empty() || m_cacheFilePath == ":memory:") ? "" : m_cacheFilePath + ".ses"; if (!sessionPath.empty()) { if (MAT::FileExists(sessionPath.c_str())) @@ -127,6 +129,11 @@ namespace MAT_NS_BEGIN writeFileContents(sessionPath, sessionFirstTimeLaunch, sessionSDKUid); } } + else if (m_cacheFilePath == ":memory:") + { + sessionFirstTimeLaunch = PAL::getUtcSystemTimeMs(); + sessionSDKUid = PAL::generateUuidString(); + } m_logSessionData.reset(new LogSessionData(sessionFirstTimeLaunch, sessionSDKUid)); } @@ -209,4 +216,3 @@ namespace MAT_NS_BEGIN } } MAT_NS_END - diff --git a/lib/offline/MemoryStorage.cpp b/lib/offline/MemoryStorage.cpp index 1d4ec5664..77ff0fc7c 100644 --- a/lib/offline/MemoryStorage.cpp +++ b/lib/offline/MemoryStorage.cpp @@ -224,6 +224,16 @@ namespace MAT_NS_BEGIN { void MemoryStorage::DeleteRecords(const std::map & whereFilter) { + // An empty filter matches every record. Never silently wipe the whole + // in-memory queue from a no-op predicate; callers must use + // DeleteAllRecords() for an intentional full clear. This mirrors the + // fail-closed behavior of OfflineStorage_SQLite::DeleteRecords. + if (whereFilter.empty()) + { + LOG_WARN("DeleteRecords called with an empty filter; ignoring to avoid deleting all records."); + return; + } + auto matcher = [&](const StorageRecord &r, const std::map & whereFilter) { bool matched = true; diff --git a/lib/offline/OfflineStorageFactory.cpp b/lib/offline/OfflineStorageFactory.cpp index 221d2997b..fff62c823 100644 --- a/lib/offline/OfflineStorageFactory.cpp +++ b/lib/offline/OfflineStorageFactory.cpp @@ -8,6 +8,7 @@ #include "OfflineStorageFactory.hpp" +#include "offline/MemoryStorage.hpp" #ifdef USE_ROOM #include "offline/OfflineStorage_Room.hpp" #else @@ -18,6 +19,25 @@ namespace MAT_NS_BEGIN { + namespace + { + class DefaultOfflineStorageProvider final : public IOfflineStorageProvider + { + public: + std::shared_ptr CreateDiskStorage( + ILogManager& logManager, IRuntimeConfig& runtimeConfig) override + { + return OfflineStorageFactory::Create(logManager, runtimeConfig); + } + + std::shared_ptr CreateMemoryStorage( + ILogManager& logManager, IRuntimeConfig& runtimeConfig) override + { + return std::make_shared(logManager, runtimeConfig); + } + }; + } + std::shared_ptr OfflineStorageFactory::Create(ILogManager& logManager, IRuntimeConfig& runtimeConfig) { #ifdef HAVE_MAT_STORAGE @@ -40,6 +60,14 @@ namespace MAT_NS_BEGIN return nullptr; #endif //HAVE_MAT_STORAGE } + + std::shared_ptr OfflineStorageFactory::GetDefaultProvider() + { + // The default provider is stateless; sharing it avoids per-handler + // allocation while preserving a stable provider lifetime. + static std::shared_ptr provider = + std::make_shared(); + return provider; + } } MAT_NS_END - diff --git a/lib/offline/OfflineStorageFactory.hpp b/lib/offline/OfflineStorageFactory.hpp index 103bb078a..3a8f38d13 100644 --- a/lib/offline/OfflineStorageFactory.hpp +++ b/lib/offline/OfflineStorageFactory.hpp @@ -6,6 +6,7 @@ #define OFFLINESTORAGEFACTORY_HPP #include "IOfflineStorage.hpp" +#include "IOfflineStorageProvider.hpp" #include "api/IRuntimeConfig.hpp" namespace MAT_NS_BEGIN @@ -14,9 +15,9 @@ namespace MAT_NS_BEGIN { public: static std::shared_ptr Create(ILogManager& logManager, IRuntimeConfig& runtimeConfig); + static std::shared_ptr GetDefaultProvider(); }; } MAT_NS_END #endif // HTTPCLIENTFACTORY_HPP - diff --git a/lib/offline/OfflineStorageHandler.cpp b/lib/offline/OfflineStorageHandler.cpp index 52ce15515..50666e0de 100644 --- a/lib/offline/OfflineStorageHandler.cpp +++ b/lib/offline/OfflineStorageHandler.cpp @@ -7,22 +7,42 @@ #include "OfflineStorageFactory.hpp" #include "offline/MemoryStorage.hpp" +#include "offline/StorageRecordValidation.hpp" #include "ILogManager.hpp" +#include "utils/Utils.hpp" #include +#include +#include +#include +#include #include #include +#include namespace MAT_NS_BEGIN { + namespace + { + // Keep each persistence transaction bounded so a large in-memory backlog + // cannot monopolize memory or database locks. + constexpr unsigned MAX_RECORDS_PER_STORAGE_BATCH = 2000; + } + MATSDK_LOG_INST_COMPONENT_CLASS(OfflineStorageHandler, "EventsSDK.StorageHandler", "Events telemetry client - OfflineStorageHandler class") OfflineStorageHandler::OfflineStorageHandler(ILogManager& logManager, IRuntimeConfig& runtimeConfig, ITaskDispatcher& taskDispatcher) : + OfflineStorageHandler(logManager, runtimeConfig, taskDispatcher, OfflineStorageFactory::GetDefaultProvider()) + { + } + + OfflineStorageHandler::OfflineStorageHandler(ILogManager& logManager, IRuntimeConfig& runtimeConfig, ITaskDispatcher& taskDispatcher, std::shared_ptr storageProvider) : m_observer(nullptr), m_logManager(logManager), m_config(runtimeConfig), m_taskDispatcher(taskDispatcher), + m_storageProvider(std::move(storageProvider)), m_killSwitchManager(), m_clockSkewManager(), m_flushPending(false), @@ -36,6 +56,11 @@ namespace MAT_NS_BEGIN { m_cacheMemorySizeLimitInBytes(0), m_isStorageFullNotificationSend(false) { + if (!m_storageProvider) + { + MATSDK_THROW(std::invalid_argument("OfflineStorageHandler requires a storage provider")); + } + // TODO: [MG] - OfflineStorage_SQLite.cpp is performing similar checks uint32_t percentage = m_config[CFG_INT_RAMCACHE_FULL_PCT]; uint32_t cacheMemorySizeLimitInBytes = m_config[CFG_INT_RAM_QUEUE_SIZE]; @@ -50,6 +75,60 @@ namespace MAT_NS_BEGIN { } } + /// + /// RAII guard around ILogManager::StartActivity()/EndActivity(). Flush() + /// used to pair these manually (StartActivity() at the top, EndActivity() + /// on the last line), so an exception thrown by disk I/O or by + /// IOfflineStorageObserver::OnStorageRecordsSaved() partway through would + /// skip EndActivity() and permanently leak the pause-activity count -- + /// deadlocking every later FlushAndTeardown()'s WaitPause(). This guard + /// guarantees EndActivity() runs on every exit path, matching the existing + /// safe pattern used by PauseGuard (TransmissionPolicyManager.cpp) and + /// ActiveLoggerCall (Logger.cpp). + /// + class ActivityGuard + { + public: + explicit ActivityGuard(ILogManager& logManager) : + m_logManager(logManager), + m_active(logManager.StartActivity()), + m_allowInactive(false) + { + } + + ActivityGuard(ILogManager& logManager, bool allowInactive) : + m_logManager(logManager), + m_active(logManager.StartActivity()), + m_allowInactive(allowInactive) + { + } + + ~ActivityGuard() noexcept + { + if (m_active) + { + MATSDK_TRY + { + m_logManager.EndActivity(); + } + MATSDK_CATCH(...) + { + std::fputs("Failed to end telemetry activity\n", stderr); + } + } + } + + ActivityGuard(ActivityGuard const&) = delete; + ActivityGuard& operator=(ActivityGuard const&) = delete; + + bool IsActive() const noexcept { return m_active || m_allowInactive; } + + private: + ILogManager& m_logManager; + bool m_active; + bool m_allowInactive; + }; + bool OfflineStorageHandler::isKilled(StorageRecord const& record) { return ( @@ -64,7 +143,8 @@ namespace MAT_NS_BEGIN { if (!m_flushPending) return; } - LOG_INFO("Waiting for pending Flush (%p) to complete...", m_flushHandle.m_task); + LOG_INFO("Waiting for pending Flush (%p) to complete...", + static_cast(m_flushHandle.GetTask())); m_flushComplete.wait(); } @@ -86,7 +166,7 @@ namespace MAT_NS_BEGIN { m_observer = &observer; m_cacheMemorySizeLimitInBytes = m_config[CFG_INT_RAM_QUEUE_SIZE]; - m_offlineStorageDisk = OfflineStorageFactory::Create(m_logManager, m_config); + m_offlineStorageDisk = m_storageProvider->CreateDiskStorage(m_logManager, m_config); if (m_offlineStorageDisk) { m_offlineStorageDisk->Initialize(*this); @@ -97,7 +177,7 @@ namespace MAT_NS_BEGIN { // disk. if (m_cacheMemorySizeLimitInBytes > 0) { - m_offlineStorageMemory.reset(new MemoryStorage(m_logManager, m_config)); + m_offlineStorageMemory = m_storageProvider->CreateMemoryStorage(m_logManager, m_config); m_offlineStorageMemory->Initialize(*this); } @@ -163,65 +243,147 @@ namespace MAT_NS_BEGIN { void OfflineStorageHandler::Flush() { - if (!m_logManager.StartActivity()) { + // Shutdown has already paused normal logging, but its synchronous final + // flush must still persist the in-memory records before storage closes. + ActivityGuard activityGuard(m_logManager, m_shutdownStarted); + if (!activityGuard.IsActive()) { + // The LogManager is shutting down, so the flush cannot run. Still + // signal completion and clear the pending flag so a concurrent + // WaitForFlush() (e.g. during teardown) does not block forever + // waiting for m_flushComplete. + LOCKGUARD(m_flushLock); + m_flushHandle.Cancel(); + m_flushComplete.post(); + m_flushPending = false; return; } - // Flush could be executed from context of worker thread, as well as from TPM and - // after HTTP callback. Make sure it is atomic / thread-safe. - LOCKGUARD(m_flushLock); - - // If item isn't scheduled yet, it gets canceled, so that we don't do two flushes. - // If we are running that item right now (our thread), then nothing happens other - // than the handle gets replaced by nullptr in this DeferredCallbackHandle obj. - m_flushHandle.Cancel(); - - size_t dbSizeBeforeFlush = (m_offlineStorageMemory != nullptr) ? m_offlineStorageMemory->GetSize() : 0; - if ((m_offlineStorageMemory) && (dbSizeBeforeFlush > 0) && (m_offlineStorageDisk)) + std::vector recordsToRecover; + MATSDK_TRY { - // This will block on and then take a lock for the duration of this move, and - // StoreRecord() will then block until the move completes. - auto records = m_offlineStorageMemory->GetRecords(false, EventLatency_Unspecified); - std::vector ids; - - // TODO: [MG] - consider running the batch in transaction - // if (sqlite) - // sqlite->Execute("BEGIN"); + // Flush could be executed from context of worker thread, as well as from TPM and + // after HTTP callback. Make sure it is atomic / thread-safe. + LOCKGUARD(m_flushLock); - size_t totalSaved = m_offlineStorageDisk->StoreRecords(records); + // If item isn't scheduled yet, it gets canceled, so that we don't do two flushes. + // If we are running that item right now (our thread), then nothing happens other + // than the handle reporting nullptr once that task finishes. + m_flushHandle.Cancel(); - // TODO: [MG] - consider running the batch in transaction - // if (sqlite) - // sqlite->Execute("END"); + size_t dbSizeBeforeFlush = (m_offlineStorageMemory != nullptr) ? m_offlineStorageMemory->GetSize() : 0; + if ((m_offlineStorageMemory) && (dbSizeBeforeFlush > 0) && (m_offlineStorageDisk)) + { + size_t totalSaved = 0; + if (IsBatchedStorageFlushEnabled()) + { + // Drain only the records present when this flush started so + // producers cannot keep the flush alive indefinitely. + size_t recordsRemaining = m_offlineStorageMemory->GetRecordCount(); + while (recordsRemaining > 0) + { + recordsToRecover = m_offlineStorageMemory->GetRecords( + false, EventLatency_Unspecified, MAX_RECORDS_PER_STORAGE_BATCH); + if (recordsToRecover.empty()) + { + break; + } + + const size_t drainedBatchSize = recordsToRecover.size(); + recordsRemaining -= std::min(recordsRemaining, drainedBatchSize); + + auto memoryOnlyBegin = std::partition( + recordsToRecover.begin(), recordsToRecover.end(), + [](StorageRecord const& record) + { + return record.persistence != EventPersistence_DoNotStoreOnDisk; + }); + std::vector memoryOnlyRecords( + std::make_move_iterator(memoryOnlyBegin), + std::make_move_iterator(recordsToRecover.end())); + recordsToRecover.erase(memoryOnlyBegin, recordsToRecover.end()); + ReturnRecordsToMemory(memoryOnlyRecords); + + recordsToRecover.erase( + std::remove_if(recordsToRecover.begin(), recordsToRecover.end(), + [this](StorageRecord const& record) + { + if (IsValidDiskStorageRecord(record)) + { + return false; + } + ReportInvalidDiskRecord(record); + return true; + }), + recordsToRecover.end()); + + const size_t batchSaved = recordsToRecover.empty() + ? 0 + : m_offlineStorageDisk->StoreRecords(recordsToRecover); + if (batchSaved != recordsToRecover.size()) + { + LOG_WARN("Flush: disk store failed for the batch of %zu records; returning it to the queue for retry", + recordsToRecover.size()); + ReturnRecordsToMemory(recordsToRecover); + recordsToRecover.clear(); + break; + } + + totalSaved += batchSaved; + recordsToRecover.clear(); + } + } + else + { + // Preserve the legacy per-record path and its unlimited drain. + recordsToRecover = m_offlineStorageMemory->GetRecords( + false, EventLatency_Unspecified); + totalSaved = StoreRecordsIndividually(recordsToRecover); + } - // Delete records from reserved on flush - HttpHeaders dummy; - bool fromMemory = true; - m_offlineStorageMemory->DeleteRecords(ids, dummy, fromMemory); + // Persistence and retry handling are complete; a later exception + // must not requeue records that were already committed. + recordsToRecover.clear(); - // Notify event listener about the records cached - OnStorageRecordsSaved(totalSaved); + if (m_offlineStorageMemory->GetSize() > dbSizeBeforeFlush) + { + // We managed to accumulate as much data as we had before the flush, + // means we cannot keep up flushing at the same speed as incoming + // obviously because the disk is slower than ram. + LOG_WARN("Data is arriving too fast!"); + } + OnStorageRecordsSaved(totalSaved); + } - if (m_offlineStorageMemory->GetSize() > dbSizeBeforeFlush) + // Checkpoint DB + if (m_offlineStorageDisk && m_config.HasConfig(CFG_BOOL_CHECKPOINT_DB_ON_FLUSH) && m_config[CFG_BOOL_CHECKPOINT_DB_ON_FLUSH]) { - // We managed to accumulate as much data as we had before the flush, - // means we cannot keep up flushing at the same speed as incoming - // obviously because the disk is slower than ram. - LOG_WARN("Data is arriving too fast!"); + m_offlineStorageDisk->Flush(); } - } - // Checkpoint DB - if (m_config.HasConfig(CFG_BOOL_CHECKPOINT_DB_ON_FLUSH) && m_config[CFG_BOOL_CHECKPOINT_DB_ON_FLUSH]) + m_isStorageFullNotificationSend = false; + m_flushComplete.post(); + m_flushPending = false; + } + MATSDK_CATCH(...) { - m_offlineStorageDisk->Flush(); +#if HAVE_EXCEPTIONS + std::exception_ptr failure = std::current_exception(); + MATSDK_TRY + { + if (m_offlineStorageMemory && !recordsToRecover.empty()) + { + ReturnRecordsToMemory(recordsToRecover); + } + } + MATSDK_CATCH(...) + { + std::fputs("Failed to recover records after flush failure\n", stderr); + } + LOCKGUARD(m_flushLock); + m_flushComplete.post(); + m_flushPending = false; + std::rethrow_exception(failure); +#endif } - - m_isStorageFullNotificationSend = false; - - // Flush is done, notify the waiters - m_flushComplete.post(); - m_flushPending = false; - m_logManager.EndActivity(); } bool OfflineStorageHandler::StoreRecord(StorageRecord const& record) @@ -247,7 +409,21 @@ namespace MAT_NS_BEGIN { // are selected and removed from the cache (but will // not block for the subsequent handoff to persistent // storage) - m_offlineStorageMemory->StoreRecord(record); + if (!m_offlineStorageMemory->StoreRecord(record)) + { + if (record.latency == EventLatency_Off) + { + // MemoryStorage intentionally returns false for latency-off + // records to mean "drop without storing", not "storage + // failed". Keep the handler's false return reserved for + // genuine storage failures so StorageObserver does not + // misclassify this normal drop as a persistence error. + return true; + } + LOG_ERROR("Failed to store event %s:%s in memory queue", + tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); + return false; + } } // Perform periodic flush to disk @@ -260,7 +436,15 @@ namespace MAT_NS_BEGIN { m_flushPending = true; m_flushComplete.Reset(); m_flushHandle = PAL::scheduleTask(&m_taskDispatcher, 0, this, &OfflineStorageHandler::Flush); - LOG_INFO("Requested Flush (%p)", m_flushHandle.m_task); + if (m_flushHandle.GetTask() == nullptr) + { + // The dispatcher may drop a task synchronously during + // shutdown. Do not leave WaitForFlush blocked forever. + m_flushPending = false; + m_flushComplete.post(); + } + LOG_INFO("Requested Flush (%p)", + static_cast(m_flushHandle.GetTask())); } m_flushLock.unlock(); } @@ -272,7 +456,9 @@ namespace MAT_NS_BEGIN { { if (record.persistence != EventPersistence::EventPersistence_DoNotStoreOnDisk) { - m_offlineStorageDisk->StoreRecord(record); + // Propagate a synchronous disk write failure to the caller so a + // failed store is not counted as successfully persisted. + return m_offlineStorageDisk->StoreRecord(record); } } } @@ -280,6 +466,137 @@ namespace MAT_NS_BEGIN { return true; } + bool OfflineStorageHandler::IsBatchedStorageFlushEnabled() + { + const bool batchingConfigured = + !m_config.HasConfig(CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH) || + m_config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH]; + const bool usingCustomStorage = + m_logManager.GetLogConfiguration().GetModule(CFG_MODULE_OFFLINE_STORAGE) != nullptr; + return batchingConfigured && !usingCustomStorage; + } + + void OfflineStorageHandler::ReportInvalidDiskRecord(StorageRecord const& record) + { + (void)record; + LOG_ERROR("Flush: dropping event %s:%s: Invalid parameters", + tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); + OnStorageFailed("Invalid parameters"); + } + + size_t OfflineStorageHandler::StoreRecordsIndividually(std::vector& records) + { + size_t totalSaved = 0; + std::vector recordsToRetry; + std::vector memoryOnlyRecords; + size_t nextRecord = 0; + + MATSDK_TRY + { + for (; nextRecord < records.size(); ++nextRecord) + { + auto const& record = records[nextRecord]; + if (record.persistence == EventPersistence_DoNotStoreOnDisk) + { + memoryOnlyRecords.push_back(record); + continue; + } + + if (!IsValidDiskStorageRecord(record)) + { + ReportInvalidDiskRecord(record); + continue; + } + + if (m_offlineStorageDisk->StoreRecord(record)) + { + ++totalSaved; + continue; + } + + for (size_t retryIndex = nextRecord; retryIndex < records.size(); ++retryIndex) + { + auto const& retryRecord = records[retryIndex]; + if (IsValidDiskStorageRecord(retryRecord)) + { + recordsToRetry.push_back(retryRecord); + } + else + { + ReportInvalidDiskRecord(retryRecord); + } + } + break; + } + } + MATSDK_CATCH(...) + { +#if HAVE_EXCEPTIONS + recordsToRetry.clear(); + for (size_t retryIndex = nextRecord; retryIndex < records.size(); ++retryIndex) + { + if (IsValidDiskStorageRecord(records[retryIndex])) + { + recordsToRetry.push_back(records[retryIndex]); + } + } + records.clear(); + ReturnRecordsToMemory(memoryOnlyRecords); + ReturnRecordsToMemory(recordsToRetry); + std::rethrow_exception(std::current_exception()); +#endif + } + + ReturnRecordsToMemory(memoryOnlyRecords); + if (!recordsToRetry.empty()) + { + LOG_WARN("Flush: per-record disk store failed after saving %zu of %zu records; returning %zu records to the queue for retry", + totalSaved, records.size(), recordsToRetry.size()); + ReturnRecordsToMemory(recordsToRetry); + } + + return totalSaved; + } + + size_t OfflineStorageHandler::ReturnRecordsToMemory(std::vector const& records) + { + size_t returned = 0; + DroppedMap dropped; + + for (auto const& record : records) + { + MATSDK_TRY + { + if (m_offlineStorageMemory && m_offlineStorageMemory->StoreRecord(record)) + { + ++returned; + continue; + } + LOG_ERROR("Flush: failed to return event %s:%s to memory queue after disk store failure; dropping record", + tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); + dropped[record.tenantToken]++; + } + MATSDK_CATCH(...) + { + std::fputs("Failed to recover a record after flush failure\n", stderr); + } + } + + if (!dropped.empty()) + { + MATSDK_TRY + { + OnStorageRecordsDropped(dropped); + } + MATSDK_CATCH(...) + { + std::fputs("Failed to report dropped records after flush failure\n", stderr); + } + } + + return returned; + } + size_t OfflineStorageHandler::StoreRecords(std::vector& records) { size_t stored = 0; diff --git a/lib/offline/OfflineStorageHandler.hpp b/lib/offline/OfflineStorageHandler.hpp index 9a1131aff..0eae7510b 100644 --- a/lib/offline/OfflineStorageHandler.hpp +++ b/lib/offline/OfflineStorageHandler.hpp @@ -8,6 +8,7 @@ #include "pal/PAL.hpp" #include "IOfflineStorage.hpp" +#include "IOfflineStorageProvider.hpp" #include "api/IRuntimeConfig.hpp" #include "ILogManager.hpp" @@ -27,6 +28,8 @@ namespace MAT_NS_BEGIN { { public: OfflineStorageHandler(ILogManager& logManager, IRuntimeConfig& runtimeConfig, ITaskDispatcher& taskDispatcher); + OfflineStorageHandler(ILogManager& logManager, IRuntimeConfig& runtimeConfig, + ITaskDispatcher& taskDispatcher, std::shared_ptr storageProvider); virtual ~OfflineStorageHandler() override; virtual void Initialize(IOfflineStorageObserver& observer) override; virtual void Shutdown() override; @@ -71,6 +74,7 @@ namespace MAT_NS_BEGIN { std::string m_databasePath; IRuntimeConfig& m_config; ITaskDispatcher& m_taskDispatcher; + std::shared_ptr m_storageProvider; KillSwitchManager m_killSwitchManager; ClockSkewManager m_clockSkewManager; @@ -82,11 +86,11 @@ namespace MAT_NS_BEGIN { PAL::DeferredCallbackHandle m_flushHandle; PAL::Event m_flushComplete; - std::unique_ptr m_offlineStorageMemory; + std::shared_ptr m_offlineStorageMemory; std::shared_ptr m_offlineStorageDisk; - bool m_readFromMemory; - unsigned m_lastReadCount; + std::atomic m_readFromMemory; + std::atomic m_lastReadCount; bool m_shutdownStarted; unsigned m_memoryDbSize; @@ -100,6 +104,10 @@ namespace MAT_NS_BEGIN { private: void WaitForFlush(); + bool IsBatchedStorageFlushEnabled(); + void ReportInvalidDiskRecord(StorageRecord const& record); + size_t StoreRecordsIndividually(std::vector& records); + size_t ReturnRecordsToMemory(std::vector const& records); }; diff --git a/lib/offline/OfflineStorage_Room.cpp b/lib/offline/OfflineStorage_Room.cpp index 5ea0611e0..4f988515e 100644 --- a/lib/offline/OfflineStorage_Room.cpp +++ b/lib/offline/OfflineStorage_Room.cpp @@ -289,7 +289,6 @@ namespace MAT_NS_BEGIN auto room_class = env->GetObjectClass(m_room); auto method = env->GetMethodID(room_class, "deleteById", "([J)J"); ThrowLogic(env, "Unable to get deleteById method"); - size_t index = 0; /* Convert string identifiers to int64_t */ @@ -506,9 +505,9 @@ namespace MAT_NS_BEGIN record, latency_id)))); ThrowLogic(env, "get latency"); - auto persistence = static_cast(std::max(latency_lb, + auto persistence = static_cast(std::max(persist_lb, std::min( - latency_ub, + persist_ub, env->GetIntField( record, persistence_id)))); @@ -852,8 +851,6 @@ namespace MAT_NS_BEGIN return 0; } - static constexpr char newRecordSignature[] = - "(JIIJIJ[B)Lcom/microsoft/applications/events/StorageRecord;"; if (!m_room) { return 0; @@ -1321,7 +1318,7 @@ namespace MAT_NS_BEGIN ThrowRuntime(env, "call getRecords"); auto result_count = env->GetArrayLength(java_records); records.reserve(result_count); - for (size_t record_index = 0; record_index < result_count; ++record_index) + for (jsize record_index = 0; record_index < result_count; ++record_index) { env.pushLocalFrame(64); auto record = env->GetObjectArrayElement(java_records, record_index); diff --git a/lib/offline/OfflineStorage_SQLite.cpp b/lib/offline/OfflineStorage_SQLite.cpp index b9b2ed83d..516629163 100644 --- a/lib/offline/OfflineStorage_SQLite.cpp +++ b/lib/offline/OfflineStorage_SQLite.cpp @@ -8,6 +8,7 @@ #include "OfflineStorage_SQLite.hpp" #include "ILogManager.hpp" #include "SQLiteWrapper.hpp" +#include "StorageRecordValidation.hpp" #include "utils/StringUtils.hpp" #include #include @@ -18,11 +19,32 @@ namespace MAT_NS_BEGIN { constexpr static size_t kBlockSize = 8192; + EventLatency NormalizePersistedLatency(int latency) + { + if (latency < EventLatency_Off || latency > EventLatency_Max) + { + return EventLatency_Normal; + } + return static_cast(latency); + } + std::mutex OfflineStorage_SQLite::m_initAndShutdownLock; int OfflineStorage_SQLite::m_instanceCount = 0; + bool OfflineStorage_SQLite::m_ownsTempDirectory = false; + + static std::string GetRequiredSqliteTempDirectory() + { +#if defined(ANDROID) || defined(_WINRT_DLL) + return GetTempDirectory(); +#else + return {}; +#endif + } class DbTransaction { SqliteDB* m_db; + bool m_rollback = false; + bool m_finished = false; public: bool locked; @@ -34,11 +56,43 @@ namespace MAT_NS_BEGIN { } } + // Discard the transaction (ROLLBACK) instead of committing it on destruction. + void markForRollback() + { + m_rollback = true; + } + + // Commit the transaction now and report whether COMMIT succeeded. On a + // COMMIT failure the transaction is rolled back so it is never left open, + // and false is returned so the caller does not treat undurable writes as + // stored. After this call the destructor performs no further COMMIT/ROLLBACK. + bool commit() + { + if (!locked || m_finished) + { + return false; + } + m_finished = true; + if (m_db->unlock()) + { + return true; + } + m_db->rollback(); + return false; + } + ~DbTransaction() { - if (locked) + if (locked && !m_finished) { - m_db->unlock(); + if (m_rollback) + { + m_db->rollback(); + } + else + { + m_db->unlock(); + } } } }; @@ -88,22 +142,22 @@ namespace MAT_NS_BEGIN { } } - OfflineStorage_SQLite::~OfflineStorage_SQLite() - { - assert(!m_db); - } + OfflineStorage_SQLite::~OfflineStorage_SQLite() = default; void OfflineStorage_SQLite::Initialize(IOfflineStorageObserver& observer) { + LOCKGUARD(m_lock); m_observer = &observer; assert(!m_db); m_db.reset(new SqliteDB(m_skipInitAndShutdown, &m_initAndShutdownLock, - &m_instanceCount)); + &m_instanceCount, &m_ownsTempDirectory)); LOG_TRACE("Initializing offline storage: %s", m_offlineStorageFileName.c_str()); auto sqlStartTime = GetUptimeMs(); - if (m_db->initialize(m_offlineStorageFileName, false, m_DbSizeHeapLimit) && initializeDatabase()) { + if (m_db->initialize(m_offlineStorageFileName, false, m_DbSizeHeapLimit, + GetRequiredSqliteTempDirectory()) && + initializeDatabase()) { LOG_INFO("Using configured on-disk database"); m_observer->OnStorageOpened("SQLite/Default"); sqlStartTime = GetUptimeMs() - sqlStartTime; @@ -127,60 +181,51 @@ namespace MAT_NS_BEGIN { LOG_TRACE("Shutting down offline storage %s", m_offlineStorageFileName.c_str()); LOCKGUARD(m_lock); if (m_db) { - if (m_isOpened) { - m_db->shutdown(); - m_db.reset(); - } + m_db->shutdown(); + m_db.reset(); m_isOpened = false; } } void OfflineStorage_SQLite::Flush() { + LOCKGUARD(m_lock); if (m_db) m_db->flush(); } void OfflineStorage_SQLite::Execute(std::string command) { + LOCKGUARD(m_lock); if (m_db) m_db->execute(command.c_str()); } - bool OfflineStorage_SQLite::StoreRecord(StorageRecord const& record) + bool OfflineStorage_SQLite::isValidRecord(StorageRecord const& record) const { - // TODO: [MG] - this works, but may not play nicely with several LogManager instances - // static SqliteStatement sql_insert(*m_db, m_stmtInsertEvent_id_tenant_prio_ts_data); - - if (record.id.empty() || record.tenantToken.empty() || static_cast(record.latency) < 0 || record.timestamp <= 0) { + if (!IsValidDiskStorageRecord(record)) { LOG_ERROR("Failed to store event %s:%s: Invalid parameters", tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); m_observer->OnStorageFailed("Invalid parameters"); return false; } + return true; + } - if (!m_db) { - LOG_ERROR("Failed to store event %s:%s: Database is not open", + bool OfflineStorage_SQLite::insertRecordUnsafe(StorageRecord const& record) + { + if (!SqliteStatement(*m_db, m_stmtInsertEvent_id_tenant_prio_ts_data).execute(record.id, record.tenantToken, static_cast(record.latency), static_cast(record.persistence), record.timestamp, record.blob)) + { + LOG_ERROR("Failed to store event %s:%s: database write failed", tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); - m_observer->OnStorageOpenFailed("Database is not open"); return false; } + m_DbSizeEstimate += record.id.size() + record.tenantToken.size() + record.blob.size(); + return true; + } - { -#ifdef ENABLE_LOCKING - LOCKGUARD(m_lock); - DbTransaction transaction(m_db.get()); - if (!transaction.locked) - { - LOG_ERROR("Failed to store event %s:%s: Database error", tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); - m_observer->OnStorageFailed("Database error"); - return false; - } -#endif - SqliteStatement(*m_db, m_stmtInsertEvent_id_tenant_prio_ts_data).execute(record.id, record.tenantToken, static_cast(record.latency), static_cast(record.persistence), record.timestamp, record.blob); - m_DbSizeEstimate += record.id.size() + record.tenantToken.size() + record.blob.size(); - } - + void OfflineStorage_SQLite::checkStorageSizeLimits() + { if ((m_DbSizeNotificationLimit != 0) && (m_DbSizeEstimate>m_DbSizeNotificationLimit)) { auto now = PAL::getMonotonicTimeMs(); @@ -210,20 +255,175 @@ namespace MAT_NS_BEGIN { m_resizing = false; } } + } - return true; + bool OfflineStorage_SQLite::StoreRecord(StorageRecord const& record) + { + // TODO: [MG] - this works, but may not play nicely with several LogManager instances + // static SqliteStatement sql_insert(*m_db, m_stmtInsertEvent_id_tenant_prio_ts_data); + + if (!isValidRecord(record)) { + return false; + } + + bool stored = false; + { + LOCKGUARD(m_lock); + if (!m_db) { + LOG_ERROR("Failed to store event %s:%s: Database is not open", + tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); + m_observer->OnStorageOpenFailed("Database is not open"); + return false; + } +#ifdef ENABLE_LOCKING + DbTransaction transaction(m_db.get()); + if (!transaction.locked) + { + LOG_ERROR("Failed to store event %s:%s: Database error", tenantTokenToId(record.tenantToken).c_str(), record.id.c_str()); + m_observer->OnStorageFailed("Database error"); + return false; + } + if (insertRecordUnsafe(record)) + { + // Verify the COMMIT: a COMMIT that fails must not be reported as a + // successful store, or the caller treats an undurable write as saved. + stored = transaction.commit(); + if (!stored) + { + m_DbSizeEstimate -= std::min(m_DbSizeEstimate.load(), + record.id.size() + record.tenantToken.size() + record.blob.size()); + } + } + else + { + transaction.markForRollback(); + } +#else + stored = insertRecordUnsafe(record); +#endif + } + + if (!stored) { + // Report the write failure after the transaction has closed, so the + // observer callback never runs while BEGIN EXCLUSIVE is held. + m_observer->OnStorageFailed("Database write failed"); + } + + // Run the size-limit check after the transaction, matching the original + // per-record path (which ran it on every StoreRecord call). + checkStorageSizeLimits(); + + return stored; } size_t OfflineStorage_SQLite::StoreRecords(std::vector & records) { - size_t stored = 0; - for (auto & i : records) { - if (StoreRecord(i)) { - ++stored; + if (records.empty()) { + return 0; + } + + // Drop invalid records up front (each is reported by isValidRecord) so a + // permanently-invalid record is discarded rather than failing the whole + // batch. Removing them from the vector means a caller that re-queues on a + // short return (e.g. Flush) never re-queues a poison record -- which would + // be re-drained and re-rejected on every flush, blocking every valid record + // behind it -- while the valid remainder stays all-or-nothing. + records.erase( + std::remove_if(records.begin(), records.end(), + [this](StorageRecord const& record) { return !isValidRecord(record); }), + records.end()); + + if (records.empty()) { + // Every record was invalid (already reported). + return 0; + } + + size_t addedSize = 0; + bool committed = false; + { + LOCKGUARD(m_lock); + if (!m_db) { + LOG_ERROR("Failed to store %zu events: Database is not open", records.size()); + m_observer->OnStorageOpenFailed("Database is not open"); + return 0; + } + // Batch all inserts into a single transaction: one BEGIN EXCLUSIVE / + // COMMIT (one fsync) for the whole flush instead of one per record. + // All-or-nothing: if any insert OR the COMMIT fails the transaction is + // rolled back, so callers (e.g. Flush) can re-queue the whole batch + // without risking duplicate rows (the events table has no unique + // record_id constraint). + bool allInserted = true; +#ifdef ENABLE_LOCKING + DbTransaction transaction(m_db.get()); + if (!transaction.locked) + { + LOG_ERROR("Failed to store %zu events: Database error", records.size()); + m_observer->OnStorageFailed("Database error"); + return 0; + } +#endif + MATSDK_TRY + { + for (auto const& r : records) { + if (insertRecordUnsafe(r)) { + addedSize += r.id.size() + r.tenantToken.size() + r.blob.size(); + } + else { + allInserted = false; + break; + } + } + } +#if HAVE_EXCEPTIONS + MATSDK_CATCH(...) + { +#ifdef ENABLE_LOCKING + // DbTransaction commits on destruction by default for legacy + // callers. An exception during a batch must explicitly roll + // back so Flush can safely requeue the entire batch. + transaction.markForRollback(); +#endif + // insertRecordUnsafe updates the estimate before the + // transaction commits; undo inserts that will be rolled back. + m_DbSizeEstimate -= std::min(m_DbSizeEstimate.load(), addedSize); + MATSDK_THROW; + } +#endif + +#ifdef ENABLE_LOCKING + if (allInserted) { + // Verify the COMMIT: a COMMIT that fails (e.g. SQLITE_FULL/IOERR) + // must not be reported as success, or Flush would drop the records + // it already drained from memory. + committed = transaction.commit(); + } + else { + transaction.markForRollback(); + } +#else + committed = allInserted; +#endif + + if (!committed) { + // Nothing durably stored; undo the size estimate added by the + // (rolled-back) inserts. + m_DbSizeEstimate -= std::min(m_DbSizeEstimate.load(), addedSize); } } - return stored; + + if (!committed) { + // The whole batch was rolled back after an insert or COMMIT failure; + // report once. + m_observer->OnStorageFailed("Database write failed"); + } + + // Run the size-full notification / resize check once after the batch, + // matching the original per-record path (which ran it on every insert). + checkStorageSizeLimits(); + + return committed ? records.size() : 0; } // Debug routine to print record count in the DB @@ -249,6 +449,7 @@ namespace MAT_NS_BEGIN { /// bool OfflineStorage_SQLite::GetAndReserveRecords(std::function const& consumer, unsigned leaseTimeMs, EventLatency minLatency, unsigned maxCount) { + LOCKGUARD(m_lock); m_lastReadCount = 0; if (!m_db) { @@ -260,7 +461,6 @@ namespace MAT_NS_BEGIN { maxCount, (maxCount > 0) ? "" : " (unlimited)", minLatency, latencyToStr(static_cast(minLatency))); /* ============================================================================================================= */ - LOCKGUARD(m_lock); { #ifdef ENABLE_LOCKING DbTransaction transaction(m_db.get()); @@ -295,12 +495,7 @@ namespace MAT_NS_BEGIN { while (selectStmt.getRow(record.id, record.tenantToken, latency, record.timestamp, record.retryCount, record.reservedUntil, record.blob)) { - if (latency < EventLatency_Off || latency > EventLatency_Max) { - record.latency = EventLatency_Normal; - } - else { - record.latency = static_cast(latency); - } + record.latency = NormalizePersistedLatency(latency); consumedIds.push_back(record.id); if (!consumer(std::move(record))) { @@ -347,6 +542,7 @@ namespace MAT_NS_BEGIN { unsigned OfflineStorage_SQLite::LastReadRecordCount() { + LOCKGUARD(m_lock); return m_lastReadCount; } @@ -355,6 +551,7 @@ namespace MAT_NS_BEGIN { std::vector records; StorageRecord record; + LOCKGUARD(m_lock); if (!isOpen()) { return records; } @@ -367,7 +564,7 @@ namespace MAT_NS_BEGIN { int latency; while (selectStmt.getRow(record.id, record.tenantToken, latency, record.timestamp, record.retryCount, record.reservedUntil, record.blob)) { - record.latency = static_cast(latency); + record.latency = NormalizePersistedLatency(latency); records.push_back(record); } selectStmt.reset(); @@ -381,7 +578,7 @@ namespace MAT_NS_BEGIN { int latency; while (selectStmt.getRow(record.id, record.tenantToken, latency, record.timestamp, record.retryCount, record.reservedUntil, record.blob)) { - record.latency = static_cast(latency); + record.latency = NormalizePersistedLatency(latency); records.push_back(record); } selectStmt.reset(); @@ -399,11 +596,11 @@ namespace MAT_NS_BEGIN { void OfflineStorage_SQLite::DeleteRecords(const std::map & whereFilter) { + LOCKGUARD(m_lock); if (!isOpen()) { return; } - LOCKGUARD(m_lock); { #ifdef ENABLE_LOCKING DbTransaction transaction(m_db.get()); @@ -519,6 +716,7 @@ namespace MAT_NS_BEGIN { return; } + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to delete %u sent event(s) {%s%s}: Database is not open", static_cast(ids.size()), ids.front().c_str(), (ids.size() > 1) ? ", ..." : ""); @@ -526,7 +724,6 @@ namespace MAT_NS_BEGIN { } /* ============================================================================================================= */ - LOCKGUARD(m_lock); { #ifdef ENABLE_LOCKING DbTransaction transaction(m_db.get()); @@ -562,13 +759,13 @@ namespace MAT_NS_BEGIN { if (ids.empty()) { return; } + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to release %u event(s) {%s%s}, retry count %s: Database is not open", static_cast(ids.size()), ids.front().c_str(), (ids.size() > 1) ? ", ..." : "", incrementRetryCount ? "+1" : "not changed"); return; } - LOCKGUARD(m_lock); { #ifdef ENABLE_LOCKING DbTransaction transaction(m_db.get()); @@ -644,6 +841,7 @@ namespace MAT_NS_BEGIN { return false; } + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to set setting \"%s\": Database is not open", name.c_str()); return false; @@ -676,6 +874,7 @@ namespace MAT_NS_BEGIN { return result; } + LOCKGUARD(m_lock); if (!isOpen()) { LOG_ERROR("Oddly closed"); return result; @@ -706,6 +905,7 @@ namespace MAT_NS_BEGIN { LOG_ERROR("Failed to delete setting \"%s\": Name cannot be empty", name.c_str()); return false; } + LOCKGUARD(m_lock); if (!isOpen()) { LOG_ERROR("Oddly closed"); return false; @@ -734,7 +934,8 @@ namespace MAT_NS_BEGIN { { m_db->shutdown(); // Try again with deletePrevious = true - if (m_db->initialize(m_offlineStorageFileName, true)) { + if (m_db->initialize(m_offlineStorageFileName, true, 0, + GetRequiredSqliteTempDirectory())) { if (initializeDatabase()) { m_observer->OnStorageOpened("SQLite/Clean"); LOG_INFO("Using configured on-disk database after deleting the existing one"); @@ -756,12 +957,6 @@ namespace MAT_NS_BEGIN { SqliteStatement(*m_db, "PRAGMA auto_vacuum=FULL").select(); SqliteStatement(*m_db, "PRAGMA journal_mode=WAL").select(); SqliteStatement(*m_db, "PRAGMA synchronous=NORMAL").select(); - { - std::ostringstream tempPragma; - tempPragma << "PRAGMA temp_store_directory = '" << GetTempDirectory() << "'"; - SqliteStatement(*m_db, tempPragma.str().c_str()).select(); - LOG_INFO("Set sqlite3 temp_store_directory to '%s'", sqlite3_temp_directory); - } int openedDbVersion; { @@ -825,19 +1020,8 @@ namespace MAT_NS_BEGIN { if (!stmt.select() || !stmt.getRow(m_pageSize)) { return false; } } -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable:4296) // expression always false. -#elif defined( __clang__) -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wtype-limits" // error: comparison of unsigned expression < 0 is always false [-Werror=type-limits] -#elif defined(__GNUC__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wtype-limits" // error: comparison of unsigned expression < 0 is always false [-Werror=type-limits] -#endif - #define PREPARE_SQL(var_, stmt_) \ - if ((var_ = m_db->prepare(stmt_)) < 0) { return false; } + if ((var_ = m_db->prepare(stmt_)) == 0) { return false; } #ifdef ENABLE_LOCKING PREPARE_SQL(m_stmtBeginTransaction, @@ -923,26 +1107,18 @@ namespace MAT_NS_BEGIN { #undef PREPARE_SQL -#if defined(_MSC_VER) -#pragma warning(pop) -#elif defined(__clang__) -#pragma clang diagnostic pop -#elif defined(__GNUC__) -#pragma GCC diagnostic pop -#endif - ResizeDb(); return true; } size_t OfflineStorage_SQLite::GetSize() { + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to get DB size: database is not open"); return 0; } - LOCKGUARD(m_lock); unsigned pageCount = 0; SqliteStatement pageCountStmt(*m_db, m_stmtGetPageCount); if (!pageCountStmt.select()) @@ -977,28 +1153,29 @@ namespace MAT_NS_BEGIN { size_t OfflineStorage_SQLite::GetRecordCount(EventLatency latency = EventLatency_Unspecified) const { + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to get DB size: database is not open"); return 0; } - LOCKGUARD(m_lock); return OfflineStorage_SQLite::GetRecordCountUnsafe(latency); } bool OfflineStorage_SQLite::ResizeDb() { + LOCKGUARD(m_lock); if (!m_db) { LOG_ERROR("Failed to resize DB: database is not open"); return false; } size_t eventsDropped = 0; + bool compactDatabase = false; m_DbSizeEstimate = GetSize(); if (m_DbSizeEstimate <= m_DbSizeLimit) return false; - LOCKGUARD(m_lock); { #ifdef ENABLE_LOCKING DbTransaction transaction(m_db.get()); @@ -1012,9 +1189,17 @@ namespace MAT_NS_BEGIN { if (m_DbSizeEstimate > 2 * m_DbSizeLimit) { LOG_TRACE("DB is too big, deleting..."); - Execute("DELETE FROM " TABLE_NAME_EVENTS); - Execute("VACUUM"); + if (!SqliteStatement(*m_db, "DELETE FROM " TABLE_NAME_EVENTS).execute()) + { +#ifdef ENABLE_LOCKING + transaction.markForRollback(); +#endif + LOG_ERROR("Failed to delete events while resizing database"); + m_observer->OnStorageFailed("Database resize failed"); + return false; + } eventsDropped = count; + compactDatabase = true; } else { @@ -1029,6 +1214,26 @@ namespace MAT_NS_BEGIN { LOG_TRACE("Db resized, events dropped: %zu", eventsDropped); trimStmt.reset(); } + +#ifdef ENABLE_LOCKING + if (!transaction.commit()) + { + LOG_ERROR("Failed to commit database resize"); + m_observer->OnStorageFailed("Database resize failed"); + return false; + } +#endif + } + + // VACUUM cannot run inside a transaction. Reserve the full rewrite for + // the emergency delete-all path; routine 25% trims use auto_vacuum=FULL. + if (compactDatabase && + !SqliteStatement(*m_db, "VACUUM").execute()) + { + LOG_ERROR("Failed to compact database after resize"); + m_observer->OnStorageFailed("Database resize failed"); + m_DbSizeEstimate = GetSize(); + return false; } m_DbSizeEstimate = GetSize(); @@ -1064,4 +1269,3 @@ namespace MAT_NS_BEGIN { } MAT_NS_END #endif - diff --git a/lib/offline/OfflineStorage_SQLite.hpp b/lib/offline/OfflineStorage_SQLite.hpp index 18643cde5..2053a0246 100644 --- a/lib/offline/OfflineStorage_SQLite.hpp +++ b/lib/offline/OfflineStorage_SQLite.hpp @@ -85,6 +85,7 @@ namespace MAT_NS_BEGIN { // of this class still using SQLite. static std::mutex m_initAndShutdownLock; static int m_instanceCount; + static bool m_ownsTempDirectory; size_t m_stmtBeginTransaction {}; size_t m_stmtCommitTransaction {}; @@ -122,9 +123,17 @@ namespace MAT_NS_BEGIN { private: size_t GetRecordCountUnsafe(EventLatency latency) const; + + // Validate a record's required fields; reports OnStorageFailed on rejection. + bool isValidRecord(StorageRecord const& record) const; + // Insert one already-validated record. Caller must hold m_lock and have an + // active DbTransaction (when ENABLE_LOCKING). Updates m_DbSizeEstimate. + // Returns false (without updating the size estimate) if the insert fails. + bool insertRecordUnsafe(StorageRecord const& record); + // Run the DB-size-full notification and resize checks (after inserts). + void checkStorageSizeLimits(); }; } MAT_NS_END #endif - diff --git a/lib/offline/SQLiteWrapper.hpp b/lib/offline/SQLiteWrapper.hpp index 2a5f0d108..00363af3a 100644 --- a/lib/offline/SQLiteWrapper.hpp +++ b/lib/offline/SQLiteWrapper.hpp @@ -213,18 +213,51 @@ namespace MAT_NS_BEGIN { class SqliteDB { std::mutex m_lock; + + void releaseTempDirectoryAfterShutdown(int shutdownResult) + { + if (shutdownResult == SQLITE_OK) + { + if (m_ownsTempDirectory != nullptr && *m_ownsTempDirectory) + { + ::sqlite3_free(sqlite3_temp_directory); + sqlite3_temp_directory = nullptr; + *m_ownsTempDirectory = false; + } + } + else + { + LOG_WARN("Failed to shut down SQLite (%d); retaining the temp directory", shutdownResult); + } + } + public: SqliteDB(bool skipInitAndShutdown, std::mutex* initAndShutdownLock = nullptr, - int* instanceCount = nullptr) + int* instanceCount = nullptr, + bool* ownsTempDirectory = nullptr) : m_db(nullptr), m_skipInitAndShutdown(skipInitAndShutdown), m_initAndShutdownLock(initAndShutdownLock), - m_instanceCount(instanceCount) + m_instanceCount(instanceCount), + m_ownsTempDirectory(ownsTempDirectory) { } - bool initialize(std::string const& filename, bool deletePrevious, size_t maxHeapLimit = 0) + ~SqliteDB() + { + // Finalize prepared statements and close the database even if + // shutdown() was not called explicitly (e.g. the owning storage was + // destroyed without Shutdown()). shutdown() is idempotent -- it + // returns immediately once m_db is null -- so an earlier explicit + // shutdown() makes this a no-op. + shutdown(); + } + + bool initialize(std::string const& filename, + bool deletePrevious, + size_t maxHeapLimit = 0, + std::string const& tempDirectory = {}) { int result = SQLITE_OK; @@ -235,11 +268,32 @@ namespace MAT_NS_BEGIN { if (*m_instanceCount > 0) { *m_instanceCount += 1; } else { + // Android and WinRT may require an explicit temp directory. + // Configure SQLite's process-global value once, before the + // first SQLite initialization, and release it with the last + // connection. Other platforms pass an empty directory and + // use SQLite's native temp-directory selection. + if (!tempDirectory.empty() && sqlite3_temp_directory == nullptr) { + sqlite3_temp_directory = ::sqlite3_mprintf("%s", tempDirectory.c_str()); + if (sqlite3_temp_directory == nullptr) { + result = SQLITE_NOMEM; + } else if (m_ownsTempDirectory != nullptr) { + *m_ownsTempDirectory = true; + } + } + } + if (result == SQLITE_OK && *m_instanceCount == 0) { result = g_sqlite3Proxy->sqlite3_initialize(); if (result == SQLITE_OK) { *m_instanceCount = 1; } } + if (result != SQLITE_OK && + m_ownsTempDirectory != nullptr && + *m_ownsTempDirectory) { + const int shutdownResult = g_sqlite3Proxy->sqlite3_shutdown(); + releaseTempDirectoryAfterShutdown(shutdownResult); + } } else { result = g_sqlite3Proxy->sqlite3_initialize(); } @@ -354,7 +408,8 @@ namespace MAT_NS_BEGIN { *m_instanceCount -= 1; } else if (*m_instanceCount == 1) { *m_instanceCount = 0; - g_sqlite3Proxy->sqlite3_shutdown(); + const int shutdownResult = g_sqlite3Proxy->sqlite3_shutdown(); + releaseTempDirectoryAfterShutdown(shutdownResult); } } else { @@ -386,7 +441,7 @@ namespace MAT_NS_BEGIN { size_t prepare(char const* statement) { LOCKGUARD(m_lock); - sqlite3_stmt* stmt; + sqlite3_stmt* stmt = nullptr; int result = g_sqlite3Proxy->sqlite3_prepare_v2(m_db, statement, -1, &stmt, NULL); if (result != SQLITE_OK) { std::string excerpt(statement); @@ -490,6 +545,13 @@ namespace MAT_NS_BEGIN { return isOK(sqlite3_exec("COMMIT;")); } + /** + * @brief Roll back (discard) the current DB transaction. + */ + bool rollback() { + return isOK(sqlite3_exec("ROLLBACK;")); + } + bool lock() { #ifndef NDEBUG unsigned count = 0; @@ -564,6 +626,7 @@ namespace MAT_NS_BEGIN { bool m_skipInitAndShutdown; std::mutex* m_initAndShutdownLock; int* m_instanceCount; + bool* m_ownsTempDirectory; private: MATSDK_LOG_DECL_COMPONENT_CLASS(); @@ -865,4 +928,3 @@ namespace MAT_NS_BEGIN { } MAT_NS_END #endif - diff --git a/lib/offline/StorageRecordValidation.hpp b/lib/offline/StorageRecordValidation.hpp new file mode 100644 index 000000000..23447a11f --- /dev/null +++ b/lib/offline/StorageRecordValidation.hpp @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef STORAGERECORDVALIDATION_HPP +#define STORAGERECORDVALIDATION_HPP + +#include "IOfflineStorage.hpp" + +namespace MAT_NS_BEGIN { + + inline bool IsValidDiskStorageRecord(StorageRecord const& record) + { + return !(record.id.empty() || record.tenantToken.empty() || + static_cast(record.latency) < 0 || record.timestamp <= 0); + } + +} MAT_NS_END + +#endif diff --git a/lib/pal/PAL.cpp b/lib/pal/PAL.cpp index eac02482a..c9d750232 100644 --- a/lib/pal/PAL.cpp +++ b/lib/pal/PAL.cpp @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include @@ -58,10 +60,56 @@ namespace PAL_NS_BEGIN { +#if defined(_WIN32) || defined(_WIN64) + namespace + { + using GetSystemTimeAsFileTimeProc = VOID (WINAPI*)(LPFILETIME); + + GetSystemTimeAsFileTimeProc getPreciseSystemTimeAsFileTime() noexcept + { + static std::once_flag once; + static GetSystemTimeAsFileTimeProc proc = nullptr; + std::call_once(once, [] { + HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); + if (kernel32 != nullptr) + { + proc = reinterpret_cast( + ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")); + } + }); + return proc; + } + + void getSystemTimeAsFileTime(FILETIME& fileTime) noexcept + { + if (auto preciseProc = getPreciseSystemTimeAsFileTime()) + { + preciseProc(&fileTime); + } + else + { + ::GetSystemTimeAsFileTime(&fileTime); + } + } + } +#endif + PlatformAbstractionLayer& GetPAL() noexcept { - static PlatformAbstractionLayer pal; - return pal; + // Deliberately never destroyed. PAL::shutdown() (called from + // LogManagerImpl::FlushAndTeardown()) must find this object's members + // still alive, but PAL is constructed lazily on first use, so whether + // this function-local static is destroyed before or after that + // teardown call depends on runtime timing, not source order -- if it + // is destroyed first, shutdown() releases shared_ptr members of an + // already-destroyed object (a downstream consumer observed this as + // intermittent EXC_BAD_ACCESS in ~shared_ptr at + // process exit). Static storage avoids that ordering hazard without a + // process-lifetime heap allocation; shutdown() performs the resource + // teardown explicitly. + alignas(PlatformAbstractionLayer) static unsigned char storage[sizeof(PlatformAbstractionLayer)]; + static PlatformAbstractionLayer* pal = ::new (storage) PlatformAbstractionLayer(); + return *pal; } MATSDK_LOG_INST_COMPONENT_CLASS(PlatformAbstractionLayer, "MATSDK.PAL", "MSTel client - platform abstraction layer") @@ -198,10 +246,6 @@ namespace PAL_NS_BEGIN { #define gettid() std::this_thread::get_id() #endif -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable:4996) -#endif void log(LogLevel level, char const* component, char const* fmt, ...) { #if defined(ANDROID) && defined(HAVE_MAT_LOGGING) && !defined(ANDROID_SUPPRESS_LOGCAT) @@ -227,6 +271,7 @@ namespace PAL_NS_BEGIN { } #endif #ifdef HAVE_MAT_LOGGING + std::lock_guard lock(debugLogMutex); if (!isLoggingInited) return; @@ -253,14 +298,12 @@ namespace PAL_NS_BEGIN { buffer[std::min(len + 1, sizeof(buffer) - 1)] = '\0'; #ifdef HAVE_MAT_WIN_LOG // Log to debug log file if enabled - debugLogMutex.lock(); - if (debugLogStream->good()) + if (debugLogStream && debugLogStream->good()) { (*debugLogStream) << buffer; // flush is not very efficient, but needed to get realtime file updates debugLogStream->flush(); } - debugLogMutex.unlock(); #else ::OutputDebugStringA(buffer); #endif //HAVE_MAT_WIN_LOG @@ -298,14 +341,12 @@ namespace PAL_NS_BEGIN { // Make sure all of our debug strings contain EOL buffer[len] = '\n'; // Log to debug log file if enabled - debugLogMutex.lock(); - if (debugLogStream->good()) + if (debugLogStream && debugLogStream->good()) { (*debugLogStream) << buffer; // flush is not very efficient, but needed to get realtime file updates debugLogStream->flush(); } - debugLogMutex.unlock(); } va_end(ap); #endif @@ -315,9 +356,6 @@ namespace PAL_NS_BEGIN { (void)(fmt); #endif /* of #ifdef HAVE_MAT_LOGGING */ } -#ifdef _MSC_VER -#pragma warning(pop) -#endif } // namespace detail @@ -332,17 +370,16 @@ namespace PAL_NS_BEGIN { return m_taskDispatcher; } -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable:6031) -#endif std::string PlatformAbstractionLayer::generateUuidString() const { #ifdef _WIN32 GUID uuid = { 0, 0, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } }; - auto hr = CoCreateGuid(&uuid); - /* CoCreateGuid` will possiblity never fail, so ignoring the result */ - UNREFERENCED_PARAMETER(hr); + const HRESULT hr = CoCreateGuid(&uuid); + if (FAILED(hr)) + { + LOG_ERROR("CoCreateGuid failed: 0x%08lx", static_cast(hr)); + return {}; + } return MAT::to_string(uuid); #elif defined(__APPLE__) auto uuid {CFUUIDCreate(kCFAllocatorDefault)}; @@ -408,15 +445,15 @@ namespace PAL_NS_BEGIN { return buf; #endif } -#ifdef _MSC_VER -#pragma warning(pop) -#endif int64_t PlatformAbstractionLayer::getUtcSystemTimeMs() const { #ifdef _WIN32 + FILETIME fileTime; + getSystemTimeAsFileTime(fileTime); ULARGE_INTEGER now; - ::GetSystemTimeAsFileTime(reinterpret_cast(&now)); + now.LowPart = fileTime.dwLowDateTime; + now.HighPart = fileTime.dwHighDateTime; return (now.QuadPart - 116444736000000000ull) / 10000; #else return std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); @@ -432,26 +469,7 @@ namespace PAL_NS_BEGIN { { #ifdef _WIN32 FILETIME tocks; - // Resolve the precise API dynamically so the SDK retains its Windows 7 - // runtime compatibility and falls back when the API is unavailable. - using GetSystemTimePreciseAsFileTimeProc = VOID (WINAPI*)(LPFILETIME); - static const GetSystemTimePreciseAsFileTimeProc getSystemTimePreciseAsFileTime = - []() -> GetSystemTimePreciseAsFileTimeProc - { - HMODULE kernel32 = ::GetModuleHandleW(L"kernel32.dll"); - return kernel32 - ? reinterpret_cast( - ::GetProcAddress(kernel32, "GetSystemTimePreciseAsFileTime")) - : nullptr; - }(); - if (getSystemTimePreciseAsFileTime) - { - getSystemTimePreciseAsFileTime(&tocks); - } - else - { - ::GetSystemTimeAsFileTime(&tocks); - } + getSystemTimeAsFileTime(tocks); ULONGLONG ticks = (ULONGLONG(tocks.dwHighDateTime) << 32) | tocks.dwLowDateTime; // number of days from beginning to 1601 multiplied by ticks per day return ticks + 0x701ce1722770000ULL; @@ -472,49 +490,39 @@ namespace PAL_NS_BEGIN { { #ifdef _WIN32 __time64_t seconds = static_cast<__time64_t>(timestampMs / 1000); - int milliseconds = static_cast(timestampMs % 1000); - - tm tm; - if (::_gmtime64_s(&tm, &seconds) != 0) + tm timeParts; + if (::_gmtime64_s(&timeParts, &seconds) != 0) { - memset(&tm, 0, sizeof(tm)); + return {}; } - - char buf[sizeof("YYYY-MM-DDTHH:MM:SS.sssZ") + 1] = { 0 }; - ::_snprintf_s(buf, _TRUNCATE, "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", - 1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec, milliseconds); #else time_t seconds = static_cast(timestampMs / 1000); - int milliseconds = static_cast(timestampMs % 1000); - - tm tm; - bool valid = (gmtime_r(&seconds, &tm) != NULL); - - if (!valid) + tm timeParts; + if (gmtime_r(&seconds, &timeParts) == nullptr) { - memset(&tm, 0, sizeof(tm)); + return {}; } - - char buf[sizeof("YYYY-MM-DDTHH:MM:SS.sssZ") + 1] = { 0 }; - -#if defined(__GNUC__) && !defined(__clang__) -#include -#if __GNUC_PREREQ(7,0) // If gcc_version >= 7.0 https://gcc.gnu.org/gcc-7/changes.html -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-truncation" // error: 'T' directive output may be truncated writing 1 byte into a region of size between 0 and 16 [-Werror=format-truncation=] -#endif -#endif - (void)snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", - 1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday, - tm.tm_hour, tm.tm_min, tm.tm_sec, milliseconds); -#if defined(__GNUC__) && !defined(__clang__) -#if __GNUC_PREREQ(7,0) // If gcc_version >= 7.0 https://gcc.gnu.org/gcc-7/changes.html -#pragma GCC diagnostic pop #endif -#endif -#endif - return buf; + + const int milliseconds = static_cast(timestampMs % 1000); + char buf[128] = { 0 }; + const int length = snprintf( + buf, + sizeof(buf), + "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", + 1900 + timeParts.tm_year, + 1 + timeParts.tm_mon, + timeParts.tm_mday, + timeParts.tm_hour, + timeParts.tm_min, + timeParts.tm_sec, + milliseconds); + if (length < 0 || static_cast(length) >= sizeof(buf)) + { + LOG_ERROR("Failed to format UTC timestamp"); + return {}; + } + return std::string(buf, static_cast(length)); } /** @@ -529,20 +537,27 @@ namespace PAL_NS_BEGIN { { #ifdef USE_WIN32_PERFCOUNTER /* Win32 API implementation */ - static bool frequencyQueried = false; - static int64_t ticksPerMillisecond; - if (!frequencyQueried) - { - // There is no harm in querying twice in case of a race condition. + static std::once_flag frequencyOnce; + static int64_t frequency = 0; + std::call_once(frequencyOnce, [] { LARGE_INTEGER ticksInOneSecond; - ::QueryPerformanceFrequency(&ticksInOneSecond); - ticksPerMillisecond = ticksInOneSecond.QuadPart / 1000; - frequencyQueried = true; - } + if (::QueryPerformanceFrequency(&ticksInOneSecond)) + { + frequency = ticksInOneSecond.QuadPart; + } + }); LARGE_INTEGER now; ::QueryPerformanceCounter(&now); - return static_cast(now.QuadPart / ticksPerMillisecond); + if (frequency <= 0) + { + return std::chrono::steady_clock::now().time_since_epoch() / std::chrono::milliseconds(1); + } + + const int64_t wholeSeconds = now.QuadPart / frequency; + const int64_t remainder = now.QuadPart % frequency; + return static_cast(wholeSeconds) * 1000u + + static_cast((remainder * 1000) / frequency); #else /* Cross-platform C++11 implementation */ return std::chrono::steady_clock::now().time_since_epoch() / std::chrono::milliseconds(1); diff --git a/lib/pal/TaskDispatcher.hpp b/lib/pal/TaskDispatcher.hpp index ec6f2f690..36678ef26 100644 --- a/lib/pal/TaskDispatcher.hpp +++ b/lib/pal/TaskDispatcher.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "ITaskDispatcher.hpp" @@ -25,6 +26,12 @@ namespace PAL_NS_BEGIN { namespace detail { + struct TaskLifetimeState + { + std::recursive_mutex mutex; + MAT::Task* task {nullptr}; + }; + template class TaskCall : public Task { @@ -48,14 +55,36 @@ namespace PAL_NS_BEGIN { this->TargetTime = targetTime; } + TaskCall(TCall& call, int64_t targetTime, std::shared_ptr lifetimeState) : + Task(), + m_call(call), + m_lifetimeState(std::move(lifetimeState)) + { + this->TypeName = TYPENAME(call); + this->Type = Task::TimedCall; + this->TargetTime = targetTime; + std::lock_guard lock(m_lifetimeState->mutex); + m_lifetimeState->task = this; + } + virtual void operator()() override { m_call(); } - virtual ~TaskCall() noexcept = default; + virtual ~TaskCall() noexcept + { + if (m_lifetimeState) + { + std::lock_guard lock(m_lifetimeState->mutex); + m_lifetimeState->task = nullptr; + } + } const TCall m_call; + + private: + std::shared_ptr m_lifetimeState; }; } // namespace detail @@ -63,14 +92,11 @@ namespace PAL_NS_BEGIN { class DeferredCallbackHandle { public: - std::mutex m_mutex; - MAT::Task* m_task = nullptr; - MAT::ITaskDispatcher* m_taskDispatcher = nullptr; - - DeferredCallbackHandle(MAT::Task* task, MAT::ITaskDispatcher* taskDispatcher) : - m_task(task), + DeferredCallbackHandle(std::shared_ptr taskLifetimeState, MAT::ITaskDispatcher* taskDispatcher) : + m_taskLifetimeState(std::move(taskLifetimeState)), m_taskDispatcher(taskDispatcher) { } - DeferredCallbackHandle() {} + + DeferredCallbackHandle() = default; DeferredCallbackHandle(DeferredCallbackHandle&& h) { *this = std::move(h); @@ -78,30 +104,82 @@ namespace PAL_NS_BEGIN { DeferredCallbackHandle& operator=(DeferredCallbackHandle&& other) { - std::lock_guard lock(m_mutex); - std::lock_guard otherLock(other.m_mutex); - m_task = other.m_task; - other.m_task = nullptr; + if (this == &other) + { + return *this; + } + + std::unique_lock lock(m_mutex, std::defer_lock); + std::unique_lock otherLock(other.m_mutex, std::defer_lock); + std::lock(lock, otherLock); + m_taskLifetimeState = std::move(other.m_taskLifetimeState); m_taskDispatcher = other.m_taskDispatcher; + other.m_taskDispatcher = nullptr; return *this; } - bool Cancel(uint64_t waitTime = 0) + MAT::Task* GetTask() const { std::lock_guard lock(m_mutex); - if (m_task) + if (m_taskLifetimeState == nullptr) { - bool result = (m_taskDispatcher != nullptr) && (m_taskDispatcher->Cancel(m_task, waitTime)); - return result; + return nullptr; } - else { - // Canceled nothing successfully + std::lock_guard lifetimeLock(m_taskLifetimeState->mutex); + return m_taskLifetimeState->task; + } + + bool Cancel(uint64_t waitTime = 0) + { + std::lock_guard lock(m_mutex); + if (m_taskLifetimeState == nullptr) + { return true; } + + // Keep task destruction serialized with the dispatcher's pointer + // lookup so this address cannot be freed and reused for a different + // task between the lookup here and Cancel(). A recursive mutex is + // required because dispatchers may delete queued tasks synchronously + // from Cancel(), re-entering TaskCall's destructor on this thread. + std::lock_guard lifetimeLock(m_taskLifetimeState->mutex); + MAT::Task* task = m_taskLifetimeState->task; + if (task) + { + bool result = (m_taskDispatcher != nullptr) && (m_taskDispatcher->Cancel(task, waitTime)); + return result || (m_taskLifetimeState->task == nullptr); + } + return true; } + + private: + mutable std::mutex m_mutex; + std::shared_ptr m_taskLifetimeState; + MAT::ITaskDispatcher* m_taskDispatcher = nullptr; }; + inline DeferredCallbackHandle scheduleTask( + MAT::ITaskDispatcher* taskDispatcher, + unsigned delayMs, + std::function call) + { + auto taskLifetime = std::make_shared(); + auto task = new detail::TaskCall>( + call, + getMonotonicTimeMs() + static_cast(delayMs), + taskLifetime); + taskDispatcher->Queue(task); + { + std::lock_guard lock(taskLifetime->mutex); + if (taskLifetime->task == nullptr) + { + return DeferredCallbackHandle(); + } + } + return DeferredCallbackHandle(taskLifetime, taskDispatcher); + } + template void dispatchTask(MAT::ITaskDispatcher* taskDispatcher, TObject* obj, void (TObject::*func)(TFuncArgs...), TPassedArgs&&... args) { @@ -121,9 +199,20 @@ namespace PAL_NS_BEGIN { DeferredCallbackHandle scheduleTask(MAT::ITaskDispatcher* taskDispatcher, unsigned delayMs, TObject* obj, void (TObject::*func)(TFuncArgs...), TPassedArgs&&... args) { auto bound = std::bind(std::mem_fn(func), obj, std::forward(args)...); - auto task = new detail::TaskCall(bound, getMonotonicTimeMs() + (int64_t)delayMs); + auto taskLifetimeState = std::make_shared(); + auto task = new detail::TaskCall( + bound, + getMonotonicTimeMs() + (int64_t)delayMs, + taskLifetimeState); taskDispatcher->Queue(task); - return DeferredCallbackHandle(task, taskDispatcher); + { + std::lock_guard lock(taskLifetimeState->mutex); + if (taskLifetimeState->task == nullptr) + { + return DeferredCallbackHandle(); + } + } + return DeferredCallbackHandle(taskLifetimeState, taskDispatcher); } template @@ -135,4 +224,3 @@ namespace PAL_NS_BEGIN { } PAL_NS_END #endif - diff --git a/lib/pal/TaskDispatcher_CAPI.cpp b/lib/pal/TaskDispatcher_CAPI.cpp index 0257d7de6..fca8c53da 100644 --- a/lib/pal/TaskDispatcher_CAPI.cpp +++ b/lib/pal/TaskDispatcher_CAPI.cpp @@ -6,11 +6,14 @@ #include #include +#include #include +#include #include #include #include #include +#include #include "ctmacros.hpp" #include "pal/PAL.hpp" @@ -32,27 +35,94 @@ namespace PAL_NS_BEGIN { Task* GetTask() { + std::lock_guard lock(m_stateLock); return m_task.get(); } + bool BeginCallback() + { + std::lock_guard lock(m_stateLock); + if (m_done || m_cancelled) + { + return false; + } + m_running = true; + m_callbackThread = std::this_thread::get_id(); + return true; + } + void OnCallback() { + if (!BeginCallback()) + { + return; + } if (m_task) { // The task is host/user code running on the external dispatcher's // thread; an exception escaping here would terminate the process. // Log it (mirroring WorkerThread) instead of swallowing silently. - try { + MATSDK_TRY { (*m_task)(); } - catch (const std::exception& ex) { +#if HAVE_EXCEPTIONS + MATSDK_CATCH(const std::exception& ex) { (void)ex; LOG_ERROR("Unhandled exception in CAPI task: %s", ex.what()); } - catch (...) { + MATSDK_CATCH(...) { LOG_ERROR("Unhandled non-standard exception in CAPI task"); } +#endif } - ReleaseItem(); + std::unique_ptr completedTask; + { + std::lock_guard lock(m_stateLock); + if (m_task) + { + m_task->Type = Task::Done; + completedTask = std::move(m_task); + } + m_running = false; + m_done = true; + } + m_doneCv.notify_all(); + // Task destruction can acquire the DeferredCallbackHandle lifetime + // lock held by a concurrent Cancel(). Keep it outside m_stateLock so + // Cancel() can observe completion and release that lifetime lock. + } + + bool RequestCancel() + { + std::lock_guard lock(m_stateLock); + if (m_done) + { + return false; + } + m_cancelled = true; + if (!m_running) + { + m_done = true; + m_doneCv.notify_all(); + } + return m_running; + } + + bool WaitForCompletion(uint64_t waitTime) + { + std::unique_lock lock(m_stateLock); + if (m_done || m_callbackThread == std::this_thread::get_id()) + { + return true; + } + if (waitTime == std::numeric_limits::max()) + { + m_doneCv.wait(lock, [this] { return m_done; }); + } + else if (waitTime > 0) + { + m_doneCv.wait_for(lock, std::chrono::milliseconds(waitTime), [this] { return m_done; }); + } + return m_done; } private: @@ -65,6 +135,12 @@ namespace PAL_NS_BEGIN { } std::unique_ptr m_task; + std::mutex m_stateLock; + std::condition_variable m_doneCv; + std::thread::id m_callbackThread; + bool m_running = false; + bool m_done = false; + bool m_cancelled = false; }; @@ -86,18 +162,26 @@ namespace PAL_NS_BEGIN { { std::shared_ptr task; - // Find and remove pending task + // Keep the task discoverable while its callback is running so a + // concurrent cancellation can wait for completion. { LOCKGUARD(s_tasksLock); auto itTask = GetPendingTasks().find(taskId); if (itTask != GetPendingTasks().end()) { task = itTask->second; - GetPendingTasks().erase(itTask); } } if (task) + { task->OnCallback(); + LOCKGUARD(s_tasksLock); + auto itTask = GetPendingTasks().find(taskId); + if (itTask != GetPendingTasks().end() && itTask->second == task) + { + GetPendingTasks().erase(itTask); + } + } } TaskDispatcher_CAPI::TaskDispatcher_CAPI(task_dispatcher_queue_fn_t queueFn, task_dispatcher_cancel_fn_t cancelFn, task_dispatcher_join_fn_t joinFn) @@ -142,10 +226,10 @@ namespace PAL_NS_BEGIN { m_queueFn(&capiTask, &OnAsyncTaskCallback); } - // TODO: currently shutdown wait on task cancellation is not implemented for C API Task Dispatcher - bool TaskDispatcher_CAPI::Cancel(Task* task, uint64_t) + bool TaskDispatcher_CAPI::Cancel(Task* task, uint64_t waitTime) { std::string taskId; + std::shared_ptr capiTask; // Find and erase pending task { @@ -157,11 +241,32 @@ namespace PAL_NS_BEGIN { if (itTask != GetPendingTasks().end()) { taskId = itTask->first; - GetPendingTasks().erase(itTask); + capiTask = itTask->second; } } - return (!taskId.empty()) ? m_cancelFn(taskId.c_str()) : false; + if (taskId.empty()) + { + return false; + } + + const bool wasRunning = capiTask->RequestCancel(); + m_cancelFn(taskId.c_str()); + if (!wasRunning) + { + LOCKGUARD(s_tasksLock); + GetPendingTasks().erase(taskId); + return true; + } + + if (capiTask->WaitForCompletion(waitTime)) + { + LOCKGUARD(s_tasksLock); + GetPendingTasks().erase(taskId); + return true; + } + + return false; } } PAL_NS_END diff --git a/lib/pal/WorkerThread.cpp b/lib/pal/WorkerThread.cpp index 07166958c..d1c9d96cb 100644 --- a/lib/pal/WorkerThread.cpp +++ b/lib/pal/WorkerThread.cpp @@ -5,8 +5,12 @@ // clang-format off #include "pal/WorkerThread.hpp" #include "pal/PAL.hpp" +#include "ctmacros.hpp" #include +#include +#include +#include #if defined(MATSDK_PAL_CPP11) || defined(MATSDK_PAL_WIN32) @@ -29,6 +33,14 @@ namespace PAL_NS_BEGIN { { protected: std::thread m_hThread; + // The worker thread's own id, captured under m_lock once threadFunc starts. + // onLastReferenceReleased() reads it (under m_lock) rather than m_hThread.get_id() + // to detect "am I running on my own worker thread?", because m_hThread.get_id() + // returns the default not-a-thread id after a detach() -- so this keeps + // self-dispose detection correct even if the thread was detached first. A plain + // std::thread::id guarded by m_lock is used rather than std::atomic, + // which is not portable (std::thread::id is not guaranteed trivially copyable). + std::thread::id m_workerId; std::recursive_mutex m_lock; std::timed_mutex m_execution_mutex; @@ -36,16 +48,23 @@ namespace PAL_NS_BEGIN { std::list m_queue; std::list m_timerQueue; Event m_event; - MAT::Task* m_itemInProgress; + MAT::Task* m_itemInProgress = nullptr; + uint64_t m_itemInProgressGeneration = 0; + bool m_itemCancellationRequested = false; int count = 0; + bool m_shuttingDown = false; + std::mutex m_joinLock; + // Set when the last reference is released by a task running on this worker + // thread, so threadFunc performs the final delete after its loop breaks + // (see onLastReferenceReleased() and WorkerThreadFactory::Create()). + std::atomic m_disposeFromThread { false }; public: WorkerThread() { - m_itemInProgress = nullptr; m_hThread = std::thread(WorkerThread::threadFunc, static_cast(this)); - LOG_INFO("Started new thread %u", m_hThread.get_id()); + LOG_INFO("Started new thread %zu", std::hash{}(m_hThread.get_id())); } ~WorkerThread() @@ -53,65 +72,142 @@ namespace PAL_NS_BEGIN { Join(); } + private: + void enqueueShutdownItemLocked() + { + if (!m_shuttingDown) { + m_shuttingDown = true; + m_queue.push_back(new WorkerThreadShutdownItem()); + m_event.post(); + } + } + + void drainPendingTasks() + { + std::list queue; + std::list timerQueue; + { + LOCKGUARD(m_lock); + queue.splice(queue.end(), m_queue); + timerQueue.splice(timerQueue.end(), m_timerQueue); + } + if (!queue.empty()) { + LOG_WARN("Shutdown with %zu queued task(s) pending", queue.size()); + } + if (!timerQueue.empty()) { + LOG_WARN("Shutdown with %zu timer(s) pending", timerQueue.size()); + } + for (auto task : queue) { delete task; } + for (auto task : timerQueue) { delete task; } + } + + public: void Join() final { - auto item = new WorkerThreadShutdownItem(); - Queue(item); + LOCKGUARD(m_joinLock); std::thread::id this_id = std::this_thread::get_id(); - try { - if (m_hThread.joinable() && (m_hThread.get_id() != this_id)) - m_hThread.join(); - else + std::thread threadToJoin; + bool joined = false; + { + LOCKGUARD(m_lock); + enqueueShutdownItemLocked(); + if (!m_hThread.joinable()) { + return; + } + if (m_hThread.get_id() == this_id) { m_hThread.detach(); + } else { + threadToJoin = std::move(m_hThread); + } + } + try { + if (threadToJoin.joinable()) { + threadToJoin.join(); + joined = true; + } + } + catch (const std::system_error& e) { + (void)e; + LOG_ERROR("Thread join/detach failed: [%d] %s", e.code().value(), e.what()); + std::terminate(); + } + catch (const std::exception& e) { + (void)e; + LOG_ERROR("Thread join/detach failed: %s", e.what()); + std::terminate(); } - catch (...) {}; - // TODO: [MG] - investigate if we ever drop work items on shutdown. - if (!m_queue.empty()) - { - LOG_WARN("m_queue is not empty!"); + // Clean up any tasks remaining in the queues after shutdown. + // Only safe after join() — the thread has fully exited. + // After detach(), the thread still needs the shutdown item + // and may still be accessing the queues. + if (joined) { + drainPendingTasks(); } - if (!m_timerQueue.empty()) + } + + // Invoked by the shared_ptr deleter when the last reference is released. + // Returns true if the caller should delete the object, false if deletion was + // deferred to the worker thread. The worker is shared process-wide, so the + // last reference can be dropped by a task running on the worker thread itself + // (e.g. a task that tears down its LogManager/PAL). In that case threadFunc is + // still on the stack below the task and keeps touching members after the task + // returns, so freeing the object here would be a use-after-free: instead + // detach, signal shutdown, mark the thread to delete itself once its loop + // breaks, and leave the object alive. On any other thread it is safe to delete + // immediately (~WorkerThread joins the worker first). + bool onLastReferenceReleased() + { + LOCKGUARD(m_lock); + if (m_workerId == std::this_thread::get_id()) { - LOG_WARN("m_timerQueue is not empty!"); + enqueueShutdownItemLocked(); + m_disposeFromThread.store(true, std::memory_order_release); + try { + if (m_hThread.joinable()) { + m_hThread.detach(); + } + } + catch (const std::exception& e) { + (void)e; + LOG_ERROR("Worker self-detach failed: %s", e.what()); + } + return false; } + return true; } void Queue(MAT::Task* item) final { - LOG_INFO("queue item=%p", &item); - LOCKGUARD(m_lock); - if (item->Type == MAT::Task::TimedCall) { - auto it = m_timerQueue.begin(); - while (it != m_timerQueue.end() && (*it)->TargetTime < item->TargetTime) { - ++it; + LOG_INFO("queue item=%p", static_cast(item)); + bool rejected = false; + { + LOCKGUARD(m_lock); + if (m_shuttingDown) { + rejected = true; + } + else if (item->Type == MAT::Task::TimedCall) { + auto it = m_timerQueue.begin(); + while (it != m_timerQueue.end() && (*it)->TargetTime < item->TargetTime) { + ++it; + } + m_timerQueue.insert(it, item); + } + else { + m_queue.push_back(item); } - m_timerQueue.insert(it, item); } - else { - m_queue.push_back(item); + if (rejected) { + LOG_WARN("Dropping queued task %p during shutdown", static_cast(item)); + delete item; + return; } - count++; m_event.post(); } - // Cancel a task or wait for task completion for up to waitTime ms: - // - // - acquire the m_lock to prevent a new task from getting scheduled. - // This may block the scheduling of a new task in queue for up to - // waitTime in case if the task being canceled - // is the one being executed right now. - // - // - if currently executing task is the one we are trying to cancel, - // then verify for recursion: if the current thread is the same - // we're waiting on, prevent the recursion (we can't cancel our own - // thread task). If it's different thread, then idle-poll-wait for - // task completion for up to waitTime ms. m_itemInProgress is nullptr - // once the item is done executing. Method may fail and return if - // waitTime given was insufficient to wait for completion. - // - // - if task being cancelled is not executing yet, then erase it from - // timer queue without any wait. + // Lock rule: never wait for m_execution_mutex while holding m_lock. + // Task callbacks may call Queue(), which needs m_lock while the callback + // owns m_execution_mutex. // // TODO: current callers of this API do not check the status code. // Refactor this code to return the following cancellation status: @@ -122,7 +218,8 @@ namespace PAL_NS_BEGIN { // bool Cancel(MAT::Task* item, uint64_t waitTime) override { - LOCKGUARD(m_lock); + MAT::Task* queuedItem = nullptr; + std::unique_lock lock(m_lock); if (item == nullptr) { return false; @@ -131,36 +228,59 @@ namespace PAL_NS_BEGIN { if (m_itemInProgress == item) { /* Can't recursively wait on completion of our own thread */ - if (m_hThread.get_id() != std::this_thread::get_id()) - { - if (waitTime > 0 && m_execution_mutex.try_lock_for(std::chrono::milliseconds(waitTime))) - { - m_itemInProgress = nullptr; - m_execution_mutex.unlock(); - } - } - else + if (m_workerId == std::this_thread::get_id()) { // The SDK may attempt to cancel itself from within its own task. // Return true and assume that the current task will finish, and therefore be cancelled. return true; } - /* Either waited long enough or the task is still executing. Return: - * true - if item in progress is different than item (other task) - * false - if item in progress is still the same (didn't wait long enough) - */ - return (m_itemInProgress != item); - } + if (waitTime == 0) + { + return false; + } - { - auto it = std::find(m_timerQueue.begin(), m_timerQueue.end(), item); - if (it != m_timerQueue.end()) { - // Still in the queue - m_timerQueue.erase(it); - delete item; + const uint64_t generation = m_itemInProgressGeneration; + m_itemCancellationRequested = true; + lock.unlock(); + + bool completed = false; + if (waitTime == std::numeric_limits::max()) + { + m_execution_mutex.lock(); + completed = true; + } + else + { + completed = + m_execution_mutex.try_lock_for(std::chrono::milliseconds(waitTime)); + } + if (completed) + { + m_execution_mutex.unlock(); + } + + lock.lock(); + const bool sameItem = + m_itemInProgress == item && + m_itemInProgressGeneration == generation; + if (completed && sameItem) + { + m_itemInProgress = nullptr; + m_itemCancellationRequested = false; } + + return completed || !sameItem; + } + + auto it = std::find(m_timerQueue.begin(), m_timerQueue.end(), item); + if (it != m_timerQueue.end()) { + // Transfer ownership under m_lock, but destroy outside all worker locks. + queuedItem = *it; + m_timerQueue.erase(it); } + lock.unlock(); + delete queuedItem; #if 0 for (;;) { { @@ -181,7 +301,11 @@ namespace PAL_NS_BEGIN { uint64_t wakeupCount = 0; WorkerThread* self = reinterpret_cast(lpThreadParameter); - LOG_INFO("Running thread %u", std::this_thread::get_id()); + { + LOCKGUARD(self->m_lock); + self->m_workerId = std::this_thread::get_id(); + } + LOG_INFO("Running thread %zu", std::hash{}(std::this_thread::get_id())); for (;;) { std::unique_ptr item = nullptr; @@ -219,6 +343,8 @@ namespace PAL_NS_BEGIN { if (item) { self->m_itemInProgress = item.get(); + ++self->m_itemInProgressGeneration; + self->m_itemCancellationRequested = false; } } @@ -229,39 +355,79 @@ namespace PAL_NS_BEGIN { } if (item->Type == MAT::Task::Shutdown) { + { + LOCKGUARD(self->m_lock); + if (self->m_itemInProgress == item.get()) { + self->m_itemInProgress = nullptr; + self->m_itemCancellationRequested = false; + } + } item.reset(); - self->m_itemInProgress = nullptr; + // Drop any tasks still queued behind the shutdown sentinel + // (e.g. future-dated timers) before exiting. The owning thread + // deletes these in Join() only after a successful join(); on the + // self-Join path it detaches and skips that cleanup, so draining + // here prevents leaking those tasks. This matches the join()-path + // behavior of dropping un-run work at shutdown. + self->drainPendingTasks(); break; } { std::lock_guard lock(self->m_execution_mutex); - // Item wasn't cancelled before it could be executed - if (self->m_itemInProgress != nullptr) { + bool executeItem = false; + { + LOCKGUARD(self->m_lock); + executeItem = + self->m_itemInProgress == item.get() && + !self->m_itemCancellationRequested; + } + + if (executeItem) { LOG_TRACE("%10llu Execute item=%p type=%s\n", wakeupCount, item.get(), item.get()->TypeName.c_str() ); // A task can run arbitrary work (storage I/O, HTTP encode, and // user DebugEventListener callbacks). An exception escaping here // would unwind out of the thread entry function and call // std::terminate, killing the host process. Contain it. - try { + MATSDK_TRY { (*item)(); } - catch (const std::exception& ex) { +#if HAVE_EXCEPTIONS + MATSDK_CATCH(const std::exception& ex) { (void)ex; LOG_ERROR("Unhandled exception in worker task: %s", ex.what()); } - catch (...) { + MATSDK_CATCH(...) { LOG_ERROR("Unhandled non-standard exception in worker task"); } - self->m_itemInProgress = nullptr; +#endif } if (item) { item->Type = MAT::Task::Done; - item = nullptr; } } + { + LOCKGUARD(self->m_lock); + if (self->m_itemInProgress == item.get()) { + self->m_itemInProgress = nullptr; + self->m_itemCancellationRequested = false; + } + } + // Task destruction may synchronize with a cancellation caller. + // Never run it while holding m_execution_mutex, which Cancel() + // waits on while that caller owns the task lifetime lock. + item = nullptr; + } + + // The loop has broken on a Shutdown item. If the last reference was + // released by a task on this worker thread, onLastReferenceReleased() + // detached and deferred deletion to us; perform it now, after all member + // access is done, so the object outlives threadFunc rather than being + // freed underneath it. + if (self->m_disposeFromThread.load(std::memory_order_acquire)) { + delete self; } } }; @@ -269,7 +435,13 @@ namespace PAL_NS_BEGIN { namespace WorkerThreadFactory { std::shared_ptr Create() { - return std::make_shared(); + // Custom deleter so that a last-reference release happening on the worker + // thread itself defers destruction to the thread (see + // onLastReferenceReleased) instead of freeing the object underneath a + // still-running threadFunc. + return std::shared_ptr( + new WorkerThread(), + [](WorkerThread* self) { if (self->onLastReferenceReleased()) delete self; }); } } diff --git a/lib/pal/desktop/WindowsDesktopDeviceInformationImpl.cpp b/lib/pal/desktop/WindowsDesktopDeviceInformationImpl.cpp index f01992940..3c8fe6baf 100644 --- a/lib/pal/desktop/WindowsDesktopDeviceInformationImpl.cpp +++ b/lib/pal/desktop/WindowsDesktopDeviceInformationImpl.cpp @@ -13,16 +13,12 @@ MATSDK_LOG_INST_COMPONENT_NS("DeviceInfo", "Win32 Desktop Device Information") -#include #include #include #include #include #include -#include -#include - #pragma comment(lib, "iphlpapi.lib") #pragma comment(lib, "AdvAPI32.Lib") @@ -149,4 +145,3 @@ namespace PAL_NS_BEGIN { } } PAL_NS_END - diff --git a/lib/pal/desktop/desktop.vcxitems b/lib/pal/desktop/desktop.vcxitems index 0d8ae8def..45c6804cd 100644 --- a/lib/pal/desktop/desktop.vcxitems +++ b/lib/pal/desktop/desktop.vcxitems @@ -13,10 +13,23 @@ - + + + + + + HAVE_MAT_WININET_HTTP_CLIENT;%(PreprocessorDefinitions) + + + + + HAVE_MAT_WINHTTP_HTTP_CLIENT;%(PreprocessorDefinitions) + + - + + ..\..;..\..\include;$(ProjectDir);$(GeneratedFilesDir);$(IntDir);%(AdditionalIncludeDirectories);$(WindowsSDK_IncludePath) diff --git a/lib/pal/desktop/desktop.vcxitems.filters b/lib/pal/desktop/desktop.vcxitems.filters index a1d6dd857..b3756c63e 100644 --- a/lib/pal/desktop/desktop.vcxitems.filters +++ b/lib/pal/desktop/desktop.vcxitems.filters @@ -1,14 +1,16 @@  - + + - + + diff --git a/lib/pal/posix/SystemInformationImpl_Android.cpp b/lib/pal/posix/SystemInformationImpl_Android.cpp index b1911f8ae..ed62bd259 100644 --- a/lib/pal/posix/SystemInformationImpl_Android.cpp +++ b/lib/pal/posix/SystemInformationImpl_Android.cpp @@ -75,10 +75,10 @@ namespace PAL_NS_BEGIN { jmethodID getDefaultLocaleMid = pEnv->GetStaticMethodID(localeClass, "getDefault", "()Ljava/util/Locale;"); // public abstract Resources getResources () - jmethodID getResourceMid = pEnv->GetMethodID(contextClass, "getResources", "()Landroid/content/res/Resources"); + jmethodID getResourceMid = pEnv->GetMethodID(contextClass, "getResources", "()Landroid/content/res/Resources;"); // public abstract Configuration getConfiguration () - jmethodID getConfigurationMid = pEnv->GetMethodID(resourcesClass, "getConfiguration", "()Landroid/content/res/Configuration"); + jmethodID getConfigurationMid = pEnv->GetMethodID(resourcesClass, "getConfiguration", "()Landroid/content/res/Configuration;"); // public abstract boolean isLayoutSizeAtLeast (int layoutSize) jmethodID isLayoutSizeAtLeastMid = pEnv->GetMethodID(configurationClass, "isLayoutSizeAtLeast", "(I)Z"); diff --git a/lib/system/TelemetrySystem.cpp b/lib/system/TelemetrySystem.cpp index 2e5059b47..31780e506 100644 --- a/lib/system/TelemetrySystem.cpp +++ b/lib/system/TelemetrySystem.cpp @@ -192,6 +192,8 @@ namespace MAT_NS_BEGIN { #endif hcm.requestDone >> clockSkewDelta.decode >> httpDecoder.decode; + hcm.requestFailed >> storage.releaseRecords >> stats.onUploadFailed; + hcm.requestFailureComplete >> tpm.eventsUploadAborted; httpDecoder.eventsAccepted >> storage.deleteRecords >> stats.onUploadSuccessful >> tpm.eventsUploadSuccessful; httpDecoder.eventsRejected >> storage.deleteRecords >> stats.onUploadRejected >> tpm.eventsUploadRejected; @@ -251,4 +253,3 @@ namespace MAT_NS_BEGIN { } } MAT_NS_END - diff --git a/lib/tpm/TransmissionPolicyManager.cpp b/lib/tpm/TransmissionPolicyManager.cpp index 83b82cf2a..c8ba7ee30 100644 --- a/lib/tpm/TransmissionPolicyManager.cpp +++ b/lib/tpm/TransmissionPolicyManager.cpp @@ -47,7 +47,8 @@ namespace MAT_NS_BEGIN { m_system(system), m_taskDispatcher(taskDispatcher), m_config(m_system.getConfig()), - m_bandwidthController(bandwidthController) + m_bandwidthController(bandwidthController), + m_scheduledUploadCallbackState(std::make_shared(this)) { m_backoff = IBackoff::createFromConfig(m_backoffConfig); assert(m_backoff); @@ -56,6 +57,7 @@ namespace MAT_NS_BEGIN { TransmissionPolicyManager::~TransmissionPolicyManager() { + m_scheduledUploadCallbackState->Invalidate(); m_deviceStateHandler.Stop(); } @@ -111,26 +113,36 @@ namespace MAT_NS_BEGIN { LOG_TRACE("Collector URL is not set, no upload."); return; } - LOCKGUARD(m_scheduledUploadMutex); - if (delay.count() < 0 || m_timerdelay.count() < 0) - { - LOG_TRACE("Negative delay(%d) or m_timerdelay(%d), no upload", delay.count(), m_timerdelay.count()); - return; - } - if (m_scheduledUploadAborted) - { - LOG_TRACE("Scheduled upload aborted, no upload."); - return; - } - if (uploadCount() >= static_cast(m_config[CFG_INT_MAX_PENDING_REQ]) ) + auto shouldSkipScheduling = [&delay, this]() -> bool { - LOG_TRACE("Maximum number of HTTP requests reached"); - return; - } + if (delay.count() < 0 || m_timerdelay.count() < 0) + { + LOG_TRACE("Negative delay(%lld) or m_timerdelay(%lld), no upload", + static_cast(delay.count()), static_cast(m_timerdelay.count())); + return true; + } + if (m_scheduledUploadAborted) + { + LOG_TRACE("Scheduled upload aborted, no upload."); + return true; + } + if (uploadCount() >= static_cast(m_config[CFG_INT_MAX_PENDING_REQ])) + { + LOG_TRACE("Maximum number of HTTP requests reached"); + return true; + } + if (m_isPaused) + { + LOG_TRACE("Paused, not uploading anything until resumed"); + return true; + } + + return false; + }; - if (m_isPaused) + LOCKGUARD(m_scheduledUploadMutex); + if (shouldSkipScheduling()) { - LOG_TRACE("Paused, not uploading anything until resumed"); return; } @@ -151,10 +163,9 @@ namespace MAT_NS_BEGIN { if (delta <= static_cast(delay.count())) { // Don't need to cancel and reschedule if it's about to happen now anyways. - // m_isUploadScheduled check does not have to be strictly atomic because // the completion of upload will schedule more uploads as-needed, we only // want to avoid the unnecessary wasteful rescheduling. - LOG_TRACE("WAIT upload %d ms for lat=%d", delta, m_runningLatency); + LOG_TRACE("WAIT upload %llu ms for lat=%d", static_cast(delta), m_runningLatency); return; } } @@ -162,19 +173,47 @@ namespace MAT_NS_BEGIN { // Cancel upload if already scheduled. if (force || delay.count() == 0) { - if (!cancelUploadTask()) + if (!cancelUploadTaskNoWaitLocked()) { LOG_TRACE("Upload either hasn't been scheduled or already done."); + // Cancel can return false when the previous upload task is + // currently executing on the worker. If uploadAsync hasn't + // yet entered its own LOCKGUARD (m_isUploadScheduled is + // still set under the mutex we hold), propagate the + // requested latency so the running task picks it up when + // it acquires m_scheduledUploadMutex. Otherwise the + // running task has already cleared the flag and the + // schedule below will queue a fresh task. + if (m_isUploadScheduled) + { + m_runningLatency = latency; + } + } + if (shouldSkipScheduling()) + { + return; } } // Schedule new upload - if (!m_isUploadScheduled.exchange(true)) + if (!m_isUploadScheduled) { + m_isUploadScheduled = true; m_scheduledUploadTime = PAL::getMonotonicTimeMs() + delay.count(); m_runningLatency = latency; - LOG_TRACE("SCHED upload %d ms for lat=%d", delay.count(), m_runningLatency); - m_scheduledUpload = PAL::scheduleTask(&m_taskDispatcher, static_cast(delay.count()), this, &TransmissionPolicyManager::uploadAsync, latency); + LOG_TRACE("SCHED upload %lld ms for lat=%d", static_cast(delay.count()), m_runningLatency); + auto callbackState = m_scheduledUploadCallbackState; + m_scheduledUpload = PAL::scheduleTask( + &m_taskDispatcher, + static_cast(delay.count()), + [callbackState, latency]() { + callbackState->Invoke(latency); + }); + if (m_scheduledUpload.GetTask() == nullptr) + { + m_isUploadScheduled = false; + m_scheduledUploadTime = std::numeric_limits::max(); + } } } @@ -184,16 +223,15 @@ namespace MAT_NS_BEGIN { if (guard.isPaused()) { return; } - m_runningLatency = latency; - m_scheduledUploadTime = std::numeric_limits::max(); - + EventLatency requestedLatency = latency; { LOCKGUARD(m_scheduledUploadMutex); + requestedLatency = m_runningLatency; + m_scheduledUploadTime = std::numeric_limits::max(); m_isUploadScheduled = false; // Allow to schedule another uploadAsync if ((m_isPaused) || (m_scheduledUploadAborted)) { - LOG_TRACE("Paused or upload aborted: cancel pending upload task."); - cancelUploadTask(); // If there is a pending upload task, kill it + LOG_TRACE("Paused or upload aborted: skip upload."); return; } } @@ -210,14 +248,14 @@ namespace MAT_NS_BEGIN { unsigned delayMs = 1000; LOG_INFO("Bandwidth controller proposed bandwidth %u bytes/sec but minimum accepted is %u, will retry %u ms later", proposedBandwidthBps, minimumBandwidthBps, delayMs); - scheduleUpload(delayMs, latency); // reschedule uploadAsync to run again 1000 ms later + scheduleUpload(std::chrono::milliseconds{delayMs}, requestedLatency); // reschedule uploadAsync to run again 1000 ms later return; } } #endif auto ctx = m_system.createEventsUploadContext(); - ctx->requestedMinLatency = m_runningLatency; + ctx->requestedMinLatency = requestedLatency; addUpload(ctx); initiateUpload(ctx); } @@ -227,8 +265,8 @@ namespace MAT_NS_BEGIN { LOG_TRACE("HTTP upload finished for ctx=%p", ctx.get()); if (!removeUpload(ctx)) { - assert(false); LOG_WARN("HTTP NOT removing non-existing ctx from active uploads ctx=%p", ctx.get()); + return; } PauseGuard guard(m_system.getLogManager()); @@ -238,7 +276,7 @@ namespace MAT_NS_BEGIN { // Rescheduling upload if (nextUpload.count() >= 0) { - LOG_TRACE("Scheduling upload in %d ms", nextUpload.count()); + LOG_TRACE("Scheduling upload in %lld ms", static_cast(nextUpload.count())); EventLatency proposed = calculateNewPriority(); scheduleUpload(nextUpload, proposed); // reschedule uploadAsync again } @@ -284,8 +322,16 @@ namespace MAT_NS_BEGIN { LOCKGUARD(m_scheduledUploadMutex); // Prevent execution of all upload tasks m_scheduledUploadAborted = true; - // Make sure we wait for completion of the upload scheduling task that may be running - cancelUploadTask(); + } + // A queued task retains only the callback state. Invalidate it first so + // an uncooperative custom dispatcher cannot run the manager callback + // after teardown; Invalidate waits for an already-running callback. + m_scheduledUploadCallbackState->Invalidate(); + cancelUploadTask(true); + { + LOCKGUARD(m_scheduledUploadMutex); + m_isUploadScheduled = false; + m_scheduledUploadTime = std::numeric_limits::max(); } // Make sure we wait for all active upload callbacks to finish @@ -342,7 +388,12 @@ namespace MAT_NS_BEGIN { } // Schedule async upload if not scheduled yet - if (!m_isUploadScheduled || TransmitProfiles::isTimerUpdateRequired()) + bool isUploadScheduled = false; + { + LOCKGUARD(m_scheduledUploadMutex); + isUploadScheduled = m_isUploadScheduled; + } + if (!isUploadScheduled || TransmitProfiles::isTimerUpdateRequired()) { if (updateTimersIfNecessary()) { @@ -374,7 +425,13 @@ namespace MAT_NS_BEGIN { return EventLatency_RealTime; } - if (m_runningLatency == EventLatency_RealTime) + EventLatency runningLatency = EventLatency_RealTime; + { + LOCKGUARD(m_scheduledUploadMutex); + runningLatency = m_runningLatency; + } + + if (runningLatency == EventLatency_RealTime) { return EventLatency_Normal; } @@ -453,16 +510,51 @@ namespace MAT_NS_BEGIN { return (m_scheduledUploadAborted) ? DefaultTaskCancelTime : std::chrono::milliseconds {}; } - bool TransmissionPolicyManager::cancelUploadTask() + bool TransmissionPolicyManager::cancelUploadTaskNoWaitLocked() + { + bool result = m_scheduledUpload.Cancel(std::chrono::milliseconds {}.count()); + + if (result) + { + m_isUploadScheduled = false; + m_scheduledUploadTime = std::numeric_limits::max(); + } + return result; + } + + bool TransmissionPolicyManager::cancelUploadTask(bool waitForCompletion) { - bool result = m_scheduledUpload.Cancel(getCancelWaitTime().count()); + uint64_t waitTime = 0; + { + LOCKGUARD(m_scheduledUploadMutex); + if (waitForCompletion) + { + // Poll with a representable finite duration so custom + // ITaskDispatcher implementations do not have to interpret an + // unsigned sentinel as an infinite signed chrono duration. + waitTime = std::max( + 1, + static_cast(DefaultTaskCancelTime.count())); + } + else + { + waitTime = static_cast(getCancelWaitTime().count()); + } + if (waitTime == 0) + { + return cancelUploadTaskNoWaitLocked(); + } + } + bool result = m_scheduledUpload.Cancel(waitTime); // TODO: There is a potential for upload tasks to not be canceled, especially if they aren't waited for. // We either need a stronger guarantee here (could impact SDK performance), or a mechanism to // ensure those tasks are canceled when the log manager is destroyed. Issue 388 if (result) { - m_isUploadScheduled.exchange(false); + LOCKGUARD(m_scheduledUploadMutex); + m_isUploadScheduled = false; + m_scheduledUploadTime = std::numeric_limits::max(); } return result; } @@ -473,9 +565,10 @@ namespace MAT_NS_BEGIN { return m_activeUploads.size(); } - bool TransmissionPolicyManager::isUploadInProgress() const noexcept + bool TransmissionPolicyManager::isUploadInProgress() const { // unfinished uploads that haven't processed callbacks or pending upload task + LOCKGUARD(m_scheduledUploadMutex); return (uploadCount() > 0) || m_isUploadScheduled; } diff --git a/lib/tpm/TransmissionPolicyManager.hpp b/lib/tpm/TransmissionPolicyManager.hpp index e1a91ad10..840dac107 100644 --- a/lib/tpm/TransmissionPolicyManager.hpp +++ b/lib/tpm/TransmissionPolicyManager.hpp @@ -24,14 +24,14 @@ #include #include #include +#include +#include #include namespace MAT_NS_BEGIN { -// This macro allows to specify max upload task cancellation wait time at compile-time, -// addressing the case when a task that we are trying to cancel is currently running. -// Default value: 500ms - sufficient for upload scheduler/batcher task to finish. -// Alternate value: UINT64_MAX - for infinite wait until the task is completed. +// This macro specifies the maximum duration of one upload-task cancellation +// attempt when the task may already be running. The default is 500 ms. #ifdef UPLOAD_TASK_CANCEL_TIME_MS static_assert(std::numeric_limits::max() >= UPLOAD_TASK_CANCEL_TIME_MS, "std::numeric_limits::max() >= UPLOAD_TASK_CANCEL_TIME_MS"); static_assert(UPLOAD_TASK_CANCEL_TIME_MS >= 0, "UPLOAD_TASK_CANCEL_TIME_MS >= 0"); @@ -51,6 +51,32 @@ constexpr const char* const DefaultBackoffConfig = "E,3000,300000,2,1"; virtual void scheduleUpload(const std::chrono::milliseconds& delay, EventLatency latency, bool force = false); protected: + struct ScheduledUploadCallbackState + { + explicit ScheduledUploadCallbackState(TransmissionPolicyManager* owner) + : manager(owner) + { + } + + void Invoke(EventLatency latency) + { + std::lock_guard lock(mutex); + if (manager != nullptr) + { + manager->uploadAsync(latency); + } + } + + void Invalidate() + { + std::lock_guard lock(mutex); + manager = nullptr; + } + + std::mutex mutex; + TransmissionPolicyManager* manager; + }; + MATSDK_LOG_DECL_COMPONENT_CLASS(); void checkBackoffConfigUpdate(); void resetBackoff(); @@ -88,11 +114,12 @@ constexpr const char* const DefaultBackoffConfig = "E,3000,300000,2,1"; std::string m_backoffConfig { DefaultBackoffConfig }; std::unique_ptr m_backoff; DeviceStateHandler m_deviceStateHandler; + std::shared_ptr m_scheduledUploadCallbackState; std::atomic m_isPaused { true }; - std::atomic m_isUploadScheduled { false }; + bool m_isUploadScheduled { false }; uint64_t m_scheduledUploadTime { std::numeric_limits::max() }; - std::mutex m_scheduledUploadMutex; + mutable std::mutex m_scheduledUploadMutex; PAL::DeferredCallbackHandle m_scheduledUpload; bool m_scheduledUploadAborted { false }; @@ -120,9 +147,16 @@ constexpr const char* const DefaultBackoffConfig = "E,3000,300000,2,1"; std::chrono::milliseconds getCancelWaitTime() const noexcept; /// - /// Cancels pending upload task. + /// Cancels a pending upload task without waiting for a running task to finish. + /// The caller must already hold m_scheduledUploadMutex. /// - bool cancelUploadTask(); + bool cancelUploadTaskNoWaitLocked(); + + /// + /// Cancels a pending upload task, optionally asking the dispatcher to + /// wait for at most DefaultTaskCancelTime. + /// + bool cancelUploadTask(bool waitForCompletion = false); /// /// Calculate the number of pending upload contexts. @@ -152,7 +186,7 @@ constexpr const char* const DefaultBackoffConfig = "E,3000,300000,2,1"; RouteSink eventsUploadFailed{ this, &TransmissionPolicyManager::handleEventsUploadFailed }; RouteSink eventsUploadAborted{ this, &TransmissionPolicyManager::handleEventsUploadAborted }; - virtual bool isUploadInProgress() const noexcept; + virtual bool isUploadInProgress() const; virtual bool isPaused() const noexcept; }; @@ -160,4 +194,3 @@ constexpr const char* const DefaultBackoffConfig = "E,3000,300000,2,1"; } MAT_NS_END #endif // TRANSMISSIONPOLICYMANAGER_HPP - diff --git a/lib/tpm/TransmitProfiles.cpp b/lib/tpm/TransmitProfiles.cpp index 5daec5f8b..b26766f6f 100644 --- a/lib/tpm/TransmitProfiles.cpp +++ b/lib/tpm/TransmitProfiles.cpp @@ -58,6 +58,7 @@ static void initTransmitProfileFields() transmitProfilePowerState["unknown"] = (PowerSource_Unknown); transmitProfilePowerState["battery"] = (PowerSource_Battery); transmitProfilePowerState["charging"] = (PowerSource_Charging); + transmitProfilePowerState["low_battery"] = (PowerSource_LowBattery); }; #endif @@ -103,11 +104,14 @@ namespace MAT_NS_BEGIN { LOG_TRACE("name=%s", profile.name.c_str()); size_t i = 0; for (auto &rule : profile.rules) { - LOG_TRACE("[%d] netCost=%2d, powState=%2d, timers=[%3d,%3d,%3d]", + // Custom profiles may supply fewer than three timers, so read + // out-of-range slots as 0 instead of indexing past the vector. + auto timerOrZero = [&rule](size_t idx) { return idx < rule.timers.size() ? rule.timers[idx] : 0; }; + LOG_TRACE("[%zu] netCost=%2d, powState=%2d, timers=[%3d,%3d,%3d]", i, rule.netCost, rule.powerState, - rule.timers[0], - rule.timers[1], - rule.timers[2]); + timerOrZero(0), + timerOrZero(1), + timerOrZero(2)); i++; } } @@ -512,14 +516,17 @@ namespace MAT_NS_BEGIN { isTimerUpdated = true; #ifdef HAVE_MAT_LOGGING auto it = profiles.find(currProfileName); - if (it != profiles.end()) { + if (it != profiles.end() && currRule < it->second.rules.size()) { /* Debug routine to print the list of currently selected timers */ TransmitProfileRule &rule = (it->second).rules[currRule]; + // The rule may carry fewer than three timers, so read out-of-range + // slots as 0 instead of indexing past the vector. + auto timerOrZero = [&rule](size_t idx) { return idx < rule.timers.size() ? rule.timers[idx] : 0; }; // Print just 3 timers for now because we support only 3 LOG_INFO("timers=[%3d,%3d,%3d]", - rule.timers[0], - rule.timers[1], - rule.timers[2]); + timerOrZero(0), + timerOrZero(1), + timerOrZero(2)); } #endif } diff --git a/lib/utils/Utils.cpp b/lib/utils/Utils.cpp index 22a48d87f..a1cf48ee7 100644 --- a/lib/utils/Utils.cpp +++ b/lib/utils/Utils.cpp @@ -103,15 +103,30 @@ namespace MAT_NS_BEGIN { if (IsRunningInApp()) { auto hr = RoInitialize(RO_INIT_MULTITHREADED); - /* Ignoring result from call to `RoInitialize` as either initialzation is successful, or else already - * initialized and it should be ok to proceed in both the scenarios */ - UNREFERENCED_PARAMETER(hr); - - ::Windows::Storage::StorageFolder ^ temp = ::Windows::Storage::ApplicationData::Current->TemporaryFolder; - // TODO: [MG] - // - verify that the path ends with a slash - // -- add exception handler in case if AppData temp folder is not accessible - return from_platform_string(temp->Path->ToString()); + // RoInitialize returns S_OK when it initializes the apartment and + // S_FALSE when it was already initialized on this thread; both add a + // reference that must be balanced with RoUninitialize. The RAII guard + // balances a successful init on every exit path, including if a WinRT + // call below throws. RPC_E_CHANGED_MODE and other failures did not + // initialize and are left unbalanced. + struct ApartmentGuard + { + HRESULT hr; + ~ApartmentGuard() { if (SUCCEEDED(hr)) { RoUninitialize(); } } + } apartmentGuard{hr}; + + std::string tempPath; + { + // Release the WinRT StorageFolder before the guard runs (at the + // end of the enclosing scope) so the object is not destroyed in an + // uninitialized apartment. + ::Windows::Storage::StorageFolder ^ temp = ::Windows::Storage::ApplicationData::Current->TemporaryFolder; + // TODO: [MG] + // - verify that the path ends with a slash + // -- add exception handler in case if AppData temp folder is not accessible + tempPath = from_platform_string(temp->Path->ToString()); + } + return tempPath; } else { @@ -177,9 +192,6 @@ namespace MAT_NS_BEGIN { EventRejectedReason validateEventName(std::string const& name) { - // Data collector uses this regex (avoided here for code size reasons): - // ^[a-zA-Z0-9]([a-zA-Z0-9]|_){2,98}[a-zA-Z0-9]$ - if (name.length() < 1 + 2 + 1 || name.length() > 1 + 98 + 1) { LOG_ERROR("Invalid event name - \"%s\": must be between 4 and 100 characters long", name.c_str()); return REJECTED_REASON_VALIDATION_FAILED; @@ -191,13 +203,6 @@ namespace MAT_NS_BEGIN { return REJECTED_REASON_VALIDATION_FAILED; } -#if 0 - if (name.front() == '_' || name.back() == '_') { - LOG_ERROR("Invalid event name - \"%s\": must not start or end with an underscore", name.c_str()); - return REJECTED_REASON_VALIDATION_FAILED; - } -#endif - return REJECTED_REASON_OK; } @@ -247,4 +252,3 @@ namespace MAT_NS_BEGIN { } } MAT_NS_END - diff --git a/lib/utils/annex_k.hpp b/lib/utils/annex_k.hpp index 5aa4b73af..eed1ad577 100644 --- a/lib/utils/annex_k.hpp +++ b/lib/utils/annex_k.hpp @@ -7,9 +7,8 @@ #include #include #include -#ifndef _MSC_VER #include -#else +#ifdef _MSC_VER #include #endif @@ -47,21 +46,24 @@ class BoundCheckFunctions private: static bool oneds_buffer_region_overlap(const char *buffer1, size_t buffer1_len, const char *buffer2, size_t buffer2_len) noexcept { - if (buffer2 >= buffer1) + // Compare half-open address ranges without pointer arithmetic: the + // arguments may refer to different objects, and invalid lengths must not + // wrap an end address before the overlap check. + if (buffer1_len == 0 || buffer2_len == 0) { - if (buffer1 + buffer1_len - 1 > buffer2 ) - { - return true; - } + return false; } - else + + uintptr_t begin1 = reinterpret_cast(buffer1); + uintptr_t begin2 = reinterpret_cast(buffer2); + if (buffer1_len > UINTPTR_MAX - begin1 || buffer2_len > UINTPTR_MAX - begin2) { - if (buffer2 + buffer2_len - 1 > buffer1) - { - return true; - } + return true; } - return false; + + uintptr_t end1 = begin1 + buffer1_len; + uintptr_t end2 = begin2 + buffer2_len; + return begin1 < end2 && begin2 < end1; } public: @@ -147,12 +149,16 @@ static errno_t oneds_strncpy_s(char * restrict dest, rsize_t destsz, const char // In case of error, the entire destination range [dest, dest+destsz) is zeroed out // (if both dest and destsz are valid)) +// +// NOTE: the constraint checks below are performed here rather than delegated to +// the platform's Annex K / CRT memcpy_s. On MSVC the CRT memcpy_s reports a +// constraint violation through the invalid parameter handler, whose default +// behaviour terminates the process (__fastfail / STATUS_STACK_BUFFER_OVERRUN) +// instead of returning EINVAL. Validating first keeps the documented +// "return EINVAL and zero the destination" contract on every platform. static errno_t oneds_memcpy_s( void *restrict dest, rsize_t destsz, const void *restrict src, rsize_t count ) noexcept { -#if (defined __STDC_LIB_EXT1__) || ( defined _MSC_VER) - return memcpy_s(dest, destsz, src, count); -#else if (dest == NULL) { return EINVAL; @@ -172,17 +178,13 @@ static errno_t oneds_memcpy_s( void *restrict dest, rsize_t destsz, return EINVAL; } // donot allow overflow - if (oneds_buffer_region_overlap((char *)dest, destsz, (char *)src, count)) { + if (oneds_buffer_region_overlap((char*)dest, count, (char*)src, count)) + { memset(dest, 0, destsz); return EINVAL; } - void *result = memcpy(dest, src, count); - if (result == (void *)NULL) - { - return -1; - } + memcpy(dest, src, count); return 0; -#endif } }; } diff --git a/tests/common/HttpServer.hpp b/tests/common/HttpServer.hpp index 9f5d96ff5..a7a616c88 100644 --- a/tests/common/HttpServer.hpp +++ b/tests/common/HttpServer.hpp @@ -236,7 +236,13 @@ class HttpServer : private Reactor::Callback int sent = conn.socket.send(conn.sendBuffer.data(), static_cast(conn.sendBuffer.size())); LOG_TRACE("HttpServer: [%s] sent %d", conn.request.client.c_str(), sent); - if (sent < 0 && conn.socket.error() != Socket::ErrorWouldBlock) { + if (sent < 0 && conn.socket.error() == Socket::ErrorWouldBlock) + { + return true; + } + if (sent <= 0) + { + handleConnectionClosed(conn); return true; } conn.sendBuffer.erase(0, sent); @@ -662,4 +668,3 @@ class HttpServer : private Reactor::Callback } // namespace testing - diff --git a/tests/common/MockIOfflineStorage.hpp b/tests/common/MockIOfflineStorage.hpp index d0bae4118..4c37df7d4 100644 --- a/tests/common/MockIOfflineStorage.hpp +++ b/tests/common/MockIOfflineStorage.hpp @@ -14,7 +14,7 @@ namespace testing { #pragma clang diagnostic ignored "-Winconsistent-missing-override" // GMock MOCK_METHOD* macros don't use override. #endif -class MockIOfflineStorage : public MAT::IOfflineStorage { +class MockIOfflineStorage : public MAT::IOfflineStorageModule { public: MockIOfflineStorage(); virtual ~MockIOfflineStorage(); @@ -46,4 +46,3 @@ class MockIOfflineStorage : public MAT::IOfflineStorage { #endif } // namespace testing - diff --git a/tests/common/MockIRuntimeConfig.hpp b/tests/common/MockIRuntimeConfig.hpp index a52ef8e8d..04a720732 100644 --- a/tests/common/MockIRuntimeConfig.hpp +++ b/tests/common/MockIRuntimeConfig.hpp @@ -19,13 +19,13 @@ namespace testing { class MockIRuntimeConfig : public MAT::RuntimeConfig_Default /* MAT::IRuntimeConfig */ { protected: - std::unique_ptr& GetStaticConfig() noexcept + static std::unique_ptr& GetStaticConfig() noexcept { static std::unique_ptr staticConfig; return staticConfig; } - MAT::ILogConfiguration& GetDefaultConfig() + static MAT::ILogConfiguration& GetDefaultConfig() { std::unique_ptr& staticConfig = GetStaticConfig(); if (!staticConfig) @@ -72,4 +72,3 @@ namespace testing { #endif } // namespace testing - diff --git a/tests/common/Reactor.cpp b/tests/common/Reactor.cpp index 6cb55f13d..c494156a8 100644 --- a/tests/common/Reactor.cpp +++ b/tests/common/Reactor.cpp @@ -179,23 +179,88 @@ namespace SocketTools { void Reactor::onThread() { LOG_INFO("Reactor: Thread started"); +#ifdef _WIN32 + size_t nextEventChunk = 0; +#endif while(!shouldTerminate()) { #ifdef _WIN32 - DWORD dwResult = ::WSAWaitForMultipleEvents(static_cast(m_events.size()), m_events.data(), FALSE, 500, FALSE); + if (m_events.empty()) + { + ::Sleep(10); + continue; + } + + const size_t maxEvents = WSA_MAXIMUM_WAIT_EVENTS; + const size_t chunkCount = (m_events.size() + maxEvents - 1) / maxEvents; + if (nextEventChunk >= chunkCount) + { + nextEventChunk = 0; + } + + DWORD dwResult = WSA_WAIT_TIMEOUT; + size_t selectedChunkStart = 0; + bool waitFailed = false; + for (size_t offset = 0; offset < chunkCount; ++offset) + { + const size_t chunk = (nextEventChunk + offset) % chunkCount; + const size_t chunkStart = chunk * maxEvents; + const DWORD chunkSize = static_cast( + std::min(maxEvents, m_events.size() - chunkStart)); + dwResult = ::WSAWaitForMultipleEvents( + chunkSize, m_events.data() + chunkStart, FALSE, 0, FALSE); + if (dwResult == WSA_WAIT_FAILED) + { + LOG_ERROR("WSAWaitForMultipleEvents failed: %d", ::WSAGetLastError()); + waitFailed = true; + continue; + } + if (dwResult != WSA_WAIT_TIMEOUT) + { + selectedChunkStart = chunkStart; + nextEventChunk = (chunk + 1) % chunkCount; + break; + } + } + if (dwResult == WSA_WAIT_TIMEOUT) { + const size_t chunkStart = nextEventChunk * maxEvents; + const DWORD chunkSize = static_cast( + std::min(maxEvents, m_events.size() - chunkStart)); + dwResult = ::WSAWaitForMultipleEvents( + chunkSize, m_events.data() + chunkStart, FALSE, 50, FALSE); + selectedChunkStart = chunkStart; + nextEventChunk = (nextEventChunk + 1) % chunkCount; + } + + if (dwResult == WSA_WAIT_TIMEOUT) + { + continue; + } + if (dwResult == WSA_WAIT_FAILED) + { + LOG_ERROR("WSAWaitForMultipleEvents failed: %d", ::WSAGetLastError()); + if (waitFailed) + { + ::Sleep(10); + } continue; } - assert(dwResult <= WSA_WAIT_EVENT_0 + m_events.size()); - int index = dwResult - WSA_WAIT_EVENT_0; + const size_t index = selectedChunkStart + + static_cast(dwResult - WSA_WAIT_EVENT_0); + if (index >= m_events.size() || index >= m_sockets.size()) + { + LOG_ERROR("WSAWaitForMultipleEvents returned invalid index %zu", index); + continue; + } Socket socket = m_sockets[index].socket; int flags = m_sockets[index].flags; WSANETWORKEVENTS ne; ::WSAEnumNetworkEvents(socket, m_events[index], &ne); - LOG_TRACE("Reactor: Handling socket 0x%x (index %d) with active flags 0x%x (armed 0x%x)", + LOG_TRACE("Reactor: Handling socket 0x%x (index %zu) with active flags 0x%x (armed 0x%x)", static_cast(socket), index, ne.lNetworkEvents, flags); if ((flags & Readable) && (ne.lNetworkEvents & FD_READ)) @@ -266,7 +331,12 @@ namespace SocketTools { struct kevent& event = m_events[i]; int fd = (int)event.ident; auto it = std::find(m_sockets.begin(), m_sockets.end(), fd); - assert(it != m_sockets.end()); + if (it == m_sockets.end()) + { + // An earlier notification in this batch may have closed and + // removed the socket. Discard any remaining stale events. + continue; + } Socket socket = it->socket; int flags = it->flags; @@ -321,4 +391,3 @@ namespace SocketTools { }; } - diff --git a/tests/common/Reactor.hpp b/tests/common/Reactor.hpp index 26b1fb46b..f24aed2ab 100644 --- a/tests/common/Reactor.hpp +++ b/tests/common/Reactor.hpp @@ -63,11 +63,8 @@ class Reactor : protected Thread Reactor(Callback& callback) : m_callback(callback) { #ifdef __linux__ -#ifdef ANDROID - m_epollFd = ::epoll_create(0); -#else m_epollFd = ::epoll_create1(0); -#endif + assert(m_epollFd >= 0); #endif #ifdef TARGET_OS_MAC bzero(&m_events[0], sizeof(m_events)); @@ -91,4 +88,3 @@ class Reactor : protected Thread #endif - diff --git a/tests/common/SocketTools.hpp b/tests/common/SocketTools.hpp index 0bfe350d3..48afe9ba1 100644 --- a/tests/common/SocketTools.hpp +++ b/tests/common/SocketTools.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -229,11 +230,23 @@ class Socket Socket(Type sock = Invalid) : m_sock(sock) { +#ifdef TARGET_OS_MAC + if (m_sock != Invalid) + { + setNoSigPipe(); + } +#endif } Socket(int af, int type, int proto) { m_sock = ::socket(af, type, proto); +#ifdef TARGET_OS_MAC + if (m_sock != Invalid) + { + setNoSigPipe(); + } +#endif } ~Socket() @@ -288,6 +301,33 @@ class Socket return (::setsockopt(m_sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&value), sizeof(value)) == 0); } + /** + * Suppress SIGPIPE when writing to a socket whose peer has already gone away. + * + * The test HTTP server writes responses on the reactor thread. When a client + * (e.g. NSURLSession on Apple) cancels an in-flight upload during teardown, the + * connection can be reset before the response is flushed, so ::send() fails with + * EPIPE and raises SIGPIPE. The test process installs no SIGPIPE handler, so the + * default disposition terminates it - which surfaces as a silent, backtrace-less + * test-runner exit/restart rather than a normal test failure. + * + * Apple/BSD only supports this per-socket via SO_NOSIGPIPE; Linux uses the + * MSG_NOSIGNAL send() flag instead (see send() below). + */ + bool setNoSigPipe() + { +#ifdef SO_NOSIGPIPE + if (m_sock == Invalid) + { + return false; + } + int value = 1; + return (::setsockopt(m_sock, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(value)) == 0); +#else + return true; +#endif + } + bool setNoDelay() { assert(m_sock != Invalid); @@ -326,7 +366,14 @@ class Socket int send(void const* buffer, unsigned size) { assert(m_sock != Invalid); - return static_cast(::send(m_sock, reinterpret_cast(buffer), size, 0)); +#if defined(MSG_NOSIGNAL) + // Linux: ask the kernel to return EPIPE instead of raising SIGPIPE. + int flags = MSG_NOSIGNAL; +#else + // Apple/Windows: handled by SO_NOSIGPIPE / not applicable. + int flags = 0; +#endif + return static_cast(::send(m_sock, reinterpret_cast(buffer), size, flags)); } bool bind(SocketAddr const& addr) @@ -361,6 +408,12 @@ class Socket socklen_t addrlen = sizeof(caddr); #endif csock = ::accept(m_sock, caddr, &addrlen); + if (!csock.invalid()) + { + // Accepted connections are written to from the reactor thread; a peer + // that resets mid-response must not kill the test process via SIGPIPE. + csock.setNoSigPipe(); + } return !csock.invalid(); } @@ -409,7 +462,7 @@ class Thread { private: std::thread m_thread; - volatile bool m_terminate { false }; + std::atomic m_terminate { false }; protected: Thread() @@ -437,7 +490,7 @@ class Thread bool shouldTerminate() const { - return m_terminate; + return m_terminate.load(); } virtual void onThread() = 0; @@ -465,5 +518,3 @@ struct SocketData } #endif - - diff --git a/tests/functests/APITest.cpp b/tests/functests/APITest.cpp index baea0112e..596470b80 100644 --- a/tests/functests/APITest.cpp +++ b/tests/functests/APITest.cpp @@ -15,7 +15,12 @@ #include #include +#include #include +#include +#include +#include +#include #include "PayloadDecoder.hpp" @@ -210,6 +215,40 @@ class TestDebugEventListener : public DebugEventListener { } }; +class HttpResponseWaiter final : public IHttpResponseCallback { +public: + void OnHttpResponse(IHttpResponse* response) override + { + std::lock_guard lock(m_mutex); + ++m_callbackCount; + m_response.reset(response); + m_cv.notify_all(); + } + + void OnHttpStateEvent(HttpStateEvent, void*, size_t) override + { + } + + std::unique_ptr WaitForResponse(std::chrono::seconds timeout) + { + std::unique_lock lock(m_mutex); + m_cv.wait_for(lock, timeout, [this]() { return m_response != nullptr; }); + return std::move(m_response); + } + + size_t CallbackCount() const + { + std::lock_guard lock(m_mutex); + return m_callbackCount; + } + +private: + mutable std::mutex m_mutex; + std::condition_variable m_cv; + std::unique_ptr m_response; + size_t m_callbackCount {0}; +}; + // Keep requests in flight until teardown cancels them, then simulate a connection // reset while honoring IHttpClient's exactly-once callback contract. class NetworkFailureHttpClient final : public IHttpClient @@ -673,38 +712,32 @@ constexpr static unsigned MAX_THREADS = 25; /// The configuration. void StressUploadLockMultiThreaded(ILogConfiguration& config) { - std::srand(static_cast(std::time(nullptr))); TestDebugEventListener debugListener; addAllListeners(debugListener); size_t numIterations = MAX_ITERATIONS_MT; - std::mutex m_threads_mtx; - std::atomic threadCount(0); - while (numIterations--) { ILogger *result = LogManager::Initialize(TEST_TOKEN, config); - // Keep spawning UploadNow threads while the main thread is trying to perform - // Initialize and Teardown, but no more than MAX_THREADS at a time. + std::vector uploadThreads; + uploadThreads.reserve(MAX_THREADS); for (size_t i = 0; i < MAX_THREADS; i++) { - if (threadCount++ < MAX_THREADS) + uploadThreads.emplace_back([]() { - auto t = std::thread([&]() - { - std::this_thread::yield(); - LogManager::UploadNow(); - const auto randTimeSub2ms = std::rand() % 2; - PAL::sleep(randTimeSub2ms); - threadCount--; - }); - t.detach(); - } - }; + std::this_thread::yield(); + LogManager::UploadNow(); + PAL::sleep(0); + }); + } EventProperties props = testing::CreateSampleEvent("event_name", EventPriority_Normal); result->LogEvent(props); LogManager::FlushAndTeardown(); + for (auto& uploadThread : uploadThreads) + { + uploadThread.join(); + } } removeAllListeners(debugListener); } @@ -1252,8 +1285,66 @@ TEST(APITest, LogManager_BadStoragePath_Test) } -#ifdef HAVE_MAT_WININET_HTTP_CLIENT -/* This test requires WinInet HTTP client */ +#if defined(_WIN32) && defined(HAVE_MAT_DEFAULT_HTTP_CLIENT) +TEST(APITest, WindowsHttpTransport_MsRoot_Check) +{ + struct RequestOutcome + { + std::unique_ptr response; + size_t callbackCount {0}; + }; + + auto sendRequest = [](bool enforceMsRoot) { + HttpResponseWaiter callback; + // A fresh client gives the checked request a cold transport session; do + // not warm this endpoint with an unchecked request first. + auto client = HttpClientFactory::Create(); +#if defined(HAVE_MAT_WININET_HTTP_CLIENT) + auto windowsClient = dynamic_cast(client.get()); +#elif defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + auto windowsClient = dynamic_cast(client.get()); +#else +#error A Windows HTTP transport must be selected. +#endif + if (windowsClient == nullptr) + { + ADD_FAILURE() << "HttpClientFactory returned the wrong Windows transport"; + return RequestOutcome{}; + } + windowsClient->SetMsRootCheck(enforceMsRoot); + + std::unique_ptr request(client->CreateRequest()); + request->SetMethod("POST"); + request->SetUrl("https://mobile.events.data.microsoft.com/OneCollector/1.0/"); + std::vector body {'{', '}'}; + request->SetBody(body); + client->SendRequestAsync(request.get(), &callback); + + auto response = callback.WaitForResponse(std::chrono::seconds(10)); + if (response == nullptr) + { + client->CancelAllRequests(); + response = callback.WaitForResponse(std::chrono::seconds(2)); + } + client.reset(); + return RequestOutcome {std::move(response), callback.CallbackCount()}; + }; + + // The negative case must execute first so its certificate decision is not + // preceded by a successful request to the same endpoint. + auto rejected = sendRequest(true); + ASSERT_NE(rejected.response, nullptr); + EXPECT_EQ(rejected.callbackCount, 1u); + EXPECT_EQ(rejected.response->GetResult(), HttpResult_NetworkFailure); + EXPECT_EQ(rejected.response->GetStatusCode(), 0u); + + auto accepted = sendRequest(false); + ASSERT_NE(accepted.response, nullptr); + EXPECT_EQ(accepted.callbackCount, 1u); + EXPECT_EQ(accepted.response->GetResult(), HttpResult_OK); +} + +/* This test verifies the certificate policy used by either Windows HTTP transport. */ TEST(APITest, LogConfiguration_MsRoot_Check) { TestDebugEventListener debugListener; @@ -1283,13 +1374,21 @@ TEST(APITest, LogConfiguration_MsRoot_Check) debugListener.reset(); addAllListeners(debugListener); logger->LogEvent("fooBar"); + LogManager::UploadNow(); + const auto deadline = PAL::getMonotonicTimeMs() + 10000; + while (PAL::getMonotonicTimeMs() < deadline && + debugListener.numHttpOK.load() == 0 && + debugListener.numHttpError.load() == 0) + { + PAL::sleep(50); + } LogManager::FlushAndTeardown(); removeAllListeners(debugListener); - // Connection is a best-effort, occasionally we can't connect, - // but we MUST NOT connect to end-point that doesn't have the - // right cert. - EXPECT_LE(debugListener.numHttpOK, expectedHttpCount); + // The successful cases establish that the runner can reach both + // endpoints, so the rejected case cannot pass merely because external + // networking is unavailable. + EXPECT_EQ(debugListener.numHttpOK.load(), expectedHttpCount); } } #endif diff --git a/tests/functests/BasicFuncTests.cpp b/tests/functests/BasicFuncTests.cpp index bc879d3e6..7f231b07b 100644 --- a/tests/functests/BasicFuncTests.cpp +++ b/tests/functests/BasicFuncTests.cpp @@ -122,12 +122,54 @@ class HttpPostListener : public DebugEventListener }; }; }; + +class DroppedEventListener : public DebugEventListener +{ +public: + void OnDebugEvent(DebugEvent& evt) override + { + if (evt.type == EVT_DROPPED) + { + if (evt.param2 == static_cast(DROPPED_REASON_OFFLINE_STORAGE_OVERFLOW)) + { + overflowDrops += evt.param1; + } + else if (evt.param2 == static_cast(DROPPED_REASON_RETRY_EXCEEDED)) + { + retryExceededDrops += evt.param1; + } + } + else if (evt.type == EVT_SEND_RETRY) + { + sendRetries++; + } + } + + bool waitForAtLeast( + std::atomic const& counter, + size_t expected, + unsigned timeoutMs) const + { + const auto deadline = PAL::getMonotonicTimeMs() + timeoutMs; + while (counter.load() < expected && PAL::getMonotonicTimeMs() < deadline) + { + PAL::sleep(10); + } + return counter.load() >= expected; + } + + std::atomic overflowDrops { 0 }; + std::atomic retryExceededDrops { 0 }; + std::atomic sendRetries { 0 }; +}; + class BasicFuncTests : public ::testing::Test, public HttpServer::Callback { protected: std::mutex mtx_requests; std::vector receivedRequests; + std::string serverBaseAddress; std::string serverAddress; HttpServer server; @@ -139,6 +181,9 @@ class BasicFuncTests : public ::testing::Test, std::condition_variable cv_gotEvents; std::mutex cv_m; + std::condition_variable cv_slowRequest; + std::mutex mtx_slowRequest; + bool slowRequestStarted = false; public: BasicFuncTests() : @@ -155,7 +200,8 @@ class BasicFuncTests : public ::testing::Test, int port = server.addListeningPort(HTTP_PORT); std::ostringstream os; os << "127.0.0.1:" << port; - serverAddress = "http://" + os.str() + "/simple/"; + serverBaseAddress = "http://" + os.str(); + serverAddress = serverBaseAddress + "/simple/"; server.setServerName(os.str()); server.addHandler("/simple/", *this); server.addHandler("/slow/", *this); @@ -186,9 +232,16 @@ class BasicFuncTests : public ::testing::Test, std::remove((fileName + "-journal").c_str()); } - virtual void Initialize() + virtual void Initialize( + int64_t maxTeardownUploadTimeInSec = 2, + int64_t cacheFileSize = 4096 * 1024, + int64_t maxRetryCount = 5, + std::string const& retryBackoff = "E,500,5000,2,1") { - receivedRequests.clear(); + { + LOCKGUARD(mtx_requests); + receivedRequests.clear(); + } auto configuration = LogManager::GetLogConfiguration(); configuration[CFG_INT_TRACE_LEVEL_MASK] = 0xFFFFFFFF; @@ -201,15 +254,16 @@ class BasicFuncTests : public ::testing::Test, configuration[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; configuration[CFG_STR_CACHE_FILE_PATH] = TEST_STORAGE_FILENAME; - configuration[CFG_INT_CACHE_FILE_SIZE] = 4096 * 1024; // 4MB default - configuration[CFG_INT_MAX_TEARDOWN_TIME] = 2; // 2 seconds wait on shutdown + configuration[CFG_INT_CACHE_FILE_SIZE] = cacheFileSize; + configuration[CFG_INT_MAX_TEARDOWN_TIME] = maxTeardownUploadTimeInSec; configuration[CFG_INT_STORAGE_FULL_PCT] = 75; // default configuration[CFG_INT_STORAGE_FULL_CHECK_TIME] = 5000; // default 5s configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; // disable compression for now - configuration[CFG_MAP_TPM][CFG_STR_TPM_BACKOFF] = "E,500,5000,2,1"; // faster retry for localhost tests + configuration[CFG_MAP_TPM][CFG_INT_TPM_MAX_RETRY] = maxRetryCount; + configuration[CFG_MAP_TPM][CFG_STR_TPM_BACKOFF] = retryBackoff; configuration[CFG_MAP_METASTATS_CONFIG][CFG_INT_METASTATS_INTERVAL] = 30 * 60; // 30 mins - configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default since #1420) + configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default) configuration["name"] = __FILE__; configuration["version"] = "1.0.0"; @@ -237,6 +291,11 @@ class BasicFuncTests : public ::testing::Test, } if (request.uri.compare(0, 6, "/slow/") == 0) { + { + std::lock_guard lock(mtx_slowRequest); + slowRequestStarted = true; + } + cv_slowRequest.notify_all(); PAL::sleep(static_cast(request.content.size() / DELAY_FACTOR_FOR_SERVER)); } @@ -251,6 +310,15 @@ class BasicFuncTests : public ::testing::Test, return 200; } + bool waitForSlowRequest(unsigned timeoutSec) + { + std::unique_lock lock(mtx_slowRequest); + return cv_slowRequest.wait_for( + lock, + std::chrono::seconds(timeoutSec), + [this] { return slowRequestStarted; }); + } + bool waitForRequests(unsigned timeOutSec, unsigned expected_count = 1) { std::unique_lock lk(cv_m); @@ -502,6 +570,7 @@ class BasicFuncTests : public ::testing::Test, std::vector records() { + LOCKGUARD(mtx_requests); std::vector result; if (receivedRequests.size()) { @@ -521,6 +590,7 @@ class BasicFuncTests : public ::testing::Test, // Find first matching event CsProtocol::Record find(const std::string& name) { + LOCKGUARD(mtx_requests); CsProtocol::Record result; result.name = ""; if (receivedRequests.size()) @@ -594,6 +664,37 @@ TEST_F(BasicFuncTests, sendOneEvent_immediatelyStop) EXPECT_GE(receivedRequests.size(), (size_t)1); // at least 1 HTTP request with customer payload and stats } +TEST_F(BasicFuncTests, teardownDuringInFlightUpload_ShutsDownCleanly) +{ + // Smoke test for teardown while an upload is in flight. + // Uploads target the /slow/ endpoint with large payloads and MAX_TEARDOWN_TIME + // is 0, so FlushAndTeardown() returns while an upload is still outstanding. + // Teardown must complete cleanly without touching freed SDK state; run under a + // sanitizer (ASan/TSan) this guards the teardown-vs-upload path. + CleanStorage(); + static int64_t const ONE_EVENT_SIZE = 256 * 1024; + + // Point Initialize() at the (slow) endpoint so uploads stay in flight. + std::string savedAddress = serverAddress; + serverAddress = serverBaseAddress + "/slow/"; + Initialize(0); + serverAddress = savedAddress; + + for (int i = 0; i < 20; ++i) + { + EventProperties event("teardown_event"); + event.SetPriority(EventPriority_Normal); + event.SetProperty("big_data", std::string(static_cast(ONE_EVENT_SIZE), 'x')); + logger->LogEvent(event); + } + LogManager::UploadNow(); + ASSERT_TRUE(waitForSlowRequest(5)) + << "Upload did not reach the /slow/ endpoint"; + // Teardown with timeout 0 returns while the upload is still outstanding. + LogManager::FlushAndTeardown(); + SUCCEED(); +} + TEST_F(BasicFuncTests, sendNoPriorityEvents) { CleanStorage(); @@ -806,19 +907,20 @@ TEST_F(BasicFuncTests, configDecorations) TEST_F(BasicFuncTests, restartRecoversEventsFromStorage) { + EventProperties event1("first_event"); + EventProperties event2("second_event"); + event1.SetProperty("property1", "value1"); + event2.SetProperty("property2", "value2"); + event1.SetLatency(MAT::EventLatency::EventLatency_RealTime); + event1.SetPersistence(MAT::EventPersistence::EventPersistence_Critical); + event2.SetLatency(MAT::EventLatency::EventLatency_RealTime); + event2.SetPersistence(MAT::EventPersistence::EventPersistence_Critical); + { CleanStorage(); Initialize(); // This code is a bit racy because ResumeTransmission is done in Initialize LogManager::PauseTransmission(); - EventProperties event1("first_event"); - EventProperties event2("second_event"); - event1.SetProperty("property1", "value1"); - event2.SetProperty("property2", "value2"); - event1.SetLatency(MAT::EventLatency::EventLatency_RealTime); - event1.SetPersistence(MAT::EventPersistence::EventPersistence_Critical); - event2.SetLatency(MAT::EventLatency::EventLatency_RealTime); - event2.SetPersistence(MAT::EventPersistence::EventPersistence_Critical); logger->LogEvent(event1); logger->LogEvent(event2); FlushAndTeardown(); @@ -833,30 +935,16 @@ TEST_F(BasicFuncTests, restartRecoversEventsFromStorage) LogManager::SetTransmitProfile(TransmitProfile_RealTime); LogManager::UploadNow(); - // 1st request for realtime event - waitForEvents(10, 5); // start, first_event, second_event, ongoing, stop, start, fooEvent - // we drop two of the events during pause, though. - EXPECT_GE(receivedRequests.size(), (size_t)1); - if (receivedRequests.size() != 0) - { - auto payload = decodeRequest(receivedRequests[receivedRequests.size() - 1], false); - } + // The first manager persists both paused customer events and its lifecycle + // metastats; the second manager then uploads those plus its own start event. + waitForEvents(10, 7); + verifyEvent(event1, find(event1.GetName())); + verifyEvent(event2, find(event2.GetName())); + verifyEvent(fooEvent, find(fooEvent.GetName())); FlushAndTeardown(); } - - /* - ASSERT_THAT(receivedRequests, SizeIs(1)); - auto payload = decodeRequest(receivedRequests[0], false); - ASSERT_THAT(payload.TokenToDataPackagesMap, Contains(Key("functests-tenant-token"))); - ASSERT_THAT(payload.TokenToDataPackagesMap["functests-tenant-token"], SizeIs(1)); - auto const& dp = payload.TokenToDataPackagesMap["functests-tenant-token"][0]; - ASSERT_THAT(payload, SizeIs(2)); - verifyEvent(event1, payload[0]); - verifyEvent(event2, payload[1]); - */ } -#if 0 // FIXME: 1445871 [v3][1DS] Offline storage size may exceed configured limit TEST_F(BasicFuncTests, storageFileSizeDoesntExceedConfiguredSize) { CleanStorage(); @@ -865,15 +953,15 @@ TEST_F(BasicFuncTests, storageFileSizeDoesntExceedConfiguredSize) static int64_t const MAX_FILE_SIZE = 8 * 1024 * 1024; static int64_t const ALLOWED_OVERFLOW = 10 * MAX_FILE_SIZE / 100; - auto &configuration = LogManager::GetLogConfiguration(); - configuration[CFG_INT_MAX_TEARDOWN_TIME] = 0; - configuration[CFG_INT_CACHE_FILE_SIZE] = MAX_FILE_SIZE; - - std::string slowServiceUrl; - slowServiceUrl.insert(slowServiceUrl.find('/', sizeof("http://")) + 1, "slow/"); - configuration[CFG_STR_COLLECTOR_URL] = slowServiceUrl.c_str(); + auto& configuration = LogManager::GetLogConfiguration(); + configuration[CFG_BOOL_ENABLE_DB_DROP_IF_FULL] = true; + DroppedEventListener listener; + LogManager::AddEventListener(DebugEventType::EVT_DROPPED, listener); + std::string savedAddress = serverAddress; + serverAddress = serverBaseAddress + "/slow/"; { - Initialize(); + Initialize(0, MAX_FILE_SIZE); + serverAddress = savedAddress; LogManager::PauseTransmission(); for (int i = 0; i < 50; i++) { EventProperties event("event" + toString(i)); @@ -882,43 +970,18 @@ TEST_F(BasicFuncTests, storageFileSizeDoesntExceedConfiguredSize) event.SetProperty("big_data", std::string(ONE_EVENT_SIZE, '\42')); logger->LogEvent(event); } - // Check meta stats after restart. Because of their high priority, they will - // be sent alone in the very first request regardless of other events. FlushAndTeardown(); std::string fileName = MAT::GetTempDirectory(); - fileName += "\\"; + fileName += PATH_SEPARATOR_CHAR; fileName += TEST_STORAGE_FILENAME; size_t fileSize = getFileSize(fileName); EXPECT_LE(fileSize, (size_t)(MAX_FILE_SIZE + ALLOWED_OVERFLOW)); + EXPECT_GT(listener.overflowDrops.load(), size_t { 0 }); } - - // Restore fast URL - configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); - - { - Initialize(); - waitForEvents(5, 8); - if (receivedRequests.size()) - { - auto payload = decodeRequest(receivedRequests[0], false); - /* auto payload = decodeRequest(receivedRequests[0], false); - ASSERT_THAT(payload.TokenToDataPackagesMap["metastats-tenant-token"], SizeIs(1)); - auto const& dp = payload.TokenToDataPackagesMap["metastats-tenant-token"][0]; - ASSERT_THAT(payload, SizeIs(2)); - EXPECT_THAT(payload[0].Id, Not(IsEmpty())); - EXPECT_THAT(payload[0].Type, Eq("client_telemetry")); - EXPECT_THAT(payload[0].Extension, Contains(Pair("stats_rollup_kind", "stop"))); - // The expected number of dropped events is hard to estimate because of database overhead, - // varying timing, some events have been sent etc. Just check that it's at least a quarter. - EXPECT_THAT(payload[0].Extension, Contains(Pair("records_dropped_offline_storage_overflow", StrAsIntGt(50 / 4)))); - */ - } - FlushAndTeardown(); - } - + LogManager::RemoveEventListener(DebugEventType::EVT_DROPPED, listener); + configuration[CFG_BOOL_ENABLE_DB_DROP_IF_FULL] = false; } -#endif TEST_F(BasicFuncTests, sendMetaStatsOnStart) { @@ -945,10 +1008,10 @@ TEST_F(BasicFuncTests, sendMetaStatsOnStart) LogManager::ResumeTransmission(); // ? LogManager::SetTransmitProfile(TransmitProfile_RealTime); LogManager::UploadNow(); - waitForEvents(5, 4); // (start + stop) + (2 events + start) + waitForEvents(5, 6); // Four lifecycle metastats plus the two persisted customer events. auto r2 = records(); - ASSERT_GE(r2.size(), (size_t)4); // (start + stop) + (2 events + start) + ASSERT_EQ(r2.size(), (size_t)6); for (const auto &evt : { event1, event2 }) { @@ -1200,7 +1263,7 @@ TEST_F(BasicFuncTests, killSwitchWorks) configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; // disable compression for now configuration[CFG_MAP_METASTATS_CONFIG]["interval"] = 30 * 60; // 30 mins - configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default since #1420) + configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default) configuration["name"] = __FILE__; configuration["version"] = "1.0.0"; @@ -1260,6 +1323,7 @@ TEST_F(BasicFuncTests, killSwitchWorks) myLogger->LogEvent(event2); } // Expect all events to be dropped + EXPECT_TRUE(listener.waitForAtLeast(listener.numDropped, 100, 10000)); EXPECT_EQ(uint32_t { 100 }, listener.numDropped); LogManager::FlushAndTeardown(); @@ -1280,9 +1344,9 @@ TEST_F(BasicFuncTests, killIsTemporary) configuration[CFG_STR_CACHE_FILE_PATH] = TEST_STORAGE_FILENAME; configuration[CFG_INT_MAX_TEARDOWN_TIME] = 2; configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); - configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; - configuration[CFG_MAP_METASTATS_CONFIG]["interval"] = 30 * 60; - configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; + configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = false; // disable compression for now + configuration[CFG_MAP_METASTATS_CONFIG]["interval"] = 30 * 60; // 30 mins + configuration[CFG_MAP_METASTATS_CONFIG]["enabled"] = true; // opt in to stats (disabled by default) configuration["name"] = __FILE__; configuration["version"] = "1.0.0"; configuration["config"] = { { "host", __FILE__ } }; @@ -1364,7 +1428,10 @@ TEST_F(BasicFuncTests, sendManyRequestsAndCancel) configuration[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; configuration[CFG_STR_CACHE_FILE_PATH] = TEST_STORAGE_FILENAME; configuration[CFG_MAP_HTTP][CFG_BOOL_HTTP_COMPRESSION] = true; - configuration[CFG_STR_COLLECTOR_URL] = COLLECTOR_URL_PROD; + // Use the fixture's local slow endpoint so cancellation does not depend + // on how the CI runner handles connections to an unused port. + const std::string slowCollectorUrl = serverBaseAddress + "/slow/"; + configuration[CFG_STR_COLLECTOR_URL] = slowCollectorUrl.c_str(); configuration[CFG_INT_MAX_TEARDOWN_TIME] = (int64_t)(i % 2); configuration[CFG_INT_TRACE_LEVEL_MASK] = 0; configuration[CFG_INT_TRACE_LEVEL_MIN] = ACTTraceLevel_Warn; @@ -1566,59 +1633,41 @@ TEST_F(BasicFuncTests, deleteEvents) for (const auto &e: events2) { verifyEvent(e, find(e.GetName())); } + FlushAndTeardown(); } #endif -#if 0 // TODO: [MG] - re-enable this long-haul test TEST_F(BasicFuncTests, serverProblemsDropEventsAfterMaxRetryCount) { CleanStorage(); - auto &configuration = LogManager::GetLogConfiguration(); - - std::string badServiceUrl; - badServiceUrl.insert(badServiceUrl.find('/', sizeof("http://")) + 1, "503/"); - - configuration[CFG_STR_COLLECTOR_URL] = badServiceUrl.c_str(); - - { - Initialize(); - - EventProperties event("event"); - event.SetProperty("property", "value"); + DroppedEventListener listener; + LogManager::AddEventListener(DebugEventType::EVT_DROPPED, listener); + LogManager::AddEventListener(DebugEventType::EVT_SEND_RETRY, listener); - logger->LogEvent(event); + Initialize(); + LogManager::PauseTransmission(); - // After initial delay of 2 seconds, the library will send a request, wait 3 seconds, send 1st retry and stop. - // 2nd retry after another 3 seconds (using the good URL again) should not come - wait 1 more second to be sure. - PAL::sleep(2000 + 2 * 3000 + 1000); - // EXPECT_THAT(receivedRequests, SizeIs(0)); + EventProperties event("event"); + event.SetLatency(EventLatency_RealTime); + event.SetPersistence(EventPersistence_Critical); + event.SetProperty("property", "value"); + logger->LogEvent(event); + FlushAndTeardown(); - // Check meta stats on restart (will be first request) - FlushAndTeardown(); - } + std::string savedAddress = serverAddress; + serverAddress = serverBaseAddress + "/503/"; + Initialize(2, 4096 * 1024, 1, "E,50,100,2,1"); + serverAddress = savedAddress; + LogManager::UploadNow(); - // Restore fast URL - configuration[CFG_STR_COLLECTOR_URL] = serverAddress.c_str(); + EXPECT_TRUE(listener.waitForAtLeast(listener.sendRetries, 2, 10000)); + EXPECT_TRUE(listener.waitForAtLeast(listener.retryExceededDrops, 1, 5000)); + EXPECT_GT(listener.retryExceededDrops.load(), size_t { 0 }); - { - configuration[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; - configuration[CFG_STR_CACHE_FILE_PATH] = TEST_STORAGE_FILENAME; - Initialize(); - waitForEvents(5, 2); - if (receivedRequests.size()) - { - auto payload = decodeRequest(receivedRequests[receivedRequests.size() - 1], false); - /* auto const& dp = payload.TokenToDataPackagesMap["metastats-tenant-token"][0]; - ASSERT_THAT(payload, SizeIs(1)); - EXPECT_THAT(payload[0].Id, Not(IsEmpty())); - EXPECT_THAT(payload[0].Type, Eq("client_telemetry")); - EXPECT_THAT(payload[0].Extension, Contains(Pair("stats_rollup_kind", "stop"))); - EXPECT_THAT(payload[0].Extension, Contains(Pair("records_dropped_retry_exceeded", "2"))); - */ - } - FlushAndTeardown(); - } + FlushAndTeardown(); + LogManager::RemoveEventListener(DebugEventType::EVT_DROPPED, listener); + LogManager::RemoveEventListener(DebugEventType::EVT_SEND_RETRY, listener); } -#endif + #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT diff --git a/tests/functests/FuncTests.vcxproj b/tests/functests/FuncTests.vcxproj index f5977c7c7..e34359c8e 100644 --- a/tests/functests/FuncTests.vcxproj +++ b/tests/functests/FuncTests.vcxproj @@ -157,7 +157,7 @@ /machine:X86 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -206,9 +206,13 @@ /machine:X86 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) - No + Debug + true + true + true + $(OutDir)$(TargetName).map %(IgnoreSpecificDefaultLibraries) Console @@ -254,7 +258,7 @@ /machine:X64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -304,7 +308,7 @@ /machine:ARM64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -353,7 +357,7 @@ /machine:X64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) Debug %(IgnoreSpecificDefaultLibraries) @@ -400,7 +404,7 @@ /machine:ARM64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) Debug %(IgnoreSpecificDefaultLibraries) @@ -413,6 +417,22 @@ true + + + HAVE_MAT_WININET_HTTP_CLIENT;%(PreprocessorDefinitions) + + + wininet.lib;%(AdditionalDependencies) + + + + + HAVE_MAT_WINHTTP_HTTP_CLIENT;%(PreprocessorDefinitions) + + + winhttp.lib;%(AdditionalDependencies) + + diff --git a/tests/functests/LogSessionDataFuncTests.cpp b/tests/functests/LogSessionDataFuncTests.cpp index f1f1dbe68..9afde6926 100644 --- a/tests/functests/LogSessionDataFuncTests.cpp +++ b/tests/functests/LogSessionDataFuncTests.cpp @@ -14,14 +14,18 @@ using namespace MAT; const std::string SessionFileArgument = "test"; const char* const SessionFile = "test.ses"; +const char* const MemorySessionFile = ":memory:.ses"; class LogSessionDataFuncTests : public ::testing::Test { void CleanupLocalSessionFile() { - if (MAT::FileExists(SessionFile)) + for (const auto* sessionFile : {SessionFile, MemorySessionFile}) { - MAT::FileDelete(SessionFile); + if (MAT::FileExists(sessionFile)) + { + MAT::FileDelete(sessionFile); + } } } @@ -76,6 +80,28 @@ TEST_F(LogSessionDataFuncTests, Constructor_SessionFile_FileCreated) ASSERT_TRUE(MAT::FileExists(SessionFile)); } +TEST_F(LogSessionDataFuncTests, Constructor_InMemoryCache_NoSessionFileCreated) +{ + auto logSessionDataProvider = LogSessionDataProvider(":memory:"); + logSessionDataProvider.CreateLogSessionData(); + const auto* logSessionData = logSessionDataProvider.GetLogSessionData(); + ASSERT_NE(logSessionData, nullptr); + EXPECT_GT(logSessionData->getSessionFirstTime(), 0ull); + EXPECT_FALSE(logSessionData->getSessionSDKUid().empty()); + const auto sessionSDKUid = logSessionData->getSessionSDKUid(); + EXPECT_FALSE(MAT::FileExists(MemorySessionFile)); + + logSessionDataProvider.ResetLogSessionData(); + logSessionData = logSessionDataProvider.GetLogSessionData(); + ASSERT_NE(logSessionData, nullptr); + EXPECT_GT(logSessionData->getSessionFirstTime(), 0ull); + EXPECT_FALSE(logSessionData->getSessionSDKUid().empty()); + EXPECT_NE(logSessionData->getSessionSDKUid(), sessionSDKUid); + EXPECT_FALSE(MAT::FileExists(MemorySessionFile)); + logSessionDataProvider.DeleteLogSessionData(); + EXPECT_FALSE(MAT::FileExists(MemorySessionFile)); +} + TEST_F(LogSessionDataFuncTests, Constructor_ValidSessionFileExists_MembersSetToExistingFile) { const std::string validSessionFirstTime{ "123456" }; diff --git a/tests/functests/MultipleLogManagersTests.cpp b/tests/functests/MultipleLogManagersTests.cpp index 7a9027b9b..420377442 100644 --- a/tests/functests/MultipleLogManagersTests.cpp +++ b/tests/functests/MultipleLogManagersTests.cpp @@ -97,12 +97,6 @@ class MultipleLogManagersTests : public ::testing::Test server.start(); -#if 0 - sqlite3_initialize(); - config1["skipSqliteInitAndShutdown"] = "true"; - config2["skipSqliteInitAndShutdown"] = "true"; -#endif - // Config for instance #1 config1["cacheFilePath"] = "lm1.db"; ::remove(config1["cacheFilePath"]); @@ -308,4 +302,3 @@ TEST_F(MultipleLogManagersTests, PrivacyGuardSharedWithTwoInstancesCoexist) #endif // !TARGET_OS_IPHONE (suite excluded on iOS; see note above) #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT - diff --git a/tests/unittests/AnnexKTests.cpp b/tests/unittests/AnnexKTests.cpp index fa74e23f5..c07ec3663 100644 --- a/tests/unittests/AnnexKTests.cpp +++ b/tests/unittests/AnnexKTests.cpp @@ -30,3 +30,11 @@ TEST(AnnexKTests, memcpy_s) EXPECT_EQ(BoundCheckFunctions::oneds_memcpy_s( dest, dest_len, src, dest_len + 1 ), EINVAL); EXPECT_EQ(BoundCheckFunctions::oneds_memcpy_s( dest, dest_len, (void *)((char *)dest + 1), src_len + 1 ), EINVAL); } + +TEST(AnnexKTests, memcpy_sAllowsAdjacentBuffers) +{ + char buffers[12] = {}; + + EXPECT_EQ(BoundCheckFunctions::oneds_memcpy_s(buffers, 4, buffers + 4, 4), 0); + EXPECT_EQ(BoundCheckFunctions::oneds_memcpy_s(buffers, 8, buffers + 8, 4), 0); +} diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt index e973ccf4c..a9cfce9fd 100644 --- a/tests/unittests/CMakeLists.txt +++ b/tests/unittests/CMakeLists.txt @@ -35,6 +35,7 @@ set(SRCS Main.cpp MemoryStorageTests.cpp MetaStatsTests.cpp + MsRootCertPolicyTests.cpp OacrTests.cpp OfflineStorageTests.cpp OfflineStorageTests_Room.cpp diff --git a/tests/unittests/EventFilterCollectionTests.cpp b/tests/unittests/EventFilterCollectionTests.cpp index 58af75f1f..f07b93b0b 100644 --- a/tests/unittests/EventFilterCollectionTests.cpp +++ b/tests/unittests/EventFilterCollectionTests.cpp @@ -13,7 +13,16 @@ using namespace MAT; class TestEventFilterCollection : public EventFilterCollection { public: - using EventFilterCollection::m_filters; + size_t FilterCount() const + { + auto filters = std::atomic_load(&m_filters); + return filters == nullptr ? 0 : filters->size(); + } + + const char* FilterName(size_t index) const + { + return std::atomic_load(&m_filters)->at(index)->GetName(); + } }; const char DefaultTestEventFilterName[] = "TestEventFilter"; @@ -34,10 +43,27 @@ class TestEventFilter : public IEventFilter bool CanEventPropertiesBeSent(const EventProperties&) const noexcept override { return CanEventPropertiesBeSentReturnValue; } }; +class UnregisteringEventFilter : public IEventFilter +{ +public: + explicit UnregisteringEventFilter(EventFilterCollection& collection) noexcept + : Collection(collection) { } + + const char* GetName() const noexcept override { return "UnregisteringEventFilter"; } + bool CanEventPropertiesBeSent(const EventProperties&) const noexcept override + { + Collection.UnregisterAllFilters(); + return true; + } + +private: + EventFilterCollection& Collection; +}; + TEST(EventFilterCollectionTests, Constructor_DefaultConstructed_NoRegisteredFilters) { TestEventFilterCollection collection; - EXPECT_EQ(collection.m_filters.size(), size_t { 0 }); + EXPECT_EQ(collection.FilterCount(), size_t { 0 }); } TEST(EventFilterCollectionTests, Empty_ZeroRegisteredFilters_ReturnsTrue) @@ -62,7 +88,7 @@ TEST(EventFilterCollectionTests, RegisterEventFilter_ValidFilter_FilterSizeIsOne { TestEventFilterCollection collection; collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); - EXPECT_EQ(collection.m_filters.size(), size_t { 1 }); + EXPECT_EQ(collection.FilterCount(), size_t { 1 }); } TEST(EventFilterCollectionTests, RegisterEventFilter_TwoValidFiltersWithTheSameName_FilterSizeIsTwo) @@ -70,7 +96,7 @@ TEST(EventFilterCollectionTests, RegisterEventFilter_TwoValidFiltersWithTheSameN TestEventFilterCollection collection; collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); - EXPECT_EQ(collection.m_filters.size(), size_t { 2 }); + EXPECT_EQ(collection.FilterCount(), size_t { 2 }); } TEST(EventFilterCollectionTests, UnregisterEventFilter_NullptrName_ThrowsArgumentException) @@ -84,7 +110,7 @@ TEST(EventFilterCollectionTests, UnregisterEventFilter_EventNameNotRegistered_Do TestEventFilterCollection collection; collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.UnregisterEventFilter("NotTheDroidsYoureLookingFor"); - EXPECT_EQ(collection.m_filters.size(), size_t { 1 }); + EXPECT_EQ(collection.FilterCount(), size_t { 1 }); } TEST(EventFilterCollectionTests, UnregisterEventFilter_EventNameRegistered_ModifiesCollection) @@ -92,7 +118,7 @@ TEST(EventFilterCollectionTests, UnregisterEventFilter_EventNameRegistered_Modif TestEventFilterCollection collection; collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.UnregisterEventFilter(DefaultTestEventFilterName); - EXPECT_EQ(collection.m_filters.size(), size_t { 0 }); + EXPECT_EQ(collection.FilterCount(), size_t { 0 }); } TEST(EventFilterCollectionTests, UnregisterEventFilter_EventNameRegisteredTwice_RemovesBoth) @@ -101,7 +127,7 @@ TEST(EventFilterCollectionTests, UnregisterEventFilter_EventNameRegisteredTwice_ collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.UnregisterEventFilter(DefaultTestEventFilterName); - EXPECT_EQ(collection.m_filters.size(), size_t { 0 }); + EXPECT_EQ(collection.FilterCount(), size_t { 0 }); } TEST(EventFilterCollectionTests, UnregisterEventFilter_TwoDifferentlyNamedFilters_RemovesOne) @@ -110,8 +136,8 @@ TEST(EventFilterCollectionTests, UnregisterEventFilter_TwoDifferentlyNamedFilter collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter("One"))); collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter("Two"))); collection.UnregisterEventFilter("One"); - EXPECT_EQ(collection.m_filters.size(), size_t { 1 }); - EXPECT_EQ(strcmp(collection.m_filters[0]->GetName(), "Two"), 0); + EXPECT_EQ(collection.FilterCount(), size_t { 1 }); + EXPECT_EQ(strcmp(collection.FilterName(0), "Two"), 0); } TEST(EventFilterCollectionTests, UnregisterAllFilters_OneRegistered_ModifiesCollection) @@ -119,7 +145,7 @@ TEST(EventFilterCollectionTests, UnregisterAllFilters_OneRegistered_ModifiesColl TestEventFilterCollection collection; collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter())); collection.UnregisterAllFilters(); - EXPECT_EQ(collection.m_filters.size(), size_t { 0 }); + EXPECT_EQ(collection.FilterCount(), size_t { 0 }); } TEST(EventFilterCollectionTests, UnregisterAllFilters_TwoRegistered_RemovesBoth) @@ -128,7 +154,7 @@ TEST(EventFilterCollectionTests, UnregisterAllFilters_TwoRegistered_RemovesBoth) collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter("One"))); collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter("Two"))); collection.UnregisterAllFilters(); - EXPECT_EQ(collection.m_filters.size(), size_t { 0 }); + EXPECT_EQ(collection.FilterCount(), size_t { 0 }); } TEST(EventFilterCollectionTests, CanEventPropertiesBeSent_ZeroRegisteredFilters_ReturnsTrue) @@ -174,3 +200,13 @@ TEST(EventFilterCollectionTests, CanEventPropertiesBeSent_TwoRegisteredFiltersOn collection.RegisterEventFilter(std::unique_ptr(new TestEventFilter(false))); EXPECT_FALSE(collection.CanEventPropertiesBeSent(EventProperties{})); } + +TEST(EventFilterCollectionTests, CanEventPropertiesBeSent_FilterUnregistersAll_DoesNotDeadlock) +{ + TestEventFilterCollection collection; + collection.RegisterEventFilter( + std::unique_ptr(new UnregisteringEventFilter(collection))); + + EXPECT_TRUE(collection.CanEventPropertiesBeSent(EventProperties{})); + EXPECT_TRUE(collection.Empty()); +} diff --git a/tests/unittests/HttpClientCAPITests.cpp b/tests/unittests/HttpClientCAPITests.cpp index 0f0e56a7e..42fa2f0c5 100644 --- a/tests/unittests/HttpClientCAPITests.cpp +++ b/tests/unittests/HttpClientCAPITests.cpp @@ -7,6 +7,12 @@ #include "http/HttpClient_CAPI.hpp" #include "mat.h" +#include +#include +#include +#include +#include + using namespace testing; using namespace MAT; using std::string; @@ -20,8 +26,9 @@ namespace virtual void OnHttpResponse(IHttpResponse* response) override { + std::unique_ptr ownedResponse(response); if (m_validateFn) - m_validateFn(response); + m_validateFn(ownedResponse.get()); } private: @@ -33,12 +40,21 @@ namespace void SetShouldSend(bool shouldSend) { m_shouldSend = shouldSend; } bool ShouldSend() { return m_shouldSend; } void SetSendValidation(std::function fn) { m_validateSendFn = fn; } + void SetSendCallbackValidation( + std::function fn) + { + m_validateSendCallbackFn = fn; + } void SetCancelValidation(std::function fn) { m_validateCancelFn = fn; } - void OnSend(http_request_t* request) + void OnSend(http_request_t* request, http_complete_fn_t callback) { + m_requestId = request->id; + m_completeFn = callback; if (m_validateSendFn) m_validateSendFn(request); + if (m_validateSendCallbackFn) + m_validateSendCallbackFn(request, callback); } void OnCancel(const char* requestId) @@ -47,10 +63,21 @@ namespace m_validateCancelFn(requestId); } + void Complete(http_result_t result, http_response_t* response = nullptr) + { + if (m_completeFn != nullptr) + { + m_completeFn(m_requestId.c_str(), result, response); + } + } + private: std::function m_validateSendFn; + std::function m_validateSendCallbackFn; std::function m_validateCancelFn; bool m_shouldSend = false; + std::string m_requestId; + http_complete_fn_t m_completeFn = nullptr; }; static std::unique_ptr s_testHelper; @@ -77,7 +104,7 @@ namespace void EVTSDK_LIBABI_CDECL OnHttpSend(http_request_t* request, http_complete_fn_t callback) { - s_testHelper->OnSend(request); + s_testHelper->OnSend(request, callback); if (s_testHelper->ShouldSend()) { @@ -97,6 +124,23 @@ void EVTSDK_LIBABI_CDECL OnHttpSend(http_request_t* request, http_complete_fn_t } } +void EVTSDK_LIBABI_CDECL OnHttpSendThrow( + http_request_t* request, + http_complete_fn_t callback) +{ + s_testHelper->OnSend(request, callback); + throw std::runtime_error("send hook failed"); +} + +void EVTSDK_LIBABI_CDECL OnHttpSendCompleteThenThrow( + http_request_t* request, + http_complete_fn_t callback) +{ + s_testHelper->OnSend(request, callback); + callback(request->id, HTTP_RESULT_OK, nullptr); + throw std::runtime_error("send hook failed after completion"); +} + void EVTSDK_LIBABI_CDECL OnHttpCancel(const char* requestId) { s_testHelper->OnCancel(requestId); @@ -173,15 +217,282 @@ TEST(HttpClientCAPITests, Cancel) cancelledId = requestId; }); + size_t responses = 0; TestHttpResponseCallback responseCallback; - responseCallback.SetResponseValidation([](IHttpResponse* /*response*/) { - FAIL() << "No response should have been received"; + responseCallback.SetResponseValidation([&responses](IHttpResponse* response) { + ++responses; + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); }); httpClient.SendRequestAsync(request, &responseCallback); httpClient.CancelRequestAsync(request->GetId()); EXPECT_EQ(cancelledId, request->GetId()); + EXPECT_EQ(responses, 1u); + + // A late external completion is ignored because cancellation already + // retired and terminally completed the operation. + testHelper->Complete(HTTP_RESULT_OK); + EXPECT_EQ(responses, 1u); +} + +TEST(HttpClientCAPITests, ThrowingSendRejectsLateCompletion) +{ + HttpClient_CAPI httpClient(&OnHttpSendThrow, &OnHttpCancel); + auto request = httpClient.CreateRequest(); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + + AutoTestHelper testHelper; + size_t responses = 0; + TestHttpResponseCallback responseCallback; + responseCallback.SetResponseValidation([&responses](IHttpResponse*) { + ++responses; + }); + + EXPECT_THROW( + httpClient.SendRequestAsync(request, &responseCallback), + std::runtime_error); + testHelper->Complete(HTTP_RESULT_OK); + EXPECT_EQ(responses, 0u); +} + +TEST(HttpClientCAPITests, CallbackThenThrowCompletesWithoutExposingException) +{ + HttpClient_CAPI httpClient(&OnHttpSendCompleteThenThrow, &OnHttpCancel); + auto request = httpClient.CreateRequest(); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + + AutoTestHelper testHelper; + size_t responses = 0; + TestHttpResponseCallback responseCallback; + responseCallback.SetResponseValidation([&responses](IHttpResponse* response) { + ++responses; + EXPECT_EQ(response->GetResult(), HttpResult_OK); + }); + + EXPECT_NO_THROW(httpClient.SendRequestAsync(request, &responseCallback)); + EXPECT_EQ(responses, 1u); +} + +TEST(HttpClientCAPITests, ConcurrentCompletionWaitsForSendHookToReturn) +{ + HttpClient_CAPI httpClient(&OnHttpSend, &OnHttpCancel); + auto request = std::unique_ptr(httpClient.CreateRequest()); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + + AutoTestHelper testHelper; + testHelper->SetShouldSend(false); + std::atomic completionStarted {false}; + std::thread completer; + testHelper->SetSendCallbackValidation( + [&](http_request_t* capiRequest, http_complete_fn_t callback) + { + std::string requestId = capiRequest->id; + completer = std::thread([&, requestId, callback] + { + completionStarted.store(true); + callback(requestId.c_str(), HTTP_RESULT_OK, nullptr); + }); + while (!completionStarted.load()) + { + std::this_thread::yield(); + } + completer.join(); + EXPECT_NE(request, nullptr); + }); + + TestHttpResponseCallback responseCallback; + responseCallback.SetResponseValidation( + [&](IHttpResponse* response) + { + EXPECT_EQ(response->GetResult(), HttpResult_OK); + request.reset(); + }); + + httpClient.SendRequestAsync(request.get(), &responseCallback); + EXPECT_EQ(request, nullptr); +} + +TEST(HttpClientCAPITests, CancelAllCompletesEveryPendingRequest) +{ + HttpClient_CAPI httpClient(&OnHttpSend, &OnHttpCancel); + AutoTestHelper testHelper; + testHelper->SetShouldSend(false); + + std::vector> requests; + std::vector> callbacks; + size_t responses = 0; + for (int i = 0; i < 2; ++i) + { + requests.emplace_back(httpClient.CreateRequest()); + requests.back()->SetUrl("https://www.microsoft.com"); + requests.back()->SetMethod("GET"); + callbacks.emplace_back(new TestHttpResponseCallback()); + callbacks.back()->SetResponseValidation( + [&responses](IHttpResponse* response) { + ++responses; + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); + }); + httpClient.SendRequestAsync(requests.back().get(), callbacks.back().get()); + } + + httpClient.CancelAllRequests(); + EXPECT_EQ(responses, 2u); + + testHelper->Complete(HTTP_RESULT_OK); + EXPECT_EQ(responses, 2u); +} + +TEST(HttpClientCAPITests, CancelWaitsForSendHookToReleaseRequestBuffers) +{ + HttpClient_CAPI httpClient(&OnHttpSend, &OnHttpCancel); + auto request = std::unique_ptr(httpClient.CreateRequest()); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + + AutoTestHelper testHelper; + testHelper->SetShouldSend(false); + std::mutex gateMutex; + std::condition_variable gateCV; + bool sendEntered = false; + bool releaseSend = false; + testHelper->SetSendValidation( + [&](http_request_t* capiRequest) { + std::unique_lock lock(gateMutex); + EXPECT_STREQ(capiRequest->id, request->GetId().c_str()); + sendEntered = true; + gateCV.notify_all(); + gateCV.wait(lock, [&] { return releaseSend; }); + }); + + std::atomic responses{0}; + TestHttpResponseCallback responseCallback; + responseCallback.SetResponseValidation( + [&](IHttpResponse* response) { + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); + ++responses; + }); + + std::thread sender([&] { + httpClient.SendRequestAsync(request.get(), &responseCallback); + }); + bool didEnterSend = false; + { + std::unique_lock lock(gateMutex); + didEnterSend = gateCV.wait_for( + lock, std::chrono::seconds(5), [&] { return sendEntered; }); + if (!didEnterSend) + { + releaseSend = true; + } + } + if (!didEnterSend) + { + gateCV.notify_all(); + sender.join(); + httpClient.CancelRequestAsync(request->GetId()); + FAIL() << "Send hook was not entered"; + } + + std::thread canceller([&] { + httpClient.CancelRequestAsync(request->GetId()); + }); + PAL::sleep(50); + EXPECT_EQ(responses.load(), 0u); + + { + std::lock_guard lock(gateMutex); + releaseSend = true; + } + gateCV.notify_all(); + sender.join(); + canceller.join(); + EXPECT_EQ(responses.load(), 1u); +} + +TEST(HttpClientCAPITests, CancelAllOnlyCompletesOwningClient) +{ + HttpClient_CAPI firstClient(&OnHttpSend, &OnHttpCancel); + HttpClient_CAPI secondClient(&OnHttpSend, &OnHttpCancel); + AutoTestHelper testHelper; + testHelper->SetShouldSend(false); + + auto firstRequest = std::unique_ptr(firstClient.CreateRequest()); + auto secondRequest = std::unique_ptr(secondClient.CreateRequest()); + firstRequest->SetUrl("https://www.microsoft.com"); + secondRequest->SetUrl("https://www.microsoft.com"); + + size_t firstResponses = 0; + size_t secondResponses = 0; + TestHttpResponseCallback firstCallback; + TestHttpResponseCallback secondCallback; + firstCallback.SetResponseValidation([&](IHttpResponse* response) { + ++firstResponses; + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); + }); + secondCallback.SetResponseValidation([&](IHttpResponse* response) { + ++secondResponses; + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); + }); + + firstClient.SendRequestAsync(firstRequest.get(), &firstCallback); + secondClient.SendRequestAsync(secondRequest.get(), &secondCallback); + + firstClient.CancelAllRequests(); + EXPECT_EQ(firstResponses, 1u); + EXPECT_EQ(secondResponses, 0u); + + secondClient.CancelAllRequests(); + EXPECT_EQ(secondResponses, 1u); +} + +TEST(HttpClientCAPITests, DestructorCompletesPendingRequestAndIgnoresLateResponse) +{ + AutoTestHelper testHelper; + testHelper->SetShouldSend(false); + + size_t responses = 0; + TestHttpResponseCallback callback; + callback.SetResponseValidation([&](IHttpResponse* response) { + ++responses; + EXPECT_EQ(response->GetResult(), HttpResult_Aborted); + }); + + { + HttpClient_CAPI httpClient(&OnHttpSend, &OnHttpCancel); + auto request = std::unique_ptr(httpClient.CreateRequest()); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + httpClient.SendRequestAsync(request.get(), &callback); + } + + EXPECT_EQ(responses, 1u); + testHelper->Complete(HTTP_RESULT_OK); + EXPECT_EQ(responses, 1u); +} + +TEST(HttpClientCAPITests, SynchronousCallbackCanDestroyClient) +{ + AutoTestHelper testHelper; + testHelper->SetShouldSend(true); + + auto httpClient = std::unique_ptr( + new HttpClient_CAPI(&OnHttpSend, &OnHttpCancel)); + auto request = std::unique_ptr(httpClient->CreateRequest()); + request->SetUrl("https://www.microsoft.com"); + request->SetMethod("GET"); + + TestHttpResponseCallback callback; + callback.SetResponseValidation([&](IHttpResponse* response) { + EXPECT_EQ(response->GetResult(), HttpResult_OK); + httpClient.reset(); + }); + + EXPECT_NO_THROW(httpClient->SendRequestAsync(request.get(), &callback)); + EXPECT_EQ(httpClient, nullptr); } TEST(HttpClientCAPITests, CancelAllThenSend) diff --git a/tests/unittests/HttpClientCurlTests.cpp b/tests/unittests/HttpClientCurlTests.cpp index 50a82a874..23598d772 100644 --- a/tests/unittests/HttpClientCurlTests.cpp +++ b/tests/unittests/HttpClientCurlTests.cpp @@ -13,6 +13,28 @@ #include "http/HttpClient_Curl.hpp" #include "config/RuntimeConfig_Default.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + using namespace testing; using namespace MAT; @@ -41,8 +63,10 @@ TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyTrue) ASSERT_NE(op.GetHandle(), nullptr); } -TEST_F(HttpClientCurlTests, CurlHttpOperation_ConstructsWithVerifyFalse) +TEST_F(HttpClientCurlTests, CurlHttpOperation_IgnoresLegacyVerifyFalse) { + // The argument remains in the internal constructor for source compatibility, + // but the operation always configures peer and hostname verification. CurlHttpOperation op("GET", "https://example.com", nullptr, m_headers, m_body, false, 5, false, ""); @@ -66,6 +90,15 @@ TEST(HttpClientCurlOperationTests, SelectsHttp2OnlyWhenRuntimeSupportsIt) EXPECT_EQ(CurlHttpOperation::GetPreferredHttpVersion(), expected); } +TEST(HttpClientCurlOperationTests, ClampsConnectionTimeoutBeforeMillisecondsConversion) +{ + EXPECT_EQ(CurlHttpOperation::ClampConnectionTimeout(5), 5L); + EXPECT_EQ( + CurlHttpOperation::ClampConnectionTimeout( + std::numeric_limits::max()), + std::numeric_limits::max() / 1000L); +} + class HttpClientCurlHeaderTests : public ::testing::Test, public HttpServer::Callback { @@ -105,7 +138,9 @@ TEST_F(HttpClientCurlHeaderTests, CapturesResponseHeadersAndBody) (void)client; // Initialize curl globally before constructing the operation. CurlHttpOperation operation("GET", m_url, nullptr, requestHeaders, requestBody); - ASSERT_EQ(operation.Send(), 200L); + operation.Send(); + ASSERT_EQ(operation.GetTransportError(), CURLE_OK); + ASSERT_EQ(operation.GetHttpStatusCode(), 200L); const auto responseHeaders = operation.GetResponseHeaders(); const auto responseBody = operation.GetResponseBody(); @@ -129,8 +164,10 @@ TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_DefaultIsEmpty) EXPECT_STREQ(caInfo, ""); } -TEST(HttpClientCurlConfigTests, LogConfiguration_SslVerify_CanBeDisabled) +TEST(HttpClientCurlConfigTests, LogConfiguration_LegacySslVerifyFalseRemainsReadable) { + // Keep parsing the legacy setting for configuration compatibility. The curl + // transport ignores false and always enables peer and hostname verification. ILogConfiguration config; config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false; bool sslVerify = config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY]; @@ -147,13 +184,14 @@ TEST(HttpClientCurlConfigTests, LogConfiguration_SslCaInfo_CanBeSet) // --- ApplySettings integration --- -TEST_F(HttpClientCurlTests, ApplySettings_ReadsSslConfigFromLogConfiguration) +TEST_F(HttpClientCurlTests, ApplySettingsAcceptsLegacySslDisableAndCaInfo) { ILogConfiguration config; config[CFG_MAP_HTTP][CFG_BOOL_HTTP_SSL_VERIFY] = false; config[CFG_MAP_HTTP][CFG_STR_HTTP_SSL_CAINFO] = "/custom/ca.pem"; m_client.ApplySettings(config); - // Verify indirectly -- constructing an operation should not fail + // The compatibility setting is accepted, but transport construction always + // applies CURLOPT_SSL_VERIFYPEER=1 and CURLOPT_SSL_VERIFYHOST=2. SUCCEED(); } @@ -184,6 +222,69 @@ TEST_F(HttpClientCurlTests, SetSslVerification_ConcurrentCallsNoRace) SUCCEED(); } +// --- Regression: EDEADLK self-join in ~CurlHttpOperation --- + +TEST_F(HttpClientCurlTests, SendAsync_DestroyOnWorkerThread_NoSelfJoin) +{ + struct TrackingCallback : public IHttpResponseCallback + { + std::atomic destroyEvents { 0 }; + void OnHttpResponse(IHttpResponse* response) override { delete response; } + void OnHttpStateEvent(HttpStateEvent state, void*, size_t) override + { + if (state == OnDestroy) + { + ++destroyEvents; + } + } + }; + + auto callback = std::make_shared(); + auto callbackDone = std::make_shared>(); + auto done = callbackDone->get_future(); + + auto op = std::make_shared( + "GET", "://malformed", callback.get(), m_headers, m_body, + false, 1 /*connTimeout*/, false /*sslVerify*/, ""); + + auto box = std::make_shared>(std::move(op)); + (*box)->SendAsync([box, callback, callbackDone](CurlHttpOperation&) { + box->reset(); + callbackDone->set_value(); + }); + + if (done.wait_for(std::chrono::seconds(5)) != std::future_status::ready) + { + ADD_FAILURE() << "curl worker did not finish before fixture teardown"; + std::abort(); + } + EXPECT_EQ(callback->destroyEvents.load(), 1); +} + +TEST_F(HttpClientCurlTests, SendAsync_CallbackCopyFailureStillCompletes) +{ + struct ThrowOnCopy + { + explicit ThrowOnCopy(bool& invoked) : invoked(&invoked) {} + ThrowOnCopy(ThrowOnCopy&&) = default; + ThrowOnCopy(const ThrowOnCopy&) { throw std::logic_error("copy failed"); } + void operator()(CurlHttpOperation&) const { *invoked = true; } + bool* invoked; + }; + + CurlHttpOperation op( + "GET", "://malformed", nullptr, m_headers, m_body, + false, 1 /*connTimeout*/, false /*sslVerify*/, ""); + bool callbackInvoked = false; + std::function callback { ThrowOnCopy(callbackInvoked) }; + + EXPECT_NO_THROW(op.SendAsync(std::move(callback))); + EXPECT_TRUE(callbackInvoked); + EXPECT_EQ(op.GetTransportError(), CURLE_FAILED_INIT); + EXPECT_EQ(op.GetSetupError(), CURLE_FAILED_INIT); + EXPECT_THROW(op.SendAsync(), std::logic_error); +} + // --- Response-size cap (memory-amplification DoS hardening) --- class HttpClientCurlResponseCapTests : public ::testing::Test, @@ -195,9 +296,7 @@ class HttpClientCurlResponseCapTests : public ::testing::Test, HttpClient_Curl m_client; // The client never takes ownership of the request (it only stores a raw pointer // and erases it); the fixture owns it and frees it in TearDown -- on the main - // thread, after the transfer has completed. Freeing it inside OnHttpResponse - // would destroy the CurlHttpOperation from within its own async task, whose - // destructor waits on that task (a self-join deadlock). + // thread, after the transfer has completed. std::unique_ptr m_request; std::string m_hostname; size_t m_responseBodySize {0}; @@ -298,4 +397,652 @@ TEST_F(HttpClientCurlResponseCapTests, AcceptsLargeResponseUnderCap) EXPECT_EQ(m_bodySize, bodySize); } +// --- Lifetime, cancellation and drain semantics --- + +namespace +{ + +// A TCP endpoint that accepts connections at the kernel level (the listen +// backlog completes the handshake) but never reads or answers them. curl +// therefore connects, writes the request, and blocks waiting for a response +// until it is cancelled. No sleeps, no timing assumptions, no dependence on a +// live network: the stall is a property of the socket, not of the schedule. +class StalledEndpoint +{ +public: + StalledEndpoint() + { + m_listener = ::socket(AF_INET, SOCK_STREAM, 0); + if (m_listener < 0) + { + return; + } + int reuse = 1; + ::setsockopt(m_listener, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + + struct sockaddr_in address; + std::memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = 0; + if (::bind(m_listener, reinterpret_cast(&address), sizeof(address)) != 0 || + ::listen(m_listener, 32) != 0) + { + ::close(m_listener); + m_listener = -1; + return; + } + + socklen_t length = sizeof(address); + if (::getsockname(m_listener, reinterpret_cast(&address), &length) == 0) + { + m_port = ntohs(address.sin_port); + } + } + + ~StalledEndpoint() + { + if (m_listener >= 0) + { + ::close(m_listener); + } + } + + StalledEndpoint(StalledEndpoint const&) = delete; + StalledEndpoint& operator=(StalledEndpoint const&) = delete; + + bool valid() const { return m_listener >= 0 && m_port != 0; } + + std::string url() const + { + return "http://127.0.0.1:" + std::to_string(m_port) + "/stall"; + } + +private: + int m_listener {-1}; + int m_port {0}; +}; + +// One-shot barrier used to pin a callback in place for as long as a test needs. +class Gate +{ +public: + void wait() + { + std::unique_lock lock(m_mutex); + m_cv.wait(lock, [this]() { return m_open; }); + } + + void open() + { + { + std::lock_guard lock(m_mutex); + m_open = true; + } + m_cv.notify_all(); + } + +private: + std::mutex m_mutex; + std::condition_variable m_cv; + bool m_open {false}; +}; + +class RecordingCallback : public IHttpResponseCallback +{ +public: + // Runs inside OnHttpResponse, after the response has been counted, so a test + // can hold the terminal callback open or re-enter the client from it. + void setResponseHook(std::function hook) + { + std::lock_guard lock(m_mutex); + m_hook = std::move(hook); + } + + void setStateHook(std::function hook) + { + std::lock_guard lock(m_mutex); + m_stateHook = std::move(hook); + } + + void OnHttpResponse(IHttpResponse* response) override + { + std::unique_ptr owned(response); + std::function hook; + { + std::lock_guard lock(m_mutex); + ++m_responses; + m_results.push_back(owned->GetResult()); + hook = m_hook; + } + m_cv.notify_all(); + if (hook != nullptr) + { + hook(); + } + } + + void OnHttpStateEvent(HttpStateEvent state, void*, size_t) override + { + std::function hook; + { + std::lock_guard lock(m_mutex); + ++m_states[static_cast(state)]; + hook = m_stateHook; + } + m_cv.notify_all(); + if (hook != nullptr) + { + hook(state); + } + } + + size_t responses() + { + std::lock_guard lock(m_mutex); + return m_responses; + } + + size_t responsesWithResult(HttpResult result) + { + std::lock_guard lock(m_mutex); + size_t count = 0; + for (auto const& item : m_results) + { + if (item == result) + { + ++count; + } + } + return count; + } + + size_t stateCount(HttpStateEvent state) + { + std::lock_guard lock(m_mutex); + auto it = m_states.find(static_cast(state)); + return (it == m_states.end()) ? 0u : it->second; + } + + bool waitForResponses(size_t count, std::chrono::milliseconds timeout) + { + std::unique_lock lock(m_mutex); + return m_cv.wait_for(lock, timeout, [&]() { return m_responses >= count; }); + } + + bool waitForState(HttpStateEvent state, size_t count, std::chrono::milliseconds timeout) + { + const int key = static_cast(state); + std::unique_lock lock(m_mutex); + return m_cv.wait_for(lock, timeout, [&]() { return m_states[key] >= count; }); + } + +private: + std::mutex m_mutex; + std::condition_variable m_cv; + size_t m_responses {0}; + std::vector m_results; + std::map m_states; + std::function m_hook; + std::function m_stateHook; +}; + +constexpr std::chrono::milliseconds kInFlightTimeout {15000}; +constexpr std::chrono::milliseconds kTerminalTimeout {15000}; + +} // namespace + +class HttpClientCurlLifetimeTests : public ::testing::Test +{ +protected: + // Declared first so it is destroyed last: the client's destructor drains + // in-flight transfers that are still pointed at this endpoint. + StalledEndpoint m_endpoint; + HttpClient_Curl m_client; + + void SetUp() override + { + ASSERT_TRUE(m_endpoint.valid()) << "could not open a loopback listening socket"; + } + + // Sends a request whose transfer is guaranteed to stall, and returns once + // the worker has actually written the request to the socket. + std::string sendStalled(std::unique_ptr& request, RecordingCallback& callback) + { + request.reset(m_client.CreateRequest()); + request->SetUrl(m_endpoint.url()); + const std::string id = request->GetId(); + m_client.SendRequestAsync(request.get(), &callback); + return id; + } +}; + +// A client destroyed with a transfer in flight must deliver the terminal +// callback before ~HttpClient_Curl returns. +TEST_F(HttpClientCurlLifetimeTests, DestroyingClientWithRequestInFlightCompletesAbortedFirst) +{ + RecordingCallback callback; + std::unique_ptr client(new HttpClient_Curl()); + std::unique_ptr request(client->CreateRequest()); + request->SetUrl(m_endpoint.url()); + client->SendRequestAsync(request.get(), &callback); + ASSERT_TRUE(callback.waitForState(OnSending, 1, kInFlightTimeout)); + + client.reset(); + + // No wait here on purpose: the drain is the assertion. + EXPECT_EQ(callback.responses(), 1u); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), 1u); +} + +// The public IHttpClient contract requires the request to stay alive until the +// terminal callback begins. This intentionally violates that contract to prove +// Curl's private cancellation registry does not retain or dereference it. +TEST_F(HttpClientCurlLifetimeTests, InternalRegistryDoesNotDereferenceDeletedRequest) +{ + RecordingCallback callback; + IHttpRequest* request = m_client.CreateRequest(); + request->SetUrl(m_endpoint.url()); + const std::string id = request->GetId(); + m_client.SendRequestAsync(request, &callback); + ASSERT_TRUE(callback.waitForState(OnSending, 1, kInFlightTimeout)); + + delete request; + m_client.CancelRequestAsync(id); + + ASSERT_TRUE(callback.waitForResponses(1, kTerminalTimeout)); + EXPECT_EQ(callback.responses(), 1u); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), 1u); + + // Cancelling a retired id is a no-op and must not produce a second callback. + m_client.CancelRequestAsync(id); + EXPECT_EQ(callback.responses(), 1u); +} + +// A full drain returns only when every operation has completed and been +// destroyed, for all of them, not just the first. +TEST_F(HttpClientCurlLifetimeTests, CancelAllRequestsFullyDrainsEveryOperation) +{ + constexpr size_t kRequests = 4; + RecordingCallback callback; + std::vector> requests(kRequests); + for (size_t i = 0; i < kRequests; ++i) + { + sendStalled(requests[i], callback); + } + ASSERT_TRUE(callback.waitForState(OnSending, kRequests, kInFlightTimeout)); + + m_client.CancelAllRequests(); + + EXPECT_EQ(callback.responses(), kRequests); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), kRequests); +} + +// The bounded overload is a soft cap: it stops waiting at the deadline even +// though a terminal callback (and therefore the operation and the shared state) +// is still alive. The callback keeps everything it touches alive itself. +TEST_F(HttpClientCurlLifetimeTests, BoundedCancelAllReturnsAtDeadlineWhileCallbackIsRunning) +{ + RecordingCallback callback; + auto gate = std::make_shared(); + callback.setResponseHook([gate]() { gate->wait(); }); + + std::unique_ptr request; + const std::string id = sendStalled(request, callback); + ASSERT_TRUE(callback.waitForState(OnSending, 1, kInFlightTimeout)); + m_client.CancelRequestAsync(id); + // The response is counted before the hook blocks, so this proves the + // terminal callback is in flight and pinned. + ASSERT_TRUE(callback.waitForResponses(1, kTerminalTimeout)); + + const auto start = std::chrono::steady_clock::now(); + m_client.CancelAllRequests(std::chrono::milliseconds(200)); + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start); + + EXPECT_GE(elapsed, std::chrono::milliseconds(150)); + EXPECT_LT(elapsed, std::chrono::seconds(5)); + + gate->open(); + // The unbounded drain now has to complete, which also makes fixture + // teardown safe. + m_client.CancelAllRequests(); + EXPECT_EQ(callback.responses(), 1u); +} + +// A terminal callback must abort every registered peer before returning from a +// reentrant CancelAllRequests call; it must not wait for either callback. +TEST_F(HttpClientCurlLifetimeTests, ReentrantCancelAllAbortsStalledPeerBeforeReturning) +{ + RecordingCallback callbackA; + RecordingCallback callbackB; + std::atomic reentrantCancelReturned {false}; + callbackA.setResponseHook([this, &reentrantCancelReturned]() { + m_client.CancelAllRequests(); + reentrantCancelReturned = true; + }); + + std::unique_ptr requestA; + std::unique_ptr requestB; + const std::string idA = sendStalled(requestA, callbackA); + sendStalled(requestB, callbackB); + ASSERT_TRUE(callbackA.waitForState(OnSending, 1, kInFlightTimeout)); + ASSERT_TRUE(callbackB.waitForState(OnSending, 1, kInFlightTimeout)); + m_client.CancelRequestAsync(idA); + ASSERT_TRUE(callbackA.waitForResponses(1, kTerminalTimeout)); + ASSERT_TRUE(callbackB.waitForResponses(1, kTerminalTimeout)); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15); + while (!reentrantCancelReturned && std::chrono::steady_clock::now() < deadline) + { + PAL::sleep(10); + } + if (!reentrantCancelReturned) + { + ADD_FAILURE() << "reentrant CancelAllRequests() did not return"; + std::abort(); + } + + m_client.CancelAllRequests(); + EXPECT_EQ(callbackA.responsesWithResult(HttpResult_Aborted), 1u); + EXPECT_EQ(callbackB.responsesWithResult(HttpResult_Aborted), 1u); +} + +TEST_F(HttpClientCurlLifetimeTests, StateCallbackMayDestroyClientDuringOperationConstruction) +{ + RecordingCallback callback; + std::unique_ptr client(new HttpClient_Curl()); + callback.setStateHook([&client](HttpStateEvent state) { + if (state == OnCreated) + { + client.reset(); + } + }); + + std::unique_ptr request(client->CreateRequest()); + request->SetUrl(m_endpoint.url()); + client->SendRequestAsync(request.get(), &callback); + + EXPECT_EQ(client.get(), nullptr); + EXPECT_EQ(callback.responses(), 1u); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), 1u); +} + +// A send that lands inside an open cancellation epoch must not start network +// work (that would let late arrivals starve the drain), and must still get +// exactly one terminal callback, synchronously, so no caller is left hanging. +TEST_F(HttpClientCurlLifetimeTests, SendDuringCancellationEpochCompletesAbortedWithoutNetwork) +{ + RecordingCallback stalledCallback; + auto gate = std::make_shared(); + stalledCallback.setResponseHook([gate]() { gate->wait(); }); + + std::unique_ptr stalledRequest; + sendStalled(stalledRequest, stalledCallback); + ASSERT_TRUE(stalledCallback.waitForState(OnSending, 1, kInFlightTimeout)); + + // The drain runs on its own thread and cannot return while the pinned + // callback is in flight, so the epoch is provably open below. + std::thread drain([this]() { m_client.CancelAllRequests(); }); + ASSERT_TRUE(stalledCallback.waitForResponses(1, kTerminalTimeout)); + + std::mutex lateEventsMutex; + std::vector lateEvents; + auto lateCallback = std::make_shared(); + lateCallback->setStateHook([&lateEventsMutex, &lateEvents](HttpStateEvent state) { + std::lock_guard lock(lateEventsMutex); + switch (state) + { + case OnCreated: lateEvents.push_back("created"); break; + case OnCreateFailed: lateEvents.push_back("create-failed"); break; + case OnConnecting: lateEvents.push_back("connecting"); break; + case OnConnectFailed: lateEvents.push_back("connect-failed"); break; + case OnSendFailed: lateEvents.push_back("send-failed"); break; + case OnSending: lateEvents.push_back("sending"); break; + case OnResponse: lateEvents.push_back("response-state"); break; + case OnDestroy: lateEvents.push_back("destroy"); break; + } + }); + lateCallback->setResponseHook([&lateEventsMutex, &lateEvents, &lateCallback]() { + { + std::lock_guard lock(lateEventsMutex); + lateEvents.push_back("response"); + } + lateCallback.reset(); + }); + + std::unique_ptr lateRequest(m_client.CreateRequest()); + lateRequest->SetUrl(m_endpoint.url()); + m_client.SendRequestAsync(lateRequest.get(), lateCallback.get()); + + // Completed synchronously, on this thread, before SendRequestAsync returned. + { + std::lock_guard lock(lateEventsMutex); + EXPECT_EQ(lateEvents, (std::vector{"created", "destroy", "response"})); + } + + gate->open(); + drain.join(); + EXPECT_EQ(stalledCallback.responses(), 1u); +} + +// A reentrant CancelRequestAsync fired from the OnCreated state event must find +// the operation (it is registered before the event fires), stop it before any +// network work begins, and yield exactly one Aborted terminal in +// OnCreated -> OnDestroy -> response order. +TEST_F(HttpClientCurlLifetimeTests, OnCreatedCancelRequestFindsOperationAndAbortsWithoutNetwork) +{ + RecordingCallback callback; + std::unique_ptr request(m_client.CreateRequest()); + request->SetUrl(m_endpoint.url()); + const std::string id = request->GetId(); + + std::mutex eventsMutex; + std::vector events; + callback.setStateHook([this, id, &eventsMutex, &events](HttpStateEvent state) { + { + std::lock_guard lock(eventsMutex); + switch (state) + { + case OnCreated: events.push_back("created"); break; + case OnCreateFailed: events.push_back("create-failed"); break; + case OnConnecting: events.push_back("connecting"); break; + case OnConnectFailed: events.push_back("connect-failed"); break; + case OnSendFailed: events.push_back("send-failed"); break; + case OnSending: events.push_back("sending"); break; + case OnResponse: events.push_back("response-state"); break; + case OnDestroy: events.push_back("destroy"); break; + } + } + if (state == OnCreated) + { + // If the operation were not registered yet, this would be a no-op and + // the transfer would proceed to the stalled endpoint. + m_client.CancelRequestAsync(id); + } + }); + callback.setResponseHook([&eventsMutex, &events]() { + std::lock_guard lock(eventsMutex); + events.push_back("response"); + }); + + m_client.SendRequestAsync(request.get(), &callback); + + ASSERT_TRUE(callback.waitForResponses(1, kTerminalTimeout)); + EXPECT_EQ(callback.responses(), 1u); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), 1u); + // No worker, no socket: the cancellation during OnCreated was honored. + EXPECT_EQ(callback.stateCount(OnConnecting), 0u); + EXPECT_EQ(callback.stateCount(OnSending), 0u); + { + std::lock_guard lock(eventsMutex); + EXPECT_EQ(events, (std::vector{"created", "destroy", "response"})); + } +} + +// The same guarantee for a reentrant CancelAllRequests fired from OnCreated: the +// operation is found among the peers, aborted before network work, and produces +// exactly one Aborted terminal. +TEST_F(HttpClientCurlLifetimeTests, OnCreatedCancelAllAbortsOperationBeforeNetwork) +{ + RecordingCallback callback; + std::unique_ptr request(m_client.CreateRequest()); + request->SetUrl(m_endpoint.url()); + callback.setStateHook([this](HttpStateEvent state) { + if (state == OnCreated) + { + m_client.CancelAllRequests(); + } + }); + + m_client.SendRequestAsync(request.get(), &callback); + + ASSERT_TRUE(callback.waitForResponses(1, kTerminalTimeout)); + EXPECT_EQ(callback.responses(), 1u); + EXPECT_EQ(callback.responsesWithResult(HttpResult_Aborted), 1u); + EXPECT_EQ(callback.stateCount(OnConnecting), 0u); + EXPECT_EQ(callback.stateCount(OnSending), 0u); + EXPECT_EQ(callback.stateCount(OnDestroy), 1u); +} + +// A cancellation reentered from the OnDestroy state event of a *successful* +// transfer may legitimately abort peers, but it must not rewrite this +// operation's already-finished result. The cancellation classification is +// frozen before OnDestroy runs, so the terminal stays OK/200. +class HttpClientCurlDestroyReentryTests : public ::testing::Test, + public HttpServer::Callback +{ +protected: + HttpServer m_server; + HttpClient_Curl m_client; + std::string m_url; + + void SetUp() override + { + const int port = m_server.addListeningPort(0); + std::ostringstream address; + address << "127.0.0.1:" << port; + m_url = "http://" + address.str() + "/ok/"; + m_server.setServerName(address.str()); + m_server.addHandler("/ok/", *this); + m_server.start(); + } + + void TearDown() override + { + m_server.stop(); + } + + int onHttpRequest(HttpServer::Request const&, HttpServer::Response& response) override + { + response.content = "ok-body"; + return 200; + } + + struct ResultCallback : public IHttpResponseCallback + { + std::mutex mutex; + std::condition_variable cv; + size_t responses {0}; + HttpResult result {}; + unsigned int statusCode {0}; + std::function stateHook; + + void OnHttpResponse(IHttpResponse* response) override + { + std::unique_ptr owned(response); + { + std::lock_guard lock(mutex); + ++responses; + result = owned->GetResult(); + statusCode = owned->GetStatusCode(); + } + cv.notify_all(); + } + + void OnHttpStateEvent(HttpStateEvent state, void*, size_t) override + { + if (stateHook != nullptr) + { + stateHook(state); + } + } + + bool waitForResponse(std::chrono::milliseconds timeout) + { + std::unique_lock lock(mutex); + return cv.wait_for(lock, timeout, [&]() { return responses >= 1; }); + } + }; +}; + +TEST_F(HttpClientCurlDestroyReentryTests, OnDestroyReentrantCancelDoesNotRewriteSuccess) +{ + ResultCallback callback; + std::unique_ptr request(m_client.CreateRequest()); + request->SetUrl(m_url); + const std::string id = request->GetId(); + + callback.stateHook = [this, id](HttpStateEvent state) { + if (state == OnDestroy) + { + // The operation is still registered during OnDestroy. Both of these + // set its live abort flag, but the frozen classification must win. + m_client.CancelRequestAsync(id); + m_client.CancelAllRequests(); + } + }; + + m_client.SendRequestAsync(request.get(), &callback); + ASSERT_TRUE(callback.waitForResponse(kTerminalTimeout)); + + std::lock_guard lock(callback.mutex); + EXPECT_EQ(callback.responses, 1u); + EXPECT_EQ(callback.result, HttpResult_OK); + EXPECT_EQ(callback.statusCode, 200u); +} + +// Clients are independent: one going away with work in flight must not disturb +// another, and the process-wide libcurl initialization must survive all of it. +TEST_F(HttpClientCurlLifetimeTests, OverlappingClientsWithActiveRequestsDestroyIndependently) +{ + constexpr size_t kClients = 4; + std::vector> callbacks; + for (size_t i = 0; i < kClients; ++i) + { + callbacks.emplace_back(new RecordingCallback()); + } + + Gate release; + std::vector threads; + for (size_t i = 0; i < kClients; ++i) + { + threads.emplace_back([this, i, &callbacks, &release]() { + HttpClient_Curl client; + std::unique_ptr request(client.CreateRequest()); + request->SetUrl(m_endpoint.url()); + client.SendRequestAsync(request.get(), callbacks[i].get()); + callbacks[i]->waitForState(OnSending, 1, kInFlightTimeout); + // Destroy all of them while every one of them has work in flight. + release.wait(); + }); + } + + for (size_t i = 0; i < kClients; ++i) + { + callbacks[i]->waitForState(OnSending, 1, kInFlightTimeout); + } + release.open(); + for (auto& thread : threads) + { + thread.join(); + } + + for (size_t i = 0; i < kClients; ++i) + { + EXPECT_EQ(callbacks[i]->responses(), 1u) << "client " << i; + EXPECT_EQ(callbacks[i]->responsesWithResult(HttpResult_Aborted), 1u) << "client " << i; + } +} + #endif // MATSDK_PAL_CPP11 && !_MSC_VER && HAVE_MAT_DEFAULT_HTTP_CLIENT diff --git a/tests/unittests/HttpClientManagerTests.cpp b/tests/unittests/HttpClientManagerTests.cpp index 287e420ed..bdfbb10a5 100644 --- a/tests/unittests/HttpClientManagerTests.cpp +++ b/tests/unittests/HttpClientManagerTests.cpp @@ -4,10 +4,18 @@ #include "common/MockIHttpClient.hpp" #include "http/IBoundedHttpClientCancel.hpp" #include "http/HttpClientManager.hpp" +#include "pal/TaskDispatcher.hpp" #include "NullObjects.hpp" #include "ILogManager.hpp" +#include +#include +#include +#include +#include +#include + using namespace testing; using namespace MAT; @@ -31,21 +39,141 @@ class HttpClientManager4Test : public HttpClientManager { } }; +class AsyncHttpClientManager4Test : public HttpClientManager { + public: + AsyncHttpClientManager4Test(IHttpClient& httpClient) + : HttpClientManager(dummyLogManager, httpClient, *PAL::getDefaultTaskDispatcher()) + { + } + + AsyncHttpClientManager4Test( + IHttpClient& httpClient, + ITaskDispatcher& taskDispatcher) : + HttpClientManager(dummyLogManager, httpClient, taskDispatcher) + { + } + + void setCancelDrainTimeout(std::chrono::milliseconds timeout) + { + m_cancelDrainTimeout = timeout; + } + + bool waitForRequestsToDrain(std::chrono::milliseconds timeout) + { + std::unique_lock lock(m_httpCallbacksMtx); + return m_httpCallbacksCV.wait_for( + lock, timeout, [this]() { return m_httpCallbacks.empty(); }); + } +}; + +class ReentrantAsyncCompletionReceiver { + public: + void onRequestDone(EventsUploadContextPtr const& ctx) + { + if (ctx->httpRequestId == "async-reentrant-first") + { + { + std::unique_lock lock(mutex); + firstEntered = true; + cv.notify_all(); + cv.wait(lock, [this]() { return releaseFirst; }); + } + auto start = std::chrono::steady_clock::now(); + manager->cancelAllRequests(/* bestEffort */ true); + cancelDuration = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start); + } + + { + std::lock_guard lock(mutex); + ++completed; + cv.notify_all(); + } + } + + HttpClientManager* manager {nullptr}; + std::mutex mutex; + std::condition_variable cv; + bool firstEntered {false}; + bool releaseFirst {false}; + size_t completed {0}; + std::chrono::milliseconds cancelDuration {0}; + RouteSink + sink {this, &ReentrantAsyncCompletionReceiver::onRequestDone}; +}; + +class BlockingAsyncCompletionReceiver { + public: + void onRequestDone(EventsUploadContextPtr const&) + { + std::unique_lock lock(mutex); + entered = true; + cv.notify_all(); + cv.wait(lock, [this]() { return released; }); + } + + std::mutex mutex; + std::condition_variable cv; + bool entered {false}; + bool released {false}; + RouteSink + sink {this, &BlockingAsyncCompletionReceiver::onRequestDone}; +}; + +class QueuedHttpResponseDelivery { + public: + void deliver(IHttpResponseCallback* callback, IHttpResponse* response) + { + callback->OnHttpResponse(response); + { + std::lock_guard lock(mutex); + ++completed; + } + cv.notify_all(); + } + + bool waitFor(size_t count) + { + std::unique_lock lock(mutex); + return cv.wait_for(lock, std::chrono::seconds(5), + [this, count]() { return completed == count; }); + } + + std::mutex mutex; + std::condition_variable cv; + size_t completed {0}; +}; + +class HttpRequestDoneReceiver +{ + public: + MOCK_METHOD1(onRequestDone, void(EventsUploadContextPtr const&)); + + RouteSink + sink{this, &HttpRequestDoneReceiver::onRequestDone}; +}; + class HttpClientManagerTests : public StrictMock { protected: MockIHttpClient httpClientMock; HttpClientManager4Test hcm; RouteSink requestDone{this, &HttpClientManagerTests::resultRequestDone}; + RouteSink requestFailed{this, &HttpClientManagerTests::resultRequestFailed}; + RouteSink requestFailureComplete{this, &HttpClientManagerTests::resultRequestFailureComplete}; protected: HttpClientManagerTests() : hcm(httpClientMock) { hcm.requestDone >> requestDone; + hcm.requestFailed >> requestFailed; + hcm.requestFailureComplete >> requestFailureComplete; } MOCK_METHOD1(resultRequestDone, void(EventsUploadContextPtr const &)); + MOCK_METHOD1(resultRequestFailed, void(EventsUploadContextPtr const &)); + MOCK_METHOD1(resultRequestFailureComplete, void(EventsUploadContextPtr const &)); }; class MockBoundedIHttpClient : public MockIHttpClient, public IBoundedHttpClientCancel { @@ -54,6 +182,50 @@ class MockBoundedIHttpClient : public MockIHttpClient, public IBoundedHttpClient MOCK_METHOD1(CancelAllRequests, void(std::chrono::milliseconds)); }; +class ThrowingCancelAllHttpClient : public MockIHttpClient { + public: + void CancelAllRequests() override + { + throw std::runtime_error("cancel all failed"); + } +}; + +#ifndef _WIN32 +class DroppingHttpResponseTaskDispatcher : public ITaskDispatcher +{ + public: + void Join() override + { + } + void Queue(Task* task) override + { + delete task; + } + bool Cancel(Task*, uint64_t = 0) override + { + return false; + } +}; + +#if HAVE_EXCEPTIONS +class ThrowingHttpResponseTaskDispatcher : public ITaskDispatcher +{ + public: + void Join() override + { + } + void Queue(Task* task) override + { + delete task; + throw std::runtime_error("queue failed"); + } + bool Cancel(Task*, uint64_t = 0) override + { + return false; + } +}; +#endif +#endif TEST_F(HttpClientManagerTests, HandlesRequestFlow) { @@ -87,6 +259,348 @@ TEST_F(HttpClientManagerTests, HandlesRequestFlow) EXPECT_THAT(ctx->durationMs, Gt(199)); } +TEST_F(HttpClientManagerTests, ThrowingRequestDoneSettlesFailureAndDrainsCallback) +{ + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("throwing-request-done"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + + EXPECT_CALL(*this, resultRequestDone(ctx)) + .WillOnce(Throw(std::runtime_error("listener failed"))); + { + InSequence sequence; + EXPECT_CALL(*this, resultRequestFailed(ctx)); + EXPECT_CALL(*this, resultRequestFailureComplete(ctx)); + } + + EXPECT_NO_THROW(callback->OnHttpResponse(new SimpleHttpResponse("throwing-request-done"))); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, ThrowingFailureReleaseStillCompletesRequest) +{ + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("throwing-failure-release"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + + EXPECT_CALL(*this, resultRequestDone(ctx)) + .WillOnce(Throw(std::runtime_error("listener failed"))); + EXPECT_CALL(*this, resultRequestFailed(ctx)) + .WillOnce(Throw(std::runtime_error("release failed"))); + EXPECT_CALL(*this, resultRequestFailureComplete(ctx)); + + EXPECT_NO_THROW(callback->OnHttpResponse( + new SimpleHttpResponse("throwing-failure-release"))); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, ThrowingSendProducesOneTerminalFailure) +{ + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("throwing-send"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(Throw(std::runtime_error("send failed"))); + EXPECT_CALL(*this, resultRequestDone(ctx)) + .WillOnce(Invoke([](EventsUploadContextPtr const& completed) { + ASSERT_THAT(completed->httpResponse, NotNull()); + EXPECT_EQ(completed->httpResponse->GetId(), "throwing-send"); + EXPECT_EQ(completed->httpResponse->GetResult(), HttpResult_LocalFailure); + })); + + EXPECT_NO_THROW(hcm.sendRequest(ctx)); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, CallbackThenThrowDoesNotCompleteTwice) +{ + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("callback-then-throw"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + EXPECT_CALL(*this, resultRequestDone(ctx)) + .WillOnce(Invoke([](EventsUploadContextPtr const& completed) { + ASSERT_THAT(completed->httpResponse, NotNull()); + EXPECT_EQ(completed->httpResponse->GetId(), "original-response"); + EXPECT_EQ(completed->httpResponse->GetResult(), HttpResult_OK); + })); + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(Invoke([](IHttpRequest*, IHttpResponseCallback* callback) { + auto response = new SimpleHttpResponse("original-response"); + response->m_result = HttpResult_OK; + callback->OnHttpResponse(response); + throw std::runtime_error("invalid throw after callback"); + })); + + EXPECT_NO_THROW(hcm.sendRequest(ctx)); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, RequestDoneCanCancelAllRequests) +{ + SimpleHttpRequest* req = new SimpleHttpRequest("reentrant-cancel"); + auto ctx = std::make_shared(); + ctx->httpRequestId = req->GetId(); + ctx->httpRequest = req; + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + + EXPECT_CALL(*this, resultRequestDone(ctx)) + .WillOnce(Invoke([this](EventsUploadContextPtr const&) { + hcm.cancelAllRequests(); + })); + callback->OnHttpResponse(new SimpleHttpResponse("reentrant-cancel")); + + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, ConcurrentRequestDoneCallbacksCanCancelAllRequests) +{ + std::vector callbacks; + std::vector contexts; + for (size_t i = 0; i < 2; ++i) + { + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest( + "concurrent-reentrant-cancel-" + std::to_string(i)); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + callbacks.push_back(callback); + contexts.push_back(std::move(ctx)); + } + + std::mutex barrierMutex; + std::condition_variable barrierCv; + size_t callbacksEntered = 0; + EXPECT_CALL(*this, resultRequestDone(_)) + .Times(2) + .WillRepeatedly(Invoke([this, &barrierMutex, &barrierCv, &callbacksEntered]( + EventsUploadContextPtr const&) { + { + std::unique_lock lock(barrierMutex); + ++callbacksEntered; + barrierCv.notify_all(); + barrierCv.wait_for(lock, std::chrono::seconds(5), + [&callbacksEntered]() { return callbacksEntered == 2; }); + } + hcm.cancelAllRequests(); + })); + + std::thread first([&callbacks]() { + callbacks[0]->OnHttpResponse( + new SimpleHttpResponse("concurrent-reentrant-cancel-0")); + }); + std::thread second([&callbacks]() { + callbacks[1]->OnHttpResponse( + new SimpleHttpResponse("concurrent-reentrant-cancel-1")); + }); + first.join(); + second.join(); + + EXPECT_THAT(callbacksEntered, 2u); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST(HttpClientManagerAsyncTests, ReentrantCancelDoesNotBlockQueuedCallbacks) +{ + MockIHttpClient httpClient; + AsyncHttpClientManager4Test manager(httpClient); + manager.setCancelDrainTimeout(std::chrono::seconds(1)); + ReentrantAsyncCompletionReceiver receiver; + receiver.manager = &manager; + manager.requestDone >> receiver.sink; + + std::vector callbacks; + for (const char* id : {"async-reentrant-first", "async-reentrant-second"}) + { + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest(id); + ctx->httpRequestId = id; + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClient, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + manager.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + callbacks.push_back(callback); + } + + EXPECT_CALL(httpClient, CancelRequestAsync("async-reentrant-first")); + EXPECT_CALL(httpClient, CancelRequestAsync("async-reentrant-second")); + + QueuedHttpResponseDelivery delivery; + auto dispatcher = PAL::getDefaultTaskDispatcher(); + PAL::scheduleTask( + dispatcher.get(), 0, &delivery, &QueuedHttpResponseDelivery::deliver, + callbacks[0], new SimpleHttpResponse("async-reentrant-first")); + { + std::unique_lock lock(receiver.mutex); + ASSERT_TRUE(receiver.cv.wait_for(lock, std::chrono::seconds(5), + [&receiver]() { return receiver.firstEntered; })); + } + + // This completion is now queued behind the first one on PAL's default + // single-thread dispatcher. + PAL::scheduleTask( + dispatcher.get(), 0, &delivery, &QueuedHttpResponseDelivery::deliver, + callbacks[1], new SimpleHttpResponse("async-reentrant-second")); + { + std::lock_guard lock(receiver.mutex); + receiver.releaseFirst = true; + } + receiver.cv.notify_all(); + + { + std::unique_lock lock(receiver.mutex); + ASSERT_TRUE(receiver.cv.wait_for(lock, std::chrono::seconds(5), + [&receiver]() { return receiver.completed == 2; })); + } + EXPECT_THAT(receiver.cancelDuration, Lt(std::chrono::milliseconds(500))); + ASSERT_TRUE(manager.waitForRequestsToDrain(std::chrono::seconds(5))); + EXPECT_THAT(manager.requestCount(), 0u); + EXPECT_TRUE(delivery.waitFor(2)); +} + +TEST(HttpClientManagerAsyncTests, DestructorWaitsForActiveCallback) +{ + MockIHttpClient httpClient; + auto manager = std::make_unique(httpClient); + BlockingAsyncCompletionReceiver receiver; + manager->requestDone >> receiver.sink; + + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("async-destructor"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClient, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + manager->sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + QueuedHttpResponseDelivery delivery; + auto dispatcher = PAL::getDefaultTaskDispatcher(); + PAL::scheduleTask( + dispatcher.get(), 0, &delivery, &QueuedHttpResponseDelivery::deliver, + callback, new SimpleHttpResponse("async-destructor")); + + { + std::unique_lock lock(receiver.mutex); + ASSERT_TRUE(receiver.cv.wait_for(lock, std::chrono::seconds(5), + [&receiver]() { return receiver.entered; })); + } + + std::atomic destructorReturned {false}; + std::thread destroyer([&manager, &destructorReturned]() { + manager.reset(); + destructorReturned.store(true); + }); + + PAL::sleep(100); + EXPECT_FALSE(destructorReturned.load()); + { + std::lock_guard lock(receiver.mutex); + receiver.released = true; + } + receiver.cv.notify_all(); + destroyer.join(); + EXPECT_TRUE(destructorReturned.load()); + EXPECT_TRUE(delivery.waitFor(1)); +} + +#ifndef _WIN32 +TEST(HttpClientManagerAsyncTests, DroppedResponseTaskCompletesInline) +{ + MockIHttpClient httpClient; + DroppingHttpResponseTaskDispatcher dispatcher; + AsyncHttpClientManager4Test manager(httpClient, dispatcher); + HttpRequestDoneReceiver receiver; + manager.requestDone >> receiver.sink; + + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("dropped-response-task"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClient, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + manager.sendRequest(ctx); + + EXPECT_CALL(receiver, onRequestDone(ctx)); + callback->OnHttpResponse(new SimpleHttpResponse(ctx->httpRequestId)); + + EXPECT_THAT(manager.requestCount(), 0u); +} + +#if HAVE_EXCEPTIONS +TEST(HttpClientManagerAsyncTests, ThrowingResponseQueueCompletesInline) +{ + MockIHttpClient httpClient; + ThrowingHttpResponseTaskDispatcher dispatcher; + AsyncHttpClientManager4Test manager(httpClient, dispatcher); + HttpRequestDoneReceiver receiver; + manager.requestDone >> receiver.sink; + + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("throwing-response-queue"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClient, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + manager.sendRequest(ctx); + + EXPECT_CALL(receiver, onRequestDone(ctx)); + callback->OnHttpResponse(new SimpleHttpResponse(ctx->httpRequestId)); + + EXPECT_THAT(manager.requestCount(), 0u); +} +#endif +#endif + // Regression test: cancelAllRequests() must not spin/hang forever // when an in-flight callback never drains (e.g. the dispatcher or HTTP stack is // stalled). It waits for the drain via a condition variable, bounded by a timeout. @@ -156,3 +670,105 @@ TEST_F(HttpClientManagerTests, CancelAllRequests_UsesBoundedCancelCapability) EXPECT_CALL(*this, resultRequestDone(ctx)).WillOnce(Return()); callback->OnHttpResponse(new SimpleHttpResponse("bounded")); } + +TEST_F(HttpClientManagerTests, ZeroBudgetPauseCancelsWithoutWaiting) +{ + hcm.setCancelDrainTimeout(std::chrono::milliseconds::zero()); + + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("zero-budget"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + + EXPECT_CALL(httpClientMock, CancelRequestAsync(ctx->httpRequestId)); + EXPECT_NO_THROW(hcm.cancelAllRequests(/* bestEffort */ true)); + EXPECT_THAT(hcm.requestCount(), 1u); + + EXPECT_CALL(*this, resultRequestDone(ctx)).WillOnce(Return()); + callback->OnHttpResponse(new SimpleHttpResponse("zero-budget")); + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST_F(HttpClientManagerTests, ZeroBudgetPauseContinuesAfterCancelThrows) +{ + hcm.setCancelDrainTimeout(std::chrono::milliseconds::zero()); + + std::vector callbacks; + std::vector contexts; + for (const char* id : {"cancel-throws", "cancel-continues"}) + { + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest(id); + ctx->httpRequestId = id; + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClientMock, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + hcm.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + callbacks.push_back(callback); + contexts.push_back(std::move(ctx)); + } + + { + InSequence sequence; + EXPECT_CALL(httpClientMock, CancelRequestAsync("cancel-throws")) + .WillOnce(Throw(std::runtime_error("cancel failed"))); + EXPECT_CALL(httpClientMock, CancelRequestAsync("cancel-continues")); + } + EXPECT_NO_THROW(hcm.cancelAllRequests(/* bestEffort */ true)); + + for (size_t i = 0; i < callbacks.size(); ++i) + { + EXPECT_CALL(*this, resultRequestDone(contexts[i])).WillOnce(Return()); + callbacks[i]->OnHttpResponse( + new SimpleHttpResponse(contexts[i]->httpRequestId)); + } + EXPECT_THAT(hcm.requestCount(), 0u); +} + +TEST(HttpClientManagerExceptionTests, FullCancellationContainsClientException) +{ + ThrowingCancelAllHttpClient httpClient; + HttpClientManager4Test manager(httpClient); + + EXPECT_NO_THROW(manager.cancelAllRequests()); +} + +TEST(HttpClientManagerExceptionTests, BoundedCancellationFallsBackAfterException) +{ + MockBoundedIHttpClient httpClient; + HttpClientManager4Test manager(httpClient); + manager.setCancelDrainTimeout(std::chrono::milliseconds(50)); + + auto ctx = std::make_shared(); + ctx->httpRequest = new SimpleHttpRequest("bounded-throws"); + ctx->httpRequestId = ctx->httpRequest->GetId(); + ctx->recordIdsAndTenantIds["r1"] = "t1"; + ctx->latency = EventLatency_Normal; + ctx->packageIds["tenant1-token"] = 0; + + IHttpResponseCallback* callback = nullptr; + EXPECT_CALL(httpClient, SendRequestAsync(ctx->httpRequest, _)) + .WillOnce(SaveArg<1>(&callback)); + manager.sendRequest(ctx); + ASSERT_THAT(callback, NotNull()); + + EXPECT_CALL(httpClient, CancelAllRequests(std::chrono::milliseconds(50))) + .WillOnce(Throw(std::runtime_error("bounded cancel failed"))); + EXPECT_CALL(httpClient, CancelRequestAsync(ctx->httpRequestId)); + manager.cancelAllRequests(/* bestEffort */ true); + + callback->OnHttpResponse(new SimpleHttpResponse("bounded-throws")); +} diff --git a/tests/unittests/HttpClientTests.cpp b/tests/unittests/HttpClientTests.cpp index 4b17bcce5..1d25e0733 100644 --- a/tests/unittests/HttpClientTests.cpp +++ b/tests/unittests/HttpClientTests.cpp @@ -2,14 +2,36 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // -#ifdef HAVE_MAT_DEFAULT_HTTP_CLIENT #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #endif +// Must precede the guard below: HAVE_MAT_DEFAULT_HTTP_CLIENT comes from the SDK +// configuration header, so testing it before including this silently compiles +// the whole suite away (same ordering as HttpClientCurlTests.cpp). +#include "mat/config.h" + +#ifdef HAVE_MAT_DEFAULT_HTTP_CLIENT #include "common/Common.hpp" #include "common/HttpServer.hpp" #include "http/HttpClientFactory.hpp" +// Mirror HttpClientFactory's selection of HttpClient_Apple so the Apple-specific +// tests below only compile when the factory actually hands back that transport. +// On macOS desktop without APPLE_HTTP the factory builds HttpClient_Curl instead, +// and gating merely on __APPLE__ would run these expectations against the wrong +// client. +#if defined(__APPLE__) +#include +#if TARGET_OS_IPHONE || defined(APPLE_HTTP) +#define MAT_TEST_APPLE_TRANSPORT 1 +#endif +#endif + +#include +#include +#include +#include + using namespace testing; using namespace MAT; @@ -29,6 +51,27 @@ class HttpClientTests : public ::testing::Test, enum RequestState { Planned, Sent, Processed, Done }; std::vector _countedRequests; std::mutex _lock; + std::condition_variable _responseCv; + std::condition_variable _blockedRequestCv; + std::mutex _blockedRequestLock; + bool _blockedRequestReceived {false}; + bool _releaseBlockedRequest {false}; + bool _cancelOnConnecting {false}; + bool _blockStateEvent {false}; + HttpStateEvent _stateEventToBlock {OnConnecting}; + bool _stateEventEntered {false}; + bool _releaseConnecting {false}; + bool _blockResponseCallback {false}; + bool _responseCallbackEntered {false}; + bool _releaseResponseCallback {false}; + std::atomic _cancelAllOnResponse {0}; + std::atomic _synchronizeCancelAllResponses {false}; + size_t _cancelAllResponsesEntered {0}; + std::atomic _sendRequestOnResponse {false}; + std::atomic _cookieRequestCount {0}; + std::atomic _cookieHeaderSeen {false}; + bool _destroyClientOnConnecting {false}; + std::string _lateRequestId; public: HttpClientTests() @@ -59,19 +102,44 @@ class HttpClientTests : public ::testing::Test, _server.addHandler("/simple/", *this); _server.addHandler("/echo/", *this); _server.addHandler("/count/", *this); + _server.addHandler("/block/", *this); + _server.addHandler("/large/", *this); + _server.addHandler("/redirect/", *this); + _server.addHandler("/cookie/", *this); + _server.addHandler("/query", *this); _server.start(); + _cookieRequestCount = 0; + _cookieHeaderSeen = false; Clear(); } virtual void TearDown() override { + { + std::lock_guard lock(_blockedRequestLock); + _releaseBlockedRequest = true; + _releaseConnecting = true; + _releaseResponseCallback = true; + } + _blockedRequestCv.notify_all(); _server.stop(); _client.reset(); Clear(); } protected: + // Deterministic filler whose every byte depends on its offset, so a + // truncated, duplicated or misordered chunk cannot pass unnoticed. + static std::string LargePayload(size_t size) + { + std::string payload(size, '\0'); + for (size_t i = 0; i < size; ++i) { + payload[i] = static_cast('a' + (i % 26)); + } + return payload; + } + virtual int onHttpRequest(HttpServer::Request const& request, HttpServer::Response& inResponse) override { if (request.uri.substr(0, 8) == "/simple/") { @@ -87,6 +155,42 @@ class HttpClientTests : public ::testing::Test, return 200; } + if (request.uri == "/query?key=value") { + return 200; + } + + if (request.uri == "/block/") { + { + std::lock_guard lock(_blockedRequestLock); + _blockedRequestReceived = true; + } + _blockedRequestCv.notify_all(); + std::unique_lock lock(_blockedRequestLock); + _blockedRequestCv.wait(lock, [this]() { return _releaseBlockedRequest; }); + return 200; + } + + if (request.uri == "/redirect/") { + inResponse.headers["Location"] = "http://" + _hostname + "/simple/200"; + return 302; + } + + if (request.uri == "/cookie/") { + if (_cookieRequestCount.fetch_add(1) == 0) { + inResponse.headers["Set-Cookie"] = "mat-test=should-not-return"; + } else { + _cookieHeaderSeen = request.headers.find("Cookie") != request.headers.end(); + } + return 200; + } + + if (request.uri.substr(0, 7) == "/large/") { + size_t size = static_cast(atoi(request.uri.substr(7).c_str())); + inResponse.headers["Content-Type"] = "application/octet-stream"; + inResponse.content = LargePayload(size); + return 200; + } + if (request.uri.substr(0, 7) == "/count/") { int id = atoi(request.uri.substr(7).c_str()); if (id >= 0 && static_cast(id) < _countedRequests.size()) { @@ -105,22 +209,89 @@ class HttpClientTests : public ::testing::Test, */ virtual SimpleHttpResponse* clone(IHttpResponse* inResponse) { - SimpleHttpResponse *src = static_cast(inResponse); SimpleHttpResponse *dst = new SimpleHttpResponse(""); - dst->m_id = src->m_id; - dst->m_result = src->m_result; - dst->m_statusCode = src->m_statusCode; - dst->m_headers = src->m_headers; - dst->m_body = src->m_body; + dst->m_id = inResponse->GetId(); + dst->m_result = inResponse->GetResult(); + dst->m_statusCode = inResponse->GetStatusCode(); + dst->m_headers = inResponse->GetHeaders(); + dst->m_body = inResponse->GetBody(); return dst; } virtual void OnHttpResponse(IHttpResponse* inResponse) override { + if (_sendRequestOnResponse.exchange(false)) + { + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/echo/"); + { + std::lock_guard lock(_blockedRequestLock); + _lateRequestId = request->GetId(); + } + _client->SendRequestAsync(request.release(), this); + } + bool cancelAll = false; + size_t remaining = _cancelAllOnResponse.load(); + while (remaining != 0) + { + if (_cancelAllOnResponse.compare_exchange_weak( + remaining, remaining - 1)) + { + cancelAll = true; + break; + } + } + if (cancelAll && _synchronizeCancelAllResponses.load()) + { + std::unique_lock lock(_blockedRequestLock); + ++_cancelAllResponsesEntered; + _blockedRequestCv.notify_all(); + _blockedRequestCv.wait_for(lock, std::chrono::seconds(5), [this]() { + return _cancelAllResponsesEntered == 2; + }); + } + if (cancelAll) + { + _client->CancelAllRequests(); + } + { + std::unique_lock lock(_blockedRequestLock); + if (_blockResponseCallback) + { + _responseCallbackEntered = true; + _blockedRequestCv.notify_all(); + _blockedRequestCv.wait(lock, [this]() { + return _releaseResponseCallback; + }); + } + } + std::unique_ptr response(inResponse); std::lock_guard lock(_lock); - _responses.push_back(clone(inResponse)); + _responses.push_back(clone(response.get())); + _responseCv.notify_all(); } + virtual void OnHttpStateEvent(HttpStateEvent state, void*, size_t) override + { + if (_destroyClientOnConnecting && state == OnConnecting) + { + _destroyClientOnConnecting = false; + _client.reset(); + } + if (_cancelOnConnecting && state == OnConnecting) + { + _cancelOnConnecting = false; + _client->CancelAllRequests(); + } + if (_blockStateEvent && state == _stateEventToBlock) + { + std::unique_lock lock(_blockedRequestLock); + _stateEventEntered = true; + _blockedRequestCv.notify_all(); + _blockedRequestCv.wait(lock, [this]() { return _releaseConnecting; }); + _blockStateEvent = false; + } + } }; std::vector Binary(std::string const& str) @@ -128,8 +299,122 @@ std::vector Binary(std::string const& str) return std::vector(str.data(), str.data() + str.size()); } +TEST_F(HttpClientTests, HandlesCancellationWhileResponseIsInFlight) +{ + Clear(); + { + std::lock_guard lock(_blockedRequestLock); + _blockedRequestReceived = false; + _releaseBlockedRequest = false; + } + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/block/"); + _client->SendRequestAsync(request.get(), this); + + { + std::unique_lock lock(_blockedRequestLock); + ASSERT_TRUE(_blockedRequestCv.wait_for(lock, std::chrono::seconds(10), + [this]() { return _blockedRequestReceived; })); + } + + _client->CancelRequestAsync(requestId); + { + std::lock_guard lock(_blockedRequestLock); + _releaseBlockedRequest = true; + } + _blockedRequestCv.notify_all(); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(2), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + EXPECT_THAT(response->GetResult(), HttpResult_Aborted); +#if defined(MAT_TEST_APPLE_TRANSPORT) + { + std::unique_lock lock(_lock); + EXPECT_FALSE(_responseCv.wait_for(lock, std::chrono::milliseconds(250), + [this]() { return !_responses.empty(); })); + } +#endif +} + //--- +#ifdef MATSDK_PAL_WIN32 +TEST_F(HttpClientTests, UsesConfiguredWindowsTransport) +{ +#if defined(HAVE_MAT_WININET_HTTP_CLIENT) + EXPECT_THAT(dynamic_cast(_client.get()), NotNull()); +#elif defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + EXPECT_THAT(dynamic_cast(_client.get()), NotNull()); +#else +#error A Windows HTTP transport must be selected. +#endif +} + +TEST_F(HttpClientTests, DisablesRedirectsWhenMicrosoftRootCheckIsEnabled) +{ +#if defined(HAVE_MAT_WININET_HTTP_CLIENT) + auto windowsClient = dynamic_cast(_client.get()); +#elif defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) + auto windowsClient = dynamic_cast(_client.get()); +#else +#error A Windows HTTP transport must be selected. +#endif + ASSERT_THAT(windowsClient, NotNull()); + windowsClient->SetMsRootCheck(true); + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/redirect/"); + _client->SendRequestAsync(request.release(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_OK); + EXPECT_THAT(_responses[0]->GetStatusCode(), 302u); +} + +#if defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) +TEST_F(HttpClientTests, WinHttpDoesNotReplayResponseCookies) +{ + auto sendRequest = [this]() + { + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/cookie/"); + _client->SendRequestAsync(request.release(), this); + }; + + sendRequest(); + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _responses.size() >= 1; })); + } + + sendRequest(); + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _responses.size() >= 2; })); + } + + EXPECT_EQ(_cookieRequestCount.load(), 2); + EXPECT_FALSE(_cookieHeaderSeen.load()); +} +#endif +#endif + TEST_F(HttpClientTests, HandlesSimpleRequest) { Clear(); @@ -218,6 +503,77 @@ TEST_F(HttpClientTests, HandlesLocalErrors) _response.release(); } +#if defined(MAT_TEST_APPLE_TRANSPORT) +TEST_F(HttpClientTests, InvalidUtf8UrlCompletesExactlyOnce) +{ + // The request must outlive the whole exchange: keep ownership here (the Apple + // transport never deletes it) and hand only a borrowed pointer to the client. + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + std::string invalidUrl("http://invalid-url/"); + invalidUrl.push_back(static_cast(0xff)); + request->SetUrl(invalidUrl); + _client->SendRequestAsync(request.get(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + EXPECT_THAT(_responses[0]->GetId(), requestId); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_LocalFailure); + EXPECT_FALSE(_responseCv.wait_for(lock, std::chrono::milliseconds(250), + [this]() { return _responses.size() > 1; })); +} + +TEST_F(HttpClientTests, CancelBeforeSendCompletesExactlyOneAborted) +{ + // A cancel issued before SendRequestAsync must only arm the cancel flag; the + // single Aborted has to be delivered by Send once the callback is known, and + // never twice. The request is kept alive by this fixture for the duration. + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/simple/200"); + + _client->CancelRequestAsync(requestId); + _client->SendRequestAsync(request.get(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + EXPECT_THAT(_responses[0]->GetId(), requestId); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_Aborted); + EXPECT_FALSE(_responseCv.wait_for(lock, std::chrono::milliseconds(250), + [this]() { return _responses.size() > 1; })); +} + +TEST_F(HttpClientTests, CancelAllReturnsWithUnsentRequest) +{ + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/simple/200"); + + auto cancel = std::async(std::launch::async, [this]() + { _client->CancelAllRequests(); }); + ASSERT_EQ(cancel.wait_for(std::chrono::seconds(5)), std::future_status::ready); + cancel.get(); + + _client->SendRequestAsync(request.get(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() + { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + EXPECT_THAT(_responses[0]->GetId(), requestId); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_Aborted); + EXPECT_FALSE(_responseCv.wait_for(lock, std::chrono::milliseconds(250), + [this]() + { return _responses.size() > 1; })); +} + +#endif + TEST_F(HttpClientTests, HandlesDnsError) { Clear(); @@ -276,6 +632,305 @@ TEST_F(HttpClientTests, HandlesCancellation) _response.release(); } +#if defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) || defined(HAVE_MAT_WININET_HTTP_CLIENT) +TEST_F(HttpClientTests, HandlesCancellationFromStateEvent) +{ + Clear(); + _cancelOnConnecting = true; + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/echo/"); + _client->SendRequestAsync(request.release(), this); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(2), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + EXPECT_THAT(response->GetResult(), HttpResult_Aborted); +} + +TEST_F(HttpClientTests, HandlesConcurrentCancellationDuringStateEvent) +{ + Clear(); + { + std::lock_guard lock(_blockedRequestLock); + _blockStateEvent = true; + _stateEventToBlock = OnSending; + _stateEventEntered = false; + _releaseConnecting = false; + } + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/echo/"); + IHttpRequest* requestPtr = request.release(); + std::thread sender([this, requestPtr]() { + _client->SendRequestAsync(requestPtr, this); + }); + + { + std::unique_lock lock(_blockedRequestLock); + ASSERT_TRUE(_blockedRequestCv.wait_for(lock, std::chrono::seconds(2), + [this]() { return _stateEventEntered; })); + } + _client->CancelRequestAsync(requestId); + { + std::lock_guard lock(_lock); + EXPECT_TRUE(_responses.empty()) + << "Terminal response overlapped the active state callback"; + } + { + std::lock_guard lock(_blockedRequestLock); + _releaseConnecting = true; + } + _blockedRequestCv.notify_all(); + sender.join(); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(2), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + EXPECT_THAT(response->GetResult(), HttpResult_Aborted); +} +#endif + +#if defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) || defined(HAVE_MAT_WININET_HTTP_CLIENT) +TEST_F(HttpClientTests, CancelAllWaitsForActiveStateCallback) +{ + { + std::lock_guard lock(_blockedRequestLock); + _blockStateEvent = true; + _stateEventToBlock = OnSending; + } + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/echo/"); + IHttpRequest* requestPtr = request.release(); + std::thread sender([this, requestPtr]() { + _client->SendRequestAsync(requestPtr, this); + }); + + { + std::unique_lock lock(_blockedRequestLock); + ASSERT_TRUE(_blockedRequestCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _stateEventEntered; })); + } + + std::atomic cancelReturned {false}; + std::thread canceller([this, &cancelReturned]() { + _client->CancelAllRequests(); + cancelReturned.store(true); + }); + + PAL::sleep(100); + EXPECT_FALSE(cancelReturned.load()); + { + std::lock_guard lock(_blockedRequestLock); + _releaseConnecting = true; + } + _blockedRequestCv.notify_all(); + sender.join(); + canceller.join(); + EXPECT_TRUE(cancelReturned.load()); +} + +TEST_F(HttpClientTests, CancelAllWaitsForTerminalCallback) +{ + { + std::lock_guard lock(_blockedRequestLock); + _blockResponseCallback = true; + } + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/simple/200"); + _client->SendRequestAsync(request.release(), this); + + { + std::unique_lock lock(_blockedRequestLock); + ASSERT_TRUE(_blockedRequestCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _responseCallbackEntered; })); + } + + std::atomic cancelReturned {false}; + std::thread canceller([this, &cancelReturned]() { + _client->CancelAllRequests(); + cancelReturned.store(true); + }); + + PAL::sleep(100); + EXPECT_FALSE(cancelReturned.load()); + { + std::lock_guard lock(_blockedRequestLock); + _releaseResponseCallback = true; + } + _blockedRequestCv.notify_all(); + canceller.join(); + EXPECT_TRUE(cancelReturned.load()); +} + +TEST_F(HttpClientTests, TerminalCallbackCanCancelAllRequests) +{ + _cancelAllOnResponse.store(1); + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/simple/200"); + _client->SendRequestAsync(request.release(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_OK); +} + +#if defined(HAVE_MAT_WINHTTP_HTTP_CLIENT) +TEST_F(HttpClientTests, QueryStringIsPreservedWithoutFragment) +{ + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/query?key=value#client-only"); + _client->SendRequestAsync(request.release(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + EXPECT_THAT(_responses[0]->GetStatusCode(), 200u); +} + +TEST_F(HttpClientTests, SynchronousFailureCallbackCanCancelAllRequests) +{ + _cancelAllOnResponse.store(1); + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("://invalid-url"); + _client->SendRequestAsync(request.release(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_LocalFailure); +} +#endif + +TEST_F(HttpClientTests, ConcurrentTerminalCallbacksCanCancelAllRequests) +{ + _synchronizeCancelAllResponses.store(true); + _cancelAllOnResponse.store(2); + + for (size_t i = 0; i < 2; ++i) + { + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/simple/200"); + _client->SendRequestAsync(request.release(), this); + } + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(10), + [this]() { return _responses.size() == 2; })); + EXPECT_THAT(_cancelAllResponsesEntered, 2u); +} + +TEST_F(HttpClientTests, StateCallbackCanDestroyClient) +{ + _destroyClientOnConnecting = true; + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/simple/200"); + _client->SendRequestAsync(request.release(), this); + + EXPECT_THAT(_client, IsNull()); + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + EXPECT_THAT(_responses[0]->GetId(), requestId); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_Aborted); +} + +TEST_F(HttpClientTests, CancelAllIncludesRequestRegisteredDuringDrain) +{ + { + std::lock_guard lock(_blockedRequestLock); + _blockStateEvent = true; + _stateEventToBlock = OnSending; + _stateEventEntered = false; + _releaseConnecting = false; + } + _sendRequestOnResponse.store(true); + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/echo/"); + IHttpRequest* requestPtr = request.release(); + std::thread sender([this, requestPtr]() { + _client->SendRequestAsync(requestPtr, this); + }); + + { + std::unique_lock lock(_blockedRequestLock); + ASSERT_TRUE(_blockedRequestCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _stateEventEntered; })); + } + + std::atomic cancelStarted {false}; + std::thread canceller([this, &cancelStarted]() { + cancelStarted.store(true); + _client->CancelAllRequests(); + }); + while (!cancelStarted.load()) + { + std::this_thread::yield(); + } + PAL::sleep(100); + + { + std::lock_guard lock(_blockedRequestLock); + _stateEventEntered = false; + _releaseConnecting = true; + } + _blockedRequestCv.notify_all(); + + sender.join(); + canceller.join(); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return _responses.size() == 2; })); + auto lateResponse = std::find_if( + _responses.begin(), _responses.end(), [this](IHttpResponse* response) { + return response->GetId() == _lateRequestId; + }); + ASSERT_THAT(lateResponse, Ne(_responses.end())); + EXPECT_THAT((*lateResponse)->GetResult(), HttpResult_Aborted); +} + +TEST_F(HttpClientTests, ClientRemainsReusableAfterCancelAll) +{ + _client->CancelAllRequests(); + + std::unique_ptr request(_client->CreateRequest()); + request->SetUrl("http://" + _hostname + "/simple/200"); + _client->SendRequestAsync(request.release(), this); + + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(5), + [this]() { return !_responses.empty(); })); + EXPECT_THAT(_responses[0]->GetResult(), HttpResult_OK); +} +#endif + TEST_F(HttpClientTests, Handles100Continue) { Clear(); @@ -304,6 +959,104 @@ TEST_F(HttpClientTests, Handles100Continue) _response.release(); } +TEST_F(HttpClientTests, HandlesResponseLargerThanReadBuffer) +{ + Clear(); + // Several times the transport's fixed 8 KB read buffer, so the response can + // only be assembled by chaining many read completions. + const size_t responseSize = 300 * 1024; + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/large/" + std::to_string(responseSize)); + _client->SendRequestAsync(request.release(), this); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(30), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + EXPECT_THAT(response->GetResult(), HttpResult_OK); + EXPECT_THAT(response->GetStatusCode(), 200u); + ASSERT_THAT(response->GetBody().size(), responseSize); + EXPECT_THAT(response->GetBody(), Eq(Binary(LargePayload(responseSize)))); +} + +TEST_F(HttpClientTests, HandlesRequestAndResponseLargerThanReadBuffer) +{ + Clear(); + // Exercises the send side too: the body is written separately from the + // request headers, and the echoed response is then drained in chunks. + const size_t bodySize = 200 * 1024; + auto body = Binary(LargePayload(bodySize)); + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetMethod("POST"); + request->GetHeaders().set("Content-Type", "application/octet-stream"); + request->SetUrl("http://" + _hostname + "/echo/"); + request->SetBody(body); + _client->SendRequestAsync(request.release(), this); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(30), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + EXPECT_THAT(response->GetResult(), HttpResult_OK); + EXPECT_THAT(response->GetStatusCode(), 200u); + ASSERT_THAT(response->GetBody().size(), bodySize); + EXPECT_THAT(response->GetBody(), Eq(Binary(LargePayload(bodySize)))); +} + +TEST_F(HttpClientTests, HandlesCancellationOfLargeResponse) +{ + Clear(); + // Cancel while the response is still being drained through the read buffer: + // the request must still produce exactly one terminal response, and the + // buffers WinHTTP was given must outlive it. + const size_t responseSize = 4 * 1024 * 1024; + + std::unique_ptr request(_client->CreateRequest()); + std::string requestId = request->GetId(); + request->SetUrl("http://" + _hostname + "/large/" + std::to_string(responseSize)); + _client->SendRequestAsync(request.release(), this); + _client->CancelRequestAsync(requestId); + + std::unique_ptr response; + { + std::unique_lock lock(_lock); + ASSERT_TRUE(_responseCv.wait_for(lock, std::chrono::seconds(30), + [this]() { return !_responses.empty(); })); + ASSERT_EQ(_responses.size(), 1u); + response.reset(_responses[0]); + _responses.clear(); + } + + EXPECT_THAT(response->GetId(), requestId); + // The race is intentional: cancellation may land before or after the + // response has been fully read, but never both results and never neither. + EXPECT_TRUE(response->GetResult() == HttpResult_Aborted || + response->GetResult() == HttpResult_OK); + + // No duplicate terminal response arrives afterwards. + std::unique_lock lock(_lock); + EXPECT_FALSE(_responseCv.wait_for(lock, std::chrono::milliseconds(500), + [this]() { return !_responses.empty(); })); +} + TEST_F(HttpClientTests, SurvivesManyRequests) { Clear(); @@ -346,4 +1099,3 @@ TEST_F(HttpClientTests, SurvivesManyRequests) } #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT - diff --git a/tests/unittests/HttpResponseDecoderTests.cpp b/tests/unittests/HttpResponseDecoderTests.cpp index 314cdb513..7d11ae4b8 100644 --- a/tests/unittests/HttpResponseDecoderTests.cpp +++ b/tests/unittests/HttpResponseDecoderTests.cpp @@ -88,20 +88,29 @@ TEST_F(HttpResponseDecoderTests, UnderstandsTemporaryServerFailures) TEST_F(HttpResponseDecoderTests, UnderstandsTemporaryNetworkFailures) { auto ctx = createContextWith(HttpResult_LocalFailure, -1, ""); - EXPECT_CALL(*this, resultTemporaryNetworkFailure(ctx)) - .WillOnce(Return()); + EXPECT_CALL(*this, resultTemporaryNetworkFailure(ctx)).WillOnce(Invoke([](EventsUploadContextPtr const& routedCtx) { + ASSERT_THAT(routedCtx->httpResponse, NotNull()); + EXPECT_THAT(routedCtx->httpResponse->GetResult(), HttpResult_LocalFailure); + EXPECT_THAT(routedCtx->httpResponse->GetStatusCode(), static_cast(-1)); + })); decoder.decode(ctx); ctx = createContextWith(HttpResult_NetworkFailure, -1, ""); - EXPECT_CALL(*this, resultTemporaryNetworkFailure(ctx)) - .WillOnce(Return()); + EXPECT_CALL(*this, resultTemporaryNetworkFailure(ctx)).WillOnce(Invoke([](EventsUploadContextPtr const& routedCtx) { + ASSERT_THAT(routedCtx->httpResponse, NotNull()); + EXPECT_THAT(routedCtx->httpResponse->GetResult(), HttpResult_NetworkFailure); + EXPECT_THAT(routedCtx->httpResponse->GetStatusCode(), static_cast(-1)); + })); decoder.decode(ctx); } TEST_F(HttpResponseDecoderTests, SkipsAbortedRequests) { auto ctx = createContextWith(HttpResult_Aborted, -1, ""); - EXPECT_CALL(*this, resultRequestAborted(ctx)) - .WillOnce(Return()); + EXPECT_CALL(*this, resultRequestAborted(ctx)).WillOnce(Invoke([](EventsUploadContextPtr const& routedCtx) { + ASSERT_THAT(routedCtx->httpResponse, NotNull()); + EXPECT_THAT(routedCtx->httpResponse->GetResult(), HttpResult_Aborted); + EXPECT_THAT(routedCtx->httpResponse->GetStatusCode(), static_cast(-1)); + })); decoder.decode(ctx); } diff --git a/tests/unittests/LogSessionDataDBTests.cpp b/tests/unittests/LogSessionDataDBTests.cpp index 4788c5302..dbda5fd27 100644 --- a/tests/unittests/LogSessionDataDBTests.cpp +++ b/tests/unittests/LogSessionDataDBTests.cpp @@ -50,7 +50,8 @@ class LogSessionDataDBTests : public ::testing::Test StrictMock configMock; LogSessionDataProvider *logSessionDataProvider; std::ostringstream name; - unsigned long long now = PAL::getUtcSystemTimeMs(); + uint64_t sessionCreationStart = 0; + uint64_t sessionCreationEnd = 0; virtual void SetUp() override { @@ -67,7 +68,9 @@ class LogSessionDataDBTests : public ::testing::Test logSessionDataProvider = new LogSessionDataProvider(offlineStorage.get()); logSessionDataProvider->CreateLogSessionData(); offlineStorage->Initialize(observerMock); + sessionCreationStart = PAL::getUtcSystemTimeMs(); logSessionDataProvider->CreateLogSessionData(); + sessionCreationEnd = PAL::getUtcSystemTimeMs(); } virtual void TearDown() override @@ -83,7 +86,7 @@ TEST_F(LogSessionDataDBTests, subTest) { #ifndef USE_ROOM logSessionData = logSessionDataProvider->GetLogSessionData(); auto sessionFirstTime= logSessionData->getSessionFirstTime(); - EXPECT_IN_RANGE(sessionFirstTime, now , now + 1000); + EXPECT_IN_RANGE(sessionFirstTime, sessionCreationStart, sessionCreationEnd); auto sdkUid = logSessionData->getSessionSDKUid(); EXPECT_TRUE(sdkUid.size()); @@ -97,4 +100,3 @@ TEST_F(LogSessionDataDBTests, subTest) { ASSERT_EQ(1, 1); #endif } - diff --git a/tests/unittests/LogSessionDataTests.cpp b/tests/unittests/LogSessionDataTests.cpp index 9adc9c1f1..47a7f035a 100644 --- a/tests/unittests/LogSessionDataTests.cpp +++ b/tests/unittests/LogSessionDataTests.cpp @@ -18,8 +18,6 @@ class TestLogSessionDataProvider : public LogSessionDataProvider }; const char* const PathToTestSesFile = ""; -const char* const PathToNonEmptyTestSesFile = "sesfile"; - std::string sessionSDKUid; uint64_t sessionFirstTimeLaunch; @@ -71,12 +69,16 @@ TEST(LogSessionDataTests, parse_ValidInput_ReturnsTrue) TEST(LogSessionDataTests, getLogSessionData_ValidInput_SessionDataPersists) { - TestLogSessionDataProvider logSessionDataProvider1(PathToNonEmptyTestSesFile); + const std::string sessionFile = + GetTempDirectory() + "sesfile-" + std::to_string(PAL::getUtcSystemTimeMs()); + std::remove(sessionFile.c_str()); + + TestLogSessionDataProvider logSessionDataProvider1(sessionFile); logSessionDataProvider1.CreateLogSessionData(); const auto* logSessionData1 = logSessionDataProvider1.GetLogSessionData(); // Create another provider instance and validate session data is not re-generated - TestLogSessionDataProvider logSessionDataProvider2(PathToNonEmptyTestSesFile); + TestLogSessionDataProvider logSessionDataProvider2(sessionFile); logSessionDataProvider2.CreateLogSessionData(); const auto* logSessionData2 = logSessionDataProvider2.GetLogSessionData(); @@ -86,4 +88,3 @@ TEST(LogSessionDataTests, getLogSessionData_ValidInput_SessionDataPersists) logSessionDataProvider1.DeleteLogSessionData(); logSessionDataProvider2.DeleteLogSessionData(); } - diff --git a/tests/unittests/LoggerTests.cpp b/tests/unittests/LoggerTests.cpp index 4ea4ca116..56906649f 100644 --- a/tests/unittests/LoggerTests.cpp +++ b/tests/unittests/LoggerTests.cpp @@ -44,6 +44,7 @@ class LoggerTests : public ::testing::Test virtual void SetUp() override { + logger.GetEventFilters().UnregisterAllFilters(); logManager.GetEventFilters().UnregisterAllFilters(); } @@ -324,4 +325,3 @@ TEST_F(LoggerTests, LogSession_CanEventPropertiesBeSentReturnsTrue_CallsSubmit) EXPECT_TRUE(logger.SubmitCalled); } - diff --git a/tests/unittests/Main.cpp b/tests/unittests/Main.cpp index 303174749..4bb7b3c7a 100644 --- a/tests/unittests/Main.cpp +++ b/tests/unittests/Main.cpp @@ -52,4 +52,3 @@ int MAIN_CDECL main(int argc, char** argv) return result; } - diff --git a/tests/unittests/MemoryStorageTests.cpp b/tests/unittests/MemoryStorageTests.cpp index a736d125f..d33d152ce 100644 --- a/tests/unittests/MemoryStorageTests.cpp +++ b/tests/unittests/MemoryStorageTests.cpp @@ -213,6 +213,24 @@ TEST_F(MemoryStorageTests, DeleteAllRecords) EXPECT_THAT(storage.GetReservedCount(), 0); } +TEST_F(MemoryStorageTests, DeleteRecordsWithEmptyFilterDoesNotDeleteAll) +{ + MemoryStorage storage(testLogManager, *testConfig); + + // Add some events to storage + auto total_db_size = addEvents(storage); + EXPECT_THAT(storage.GetSize(), total_db_size); + auto count_before = storage.GetRecordCount(); + EXPECT_GT(count_before, static_cast(0)); + + // An empty where-filter matches every record; it must NOT wipe the queue. + // Intentional full clears go through DeleteAllRecords(). + storage.DeleteRecords(std::map{}); + + EXPECT_THAT(storage.GetRecordCount(), count_before); + EXPECT_THAT(storage.GetSize(), total_db_size); +} + TEST_F(MemoryStorageTests, ReleaseRecords) { @@ -262,18 +280,11 @@ TEST_F(MemoryStorageTests, GetAndReserveSome) storage.Initialize(testObserver); addEvents(storage); auto totalCount = storage.GetRecordCount(); - constexpr size_t howMany = 32; + static constexpr size_t howMany = 32; std::vector someRecords; -#if defined(__clang__) -#pragma clang diagnostic push // This appears to be a detection bug with constexpr variables in Clang9 -#pragma clang diagnostic ignored "-Wunused-lambda-capture" // error : lambda capture 'howMany' is not required to be captured for this use[-Werror, -Wunused - lambda - capture] -#elif defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable : 5258) // warning C5258: explicit capture of 'howMany' is not required for this use -#endif storage.GetAndReserveRecords( - [&someRecords, howMany] (StorageRecord && record)->bool + [&someRecords] (StorageRecord && record)->bool { if (someRecords.size() >= howMany) { return false; @@ -283,11 +294,6 @@ TEST_F(MemoryStorageTests, GetAndReserveSome) }, EventLatency_Normal ); -#if defined(__clang__) -#pragma clang diagnostic pop -#elif defined(_MSC_VER) -#pragma warning(pop) -#endif EXPECT_EQ(howMany, someRecords.size()); EXPECT_EQ(howMany, storage.LastReadRecordCount()); @@ -377,4 +383,3 @@ TEST_F(MemoryStorageTests, MultiThreadPerfTest) EXPECT_THAT(storage.GetSize(), 0); } - diff --git a/tests/unittests/MsRootCertPolicyTests.cpp b/tests/unittests/MsRootCertPolicyTests.cpp new file mode 100644 index 000000000..d7056c401 --- /dev/null +++ b/tests/unittests/MsRootCertPolicyTests.cpp @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Unit tests for the pure MS-root certificate policy decision helper. These run +// on any platform with no live network and no WinInet/Wincrypt dependency: they +// exercise the tri-state (Allow / Reject / Unable) that the transport relies on. +// +// They deliberately encode two properties the legacy two-state boolean design +// could not represent, so they FAIL against the old behavior: +// 1. "could not evaluate" is distinct from "evaluated and rejected" +// (tri-state), and +// 2. every "could not evaluate" case fails closed (ShouldProceed == false). +// +#include "common/Common.hpp" + +#include "http/detail/MsRootCertPolicy.hpp" + +using namespace testing; +using namespace MAT; +using MAT::detail::EvaluateMsRootPolicy; +using MAT::detail::MsRootCertQuery; +using MAT::detail::MsRootPolicyDecision; +using MAT::detail::ShouldProceed; + +namespace +{ + // A fully successful HTTPS chain query that roots to the Microsoft root. + MsRootCertQuery MakeSuccessfulHttpsQuery() + { + MsRootCertQuery query; + query.httpsScheme = true; + query.chainQuerySucceeded = true; + query.chainContextPresent = true; + query.policyCheckPerformed = true; + query.policyStatusError = 0u; // ERROR_SUCCESS + return query; + } +} // namespace + +// success => Allow (and proceeds) +TEST(MsRootCertPolicyTests, SuccessfulMsRootedChainIsAllow) +{ + auto query = MakeSuccessfulHttpsQuery(); + EXPECT_EQ(EvaluateMsRootPolicy(query), MsRootPolicyDecision::Allow); + EXPECT_TRUE(ShouldProceed(EvaluateMsRootPolicy(query))); +} + +// explicit policy error => Reject (and does NOT proceed) +TEST(MsRootCertPolicyTests, EvaluatedNonMsRootedChainIsReject) +{ + auto query = MakeSuccessfulHttpsQuery(); + query.policyStatusError = 0x800B0109u; // e.g. CERT_E_UNTRUSTEDROOT + EXPECT_EQ(EvaluateMsRootPolicy(query), MsRootPolicyDecision::Reject); + EXPECT_FALSE(ShouldProceed(EvaluateMsRootPolicy(query))); +} + +// query unavailable => Unable, and fails closed. +TEST(MsRootCertPolicyTests, ChainQueryUnavailableIsUnableAndFailsClosed) +{ + MsRootCertQuery query; + query.httpsScheme = true; + query.chainQuerySucceeded = false; // InternetQueryOption failed + query.chainContextPresent = false; + query.policyCheckPerformed = false; + + EXPECT_EQ(EvaluateMsRootPolicy(query), MsRootPolicyDecision::Unable); + EXPECT_FALSE(ShouldProceed(EvaluateMsRootPolicy(query))); +} + +// query succeeds but yields no chain context => Unable / fail closed. +TEST(MsRootCertPolicyTests, ChainQuerySucceedsButNoContextIsUnableAndFailsClosed) +{ + MsRootCertQuery query; + query.httpsScheme = true; + query.chainQuerySucceeded = true; + query.chainContextPresent = false; // nothing to verify + query.policyCheckPerformed = false; + + EXPECT_EQ(EvaluateMsRootPolicy(query), MsRootPolicyDecision::Unable); + EXPECT_FALSE(ShouldProceed(EvaluateMsRootPolicy(query))); +} + +// policy API failure => Unable and fail closed, while remaining distinguishable +// from an evaluated rejection for diagnostics. +TEST(MsRootCertPolicyTests, PolicyApiFailureIsUnableAndFailsClosed) +{ + MsRootCertQuery query; + query.httpsScheme = true; + query.chainQuerySucceeded = true; + query.chainContextPresent = true; + query.policyCheckPerformed = false; // CertVerifyCertificateChainPolicy returned FALSE + query.policyStatusError = 0u; + + auto decision = EvaluateMsRootPolicy(query); + EXPECT_EQ(decision, MsRootPolicyDecision::Unable); + EXPECT_NE(decision, MsRootPolicyDecision::Reject); + EXPECT_FALSE(ShouldProceed(decision)); +} + +// Non-HTTPS is never subject to the MS-root policy, regardless of other inputs. +TEST(MsRootCertPolicyTests, NonHttpsIsAlwaysAllow) +{ + MsRootCertQuery query; + query.httpsScheme = false; + query.chainQuerySucceeded = true; + query.chainContextPresent = true; + query.policyCheckPerformed = true; + query.policyStatusError = 0x800B0109u; // would be a reject if HTTPS + + EXPECT_EQ(EvaluateMsRootPolicy(query), MsRootPolicyDecision::Allow); + EXPECT_TRUE(ShouldProceed(EvaluateMsRootPolicy(query))); +} + +// The three outcomes are genuinely distinct: a test that only knew about a +// two-state (trusted/untrusted) result could not satisfy all of these at once. +TEST(MsRootCertPolicyTests, AllowRejectUnableAreDistinct) +{ + auto allow = EvaluateMsRootPolicy(MakeSuccessfulHttpsQuery()); + + auto rejectQuery = MakeSuccessfulHttpsQuery(); + rejectQuery.policyStatusError = 0x800B0109u; + auto reject = EvaluateMsRootPolicy(rejectQuery); + + MsRootCertQuery unableQuery; + unableQuery.httpsScheme = true; + auto unable = EvaluateMsRootPolicy(unableQuery); + + EXPECT_NE(allow, reject); + EXPECT_NE(allow, unable); + EXPECT_NE(reject, unable); + + // Fail-closed contract: only Allow permits the request. + EXPECT_TRUE(ShouldProceed(allow)); + EXPECT_FALSE(ShouldProceed(reject)); + EXPECT_FALSE(ShouldProceed(unable)); +} diff --git a/tests/unittests/OfflineStorageTests.cpp b/tests/unittests/OfflineStorageTests.cpp index bbb8da8e0..02297e481 100644 --- a/tests/unittests/OfflineStorageTests.cpp +++ b/tests/unittests/OfflineStorageTests.cpp @@ -2,7 +2,18 @@ #include "common/Common.hpp" #include "common/MockIOfflineStorage.hpp" +#include "common/MockIOfflineStorageObserver.hpp" +#include "common/MockIRuntimeConfig.hpp" +#include "offline/OfflineStorageHandler.hpp" +#include "offline/IOfflineStorageProvider.hpp" #include "offline/StorageObserver.hpp" +#include "NullObjects.hpp" + +#include +#include +#include +#include +#include using namespace testing; using namespace MAT; @@ -65,18 +76,18 @@ TEST_F(OfflineStorageTests, StopShutsDown) TEST_F(OfflineStorageTests, StoreRecordIsForwarded) { - auto ctx = new IncomingEventContext(); + IncomingEventContext ctx; - EXPECT_CALL(offlineStorageMock, StoreRecord(Ref(ctx->record))) + EXPECT_CALL(offlineStorageMock, StoreRecord(Ref(ctx.record))) .WillOnce(Return(true)); - EXPECT_THAT(offlineStorage.storeRecord(ctx), true); - EXPECT_THAT(ctx->record.timestamp, Near(PAL::getUtcSystemTimeMs(), 1000)); + EXPECT_THAT(offlineStorage.storeRecord(&ctx), true); + EXPECT_THAT(ctx.record.timestamp, Near(PAL::getUtcSystemTimeMs(), 1000)); - EXPECT_CALL(offlineStorageMock, StoreRecord(Ref(ctx->record))) + EXPECT_CALL(offlineStorageMock, StoreRecord(Ref(ctx.record))) .WillOnce(Return(false)); - EXPECT_CALL(*this, resultStoreRecordFailed(ctx)) + EXPECT_CALL(*this, resultStoreRecordFailed(&ctx)) .WillOnce(Return()); - EXPECT_THAT(offlineStorage.storeRecord(ctx), false); + EXPECT_THAT(offlineStorage.storeRecord(&ctx), false); } TEST_F(OfflineStorageTests, RetrieveEventsPassesRecordsThrough) @@ -162,3 +173,554 @@ TEST_F(OfflineStorageTests, ReleaseRecordsIsForwarded) .WillOnce(Return()); EXPECT_THAT(offlineStorage.releaseRecordsIncRetryCount(ctx), true); } + +namespace +{ + class ConfigurableLogManager : public NullLogManager + { + public: + ILogConfiguration config; + ILogConfiguration& GetLogConfiguration() override { return config; } + }; + + // Remove a SQLite db file along with its WAL-mode companion files + // (-wal/-shm/-journal), which would otherwise accumulate in the temp dir. + void RemoveDbFiles(const std::string& path) + { + std::remove(path.c_str()); + std::remove((path + "-wal").c_str()); + std::remove((path + "-shm").c_str()); + std::remove((path + "-journal").c_str()); + } + + // No-op dispatcher that owns queued tasks and frees them, so flushes only + // run when invoked directly and scheduled tasks (if any) are not leaked. + class NoopTaskDispatcher : public ITaskDispatcher + { + public: + void Join() override { clear(); } + void Queue(Task* task) override { m_tasks.push_back(task); } + bool Cancel(Task* task, uint64_t waitTime = 0) override + { + UNREFERENCED_PARAMETER(waitTime); + auto it = std::find(m_tasks.begin(), m_tasks.end(), task); + if (it != m_tasks.end()) + { + delete *it; + m_tasks.erase(it); + return true; + } + return false; + } + ~NoopTaskDispatcher() override { clear(); } + + private: + void clear() + { + for (auto* t : m_tasks) + delete t; + m_tasks.clear(); + } + std::vector m_tasks; + }; +} + +namespace MAT_NS_BEGIN { + + class MockOfflineStorageProvider : public IOfflineStorageProvider + { + public: + MockOfflineStorageProvider( + std::shared_ptr memory, + std::shared_ptr disk) + : memory(std::move(memory)), disk(std::move(disk)) + { + } + + std::shared_ptr CreateDiskStorage( + ILogManager&, IRuntimeConfig&) override + { + return disk; + } + + std::shared_ptr CreateMemoryStorage( + ILogManager&, IRuntimeConfig&) override + { + return memory; + } + + private: + std::shared_ptr memory; + std::shared_ptr disk; + }; + +} MAT_NS_END + +TEST(OfflineStorageHandlerFlushTests, FailedMemoryRequeueIsReportedAndDropped) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = true; + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector records; + records.push_back(StorageRecord("retry-ok", "tenant-one-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' })); + records.push_back(StorageRecord("retry-drop", "tenant-two-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'y' })); + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(records.size())) + .WillOnce(Return(records.size())); + EXPECT_CALL(*memory, GetRecordCount(EventLatency_Unspecified)) + .WillOnce(Return(records.size())); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 2000)) + .WillOnce(Return(records)); + EXPECT_CALL(*disk, StoreRecords(_)).WillOnce(Return(0)); + EXPECT_CALL(*memory, StoreRecord(_)) + .WillOnce(Return(true)) + .WillOnce(Return(false)); + EXPECT_CALL(observer, OnStorageRecordsSaved(0)); + EXPECT_CALL(observer, OnStorageRecordsDropped(_)) + .WillOnce(Invoke([](std::map const& dropped) { + auto found = dropped.find("tenant-two-token"); + ASSERT_NE(found, dropped.end()); + EXPECT_EQ(found->second, static_cast(1)); + })); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, BatchingOptOutUsesPerRecordDiskStores) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = false; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector records; + records.push_back(StorageRecord("per-record-1", "tenant-one-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' })); + records.push_back(StorageRecord("per-record-2", "tenant-two-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'y' })); + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(static_cast(records.size()))) + .WillOnce(Return(static_cast(0))); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 0)) + .WillOnce(Return(records)); + EXPECT_CALL(*disk, StoreRecords(_)).Times(0); + EXPECT_CALL(*disk, StoreRecord(_)) + .Times(static_cast(records.size())) + .WillRepeatedly(Return(true)); + EXPECT_CALL(observer, OnStorageRecordsSaved(records.size())); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, BatchedFlushKeepsMemoryOnlyRecordsOffDisk) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = true; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector records; + records.push_back(StorageRecord("persisted", "tenant-token", + EventLatency_Normal, EventPersistence_Normal, 1, std::vector{'x'})); + records.push_back(StorageRecord("memory-only", "tenant-token", + EventLatency_Normal, EventPersistence_DoNotStoreOnDisk, 1, std::vector{'y'})); + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(records.size())) + .WillOnce(Return(static_cast(1))); + EXPECT_CALL(*memory, GetRecordCount(EventLatency_Unspecified)) + .WillOnce(Return(records.size())); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 2000)) + .WillOnce(Return(records)); + EXPECT_CALL(*memory, StoreRecord(Field(&StorageRecord::id, "memory-only"))) + .WillOnce(Return(true)); + EXPECT_CALL(*disk, StoreRecord(_)).Times(0); + EXPECT_CALL(*disk, StoreRecords(_)).WillOnce(Return(1)); + EXPECT_CALL(observer, OnStorageRecordsSaved(1)); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, PerRecordFlushKeepsMemoryOnlyRecordsOffDisk) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = false; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector records; + records.push_back(StorageRecord("persisted", "tenant-token", + EventLatency_Normal, EventPersistence_Normal, 1, std::vector{'x'})); + records.push_back(StorageRecord("memory-only", "tenant-token", + EventLatency_Normal, EventPersistence_DoNotStoreOnDisk, 1, std::vector{'y'})); + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(records.size())) + .WillOnce(Return(static_cast(1))); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 0)) + .WillOnce(Return(records)); + EXPECT_CALL(*memory, StoreRecord(Field(&StorageRecord::id, "memory-only"))) + .WillOnce(Return(true)); + EXPECT_CALL(*disk, StoreRecords(_)).Times(0); + EXPECT_CALL(*disk, StoreRecord(Field(&StorageRecord::id, "persisted"))) + .WillOnce(Return(true)); + EXPECT_CALL(observer, OnStorageRecordsSaved(1)); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, BatchedFlushLimitsEachDiskWrite) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = true; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector firstBatch; + std::vector secondBatch; + std::vector finalBatch; + for (size_t i = 0; i < 4005; ++i) + { + StorageRecord record("batch-" + std::to_string(i), "tenant-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' }); + if (i < 2000) + { + firstBatch.push_back(record); + } + else if (i < 4000) + { + secondBatch.push_back(record); + } + else + { + finalBatch.push_back(record); + } + } + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(static_cast(4005))) + .WillOnce(Return(static_cast(0))); + EXPECT_CALL(*memory, GetRecordCount(EventLatency_Unspecified)) + .WillOnce(Return(static_cast(4005))); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 2000)) + .WillOnce(Return(firstBatch)) + .WillOnce(Return(secondBatch)) + .WillOnce(Return(finalBatch)); + EXPECT_CALL(*disk, StoreRecords(_)) + .WillOnce(Invoke([](std::vector& records) { + EXPECT_EQ(records.size(), static_cast(2000)); + return records.size(); + })) + .WillOnce(Invoke([](std::vector& records) { + EXPECT_EQ(records.size(), static_cast(2000)); + return records.size(); + })) + .WillOnce(Invoke([](std::vector& records) { + EXPECT_EQ(records.size(), static_cast(5)); + return records.size(); + })); + EXPECT_CALL(observer, OnStorageRecordsSaved(4005)); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, FailedBatchRequeuesOnlyThatBatch) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = true; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto memory = std::make_shared>(); + auto disk = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector firstBatch; + std::vector failedBatch; + for (size_t i = 0; i < 4000; ++i) + { + StorageRecord record("failed-batch-" + std::to_string(i), "tenant-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' }); + (i < 2000 ? firstBatch : failedBatch).push_back(record); + } + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(static_cast(4000))) + .WillOnce(Return(static_cast(4000))); + EXPECT_CALL(*memory, GetRecordCount(EventLatency_Unspecified)) + .WillOnce(Return(static_cast(4000))); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 2000)) + .WillOnce(Return(firstBatch)) + .WillOnce(Return(failedBatch)); + EXPECT_CALL(*memory, StoreRecord(_)) + .Times(2000) + .WillRepeatedly(Return(true)); + EXPECT_CALL(*disk, StoreRecords(_)) + .WillOnce(Return(static_cast(2000))) + .WillOnce(Return(static_cast(0))); + EXPECT_CALL(observer, OnStorageRecordsSaved(2000)); + + handler.Flush(); +} + +TEST(OfflineStorageHandlerFlushTests, CustomStorageUsesPerRecordWrites) +{ + ConfigurableLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + StrictMock observer; + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = true; + config[CFG_INT_RAM_QUEUE_SIZE] = 4096 * 20; + + auto disk = std::make_shared>(); + logManager.config.AddModule(CFG_MODULE_OFFLINE_STORAGE, disk); + + auto memory = std::make_shared>(); + auto provider = std::make_shared(memory, disk); + OfflineStorageHandler handler(logManager, config, dispatcher, provider); + EXPECT_CALL(*memory, Initialize(Ref(handler))).WillOnce(Return()); + EXPECT_CALL(*disk, Initialize(Ref(handler))).WillOnce(Return()); + handler.Initialize(observer); + + std::vector records; + records.push_back(StorageRecord("custom-1", "tenant-token", + EventLatency_Normal, EventPersistence_Normal, 1, std::vector{ 'x' })); + records.push_back(StorageRecord("custom-2", "tenant-token", + EventLatency_Normal, EventPersistence_Normal, 1, std::vector{ 'y' })); + + EXPECT_CALL(*memory, GetSize()) + .WillOnce(Return(records.size())) + .WillOnce(Return(static_cast(0))); + EXPECT_CALL(*memory, GetRecords(false, EventLatency_Unspecified, 0)) + .WillOnce(Return(records)); + EXPECT_CALL(*disk, StoreRecords(_)).Times(0); + EXPECT_CALL(*disk, StoreRecord(_)).Times(2).WillRepeatedly(Return(true)); + EXPECT_CALL(observer, OnStorageRecordsSaved(records.size())); + + handler.Flush(); +} + +// Regression test: when valid records drained from the in-memory queue fail to +// be persisted by the disk backend during Flush() (a transient failure -- here +// an unopenable database), they must be returned to the queue rather than lost. +TEST(OfflineStorageHandlerFlushTests, FailedDiskStoreDuringFlushReturnsRecordsToMemory) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + NiceMock observer; + + ON_CALL(config, GetOfflineStorageMaximumSizeBytes()).WillByDefault(Return(32 * 4096)); + ON_CALL(config, GetMaximumRetryCount()).WillByDefault(Return(5)); + + // A path inside a non-existent directory cannot be opened by SQLite (it does + // not create parent directories), so every disk StoreRecords() returns 0 -- + // a transient failure with otherwise-valid records. + std::ostringstream dbPath; + dbPath << GetTempDirectory() << "no_such_dir_" << PAL::getUtcSystemTimeMs() + << "/FlushReserveTest.db"; + config[CFG_STR_CACHE_FILE_PATH] = dbPath.str(); + config[CFG_INT_RAM_QUEUE_SIZE] = 1024 * 1024; // enable the in-memory queue + + OfflineStorageHandler handler(logManager, config, dispatcher); + handler.Initialize(observer); + + const size_t kCount = 5; + for (size_t i = 0; i < kCount; i++) + { + StorageRecord r("flush-id-" + std::to_string(i), "tenant-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' }); + handler.StoreRecord(r); + } + EXPECT_EQ(handler.GetRecordCount(), kCount); + + handler.Flush(); + + // The disk could not persist the batch; with the fix the valid records are + // returned to the in-memory queue rather than silently dropped. + EXPECT_EQ(handler.GetRecordCount(), kCount); + + handler.Shutdown(); +} + +TEST(OfflineStorageHandlerFlushTests, EventLatencyOffIsDroppedWithoutReportingStoreFailure) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + NiceMock observer; + + ON_CALL(config, GetOfflineStorageMaximumSizeBytes()).WillByDefault(Return(32 * 4096)); + + config[CFG_STR_CACHE_FILE_PATH] = ":memory:"; + config[CFG_INT_RAM_QUEUE_SIZE] = 1024 * 1024; // enable the in-memory queue + + OfflineStorageHandler handler(logManager, config, dispatcher); + handler.Initialize(observer); + + StorageRecord record("latency-off", "tenant-token", + EventLatency_Off, EventPersistence_Normal, /*timestamp*/ 1, + std::vector{ 'x' }); + + EXPECT_TRUE(handler.StoreRecord(record)); + EXPECT_EQ(handler.GetRecordCount(), static_cast(0)); + + handler.Shutdown(); +} + +// Regression test: a permanently-invalid record (rejected by the disk backend's +// validation) must be dropped on Flush(), not returned to the queue -- otherwise +// one poison record would be re-drained and re-rejected on every flush, wedging +// the queue and blocking every valid record behind it. +TEST(OfflineStorageHandlerFlushTests, FlushDropsInvalidRecordsInsteadOfWedging) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + NiceMock observer; + + ON_CALL(config, GetOfflineStorageMaximumSizeBytes()).WillByDefault(Return(32 * 4096)); + ON_CALL(config, GetMaximumRetryCount()).WillByDefault(Return(5)); + + std::ostringstream dbPath; + dbPath << GetTempDirectory() << "FlushDropInvalid-" << PAL::getUtcSystemTimeMs() << ".db"; + RemoveDbFiles(dbPath.str()); + config[CFG_STR_CACHE_FILE_PATH] = dbPath.str(); + config[CFG_INT_RAM_QUEUE_SIZE] = 1024 * 1024; // enable the in-memory queue + + OfflineStorageHandler handler(logManager, config, dispatcher); + handler.Initialize(observer); + + // A timestamp <= 0 is accepted by the in-memory queue but permanently rejected + // by the SQLite disk store's validation, so it can never be persisted. + const size_t kCount = 5; + for (size_t i = 0; i < kCount; i++) + { + StorageRecord r("bad-id-" + std::to_string(i), "tenant-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 0, + std::vector{ 'x' }); + handler.StoreRecord(r); + } + EXPECT_EQ(handler.GetRecordCount(), kCount); + + handler.Flush(); + + // The invalid records are dropped, not returned to the queue, so the queue + // drains and is not wedged. + EXPECT_EQ(handler.GetRecordCount(), static_cast(0)); + + handler.Shutdown(); + RemoveDbFiles(dbPath.str()); +} + +TEST(OfflineStorageHandlerFlushTests, FlushOptOutDropsInvalidRecordsInsteadOfWedging) +{ + NullLogManager logManager; + NiceMock config; + NoopTaskDispatcher dispatcher; + NiceMock observer; + + ON_CALL(config, GetOfflineStorageMaximumSizeBytes()).WillByDefault(Return(32 * 4096)); + ON_CALL(config, GetMaximumRetryCount()).WillByDefault(Return(5)); + + std::ostringstream dbPath; + dbPath << GetTempDirectory() << "FlushOptOutDropInvalid-" << PAL::getUtcSystemTimeMs() << ".db"; + RemoveDbFiles(dbPath.str()); + config[CFG_STR_CACHE_FILE_PATH] = dbPath.str(); + config[CFG_INT_RAM_QUEUE_SIZE] = 1024 * 1024; + config[CFG_BOOL_ENABLE_BATCHED_STORAGE_FLUSH] = false; + + OfflineStorageHandler handler(logManager, config, dispatcher); + handler.Initialize(observer); + + const size_t kCount = 3; + for (size_t i = 0; i < kCount; i++) + { + StorageRecord r("bad-opt-out-id-" + std::to_string(i), "tenant-token", + EventLatency_Normal, EventPersistence_Normal, /*timestamp*/ 0, + std::vector{ 'x' }); + handler.StoreRecord(r); + } + EXPECT_EQ(handler.GetRecordCount(), kCount); + + handler.Flush(); + + EXPECT_EQ(handler.GetRecordCount(), static_cast(0)); + + handler.Shutdown(); + RemoveDbFiles(dbPath.str()); +} diff --git a/tests/unittests/OfflineStorageTests_SQLite.cpp b/tests/unittests/OfflineStorageTests_SQLite.cpp index 015e197d7..1d9ca9a7a 100644 --- a/tests/unittests/OfflineStorageTests_SQLite.cpp +++ b/tests/unittests/OfflineStorageTests_SQLite.cpp @@ -9,9 +9,14 @@ #include "common/MockIOfflineStorageObserver.hpp" #include "common/MockIRuntimeConfig.hpp" #include "utils/Utils.hpp" +#include "sqlite3.h" +#include "offline/ISqlite3Proxy.hpp" #include "offline/OfflineStorage_SQLite.hpp" +#include +#include #include #include +#include #if !defined(_WIN32) #include #endif @@ -39,9 +44,130 @@ class OfflineStorage_SQLiteNoAutoCommit : public OfflineStorage_SQLite return m_instanceCount; } + static bool OwnsTempDirectory() + { + std::lock_guard lock(m_initAndShutdownLock); + return m_ownsTempDirectory; + } + + static void SetOwnsTempDirectory(bool owns) + { + std::lock_guard lock(m_initAndShutdownLock); + m_ownsTempDirectory = owns; + } + virtual void scheduleAutoCommitTransaction() { } + + size_t DbSizeEstimate() const + { + return m_DbSizeEstimate.load(); + } +}; + +class FaultInjectingSqlite3Proxy : public ISqlite3Proxy +{ + public: + explicit FaultInjectingSqlite3Proxy(ISqlite3Proxy& delegate) + : m_delegate(delegate) + { + } + + bool failCachedStatementPrepare = false; + bool failNextInsertStep = false; + bool failNextShutdown = false; + + int sqlite3_bind_blob(sqlite3_stmt* stmt, int idx, void const* value, int size, void (* d)(void*)) override { return m_delegate.sqlite3_bind_blob(stmt, idx, value, size, d); } + int sqlite3_bind_int(sqlite3_stmt* stmt, int idx, int value) override { return m_delegate.sqlite3_bind_int(stmt, idx, value); } + int sqlite3_bind_int64(sqlite3_stmt* stmt, int idx, int64_t value) override { return m_delegate.sqlite3_bind_int64(stmt, idx, value); } + int sqlite3_bind_text(sqlite3_stmt* stmt, int idx, char const* value, int size, void (* d)(void*)) override { return m_delegate.sqlite3_bind_text(stmt, idx, value, size, d); } + int sqlite3_changes(sqlite3* db) override { return m_delegate.sqlite3_changes(db); } + int sqlite3_clear_bindings(sqlite3_stmt* stmt) override { return m_delegate.sqlite3_clear_bindings(stmt); } + int sqlite3_close(sqlite3* db) override { return m_delegate.sqlite3_close(db); } + int sqlite3_close_v2(sqlite3* db) override { return m_delegate.sqlite3_close_v2(db); } + void const* sqlite3_column_blob(sqlite3_stmt* stmt, int iCol) override { return m_delegate.sqlite3_column_blob(stmt, iCol); } + int sqlite3_column_bytes(sqlite3_stmt* stmt, int iCol) override { return m_delegate.sqlite3_column_bytes(stmt, iCol); } + int sqlite3_column_int(sqlite3_stmt* stmt, int iCol) override { return m_delegate.sqlite3_column_int(stmt, iCol); } + int64_t sqlite3_column_int64(sqlite3_stmt* stmt, int iCol) override { return m_delegate.sqlite3_column_int64(stmt, iCol); } + unsigned char const* sqlite3_column_text(sqlite3_stmt* stmt, int iCol) override { return m_delegate.sqlite3_column_text(stmt, iCol); } + int sqlite3_create_function_v2(sqlite3* db, char const* zFunctionName, int nArg, int eTextRep, void* pApp, + void (* xFunc)(sqlite3_context*, int, sqlite3_value**), void (* xStep)(sqlite3_context*, int, sqlite3_value**), + void (* xFinal)(sqlite3_context*), void (* xDestroy)(void*)) override + { + return m_delegate.sqlite3_create_function_v2(db, zFunctionName, nArg, eTextRep, pApp, xFunc, xStep, xFinal, xDestroy); + } + char const* sqlite3_errmsg(sqlite3* db) override { return m_delegate.sqlite3_errmsg(db); } + int sqlite3_extended_result_codes(sqlite3* db, int on) override { return m_delegate.sqlite3_extended_result_codes(db, on); } + int sqlite3_finalize(sqlite3_stmt* stmt) override { return m_delegate.sqlite3_finalize(stmt); } + void* sqlite3_get_auxdata(sqlite3_context* ctx, int N) override { return m_delegate.sqlite3_get_auxdata(ctx, N); } + int sqlite3_initialize() override { return m_delegate.sqlite3_initialize(); } + int sqlite3_open_v2(char const* file, sqlite3** pdb, int flags, char const* zvfs) override { return m_delegate.sqlite3_open_v2(file, pdb, flags, zvfs); } + int sqlite3_prepare_v2(sqlite3* db, char const* zsql, int size, sqlite3_stmt** pstmt, char const** pztail) override + { + if (failCachedStatementPrepare && std::string(zsql) == "PRAGMA page_count") + { + failCachedStatementPrepare = false; + *pstmt = nullptr; + return SQLITE_ERROR; + } + + int result = m_delegate.sqlite3_prepare_v2(db, zsql, size, pstmt, pztail); + if (result == SQLITE_OK && std::string(zsql).find("REPLACE INTO events") != std::string::npos) + { + m_insertStatement = *pstmt; + } + return result; + } + int sqlite3_reset(sqlite3_stmt* stmt) override { return m_delegate.sqlite3_reset(stmt); } + void sqlite3_result_null(sqlite3_context* ctx) override { m_delegate.sqlite3_result_null(ctx); } + void sqlite3_result_text(sqlite3_context* ctx, char const* value, int size, void (* d)(void*)) override { m_delegate.sqlite3_result_text(ctx, value, size, d); } + void sqlite3_set_auxdata(sqlite3_context* ctx, int N, void* data, void (* d)(void*)) override { m_delegate.sqlite3_set_auxdata(ctx, N, data, d); } + int sqlite3_shutdown() override + { + if (failNextShutdown) + { + failNextShutdown = false; + return SQLITE_BUSY; + } + return m_delegate.sqlite3_shutdown(); + } + int sqlite3_step(sqlite3_stmt* stmt) override + { + if (failNextInsertStep && stmt == m_insertStatement) + { + failNextInsertStep = false; + return SQLITE_IOERR; + } + return m_delegate.sqlite3_step(stmt); + } + int64_t sqlite3_soft_heap_limit64(int64_t N) override { return m_delegate.sqlite3_soft_heap_limit64(N); } + void const* sqlite3_value_blob(sqlite3_value* value) override { return m_delegate.sqlite3_value_blob(value); } + int sqlite3_value_bytes(sqlite3_value* value) override { return m_delegate.sqlite3_value_bytes(value); } + sqlite3_vfs* sqlite3_vfs_find(char const* zVfsName) override { return m_delegate.sqlite3_vfs_find(zVfsName); } + void sqlite3_wal_checkpoint(sqlite3* db) override { m_delegate.sqlite3_wal_checkpoint(db); } + + private: + ISqlite3Proxy& m_delegate; + sqlite3_stmt* m_insertStatement = nullptr; +}; + +class Sqlite3ProxySwap +{ + public: + explicit Sqlite3ProxySwap(ISqlite3Proxy& replacement) + : m_original(g_sqlite3Proxy) + { + g_sqlite3Proxy = &replacement; + } + + ~Sqlite3ProxySwap() + { + g_sqlite3Proxy = m_original; + } + + private: + ISqlite3Proxy* m_original; }; @@ -107,7 +233,6 @@ struct OfflineStorageTests_SQLite : public Test } }; - class TestRecordConsumer { public: operator std::function() @@ -132,6 +257,77 @@ TEST_F(OfflineStorageTests_SQLite, InitializeAndShutdownCreateFileThatCanBeDelet initializeStorage(); } +TEST_F(OfflineStorageTests_SQLite, CachedStatementPrepareFailureRecreatesDatabase) +{ + EXPECT_CALL(configMock, GetOfflineStorageMaximumSizeBytes()).WillRepeatedly(Return(UINT_MAX)); + storageInitialized = true; + offlineStorage.reset(new OfflineStorage_SQLiteNoAutoCommit(*logManager, configMock)); + + FaultInjectingSqlite3Proxy proxy(*g_sqlite3Proxy); + proxy.failCachedStatementPrepare = true; + Sqlite3ProxySwap swap(proxy); + + EXPECT_CALL(observerMock, OnStorageFailed("1")); + EXPECT_CALL(observerMock, OnStorageOpened("SQLite/Clean")); + offlineStorage->Initialize(observerMock); + + EXPECT_THAT(offlineStorage->GetSize(), Gt(size_t{0})); +} + +TEST_F(OfflineStorageTests_SQLite, ConcurrentAccessAndShutdownAreSerialized) +{ + initializeStorage(); + EXPECT_CALL(observerMock, OnStorageOpenFailed("Database is not open")) + .Times(AnyNumber()); + EXPECT_CALL(observerMock, OnStorageFailed("Database is not open")) + .Times(AnyNumber()); + + std::atomic start{ false }; + std::atomic writerProgress{ 0 }; + std::atomic readerProgress{ 0 }; + + std::thread writer([&]() { + while (!start.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + for (unsigned i = 0; i < 200; ++i) + { + offlineStorage->StoreRecord({ + "concurrent-" + std::to_string(i), + "token", + EventLatency_Normal, + EventPersistence_Normal, + static_cast(i + 1), + {} }); + writerProgress.store(i + 1, std::memory_order_release); + } + }); + + std::thread reader([&]() { + while (!start.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + for (unsigned i = 0; i < 200; ++i) + { + (void)offlineStorage->GetRecords(false, EventLatency_Off, 1); + readerProgress.store(i + 1, std::memory_order_release); + } + }); + + start.store(true, std::memory_order_release); + while (writerProgress.load(std::memory_order_acquire) == 0 || + readerProgress.load(std::memory_order_acquire) == 0) + { + std::this_thread::yield(); + } + + offlineStorage->Shutdown(); + writer.join(); + reader.join(); +} + TEST_F(OfflineStorageTests_SQLite, StorageRecordConstructorSetsAllFields) { initializeStorage(); @@ -145,6 +341,31 @@ TEST_F(OfflineStorageTests_SQLite, StorageRecordConstructorSetsAllFields) EXPECT_THAT(record.reservedUntil, INT64_MAX - 1); } +TEST_F(OfflineStorageTests_SQLite, FailedInsertDoesNotPersistOrIncreaseSizeEstimate) +{ + FaultInjectingSqlite3Proxy proxy(*g_sqlite3Proxy); + Sqlite3ProxySwap swap(proxy); + initializeStorage(); + + StorageRecord const failedRecord{ "failed", "token", EventLatency_Normal, EventPersistence_Normal, 1, { 1, 2, 3 } }; + StorageRecord const storedRecord{ "stored", "token", EventLatency_Normal, EventPersistence_Normal, 2, { 4, 5, 6, 7 } }; + size_t const initialSizeEstimate = offlineStorage->DbSizeEstimate(); + + proxy.failNextInsertStep = true; + EXPECT_CALL(observerMock, OnStorageFailed("Database write failed")); + EXPECT_THAT(offlineStorage->StoreRecord(failedRecord), false); + EXPECT_THAT(offlineStorage->GetRecordCount(EventLatency_Unspecified), 0); + EXPECT_THAT(offlineStorage->DbSizeEstimate(), initialSizeEstimate); + + ASSERT_THAT(offlineStorage->StoreRecord(storedRecord), true); + EXPECT_THAT(offlineStorage->DbSizeEstimate(), initialSizeEstimate + storedRecord.id.size() + storedRecord.tenantToken.size() + storedRecord.blob.size()); + + TestRecordConsumer consumer; + ASSERT_THAT(offlineStorage->GetAndReserveRecords(consumer, 100000), true); + ASSERT_THAT(consumer.records.size(), 1); + EXPECT_THAT(consumer.records[0].id, storedRecord.id); +} + TEST_F(OfflineStorageTests_SQLite, GetAndReservedReturnsStoredRecord) { initializeStorage(); @@ -162,6 +383,84 @@ TEST_F(OfflineStorageTests_SQLite, GetAndReservedReturnsStoredRecord) EXPECT_THAT(consumer.records[0].reservedUntil, 0); } +TEST_F(OfflineStorageTests_SQLite, MalformedPersistedLatencyFallsBackToNormal) +{ + initializeStorage(); + offlineStorage->Execute( + "INSERT INTO events " + "(record_id,tenant_token,latency,persistence,timestamp,payload) " + "VALUES ('malformed-latency','token',987,1,1,X'010203')"); + + auto records = offlineStorage->GetRecords(false, EventLatency_Off); + ASSERT_THAT(records.size(), 1); + EXPECT_THAT(records[0].id, "malformed-latency"); + EXPECT_THAT(records[0].latency, EventLatency_Normal); + EXPECT_THAT(records[0].blob, StorageBlob({ 1, 2, 3 })); + + TestRecordConsumer consumer; + EXPECT_THAT( + offlineStorage->GetAndReserveRecords( + consumer, 100000, EventLatency_Off), + true); + ASSERT_THAT(consumer.records.size(), 1); + EXPECT_THAT(consumer.records[0].id, "malformed-latency"); + EXPECT_THAT(consumer.records[0].latency, EventLatency_Normal); + EXPECT_THAT(consumer.records[0].blob, StorageBlob({ 1, 2, 3 })); +} + +TEST_F(OfflineStorageTests_SQLite, StoreRecordsBatchStoresAllRecords) +{ + initializeStorage(); + std::vector batch; + const size_t kCount = 8; + for (size_t i = 0; i < kCount; i++) + { + batch.push_back({ "g" + std::to_string(i), "token", EventLatency_Normal, + EventPersistence_Normal, static_cast(i + 1), { static_cast(i) } }); + } + + // Every record in the batch is stored and individually retrievable. (The + // single-transaction batching is a performance optimization verified by + // benchmarking; this test covers the batch's storage correctness.) + EXPECT_THAT(offlineStorage->StoreRecords(batch), kCount); + + TestRecordConsumer consumer; + EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 100000), true); + ASSERT_THAT(consumer.records.size(), kCount); + for (size_t i = 0; i < kCount; i++) + { + std::string expectedId = "g" + std::to_string(i); + bool found = false; + for (auto const& r : consumer.records) + { + if (r.id == expectedId) { found = true; break; } + } + EXPECT_TRUE(found) << "record " << expectedId << " was not retrieved"; + } +} + +TEST_F(OfflineStorageTests_SQLite, StoreRecordsBatchDropsInvalidAndStoresValid) +{ + initializeStorage(); + std::vector batch = { + { "g1", "token", EventLatency_Normal, EventPersistence_Normal, 1, { 1 } }, // valid + { "g2", "token", EventLatency_Normal, EventPersistence_Normal, 0, { 2 } }, // invalid: timestamp <= 0 + }; + + // The invalid record is reported once during validation. + EXPECT_CALL(observerMock, OnStorageFailed("Invalid parameters")); + + // A permanently-invalid record is dropped (reported once) and the valid + // remainder is still stored. One bad record can never wedge the batch or, via + // a caller that re-queues on a short return (e.g. Flush), block the queue. + EXPECT_THAT(offlineStorage->StoreRecords(batch), static_cast(1)); + + TestRecordConsumer consumer; + EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 100000), true); + ASSERT_THAT(consumer.records.size(), static_cast(1)); + EXPECT_THAT(consumer.records[0].id, "g1"); +} + TEST_F(OfflineStorageTests_SQLite, ReservedRecordIsNotReturned) { initializeStorage(); @@ -566,9 +865,12 @@ TEST_F(OfflineStorageTests_SQLite, StoreThousandEventsTakesLessThanASecond) initializeStorage(); auto startTimeMs = PAL::getMonotonicTimeMs(); + std::vector records; + records.reserve(1000); for (int i = 0; i < 1000; ++i) { - EXPECT_THAT(offlineStorage->StoreRecord({std::to_string(i), "token", EventLatency_Normal, EventPersistence_Normal, 1, {}}), true); + records.push_back({std::to_string(i), "token", EventLatency_Normal, EventPersistence_Normal, 1, {}}); } + EXPECT_THAT(offlineStorage->StoreRecords(records), 1000u); TestRecordConsumer consumer; EXPECT_THAT(offlineStorage->GetAndReserveRecords(consumer, 10000, EventLatency_Normal, 1000), true); @@ -697,8 +999,7 @@ StorageRecord GOOD_RECORDS[] = { StorageRecord BAD_RECORDS[] = { { "", "tenant-token", EventLatency_Normal, EventPersistence_Normal, 2, { 1, 2, 3 } }, { "guid", "", EventLatency_Normal, EventPersistence_Normal, 2, { 1, 2, 3 } }, - { "guid", "tenant-token", EventLatency_Unspecified,EventPersistence_Normal, 0, {} }, - { "guid", "tenant-token", static_cast(987),EventPersistence_Normal, 0, {} }, + { "guid", "tenant-token", EventLatency_Unspecified, EventPersistence_Normal, 1, {} }, { "guid", "tenant-token", EventLatency_Normal, EventPersistence_Normal, -1, {} } }; @@ -804,6 +1105,30 @@ TEST_F(OfflineStorageTests_SQLite, ExceededStorageSizeCausesDbToDropOldestEvents ASSERT_THAT(consumer.records.size(), 0); } +TEST_F(OfflineStorageTests_SQLite, ResizeDbCompactsThePhysicalDatabase) +{ + constexpr size_t maximumSize = 5 * 1024 * 1024; + EXPECT_CALL(configMock, GetOfflineStorageMaximumSizeBytes()) + .WillRepeatedly(Return(maximumSize)); + configMock[CFG_BOOL_ENABLE_DB_DROP_IF_FULL] = true; + initializeStorage(false); + + std::vector records; + for (int i = 0; i < 12; ++i) + { + records.push_back({ + "record-" + std::to_string(i), + "token", + EventLatency_Normal, + EventPersistence_Normal, + i + 1, + StorageBlob(1024 * 1024) }); + } + + ASSERT_THAT(offlineStorage->StoreRecords(records), records.size()); + EXPECT_LE(offlineStorage->GetSize(), maximumSize); +} + TEST_F(OfflineStorageTests_SQLite, TrimmingAlwaysDropsAtLeastOneEvent) { EXPECT_CALL(configMock, GetOfflineStorageMaximumSizeBytes()) @@ -848,6 +1173,49 @@ TEST_F(OfflineStorageTests_SQLite, SqliteDbInstancesAreCounted) EXPECT_EQ(offlineStorage->GetDbInstanceCount(), 0); } +TEST_F(OfflineStorageTests_SQLite, DestructionWithoutShutdownClosesDatabase) +{ + initializeStorage(); + EXPECT_EQ(OfflineStorage_SQLiteNoAutoCommit::GetDbInstanceCount(), 1); + + storageInitialized = false; + offlineStorage.reset(); + + EXPECT_EQ(OfflineStorage_SQLiteNoAutoCommit::GetDbInstanceCount(), 0); + EXPECT_THAT(fileExists(storageFilename), true); + ::remove(storageFilename.c_str()); + for (const char* suffix : { "-wal", "-shm", "-journal" }) + { + ::remove((storageFilename + suffix).c_str()); + } +} + +TEST_F(OfflineStorageTests_SQLite, FailedShutdownRetainsOwnedTempDirectoryUntilRetry) +{ + ASSERT_EQ(nullptr, sqlite3_temp_directory); + sqlite3_temp_directory = sqlite3_mprintf("%s", MAT::GetAppLocalTempDirectory().c_str()); + ASSERT_NE(nullptr, sqlite3_temp_directory); + char* const ownedTempDirectory = sqlite3_temp_directory; + OfflineStorage_SQLiteNoAutoCommit::SetOwnsTempDirectory(true); + + FaultInjectingSqlite3Proxy proxy(*g_sqlite3Proxy); + Sqlite3ProxySwap proxySwap(proxy); + initializeStorage(); + proxy.failNextShutdown = true; + + shutdownAndRemoveFile(); + + EXPECT_EQ(0, OfflineStorage_SQLiteNoAutoCommit::GetDbInstanceCount()); + EXPECT_TRUE(OfflineStorage_SQLiteNoAutoCommit::OwnsTempDirectory()); + EXPECT_EQ(ownedTempDirectory, sqlite3_temp_directory); + + initializeStorage(); + shutdownAndRemoveFile(); + + EXPECT_FALSE(OfflineStorage_SQLiteNoAutoCommit::OwnsTempDirectory()); + EXPECT_EQ(nullptr, sqlite3_temp_directory); +} + #if !defined(_WIN32) // SECURITY: the offline cache buffers pending telemetry/audit events, so it must // not be world-readable. SQLite creates the file 0644 by default; SQLiteWrapper diff --git a/tests/unittests/PalTests.cpp b/tests/unittests/PalTests.cpp index c931ff376..77429a582 100644 --- a/tests/unittests/PalTests.cpp +++ b/tests/unittests/PalTests.cpp @@ -10,10 +10,18 @@ #include "Version.hpp" #include +#include +#include #include +#include +#include +#include +#include +#include #include #include #include +#include #ifdef HAVE_MAT_LOGGING #include "pal/PAL.hpp" @@ -225,6 +233,146 @@ namespace void ThrowNonStdException() { throw 123; } void Signal(std::atomic* ran) { ran->store(true); } }; + + class DroppingTaskDispatcher final : public ITaskDispatcher + { + public: + void Join() override {} + void Queue(Task* task) override { delete task; } + + bool Cancel(Task*, uint64_t = 0) override + { + cancelCalled = true; + return false; + } + + bool cancelCalled = false; + }; + + class ScheduledTaskTarget + { + public: + explicit ScheduledTaskTarget(std::atomic& callbackRan) : + m_callbackRan(callbackRan) + { + } + + void Callback() + { + m_callbackRan.store(true); + } + + private: + std::atomic& m_callbackRan; + }; + + class BlockingScheduledTaskTarget + { + public: + void Callback() + { + std::unique_lock lock(m_mutex); + m_entered = true; + m_condition.notify_all(); + m_condition.wait(lock, [this]() { return m_released; }); + } + + bool WaitUntilEntered() + { + std::unique_lock lock(m_mutex); + return m_condition.wait_for( + lock, std::chrono::seconds(2), [this]() { return m_entered; }); + } + + void Release() + { + { + std::lock_guard lock(m_mutex); + m_released = true; + } + m_condition.notify_all(); + } + + private: + std::mutex m_mutex; + std::condition_variable m_condition; + bool m_entered {false}; + bool m_released {false}; + }; + + class ReentrantQueueScheduledTaskTarget + { + public: + explicit ReentrantQueueScheduledTaskTarget(ITaskDispatcher* dispatcher) : + m_dispatcher(dispatcher) + { + } + + void Callback() + { + { + std::unique_lock lock(m_mutex); + m_entered = true; + m_condition.notify_all(); + m_condition.wait(lock, [this]() { return m_queueAllowed; }); + } + + PAL::dispatchTask( + m_dispatcher, this, &ReentrantQueueScheduledTaskTarget::FollowUp); + + { + std::lock_guard lock(m_mutex); + m_queueReturned = true; + } + m_condition.notify_all(); + } + + void FollowUp() + { + std::lock_guard lock(m_mutex); + m_followUpRan = true; + m_condition.notify_all(); + } + + bool WaitUntilEntered() + { + std::unique_lock lock(m_mutex); + return m_condition.wait_for( + lock, std::chrono::seconds(2), [this]() { return m_entered; }); + } + + void AllowQueue() + { + { + std::lock_guard lock(m_mutex); + m_queueAllowed = true; + } + m_condition.notify_all(); + } + + bool WaitUntilQueueReturned() + { + std::unique_lock lock(m_mutex); + return m_condition.wait_for( + lock, std::chrono::seconds(1), [this]() { return m_queueReturned; }); + } + + bool WaitUntilFollowUpRan() + { + std::unique_lock lock(m_mutex); + return m_condition.wait_for( + lock, std::chrono::seconds(2), [this]() { return m_followUpRan; }); + } + + private: + ITaskDispatcher* m_dispatcher; + std::mutex m_mutex; + std::condition_variable m_condition; + bool m_entered {false}; + bool m_queueAllowed {false}; + bool m_queueReturned {false}; + bool m_followUpRan {false}; + }; } // A task throwing an exception must be contained by the worker thread loop; @@ -253,6 +401,330 @@ TEST_F(PalTests, WorkerThreadContainsThrowingTask) dispatcher->Join(); } +TEST_F(PalTests, ScheduleTaskReturnsNoOpHandleWhenDispatcherDropsTask) +{ + DroppingTaskDispatcher dispatcher; + std::atomic callbackRan(false); + ScheduledTaskTarget target(callbackRan); + + auto handle = PAL::scheduleTask(&dispatcher, 0, &target, &ScheduledTaskTarget::Callback); + + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + EXPECT_FALSE(dispatcher.cancelCalled); + EXPECT_FALSE(callbackRan.load()); +} + +TEST_F(PalTests, ScheduleTaskHandleClearsAfterCallbackCompletes) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + std::atomic callbackRan(false); + ScheduledTaskTarget target(callbackRan); + auto handle = PAL::scheduleTask(dispatcher.get(), 0, &target, &ScheduledTaskTarget::Callback); + + for (int i = 0; i < 500 && (!callbackRan.load() || handle.GetTask() != nullptr); ++i) + { + PAL::sleep(10); + } + + EXPECT_TRUE(callbackRan.load()); + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + + dispatcher->Join(); +} + +TEST_F(PalTests, ScheduleTaskCancelSerializesTaskDestruction) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + std::atomic callbackRan(false); + ScheduledTaskTarget target(callbackRan); + auto handle = PAL::scheduleTask( + dispatcher.get(), 60000, &target, &ScheduledTaskTarget::Callback); + + ASSERT_NE(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_FALSE(callbackRan.load()); + + dispatcher->Join(); +} + +TEST_F(PalTests, ScheduleTaskCancelWaitDoesNotDeadlockTaskDestruction) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + BlockingScheduledTaskTarget target; + auto handle = PAL::scheduleTask( + dispatcher.get(), 0, &target, &BlockingScheduledTaskTarget::Callback); + + if (!target.WaitUntilEntered()) + { + target.Release(); + handle.Cancel(2000); + dispatcher->Join(); + FAIL() << "scheduled task did not start"; + } + + std::atomic cancelReturned(false); + bool cancelResult = false; + std::thread canceller([&]() { + cancelResult = handle.Cancel(2000); + cancelReturned.store(true); + }); + + PAL::sleep(50); + target.Release(); + for (int i = 0; i < 50 && !cancelReturned.load(); ++i) + { + PAL::sleep(10); + } + + EXPECT_TRUE(cancelReturned.load()); + canceller.join(); + EXPECT_TRUE(cancelResult); + for (int i = 0; i < 50 && handle.GetTask() != nullptr; ++i) + { + PAL::sleep(10); + } + EXPECT_EQ(handle.GetTask(), nullptr); + + dispatcher->Join(); +} + +TEST_F(PalTests, ScheduleTaskCancelWaitAllowsRunningTaskToQueue) +{ + constexpr uint64_t CancelWaitMs = 3000; + auto dispatcher = PAL::WorkerThreadFactory::Create(); + ReentrantQueueScheduledTaskTarget target(dispatcher.get()); + auto handle = PAL::scheduleTask( + dispatcher.get(), 0, &target, &ReentrantQueueScheduledTaskTarget::Callback); + + if (!target.WaitUntilEntered()) + { + target.AllowQueue(); + handle.Cancel(CancelWaitMs); + dispatcher->Join(); + FAIL() << "scheduled task did not start"; + } + + std::promise cancelStarted; + std::future cancelStartedFuture = cancelStarted.get_future(); + std::promise cancelFinished; + std::future cancelFinishedFuture = cancelFinished.get_future(); + bool cancelResult = false; + std::thread canceller([&]() { + cancelStarted.set_value(); + cancelResult = handle.Cancel(CancelWaitMs); + cancelFinished.set_value(); + }); + + EXPECT_EQ(cancelStartedFuture.wait_for(std::chrono::seconds(2)), std::future_status::ready); + EXPECT_EQ( + cancelFinishedFuture.wait_for(std::chrono::milliseconds(100)), + std::future_status::timeout); + + target.AllowQueue(); + + EXPECT_TRUE(target.WaitUntilQueueReturned()); + EXPECT_EQ( + cancelFinishedFuture.wait_for(std::chrono::seconds(1)), + std::future_status::ready); + + canceller.join(); + EXPECT_TRUE(cancelResult); + EXPECT_TRUE(target.WaitUntilFollowUpRan()); + + dispatcher->Join(); +} + +namespace +{ + class WorkerThreadScheduleTarget + { + public: + void Callback() {} + }; + + class BlockingCancellationTarget + { + public: + void Block() + { + std::unique_lock lock(m_lock); + m_entered = true; + m_stateChanged.notify_all(); + m_stateChanged.wait(lock, [this]() { return m_release; }); + } + + void Signal() + { + std::lock_guard lock(m_lock); + m_successorRan = true; + m_stateChanged.notify_all(); + } + + bool WaitUntilEntered() + { + std::unique_lock lock(m_lock); + return m_stateChanged.wait_for( + lock, std::chrono::seconds{5}, [this]() { return m_entered; }); + } + + bool WaitUntilSuccessorRan() + { + std::unique_lock lock(m_lock); + return m_stateChanged.wait_for( + lock, std::chrono::seconds{5}, [this]() { return m_successorRan; }); + } + + void Release() + { + std::lock_guard lock(m_lock); + m_release = true; + m_stateChanged.notify_all(); + } + + private: + std::mutex m_lock; + std::condition_variable m_stateChanged; + bool m_entered = false; + bool m_release = false; + bool m_successorRan = false; + }; + + class SelfDisposeHelper + { + public: + std::function releaseLastRef; + std::atomic* done = nullptr; + + void Run() + { + releaseLastRef(); + done->store(true); + } + }; +} + +TEST_F(PalTests, ScheduleTaskAfterWorkerThreadJoinReturnsNoOpHandle) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + dispatcher->Join(); + WorkerThreadScheduleTarget target; + + auto handle = PAL::scheduleTask( + dispatcher.get(), 100, &target, &WorkerThreadScheduleTarget::Callback); + + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); +} + +TEST_F(PalTests, ScheduleTaskHandleClearsAfterWorkerThreadCallbackCompletes) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + std::atomic callbackRan(false); + + class WorkerThreadCompletionTarget + { + public: + explicit WorkerThreadCompletionTarget(std::atomic& callbackRan) : + m_callbackRan(callbackRan) + { + } + + void Callback() { m_callbackRan.store(true); } + + private: + std::atomic& m_callbackRan; + } target(callbackRan); + + auto handle = PAL::scheduleTask( + dispatcher.get(), 0, &target, &WorkerThreadCompletionTarget::Callback); + + for (int i = 0; i < 500 && !callbackRan.load(); ++i) + { + PAL::sleep(10); + } + + ASSERT_TRUE(callbackRan.load()); + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + + dispatcher->Join(); +} + +TEST_F(PalTests, CancellingRunningTaskDoesNotDropSuccessor) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + constexpr int Iterations = 400; + + for (int iteration = 0; iteration < Iterations; ++iteration) + { + BlockingCancellationTarget target; + auto running = PAL::scheduleTask( + dispatcher.get(), 0, &target, &BlockingCancellationTarget::Block); + + if (!target.WaitUntilEntered()) + { + target.Release(); + dispatcher->Join(); + FAIL() << "Worker did not start the blocking task"; + return; + } + + auto successor = PAL::scheduleTask( + dispatcher.get(), 0, &target, &BlockingCancellationTarget::Signal); + std::promise cancelStarted; + auto cancelStartedFuture = cancelStarted.get_future(); + bool cancelResult = false; + std::thread cancelThread([&]() { + cancelStarted.set_value(); + cancelResult = running.Cancel(std::numeric_limits::max()); + }); + + cancelStartedFuture.wait(); + for (int i = 0; i < 100; ++i) + { + std::this_thread::yield(); + } + target.Release(); + cancelThread.join(); + + EXPECT_TRUE(cancelResult); + if (!target.WaitUntilSuccessorRan()) + { + dispatcher->Join(); + FAIL() << "Cancellation dropped the successor task at iteration " << iteration; + return; + } + (void)successor; + } + + dispatcher->Join(); +} + +TEST_F(PalTests, WorkerThreadSelfDisposeOnOwnThreadIsSafe) +{ + auto dispatcher = PAL::WorkerThreadFactory::Create(); + auto* raw = dispatcher.get(); + auto box = std::make_shared(std::move(dispatcher)); + + std::atomic done(false); + SelfDisposeHelper helper; + helper.releaseLastRef = [box]() { box->reset(); }; + helper.done = &done; + + PAL::dispatchTask(raw, &helper, &SelfDisposeHelper::Run); + + for (int i = 0; i < 500 && !done.load(); ++i) + { + PAL::sleep(10); + } + ASSERT_TRUE(done.load()); + + PAL::sleep(200); +} + #ifdef HAVE_MAT_LOGGING class LogInitTest : public Test { diff --git a/tests/unittests/TaskDispatcherCAPITests.cpp b/tests/unittests/TaskDispatcherCAPITests.cpp index b227deb13..0a7a7814c 100644 --- a/tests/unittests/TaskDispatcherCAPITests.cpp +++ b/tests/unittests/TaskDispatcherCAPITests.cpp @@ -9,7 +9,11 @@ #include "pal/typename.hpp" #include "mat.h" +#include +#include +#include #include +#include using namespace testing; using namespace MAT; @@ -229,6 +233,156 @@ TEST(TaskDispatcherCAPITests, Join) EXPECT_EQ(wasJoined, true); } +namespace +{ + // Dispatcher that always drops (and deletes) the task, modeling the + // shutdown-drop path where Queue() cannot report failure. + class DroppingTaskDispatcher : public ITaskDispatcher + { + public: + bool cancelCalled = false; + void Join() override {} + void Queue(MAT::Task* task) override { delete task; } + bool Cancel(MAT::Task* /*task*/, uint64_t /*waitTime*/ = 0) override + { + cancelCalled = true; + return false; + } + }; + + struct NoopCallbackTarget + { + void Callback(int, int) {} + }; + + struct BlockingCallbackTarget + { + std::atomic entered{false}; + std::atomic release{false}; + + void Callback(int, int) + { + entered.store(true, std::memory_order_release); + while (!release.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + } + }; +} + +// When the dispatcher drops the task (for example during shutdown), scheduleTask +// must return a no-op handle rather than one pointing at the freed task, so the +// caller never holds a dangling pointer and Cancel() is a safe no-op. +TEST(TaskDispatcherCAPITests, ScheduleTaskReturnsNoOpHandleWhenTaskDropped) +{ + DroppingTaskDispatcher dispatcher; + NoopCallbackTarget target; + + auto handle = scheduleTask(&dispatcher, 100 /*delayMs*/, &target, &NoopCallbackTarget::Callback, 1, 2); + + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + EXPECT_FALSE(dispatcher.cancelCalled); +} + +namespace +{ + struct DeferredExecutionState + { + std::string taskId; + task_callback_fn_t callback = nullptr; + std::atomic cancelCalled{false}; + }; + + static std::unique_ptr s_deferredExecutionState; + + void EVTSDK_LIBABI_CDECL OnDeferredTaskDispatcherQueue(evt_task_t* task, task_callback_fn_t callback) + { + s_deferredExecutionState->taskId = task->id; + s_deferredExecutionState->callback = callback; + } + + bool EVTSDK_LIBABI_CDECL OnDeferredTaskDispatcherCancel(const char* taskId) + { + s_deferredExecutionState->cancelCalled.store(true, std::memory_order_release); + return (s_deferredExecutionState->taskId == taskId); + } + + void EVTSDK_LIBABI_CDECL OnDeferredTaskDispatcherJoin() + {} +} + +TEST(TaskDispatcherCAPITests, ScheduleTaskHandleClearsAfterAsyncCallbackCompletes) +{ + TaskDispatcher_CAPI taskDispatcher(&OnDeferredTaskDispatcherQueue, &OnDeferredTaskDispatcherCancel, &OnDeferredTaskDispatcherJoin); + s_deferredExecutionState.reset(new DeferredExecutionState()); + + NoopCallbackTarget target; + auto handle = scheduleTask(&taskDispatcher, 100 /*delayMs*/, &target, &NoopCallbackTarget::Callback, 1, 2); + + ASSERT_NE(handle.GetTask(), nullptr); + ASSERT_NE(s_deferredExecutionState->callback, nullptr); + + s_deferredExecutionState->callback(s_deferredExecutionState->taskId.c_str()); + + EXPECT_EQ(handle.GetTask(), nullptr); + EXPECT_TRUE(handle.Cancel()); + EXPECT_FALSE(s_deferredExecutionState->cancelCalled); + + s_deferredExecutionState.reset(); +} + +TEST(TaskDispatcherCAPITests, CancelWaitsForCallbackAlreadyInProgress) +{ + TaskDispatcher_CAPI taskDispatcher(&OnDeferredTaskDispatcherQueue, &OnDeferredTaskDispatcherCancel, &OnDeferredTaskDispatcherJoin); + s_deferredExecutionState.reset(new DeferredExecutionState()); + + BlockingCallbackTarget target; + auto handle = scheduleTask(&taskDispatcher, 100 /*delayMs*/, &target, &BlockingCallbackTarget::Callback, 1, 2); + ASSERT_NE(s_deferredExecutionState->callback, nullptr); + + std::thread callbackThread([&]() { + s_deferredExecutionState->callback(s_deferredExecutionState->taskId.c_str()); + }); + + while (!target.entered.load(std::memory_order_acquire)) + { + std::this_thread::yield(); + } + + std::atomic cancelReturned{false}; + bool cancelResult = false; + std::thread cancelThread([&]() { + cancelResult = handle.Cancel(std::numeric_limits::max()); + cancelReturned.store(true, std::memory_order_release); + }); + + bool cancelWasWaiting = false; + for (int i = 0; i < 1000; ++i) + { + if (cancelReturned.load(std::memory_order_acquire)) + { + break; + } + if (s_deferredExecutionState->cancelCalled.load(std::memory_order_acquire)) + { + cancelWasWaiting = true; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + target.release.store(true, std::memory_order_release); + callbackThread.join(); + cancelThread.join(); + + EXPECT_TRUE(cancelWasWaiting); + EXPECT_TRUE(cancelResult); + EXPECT_EQ(handle.GetTask(), nullptr); + s_deferredExecutionState.reset(); +} + TEST(TaskDispatcherCAPITests, ExecuteCallbackThatThrowsIsContained) { TaskDispatcher_CAPI taskDispatcher(&OnTaskDispatcherQueue, &OnTaskDispatcherCancel, &OnTaskDispatcherJoin); @@ -247,4 +401,3 @@ TEST(TaskDispatcherCAPITests, ExecuteCallbackThatThrowsIsContained) EXPECT_NO_THROW(dispatchTask(&taskDispatcher, testHelper.get(), &TestHelper::Callback, 10 /*param1*/, 20 /*param2*/)); EXPECT_EQ(wasExecuted, true); } - diff --git a/tests/unittests/TransmissionPolicyManagerTests.cpp b/tests/unittests/TransmissionPolicyManagerTests.cpp index c2ce2c3ae..0e868db9e 100644 --- a/tests/unittests/TransmissionPolicyManagerTests.cpp +++ b/tests/unittests/TransmissionPolicyManagerTests.cpp @@ -2,23 +2,47 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 // -// -// TODO: re-enable TPM testcases for backoff configuration change -// #include "common/Common.hpp" #include "common/MockIRuntimeConfig.hpp" #include "common/MockIBandwidthController.hpp" +#include "common/MockITelemetrySystem.hpp" #include "tpm/TransmissionPolicyManager.hpp" #include "TransmitProfiles.hpp" +#include +#include +#include +#include + using namespace testing; using namespace MAT; +class TransmissionPolicyManagerTestSystem : public testing::MockITelemetrySystem +{ +public: + explicit TransmissionPolicyManagerTestSystem(IRuntimeConfig& config) + : m_config(config) + { + } + + IRuntimeConfig& getConfig() override + { + return m_config; + } + +private: + IRuntimeConfig& m_config; +}; class TransmissionPolicyManager4Test : public TransmissionPolicyManager { public: + TransmissionPolicyManager4Test(ITelemetrySystem& system, ITaskDispatcher& taskDispatcher, IBandwidthController* bandwidthController) + : TransmissionPolicyManager(system, taskDispatcher, bandwidthController) + { + } + TransmissionPolicyManager4Test(ITelemetrySystem& system, IBandwidthController* bandwidthController) - : TransmissionPolicyManager(system, *PAL::getDefaultTaskDispatcher(), bandwidthController) + : TransmissionPolicyManager4Test(system, *PAL::getDefaultTaskDispatcher(), bandwidthController) { } @@ -32,6 +56,11 @@ class TransmissionPolicyManager4Test : public TransmissionPolicyManager { TransmissionPolicyManager::scheduleUpload(delay, latency, force); } + bool handleStopParent() + { + return TransmissionPolicyManager::handleStop(); + } + using TransmissionPolicyManager::increaseBackoff; using TransmissionPolicyManager::addUpload; using TransmissionPolicyManager::removeUpload; @@ -69,10 +98,174 @@ class TransmissionPolicyManager4Test : public TransmissionPolicyManager { } }; +class BlockingCancelTaskDispatcher : public ITaskDispatcher +{ +public: + ~BlockingCancelTaskDispatcher() override + { + Join(); + } + + void Join() override + { + std::lock_guard lock(m_tasksMutex); + for (auto* task : m_tasks) + { + delete task; + } + m_tasks.clear(); + } + + void Queue(Task* task) override + { + std::lock_guard lock(m_tasksMutex); + m_tasks.push_back(task); + } + + bool Cancel(Task* task, uint64_t waitTime = 0) override + { + { + std::lock_guard lock(m_tasksMutex); + auto it = std::find(m_tasks.begin(), m_tasks.end(), task); + if (it == m_tasks.end()) + { + return false; + } + delete *it; + m_tasks.erase(it); + } + + { + std::lock_guard lock(m_cancelMutex); + m_waitTime = waitTime; + m_cancelEntered = true; + } + m_cancelEnteredCv.notify_all(); + + std::unique_lock lock(m_cancelMutex); + m_cancelReleasedCv.wait(lock, [this]() { return m_cancelReleased; }); + return true; + } + + bool WaitForCancel(const std::chrono::milliseconds timeout) + { + std::unique_lock lock(m_cancelMutex); + return m_cancelEnteredCv.wait_for(lock, timeout, [this]() { return m_cancelEntered; }); + } + + void ReleaseCancel() + { + { + std::lock_guard lock(m_cancelMutex); + m_cancelReleased = true; + } + m_cancelReleasedCv.notify_all(); + } + + uint64_t WaitTime() + { + std::lock_guard lock(m_cancelMutex); + return m_waitTime; + } + +private: + std::mutex m_tasksMutex; + std::vector m_tasks; + + std::mutex m_cancelMutex; + std::condition_variable m_cancelEnteredCv; + std::condition_variable m_cancelReleasedCv; + uint64_t m_waitTime = 0; + bool m_cancelEntered = false; + bool m_cancelReleased = false; +}; + +class RunningTaskDispatcher : public ITaskDispatcher +{ +public: + ~RunningTaskDispatcher() override + { + std::lock_guard lock(m_tasksMutex); + for (auto* task : m_tasks) + { + delete task; + } + m_tasks.clear(); + } + + void Join() override + { + std::lock_guard lock(m_tasksMutex); + for (auto* task : m_tasks) + { + delete task; + } + m_tasks.clear(); + } + + void Queue(Task* task) override + { + std::lock_guard lock(m_tasksMutex); + m_tasks.push_back(task); + } + + bool Cancel(Task* task, uint64_t waitTime = 0) override + { + UNREFERENCED_PARAMETER(task); + UNREFERENCED_PARAMETER(waitTime); + // Simulate a task that is currently executing on the worker: + // cancellation can never proceed without waiting for the run + // to complete, so a no-wait cancel must return false. + std::lock_guard lock(m_tasksMutex); + m_cancelCount++; + return false; + } + + size_t QueuedCount() const + { + std::lock_guard lock(m_tasksMutex); + return m_tasks.size(); + } + + size_t CancelCount() const + { + std::lock_guard lock(m_tasksMutex); + return m_cancelCount; + } + + void RunQueuedTasks() + { + std::vector tasks; + { + std::lock_guard lock(m_tasksMutex); + tasks.swap(m_tasks); + } + for (auto* task : tasks) + { + (*task)(); + delete task; + } + } + +private: + mutable std::mutex m_tasksMutex; + std::vector m_tasks; + size_t m_cancelCount = 0; +}; + +class DroppingTaskDispatcher : public ITaskDispatcher +{ +public: + void Join() override {} + void Queue(Task* task) override { delete task; } + bool Cancel(Task*, uint64_t = 0) override { return false; } +}; + class TransmissionPolicyManagerTests : public StrictMock { protected: StrictMock runtimeConfigMock; StrictMock bandwidthControllerMock; + TransmissionPolicyManagerTestSystem system; TransmissionPolicyManager4Test tpm; RouteSink initiateUpload{this, &TransmissionPolicyManagerTests::resultInitiateUpload}; @@ -80,7 +273,8 @@ class TransmissionPolicyManagerTests : public StrictMock { protected: TransmissionPolicyManagerTests() - : tpm(testing::getSystem(), &bandwidthControllerMock) + : system(runtimeConfigMock) + , tpm(system, &bandwidthControllerMock) { tpm.initiateUpload >> initiateUpload; tpm.allUploadsFinished >> allUploadsFinished; @@ -95,23 +289,23 @@ class TransmissionPolicyManagerTests : public StrictMock { .WillRepeatedly(Return(1000000)); EXPECT_CALL(runtimeConfigMock, GetMinimumUploadBandwidthBps()) .WillRepeatedly(Return(1000000)); + EXPECT_CALL(runtimeConfigMock, GetUploadRetryBackoffConfig()) + .WillRepeatedly(Return(DefaultBackoffConfig)); ON_CALL(tpm, uploadAsync(_)). WillByDefault(Invoke(&tpm, &TransmissionPolicyManager4Test::uploadAsyncParent)); } }; -#if 0 -TEST_F(TransmissionPolicyManagerTests, StartSchedulesUploadImmediately) +TEST_F(TransmissionPolicyManagerTests, StartSchedulesUploadAfterInitialDelay) { tpm.uploadScheduled(false); tpm.paused(false); - EXPECT_CALL(tpm, scheduleUpload(0, EventLatency_Normal,false)).WillOnce(Return()); + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false)).WillOnce(Return()); EXPECT_THAT(tpm.start(), true); // EXPECT_CALL(tpm, uploadAsync(EventLatency_Normal)).WillOnce(Return()); EXPECT_THAT(tpm.paused(), false); } -#endif TEST_F(TransmissionPolicyManagerTests, StopLeavesNoScheduledUploads) { @@ -149,12 +343,23 @@ TEST_F(TransmissionPolicyManagerTests, StopLeavesNoScheduledUploads) EXPECT_THAT(tpm.activeUploads(), SizeIs(0)); } +TEST_F(TransmissionPolicyManagerTests, DuplicateTerminalNotificationIsIgnored) +{ + tpm.paused(true); + auto ctx = tpm.fakeActiveUpload(); + + tpm.eventsUploadAborted(ctx); + tpm.eventsUploadAborted(ctx); + + EXPECT_THAT(tpm.activeUploads(), IsEmpty()); +} + TEST_F(TransmissionPolicyManagerTests, IncomingEventDoesNothingWhenPaused) { tpm.paused(true); - auto event = new IncomingEventContext(); - tpm.eventArrived(event); + IncomingEventContext event; + tpm.eventArrived(&event); } TEST_F(TransmissionPolicyManagerTests, IncomingEventSchedulesUpload) @@ -174,13 +379,13 @@ TEST_F(TransmissionPolicyManagerTests, IncomingEventSchedulesUpload) EXPECT_TRUE(TransmitProfiles::load(customProfile)); EXPECT_TRUE(TransmitProfiles::setProfile("Fred")); - auto event = new IncomingEventContext(); - event->record.latency = EventLatency_Normal; + IncomingEventContext event; + event.record.latency = EventLatency_Normal; EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds { 1000 }, EventLatency_Normal, true)) .WillOnce(Return()); - tpm.eventArrived(event); + tpm.eventArrived(&event); } TEST_F(TransmissionPolicyManagerTests, ProfileAffectsSchedule) @@ -200,10 +405,10 @@ TEST_F(TransmissionPolicyManagerTests, ProfileAffectsSchedule) EXPECT_TRUE(TransmitProfiles::load(customProfile)); EXPECT_TRUE(TransmitProfiles::setProfile("Fred")); - auto event = new IncomingEventContext(); - event->record.latency = EventLatency_Normal; + IncomingEventContext event; + event.record.latency = EventLatency_Normal; EXPECT_CALL(tpm, scheduleUpload(_, _, _)).Times(0); - tpm.eventArrived(event); + tpm.eventArrived(&event); TransmitProfiles::reset(); } @@ -224,10 +429,10 @@ TEST_F(TransmissionPolicyManagerTests, NoUploadForNegative) EXPECT_TRUE(TransmitProfiles::load(customProfile)); EXPECT_TRUE(TransmitProfiles::setProfile("Fred")); - auto event = new IncomingEventContext(); - event->record.latency = EventLatency_Normal; + IncomingEventContext event; + event.record.latency = EventLatency_Normal; EXPECT_CALL(tpm, scheduleUpload(_, _, _)).Times(0); - tpm.eventArrived(event); + tpm.eventArrived(&event); EXPECT_CALL(tpm, uploadAsync(_)).Times(0); tpm.scheduleUploadParent(std::chrono::milliseconds{-1000}, EventLatency_RealTime, true); TransmitProfiles::reset(); @@ -237,12 +442,12 @@ TEST_F(TransmissionPolicyManagerTests, ImmediateIncomingEventStartsUploadImmedia { tpm.paused(false); - auto event = new IncomingEventContext(); - event->record.latency = EventLatency_Max; + IncomingEventContext event; + event.record.latency = EventLatency_Max; EventsUploadContextPtr upload; EXPECT_CALL(*this, resultInitiateUpload(_)) .WillOnce(SaveArg<0>(&upload)); - tpm.eventArrived(event); + tpm.eventArrived(&event); ASSERT_THAT(upload, NotNull()); EXPECT_THAT(upload->requestedMinLatency, EventLatency_Max); @@ -264,7 +469,7 @@ TEST_F(TransmissionPolicyManagerTests, UploadDoesNothingWhenAlreadyActive) EXPECT_CALL( tpm, uploadAsync(_) ).Times(0); } -#if 0 +#ifdef ENABLE_BW_CONTROLLER TEST_F(TransmissionPolicyManagerTests, UploadPostponedWithInsufficientAvailableBandwidth) { tpm.uploadScheduled(true); @@ -272,7 +477,7 @@ TEST_F(TransmissionPolicyManagerTests, UploadPostponedWithInsufficientAvailableB EXPECT_CALL(bandwidthControllerMock, GetProposedBandwidthBps()) .WillOnce(Return(999999)); - EXPECT_CALL(tpm, scheduleUpload(1000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.uploadAsyncParent(EventLatency_Normal); @@ -330,7 +535,6 @@ TEST_F(TransmissionPolicyManagerTests, SuccessfulUploadSchedulesNextOneImmediate tpm.eventsUploadSuccessful(upload); } -#if 0 TEST_F(TransmissionPolicyManagerTests, RejectedUploadSchedulesNextOneWithLargerDelay) { EXPECT_CALL(runtimeConfigMock, GetUploadRetryBackoffConfig()) @@ -344,76 +548,70 @@ TEST_F(TransmissionPolicyManagerTests, RejectedUploadSchedulesNextOneWithLargerD tpm.eventsUploadRejected(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(6000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 6000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadRejected(upload); } -#endif -#if 0 TEST_F(TransmissionPolicyManagerTests, FailedUploadSchedulesNextOneWithLargerDelay) { EXPECT_CALL(runtimeConfigMock, GetUploadRetryBackoffConfig()) .WillRepeatedly(Return("E,3000,300000,2,0")); auto upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(3000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 3000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(6000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 6000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); } -#endif -#if 0 TEST_F(TransmissionPolicyManagerTests, SuccessfulUploadResetsBackoffDelay) { EXPECT_CALL(runtimeConfigMock, GetUploadRetryBackoffConfig()) .WillRepeatedly(Return("E,3000,300000,2,0")); auto upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(3000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 3000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadRejected(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(0, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 0 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadSuccessful(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(3000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 3000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadRejected(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(6000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 6000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(0, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 0 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadSuccessful(upload); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(3000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 3000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); } -#endif -#if 0 TEST_F(TransmissionPolicyManagerTests, InvalidUploadRetryBackoffConfigKeepsUsingThePreviousOne) { EXPECT_CALL(runtimeConfigMock, GetUploadRetryBackoffConfig()) .WillRepeatedly(Return("E,1000,300000,2,0")); auto upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(1000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); @@ -421,11 +619,10 @@ TEST_F(TransmissionPolicyManagerTests, InvalidUploadRetryBackoffConfigKeepsUsing .WillRepeatedly(Return("x,")); upload = tpm.fakeActiveUpload(); - EXPECT_CALL(tpm, scheduleUpload(2000, EventLatency_Normal, false)) + EXPECT_CALL(tpm, scheduleUpload(std::chrono::milliseconds{ 2000 }, EventLatency_Normal, false)) .WillOnce(Return()); tpm.eventsUploadFailed(upload); } -#endif TEST_F(TransmissionPolicyManagerTests, AbortedUploadDoesNotScheduleNextOne) { @@ -491,11 +688,11 @@ TEST_F(TransmissionPolicyManagerTests, FredProfile) EXPECT_TRUE(TransmitProfiles::setProfile("Fred_Profile")); tpm.paused(false); - auto event = new IncomingEventContext(); - event->record.latency = EventLatency_Normal; + IncomingEventContext event; + event.record.latency = EventLatency_Normal; EXPECT_CALL(tpm, scheduleUpload(_, _, _)) .Times(0); - tpm.eventArrived(event); + tpm.eventArrived(&event); } TEST_F(TransmissionPolicyManagerTests, Constructor_IsPaused_True) @@ -608,6 +805,124 @@ TEST_F(TransmissionPolicyManagerTests, cancelUploadTask_ScheduledUpload_IsUpload ASSERT_FALSE(tpm.m_isUploadScheduled); } +TEST_F(TransmissionPolicyManagerTests, cancelUploadTask_WaitForCompletionUsesFiniteDispatcherWait) +{ + BlockingCancelTaskDispatcher dispatcher; + TransmissionPolicyManager4Test blockingTpm(testing::getSystem(), dispatcher, &bandwidthControllerMock); + blockingTpm.paused(false); + blockingTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + + auto cancel = std::async(std::launch::async, [&blockingTpm]() { + return blockingTpm.cancelUploadTask(true); + }); + + if (!dispatcher.WaitForCancel(std::chrono::seconds{ 5 })) + { + dispatcher.ReleaseCancel(); + cancel.get(); + FAIL() << "Timed out waiting for cancel to block"; + } + + EXPECT_EQ(dispatcher.WaitTime(), static_cast(DefaultTaskCancelTime.count())); + dispatcher.ReleaseCancel(); + EXPECT_TRUE(cancel.get()); +} + +TEST_F(TransmissionPolicyManagerTests, StopInvalidatesTaskWhenDispatcherCannotCancel) +{ + RunningTaskDispatcher dispatcher; + TransmissionPolicyManager4Test runningTpm(testing::getSystem(), dispatcher, &bandwidthControllerMock); + runningTpm.paused(false); + runningTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + + EXPECT_TRUE(runningTpm.handleStopParent()); + + EXPECT_EQ(dispatcher.CancelCount(), 1u); + EXPECT_FALSE(runningTpm.m_isUploadScheduled); + EXPECT_EQ(runningTpm.m_scheduledUploadTime, std::numeric_limits::max()); + EXPECT_CALL(runningTpm, uploadAsync(_)).Times(0); + dispatcher.RunQueuedTasks(); +} + +TEST_F(TransmissionPolicyManagerTests, ForceScheduleRetainsImmediateUploadWhenCancelBlocks) +{ + BlockingCancelTaskDispatcher dispatcher; + TransmissionPolicyManager4Test blockingTpm(testing::getSystem(), dispatcher, &bandwidthControllerMock); + blockingTpm.paused(false); + + blockingTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + auto delayedUploadTime = blockingTpm.m_scheduledUploadTime; + + auto forceSchedule = std::async(std::launch::async, [&blockingTpm]() { + blockingTpm.scheduleUploadParent(std::chrono::milliseconds{}, EventLatency_RealTime, true); + }); + + if (!dispatcher.WaitForCancel(std::chrono::seconds{ 5 })) + { + dispatcher.ReleaseCancel(); + forceSchedule.get(); + FAIL() << "Timed out waiting for cancel to block"; + } + + auto delayedSchedule = std::async(std::launch::async, [&blockingTpm]() { + blockingTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + }); + + EXPECT_EQ(delayedSchedule.wait_for(std::chrono::milliseconds{ 100 }), std::future_status::timeout); + + dispatcher.ReleaseCancel(); + + forceSchedule.get(); + delayedSchedule.get(); + + ASSERT_TRUE(blockingTpm.m_isUploadScheduled); + EXPECT_LT(blockingTpm.m_scheduledUploadTime, delayedUploadTime); +} + +TEST_F(TransmissionPolicyManagerTests, ForceScheduleAppliesLatencyWhenRunningCancelFails) +{ + RunningTaskDispatcher dispatcher; + TransmissionPolicyManager4Test runningTpm(testing::getSystem(), dispatcher, &bandwidthControllerMock); + runningTpm.paused(false); + + // Queue an initial upload so m_scheduledUpload has a non-null task and + // m_isUploadScheduled is set; the dispatcher's Cancel will fail later + // (simulating the "task currently executing on worker" race). + runningTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + ASSERT_TRUE(runningTpm.m_isUploadScheduled); + ASSERT_EQ(dispatcher.QueuedCount(), 1u); + + auto scheduledTimeBefore = runningTpm.m_scheduledUploadTime; + // Reset m_runningLatency so we can observe the force path updating it + // (the initial schedule may have bumped it depending on the active + // profile's timers). + runningTpm.runningLatency(EventLatency_Normal); + + // Force a higher-priority schedule. The dispatcher's no-wait cancel + // returns false, so the previous task remains in flight. The fix in + // scheduleUpload must propagate the new latency to m_runningLatency + // so the running task picks it up under the same mutex. + runningTpm.scheduleUploadParent(std::chrono::milliseconds{}, EventLatency_RealTime, true); + + EXPECT_GE(dispatcher.CancelCount(), 1u); + EXPECT_EQ(dispatcher.QueuedCount(), 1u); + EXPECT_TRUE(runningTpm.m_isUploadScheduled); + EXPECT_EQ(runningTpm.m_runningLatency, EventLatency_RealTime); + EXPECT_EQ(runningTpm.m_scheduledUploadTime, scheduledTimeBefore); +} + +TEST_F(TransmissionPolicyManagerTests, DroppedScheduleDoesNotLatchUploadState) +{ + DroppingTaskDispatcher dispatcher; + TransmissionPolicyManager4Test droppingTpm(testing::getSystem(), dispatcher, &bandwidthControllerMock); + droppingTpm.paused(false); + + droppingTpm.scheduleUploadParent(std::chrono::milliseconds{ 1000 }, EventLatency_Normal, false); + + EXPECT_FALSE(droppingTpm.m_isUploadScheduled); + EXPECT_EQ(droppingTpm.m_scheduledUploadTime, std::numeric_limits::max()); +} + TEST_F(TransmissionPolicyManagerTests, increaseBackoff_EmptyBackoffObject_ReturnZero) { tpm.m_backoff = nullptr; diff --git a/tests/unittests/TransmitProfilesTests.cpp b/tests/unittests/TransmitProfilesTests.cpp index 58e9d36b5..ce8839de5 100644 --- a/tests/unittests/TransmitProfilesTests.cpp +++ b/tests/unittests/TransmitProfilesTests.cpp @@ -375,6 +375,24 @@ R"([{ ASSERT_TRUE(TransmitProfiles::load(badRule)); } +TEST_F(TransmitProfilesTests, load_Json_RuleWithLowBatteryPowerState_MapsToPowerSourceLowBattery) +{ + // A rule using the "low_battery" powerState must map to PowerSource_LowBattery + // rather than silently falling back to the default PowerSource_Any. + const std::string profile = +R"([{ + "name": "LowBatteryProfile", + "rules": [ + { "powerState": "low_battery", "timers": [ 8, 4, 2 ] } + ] +}])"; + + ASSERT_TRUE(TransmitProfiles::load(profile)); + const auto& rules = TransmitProfiles::profiles[std::string{"LowBatteryProfile"}].rules; + ASSERT_EQ(rules.size(), size_t{1}); + ASSERT_EQ(rules[0].powerState, PowerSource_LowBattery); +} + /* The following tests probably should not pass. But they do. diff --git a/tests/unittests/UnitTests.vcxproj b/tests/unittests/UnitTests.vcxproj index faf465e97..0a42fde2a 100644 --- a/tests/unittests/UnitTests.vcxproj +++ b/tests/unittests/UnitTests.vcxproj @@ -157,7 +157,7 @@ /machine:X86 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -206,7 +206,7 @@ /machine:X86 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -253,7 +253,7 @@ /machine:X64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -302,7 +302,7 @@ /machine:ARM64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -351,7 +351,7 @@ /machine:X64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -398,7 +398,7 @@ /machine:ARM64 %(AdditionalOptions) - crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;wininet.lib;iphlpapi.lib;%(AdditionalDependencies) + crypt32.lib;pdh.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;comdlg32.lib;advapi32.lib;version.lib;rpcrt4.lib;iphlpapi.lib;%(AdditionalDependencies) %(AdditionalLibraryDirectories) true %(IgnoreSpecificDefaultLibraries) @@ -411,6 +411,22 @@ true + + + HAVE_MAT_WININET_HTTP_CLIENT;%(PreprocessorDefinitions) + + + wininet.lib;%(AdditionalDependencies) + + + + + HAVE_MAT_WINHTTP_HTTP_CLIENT;%(PreprocessorDefinitions) + + + winhttp.lib;%(AdditionalDependencies) + + @@ -442,9 +458,11 @@ + + diff --git a/tests/unittests/UnitTests.vcxproj.filters b/tests/unittests/UnitTests.vcxproj.filters index 6a1476519..dca4405cf 100644 --- a/tests/unittests/UnitTests.vcxproj.filters +++ b/tests/unittests/UnitTests.vcxproj.filters @@ -26,9 +26,11 @@ + + diff --git a/tests/vcpkg/README.md b/tests/vcpkg/README.md index 3e758a394..d05012ce3 100644 --- a/tests/vcpkg/README.md +++ b/tests/vcpkg/README.md @@ -35,6 +35,12 @@ Best run from a **VS Developer Command Prompt** (ensures the same compiler versi .\tests\vcpkg\test-vcpkg-windows.ps1 -VcpkgRoot C:\path\to\vcpkg ``` +Use `-WinInet` to exercise the opt-in WinInet feature instead of the default +WinHTTP transport: +```powershell +.\tests\vcpkg\test-vcpkg-windows.ps1 -VcpkgRoot C:\path\to\vcpkg -WinInet +``` + > **Note:** Visual Studio's `vcvarsall.bat` overrides the `VCPKG_ROOT` environment variable. > Always pass `-VcpkgRoot` explicitly to point at your vcpkg installation. diff --git a/tests/vcpkg/test-vcpkg-windows.ps1 b/tests/vcpkg/test-vcpkg-windows.ps1 index 5073daa56..759834413 100644 --- a/tests/vcpkg/test-vcpkg-windows.ps1 +++ b/tests/vcpkg/test-vcpkg-windows.ps1 @@ -4,7 +4,8 @@ # .\tests\vcpkg\test-vcpkg-windows.ps1 -Triplet x64-windows param( [string]$VcpkgRoot = "", - [string]$Triplet = "" + [string]$Triplet = "", + [switch]$WinInet ) $ErrorActionPreference = "Stop" @@ -55,7 +56,8 @@ if ([string]::IsNullOrEmpty($Triplet)) { $Triplet = "x64-windows-static" } } -$BuildDir = Join-Path $ScriptDir "build-windows-$Triplet" +$Transport = if ($WinInet) { "WinInet" } else { "WinHTTP" } +$BuildDir = Join-Path $ScriptDir "build-windows-$Triplet-$($Transport.ToLowerInvariant())" # Map triplet to vcvarsall architecture $VcvarsArch = switch -Regex ($Triplet) { @@ -67,6 +69,7 @@ $VcvarsArch = switch -Regex ($Triplet) { Write-Host "Repository root: $RepoRoot" Write-Host "vcpkg root: $VcpkgRoot" Write-Host "Triplet: $Triplet" +Write-Host "HTTP transport: $Transport" # Clean previous build if (Test-Path $BuildDir) { @@ -84,6 +87,9 @@ $CmakeArgs = @( "-DVCPKG_OVERLAY_PORTS=$OverlayPorts", "-DCMAKE_BUILD_TYPE=Release" ) +if ($WinInet) { + $CmakeArgs += "-DVCPKG_MANIFEST_FEATURES=wininet" +} # Detect whether cl.exe is on PATH (i.e., running from VS Developer Command Prompt) $clExe = Get-Command cl.exe -ErrorAction SilentlyContinue diff --git a/tests/vcpkg/vcpkg.json b/tests/vcpkg/vcpkg.json index 1f1f4a536..dbcee9cfc 100644 --- a/tests/vcpkg/vcpkg.json +++ b/tests/vcpkg/vcpkg.json @@ -4,5 +4,19 @@ "description": "Integration test for cpp-client-telemetry vcpkg port", "dependencies": [ "cpp-client-telemetry" - ] + ], + "features": { + "wininet": { + "description": "Exercise the cpp-client-telemetry WinInet feature on Windows.", + "supports": "windows & !mingw", + "dependencies": [ + { + "name": "cpp-client-telemetry", + "features": [ + "wininet" + ] + } + ] + } + } } diff --git a/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index fdbbf5374..9565c33b6 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -134,6 +134,11 @@ if(MATSDK_ROOT_CMAKE MATCHES "MATSDK_MINIMAL_SQLITE" list(APPEND MATSDK_PINNED_SOURCE_OPTIONS -DMATSDK_MINIMAL_SQLITE=ON) endif() +set(MATSDK_USE_WININET OFF) +if("wininet" IN_LIST FEATURES) + set(MATSDK_USE_WININET ON) +endif() + vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" OPTIONS @@ -141,6 +146,7 @@ vcpkg_cmake_configure( -DMATSDK_SQLITE_PROVIDER=${MATSDK_VCPKG_SQLITE_PROVIDER} -DBUILD_SHARED_LIBS=${MATSDK_VCPKG_BUILD_SHARED_LIBS} -DMATSDK_ANDROID_HTTP_CLIENT=${MATSDK_ANDROID_HTTP_CLIENT} + -DMATSDK_USE_WININET=${MATSDK_USE_WININET} -DMATSDK_BUILD_HEADERS=ON -DMATSDK_BUILD_LIBRARY=ON -DMATSDK_BUILD_TEST_TOOL=OFF diff --git a/tools/ports/cpp-client-telemetry/vcpkg.json b/tools/ports/cpp-client-telemetry/vcpkg.json index d183bf6ca..14ab091c5 100644 --- a/tools/ports/cpp-client-telemetry/vcpkg.json +++ b/tools/ports/cpp-client-telemetry/vcpkg.json @@ -67,7 +67,7 @@ ] }, "curl-openssl": { - "description": "Built-in libcurl HTTP client with the OpenSSL TLS backend (default). Affects Linux only; Android uses the Java/JNI bridge unless an android-curl-* feature is selected, Windows uses WinInet, and Apple uses NSURLSession.", + "description": "Built-in libcurl HTTP client with the OpenSSL TLS backend (default). Affects Linux only; Android uses the Java/JNI bridge unless an android-curl-* feature is selected, Windows uses WinHTTP by default, and Apple uses NSURLSession.", "dependencies": [ { "name": "curl", @@ -91,6 +91,10 @@ "platform": "!osx & !ios" } ] + }, + "wininet": { + "description": "On Windows, explicitly use WinInet instead of the default WinHTTP transport for IE-integrated proxy or cookie behavior.", + "supports": "windows & !mingw" } } } diff --git a/tools/sdk-create.cmd b/tools/sdk-create.cmd index 30353c4c2..4b42fb17e 100644 --- a/tools/sdk-create.cmd +++ b/tools/sdk-create.cmd @@ -31,7 +31,7 @@ echo Windows 10 managed... call sku-create.cmd uap10 win10-cs echo Windows Desktop (win32) .NET 4.x... -call sku-create.cmd win32-net40-vs2015 net40 +call sku-create.cmd win32-net48-vs2015 net48 echo Windows Desktop (win32) .dll... call sku-create.cmd win32-dll-vs2015 win32-dll @@ -50,4 +50,3 @@ echo "Copy Changelog.md" if exist "%ROOT%\CHANGELOG.md" ( copy /Y %ROOT%\CHANGELOG.md %OUTDIR%\ ) -