Skip to content

Commit 723c84e

Browse files
authored
feat: replace libcurl with .NET HttpClient for sentry-native (#4222)
* poc: custom http transport for sentry-native to eliminate curl * Replace StringContent with self-made UnmanagedHttpContent to avoid copy * Update CONTRIBUTING.md * ci: no need to install libcurl4-openssl-dev anymore * mark UnmanagedHttpContent sealed * fix UnmanagedHttpContent.TryComputeLength return value * add diagnostic log message for native transport exceptions * await async exceptions * add unsafe CreateStream() helper * clear the pointer after dispose * apply discussed changes * throw ObjectDisposedException if already disposed * ObjectDisposedException.ThrowIf() is available in net7.0+
1 parent bb4c99f commit 723c84e

File tree

9 files changed

+172
-34
lines changed

9 files changed

+172
-34
lines changed

.github/workflows/build.yml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,6 @@ jobs:
5353
if: ${{ !matrix.container }}
5454
uses: ./.github/actions/freediskspace
5555

56-
- name: Install build dependencies
57-
if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Linux' && !matrix.container
58-
run: |
59-
sudo apt update
60-
sudo apt install libcurl4-openssl-dev
61-
6256
- run: scripts/build-sentry-native.ps1
6357
if: steps.cache.outputs.cache-hit != 'true'
6458
shell: pwsh
@@ -239,12 +233,6 @@ jobs:
239233
name: ${{ github.sha }}
240234
path: src
241235

242-
- name: Install build dependencies
243-
if: runner.os == 'Linux' && !matrix.container
244-
run: |
245-
sudo apt update
246-
sudo apt install libcurl4-openssl-dev
247-
248236
- name: Setup Environment
249237
uses: ./.github/actions/environment
250238

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Rename MemoryInfo.AllocatedBytes to MemoryInfo.TotalAllocatedBytes ([#4243](https://github.com/getsentry/sentry-dotnet/pull/4243))
8+
- Replace libcurl with .NET HttpClient for sentry-native ([#4222](https://github.com/getsentry/sentry-dotnet/pull/4222))
89

910
### Fixes
1011

CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ For big feature it's advised to raise an issue to discuss it first.
3232

3333
* [`pwsh`](https://github.com/PowerShell/PowerShell#get-powershell) Core version 6 or later on PATH.
3434

35-
* `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`).
35+
* `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`).
3636

3737
* On Windows:
3838
- [.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) 4.6.2 or higher.
3939
- `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.
4040
* On macOS/Linux
4141
- [Mono 6 or higher](https://www.mono-project.com/download/stable) to run the unit tests on the `net4x` targets.
42-
* On Linux
43-
- **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.
4442

4543
## .NET MAUI Requirements
4644

scripts/build-sentry-native.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ try
6969
-D SENTRY_SDK_NAME=sentry.native.dotnet `
7070
-D SENTRY_BUILD_SHARED_LIBS=0 `
7171
-D SENTRY_BACKEND=inproc `
72+
-D SENTRY_TRANSPORT=none `
7273
$additionalArgs
7374

7475
cmake `

src/Sentry/Http/HttpTransportBase.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -187,20 +187,8 @@ protected internal virtual HttpRequestMessage CreateRequest(Envelope envelope)
187187
throw new InvalidOperationException("The DSN is expected to be set at this point.");
188188
}
189189

190-
var dsn = Dsn.Parse(_options.Dsn);
191-
var authHeader =
192-
$"Sentry sentry_version={_options.SentryVersion}," +
193-
$"sentry_client={SdkVersion.Instance.Name}/{SdkVersion.Instance.Version}," +
194-
$"sentry_key={dsn.PublicKey}" +
195-
(dsn.SecretKey is { } secretKey ? $",sentry_secret={secretKey}" : null);
196-
197-
return new HttpRequestMessage
198-
{
199-
RequestUri = dsn.GetEnvelopeEndpointUri(),
200-
Method = HttpMethod.Post,
201-
Headers = { { "X-Sentry-Auth", authHeader } },
202-
Content = new EnvelopeHttpContent(envelope, _options.DiagnosticLogger, _clock)
203-
};
190+
var content = new EnvelopeHttpContent(envelope, _options.DiagnosticLogger, _clock);
191+
return _options.CreateHttpRequest(content);
204192
}
205193

206194
/// <summary>

src/Sentry/Platforms/Native/CFunctions.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ public static bool Init(SentryOptions options)
139139
}
140140
}
141141

