diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bad323189..b01483ed0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,12 +53,6 @@ jobs: if: ${{ !matrix.container }} uses: ./.github/actions/freediskspace - - name: Install build dependencies - if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux' && !matrix.container - run: | - sudo apt update - sudo apt install libcurl4-openssl-dev - - run: scripts/build-sentry-native.ps1 if: steps.cache.outputs.cache-hit != 'true' shell: pwsh @@ -239,12 +233,6 @@ jobs: name: ${{ github.sha }} path: src - - name: Install build dependencies - if: runner.os == 'Linux' && !matrix.container - run: | - sudo apt update - sudo apt install libcurl4-openssl-dev - - name: Setup Environment uses: ./.github/actions/environment diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc6aa992e..4ea99c92b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Rename MemoryInfo.AllocatedBytes to MemoryInfo.TotalAllocatedBytes ([#4243](https://github.com/getsentry/sentry-dotnet/pull/4243)) +- Replace libcurl with .NET HttpClient for sentry-native ([#4222](https://github.com/getsentry/sentry-dotnet/pull/4222)) ## 5.9.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03e4d241a2..fe2c97b57e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,15 +32,13 @@ For big feature it's advised to raise an issue to discuss it first. * [`pwsh`](https://github.com/PowerShell/PowerShell#get-powershell) Core version 6 or later on PATH. -* `CMake` on PATH. On Windows you can install the [C++ CMake tools for Windows](https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170#installation). On macOS you can use your favourite package manager (e.g. `brew install cmake`). +* `CMake` on PATH. On Windows you can install the [C++ CMake tools for Windows](https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio?view=msvc-170#installation). On macOS and Linux you can use your favourite package manager (e.g. `brew install cmake` or `apt install cmake`). * On Windows: - [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) 4.6.2 or higher. - `Sentry.DiagnosticSource.IntegrationTests.csproj` uses [SQL LocalDb](https://docs.microsoft.com/sql/database-engine/configure-windows/sql-server-express-localdb) - [download SQL LocalDB 2019](https://download.microsoft.com/download/7/c/1/7c14e92e-bdcb-4f89-b7cf-93543e7112d1/SqlLocalDB.msi). To avoid running these tests, unload `Sentry.DiagnosticSource.IntegrationTests.csproj` from the solution. * On macOS/Linux - [Mono 6 or higher](https://www.mono-project.com/download/stable) to run the unit tests on the `net4x` targets. -* On Linux - - **curl** and **zlib** libraries (e.g. on Ubuntu: libcurl4-openssl-dev, libz-dev) to build the [sentry-native](https://github.com/getsentry/sentry-native/) module. ## .NET MAUI Requirements diff --git a/scripts/build-sentry-native.ps1 b/scripts/build-sentry-native.ps1 index a11e033b2c..8e7361901d 100644 --- a/scripts/build-sentry-native.ps1 +++ b/scripts/build-sentry-native.ps1 @@ -69,6 +69,7 @@ try -D SENTRY_SDK_NAME=sentry.native.dotnet ` -D SENTRY_BUILD_SHARED_LIBS=0 ` -D SENTRY_BACKEND=inproc ` + -D SENTRY_TRANSPORT=none ` $additionalArgs cmake ` diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs index 9379980163..846e889923 100644 --- a/src/Sentry/Http/HttpTransportBase.cs +++ b/src/Sentry/Http/HttpTransportBase.cs @@ -187,20 +187,8 @@ protected internal virtual HttpRequestMessage CreateRequest(Envelope envelope) throw new InvalidOperationException("The DSN is expected to be set at this point."); } - var dsn = Dsn.Parse(_options.Dsn); - var authHeader = - $"Sentry sentry_version={_options.SentryVersion}," + - $"sentry_client={SdkVersion.Instance.Name}/{SdkVersion.Instance.Version}," + - $"sentry_key={dsn.PublicKey}" + - (dsn.SecretKey is { } secretKey ? $",sentry_secret={secretKey}" : null); - - return new HttpRequestMessage - { - RequestUri = dsn.GetEnvelopeEndpointUri(), - Method = HttpMethod.Post, - Headers = { { "X-Sentry-Auth", authHeader } }, - Content = new EnvelopeHttpContent(envelope, _options.DiagnosticLogger, _clock) - }; + var content = new EnvelopeHttpContent(envelope, _options.DiagnosticLogger, _clock); + return _options.CreateHttpRequest(content); } /// diff --git a/src/Sentry/Platforms/Native/CFunctions.cs b/src/Sentry/Platforms/Native/CFunctions.cs index 0b5465bd6b..3d87e5d009 100644 --- a/src/Sentry/Platforms/Native/CFunctions.cs +++ b/src/Sentry/Platforms/Native/CFunctions.cs @@ -139,6 +139,14 @@ public static bool Init(SentryOptions options) } } + unsafe + { + var cTransport = sentry_transport_new(&nativeTransport); + sentry_transport_set_state(cTransport, GCHandle.ToIntPtr(GCHandle.Alloc(options))); + sentry_transport_set_free_func(cTransport, &nativeTransportFree); + sentry_options_set_transport(cOptions, cTransport); + } + options.DiagnosticLogger?.LogDebug("Initializing sentry native"); return 0 == sentry_init(cOptions); } @@ -364,6 +372,70 @@ internal struct sentry_value_t [DllImport("sentry-native")] private static extern void sentry_options_set_auto_session_tracking(IntPtr options, int debug); + [DllImport("sentry-native")] + private static extern void sentry_options_set_transport(IntPtr options, IntPtr transport); + + [DllImport("sentry-native")] + private static extern unsafe IntPtr sentry_transport_new(delegate* unmanaged sendFunc); + + [DllImport("sentry-native")] + private static extern void sentry_transport_set_state(IntPtr transport, IntPtr state); + + [DllImport("sentry-native")] + private static extern unsafe void sentry_transport_set_free_func(IntPtr transport, delegate* unmanaged freeFunc); + + [DllImport("sentry-native")] + private static extern IntPtr sentry_envelope_serialize(IntPtr envelope, out UIntPtr sizeOut); + + [DllImport("sentry-native")] + private static extern void sentry_envelope_free(IntPtr envelope); + + [DllImport("sentry-native")] + internal static extern void sentry_free(IntPtr ptr); + + [UnmanagedCallersOnly] + private static void nativeTransport(IntPtr envelope, IntPtr state) + { + var options = GCHandle.FromIntPtr(state).Target as SentryOptions; + try + { + if (options is not null) + { + var data = sentry_envelope_serialize(envelope, out var size); + using var content = new UnmanagedHttpContent(data, (int)size, options.DiagnosticLogger); + + using var client = options.GetHttpClient(); + using var request = options.CreateHttpRequest(content); +#if NET5_0_OR_GREATER + var response = client.Send(request); +#else + var response = client.SendAsync(request).GetAwaiter().GetResult(); +#endif + response.EnsureSuccessStatusCode(); + } + } + catch (HttpRequestException e) + { + options?.DiagnosticLogger?.LogError(e, "Failed to send native envelope."); + } + catch (Exception e) + { + // never allow an exception back to native code - it would crash the app + options?.DiagnosticLogger?.LogError(e, "Exception in native transport callback. The native envelope will not be sent."); + } + finally + { + sentry_envelope_free(envelope); + } + } + + [UnmanagedCallersOnly] + private static void nativeTransportFree(IntPtr state) + { + var handle = GCHandle.FromIntPtr(state); + handle.Free(); + } + [DllImport("sentry-native")] private static extern unsafe void sentry_options_set_logger(IntPtr options, delegate* unmanaged/*[Cdecl]*/ logger, IntPtr userData); diff --git a/src/Sentry/Platforms/Native/UnmanagedHttpContent.cs b/src/Sentry/Platforms/Native/UnmanagedHttpContent.cs new file mode 100644 index 0000000000..08c7ec325b --- /dev/null +++ b/src/Sentry/Platforms/Native/UnmanagedHttpContent.cs @@ -0,0 +1,78 @@ +using Sentry.Extensibility; + +namespace Sentry.Native; + +internal sealed class UnmanagedHttpContent : SerializableHttpContent +{ + private IntPtr _content; + private readonly int _length = 0; + private readonly IDiagnosticLogger? _logger; + + public UnmanagedHttpContent(IntPtr content, int length, IDiagnosticLogger? logger) + { + _content = content; + _length = length; + _logger = logger; + } + + ~UnmanagedHttpContent() + { + Dispose(false); + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + ThrowIfObjectDisposed(); + try + { + using var unmanagedStream = CreateStream(); + await unmanagedStream.CopyToAsync(stream).ConfigureAwait(false); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to serialize unmanaged content into the network stream"); + throw; + } + } + + protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + ThrowIfObjectDisposed(); + try + { + using var unmanagedStream = CreateStream(); + unmanagedStream.CopyTo(stream); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to serialize unmanaged content into the network stream"); + throw; + } + } + + protected override bool TryComputeLength(out long length) + { + length = _length; + return true; + } + + protected override void Dispose(bool disposing) + { + IntPtr content = Interlocked.Exchange(ref _content, IntPtr.Zero); + C.sentry_free(content); + base.Dispose(disposing); + } + + private unsafe UnmanagedMemoryStream CreateStream() + { + return new UnmanagedMemoryStream((byte*)_content.ToPointer(), _length); + } + + private void ThrowIfObjectDisposed() + { + if (_content == IntPtr.Zero) + { + throw new ObjectDisposedException(GetType().FullName); + } + } +} diff --git a/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets b/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets index ea1fd7584e..be2bf2bcd5 100644 --- a/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets +++ b/src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets @@ -36,8 +36,6 @@ - - @@ -46,13 +44,10 @@ - - - diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index f8d728e0d3..75760f7f63 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -245,6 +245,23 @@ internal HttpClient GetHttpClient() return factory.Create(this); } + internal HttpRequestMessage CreateHttpRequest(HttpContent content) + { + var authHeader = + $"Sentry sentry_version={SentryVersion}," + + $"sentry_client={SdkVersion.Instance.Name}/{SdkVersion.Instance.Version}," + + $"sentry_key={ParsedDsn.PublicKey}" + + (ParsedDsn.SecretKey is { } secretKey ? $",sentry_secret={secretKey}" : null); + + return new HttpRequestMessage + { + RequestUri = ParsedDsn.GetEnvelopeEndpointUri(), + Method = HttpMethod.Post, + Headers = { { "X-Sentry-Auth", authHeader } }, + Content = content + }; + } + /// /// Scope state processor. ///