142+
unsafe
143+
{
144+
var cTransport = sentry_transport_new(&nativeTransport);
145+
sentry_transport_set_state(cTransport, GCHandle.ToIntPtr(GCHandle.Alloc(options)));
146+
sentry_transport_set_free_func(cTransport, &nativeTransportFree);
147+
sentry_options_set_transport(cOptions, cTransport);
148+
}
149+
142150
options.DiagnosticLogger?.LogDebug("Initializing sentry native");
143151
return 0 == sentry_init(cOptions);
144152
}
@@ -364,6 +372,70 @@ internal struct sentry_value_t
364372
[DllImport("sentry-native")]
365373
private static extern void sentry_options_set_auto_session_tracking(IntPtr options, int debug);
366374

375+
[DllImport("sentry-native")]
376+
private static extern void sentry_options_set_transport(IntPtr options, IntPtr transport);
377+
378+
[DllImport("sentry-native")]
379+
private static extern unsafe IntPtr sentry_transport_new(delegate* unmanaged<IntPtr, IntPtr, void> sendFunc);
380+
381+
[DllImport("sentry-native")]
382+
private static extern void sentry_transport_set_state(IntPtr transport, IntPtr state);
383+
384+
[DllImport("sentry-native")]
385+
private static extern unsafe void sentry_transport_set_free_func(IntPtr transport, delegate* unmanaged<IntPtr, void> freeFunc);
386+
387+
[DllImport("sentry-native")]
388+
private static extern IntPtr sentry_envelope_serialize(IntPtr envelope, out UIntPtr sizeOut);
389+
390+
[DllImport("sentry-native")]
391+
private static extern void sentry_envelope_free(IntPtr envelope);
392+
393+
[DllImport("sentry-native")]
394+
internal static extern void sentry_free(IntPtr ptr);
395+
396+
[UnmanagedCallersOnly]
397+
private static void nativeTransport(IntPtr envelope, IntPtr state)
398+
{
399+
var options = GCHandle.FromIntPtr(state).Target as SentryOptions;
400+
try
401+
{
402+
if (options is not null)
403+
{
404+
var data = sentry_envelope_serialize(envelope, out var size);
405+
using var content = new UnmanagedHttpContent(data, (int)size, options.DiagnosticLogger);
406+
407+
using var client = options.GetHttpClient();
408+
using var request = options.CreateHttpRequest(content);
409+
#if NET5_0_OR_GREATER
410+
var response = client.Send(request);
411+
#else
412+
var response = client.SendAsync(request).GetAwaiter().GetResult();
413+
#endif
414+
response.EnsureSuccessStatusCode();
415+
}
416+
}
417+
catch (HttpRequestException e)
418+
{
419+
options?.DiagnosticLogger?.LogError(e, "Failed to send native envelope.");
420+
}
421+
catch (Exception e)
422+
{
423+
// never allow an exception back to native code - it would crash the app
424+
options?.DiagnosticLogger?.LogError(e, "Exception in native transport callback. The native envelope will not be sent.");
425+
}
426+
finally
427+
{
428+
sentry_envelope_free(envelope);
429+
}
430+
}
431+
432+
[UnmanagedCallersOnly]
433+
private static void nativeTransportFree(IntPtr state)
434+
{
435+
var handle = GCHandle.FromIntPtr(state);
436+
handle.Free();
437+
}
438+
367439
[DllImport("sentry-native")]
368440
private static extern unsafe void sentry_options_set_logger(IntPtr options, delegate* unmanaged/*[Cdecl]*/<int, IntPtr, IntPtr, IntPtr, void> logger, IntPtr userData);
369441

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Sentry.Extensibility;
2+
3+
namespace Sentry.Native;
4+
5+
internal sealed class UnmanagedHttpContent : SerializableHttpContent
6+
{
7+
private IntPtr _content;
8+
private readonly int _length = 0;
9+
private readonly IDiagnosticLogger? _logger;
10+
11+
public UnmanagedHttpContent(IntPtr content, int length, IDiagnosticLogger? logger)
12+
{
13+
_content = content;
14+
_length = length;
15+
_logger = logger;
16+
}
17+
18+
~UnmanagedHttpContent()
19+
{
20+
Dispose(false);
21+
}
22+
23+
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
24+
{
25+
ThrowIfObjectDisposed();
26+
try
27+
{
28+
using var unmanagedStream = CreateStream();
29+
await unmanagedStream.CopyToAsync(stream).ConfigureAwait(false);
30+
}
31+
catch (Exception e)
32+
{
33+
_logger?.LogError(e, "Failed to serialize unmanaged content into the network stream");
34+
throw;
35+
}
36+
}
37+
38+
protected override void SerializeToStream(Stream stream, TransportContext? context, CancellationToken cancellationToken)
39+
{
40+
ThrowIfObjectDisposed();
41+
try
42+
{
43+
using var unmanagedStream = CreateStream();
44+
unmanagedStream.CopyTo(stream);
45+
}
46+
catch (Exception e)
47+
{
48+
_logger?.LogError(e, "Failed to serialize unmanaged content into the network stream");
49+
throw;
50+
}
51+
}
52+
53+
protected override bool TryComputeLength(out long length)
54+
{
55+
length = _length;
56+
return true;
57+
}
58+
59+
protected override void Dispose(bool disposing)
60+
{
61+
IntPtr content = Interlocked.Exchange(ref _content, IntPtr.Zero);
62+
C.sentry_free(content);
63+
base.Dispose(disposing);
64+
}
65+
66+
private unsafe UnmanagedMemoryStream CreateStream()
67+
{
68+
return new UnmanagedMemoryStream((byte*)_content.ToPointer(), _length);
69+
}
70+
71+
private void ThrowIfObjectDisposed()
72+
{
73+
if (_content == IntPtr.Zero)
74+
{
75+
throw new ObjectDisposedException(GetType().FullName);
76+
}
77+
}
78+
}

src/Sentry/Platforms/Native/buildTransitive/Sentry.Native.targets

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636
<ItemGroup Condition="'$(FrameworkSupportsNative)' == 'true' and ('$(RuntimeIdentifier)' == 'linux-x64' or '$(RuntimeIdentifier)' == 'linux-arm64')">
3737
<DirectPInvoke Include="sentry-native" />
3838
<NativeLibrary Include="$(MSBuildThisFileDirectory)..\sentry-native\$(RuntimeIdentifier)\libsentry-native.a" />
39-
<!-- See: https://github.com/dotnet/runtime/issues/97414 -->
40-
<NativeSystemLibrary Include="curl" />
4139
</ItemGroup>
4240

4341
<ItemGroup Condition="'$(FrameworkSupportsNative)' == 'true' and '$(RuntimeIdentifier)' == 'linux-musl-x64'">
@@ -46,13 +44,10 @@
4644
<LinkerArg Include="-Wl,-Bstatic -Wl,--whole-archive -lunwind -Wl,--no-whole-archive -Wl,-Bdynamic" />
4745
<NativeSystemLibrary Include="lzma" />
4846
<NativeLibrary Include="$(MSBuildThisFileDirectory)..\sentry-native\linux-musl-x64\libsentry-native.a" />
49-
<!-- See: https://github.com/dotnet/runtime/issues/97414 -->
50-
<NativeSystemLibrary Include="curl" />
5147
</ItemGroup>
5248

5349
<ItemGroup Condition="'$(FrameworkSupportsNative)' == 'true' and ('$(RuntimeIdentifier)' == 'osx-x64' or '$(RuntimeIdentifier)' == 'osx-arm64')">
5450
<DirectPInvoke Include="sentry-native" />
5551
<NativeLibrary Include="$(MSBuildThisFileDirectory)..\sentry-native\osx\libsentry-native.a" />
56-
<NativeSystemLibrary Include="curl" />
5752
</ItemGroup>
5853
</Project>

src/Sentry/SentryOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,23 @@ internal HttpClient GetHttpClient()
245245
return factory.Create(this);
246246
}
247247

248+
internal HttpRequestMessage CreateHttpRequest(HttpContent content)
249+
{
250+
var authHeader =
251+
$"Sentry sentry_version={SentryVersion}," +
252+
$"sentry_client={SdkVersion.Instance.Name}/{SdkVersion.Instance.Version}," +
253+
$"sentry_key={ParsedDsn.PublicKey}" +
254+
(ParsedDsn.SecretKey is { } secretKey ? $",sentry_secret={secretKey}" : null);
255+
256+
return new HttpRequestMessage
257+
{
258+
RequestUri = ParsedDsn.GetEnvelopeEndpointUri(),
259+
Method = HttpMethod.Post,
260+
Headers = { { "X-Sentry-Auth", authHeader } },
261+
Content = content
262+
};
263+
}
264+
248265
/// <summary>
249266
/// Scope state processor.
250267
/// </summary>

0 commit comments

Comments
 (0)