• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-dotnet / 5716220196

31 Jul 2023 02:52PM UTC coverage: 82.478% (-0.3%) from 82.741%
5716220196

Pull #3261

github

aed2eb
fealebenpae
Merge remote-tracking branch 'origin/main' into yg/updated-marshaling

# Conflicts:
#	Realm/Realm/Handles/SharedRealmHandle.cs
Pull Request #3261: Use modern-er marshaling techniques

2029 of 2601 branches covered (78.01%)

Branch coverage included in aggregate %.

201 of 201 new or added lines in 21 files covered. (100.0%)

6246 of 7432 relevant lines covered (84.04%)

34048.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.72
/Realm/Realm/Handles/SessionHandle.cs
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2016 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
using System;
20
using System.Diagnostics;
21
using System.Linq;
22
using System.Runtime.InteropServices;
23
using System.Threading.Tasks;
24
using Realms.Exceptions;
25
using Realms.Exceptions.Sync;
26
using Realms.Logging;
27
using Realms.Native;
28
using Realms.Sync.ErrorHandling;
29
using Realms.Sync.Exceptions;
30
using Realms.Sync.Native;
31

32
namespace Realms.Sync
33
{
34
    internal class SessionHandle : RealmHandle
35
    {
36
        private static class NativeMethods
37
        {
38
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
39
            public delegate void SessionErrorCallback(IntPtr session_handle_ptr,
40
                                                      SyncError error,
41
                                                      IntPtr managed_sync_config_handle);
42

43
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
44
            public delegate void SessionProgressCallback(IntPtr progress_token_ptr, ulong transferred_bytes, ulong transferable_bytes);
45

46
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
47
            public delegate void SessionWaitCallback(IntPtr task_completion_source, int error_code, PrimitiveValue message);
48

49
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
50
            public delegate void SessionPropertyChangedCallback(IntPtr managed_session, NotifiableProperty property);
51

52
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
53
            public delegate IntPtr NotifyBeforeClientReset(IntPtr before_frozen, IntPtr managed_sync_config_handle);
54

55
            [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
56
            public delegate IntPtr NotifyAfterClientReset(IntPtr before_frozen, IntPtr after, IntPtr managed_sync_config_handle, bool did_recover);
57

58
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_install_callbacks", CallingConvention = CallingConvention.Cdecl)]
59
            public static extern void install_syncsession_callbacks(SessionErrorCallback error_callback,
60
                                                                    SessionProgressCallback progress_callback,
61
                                                                    SessionWaitCallback wait_callback,
62
                                                                    SessionPropertyChangedCallback property_changed_callback,
63
                                                                    NotifyBeforeClientReset notify_before,
64
                                                                    NotifyAfterClientReset notify_after);
65

66
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_get_user", CallingConvention = CallingConvention.Cdecl)]
67
            public static extern IntPtr get_user(SessionHandle session);
68

69
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_get_state", CallingConvention = CallingConvention.Cdecl)]
70
            public static extern SessionState get_state(SessionHandle session, out NativeException ex);
71

72
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_get_connection_state", CallingConvention = CallingConvention.Cdecl)]
73
            public static extern ConnectionState get_connection_state(SessionHandle session, out NativeException ex);
74

75
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_get_path", CallingConvention = CallingConvention.Cdecl)]
76
            public static extern IntPtr get_path(SessionHandle session, IntPtr buffer, IntPtr buffer_length, out NativeException ex);
77

78
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_get_raw_pointer", CallingConvention = CallingConvention.Cdecl)]
79
            public static extern IntPtr get_raw_pointer(SessionHandle session);
80

81
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_destroy", CallingConvention = CallingConvention.Cdecl)]
82
            public static extern void destroy(IntPtr handle);
83

84
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_register_progress_notifier", CallingConvention = CallingConvention.Cdecl)]
85
            public static extern ulong register_progress_notifier(SessionHandle session,
86
                                                                  IntPtr token_ptr,
87
                                                                  ProgressDirection direction,
88
                                                                  [MarshalAs(UnmanagedType.U1)] bool is_streaming,
89
                                                                  out NativeException ex);
90

91
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_unregister_progress_notifier", CallingConvention = CallingConvention.Cdecl)]
92
            public static extern void unregister_progress_notifier(SessionHandle session, ulong token, out NativeException ex);
93

94
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_register_property_changed_callback", CallingConvention = CallingConvention.Cdecl)]
95
            public static extern SessionNotificationToken register_property_changed_callback(IntPtr session, IntPtr managed_session_handle, out NativeException ex);
96

97
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_unregister_property_changed_callback", CallingConvention = CallingConvention.Cdecl)]
98
            public static extern void unregister_property_changed_callback(IntPtr session, SessionNotificationToken token, out NativeException ex);
99

100
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_wait", CallingConvention = CallingConvention.Cdecl)]
101
            public static extern void wait(SessionHandle session, IntPtr task_completion_source, ProgressDirection direction, out NativeException ex);
102

103
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_report_error_for_testing", CallingConvention = CallingConvention.Cdecl)]
104
            public static extern void report_error_for_testing(SessionHandle session, int error_code, SessionErrorCategory error_category, [MarshalAs(UnmanagedType.LPWStr)] string message, IntPtr message_len, [MarshalAs(UnmanagedType.U1)] bool is_fatal, int action);
105

106
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_stop", CallingConvention = CallingConvention.Cdecl)]
107
            public static extern void stop(SessionHandle session, out NativeException ex);
108

109
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_shutdown_and_wait", CallingConvention = CallingConvention.Cdecl)]
110
            public static extern void shutdown_and_wait(SessionHandle session, out NativeException ex);
111

112
            [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_syncsession_start", CallingConvention = CallingConvention.Cdecl)]
113
            public static extern void start(SessionHandle session, out NativeException ex);
114
        }
115

116
        private SessionNotificationToken? _notificationToken;
117

118
        public override bool ForceRootOwnership => true;
271✔
119

120
        [Preserve]
121
        public SessionHandle(SharedRealmHandle? root, IntPtr handle) : base(root, handle)
297✔
122
        {
123
        }
297✔
124

125
        public static void Initialize()
126
        {
127
            NativeMethods.SessionErrorCallback error = HandleSessionError;
1✔
128
            NativeMethods.SessionProgressCallback progress = HandleSessionProgress;
1✔
129
            NativeMethods.SessionWaitCallback wait = HandleSessionWaitCallback;
1✔
130
            NativeMethods.SessionPropertyChangedCallback propertyChanged = HandleSessionPropertyChangedCallback;
1✔
131
            NativeMethods.NotifyBeforeClientReset beforeReset = NotifyBeforeClientReset;
1✔
132
            NativeMethods.NotifyAfterClientReset afterReset = NotifyAfterClientReset;
1✔
133

134
            GCHandle.Alloc(error);
1✔
135
            GCHandle.Alloc(progress);
1✔
136
            GCHandle.Alloc(wait);
1✔
137
            GCHandle.Alloc(propertyChanged);
1✔
138
            GCHandle.Alloc(beforeReset);
1✔
139
            GCHandle.Alloc(afterReset);
1✔
140

141
            NativeMethods.install_syncsession_callbacks(error, progress, wait, propertyChanged, beforeReset, afterReset);
1✔
142
        }
1✔
143

144
        public SyncUserHandle GetUser()
145
        {
146
            var ptr = NativeMethods.get_user(this);
23✔
147
            if (ptr == IntPtr.Zero)
23!
148
            {
149
                throw new RealmException("Unable to obtain user for session. This likely means the session is being torn down.");
×
150
            }
151

152
            return new(ptr);
23✔
153
        }
154

155
        public SessionState GetState()
156
        {
157
            var state = NativeMethods.get_state(this, out var ex);
16✔
158
            ex.ThrowIfNecessary();
16✔
159
            return state;
16✔
160
        }
161

162
        public ConnectionState GetConnectionState()
163
        {
164
            var connectionState = NativeMethods.get_connection_state(this, out var ex);
12✔
165
            ex.ThrowIfNecessary();
12✔
166
            return connectionState;
12✔
167
        }
168

169
        public string GetPath()
170
        {
171
            return MarshalHelpers.GetString((IntPtr buffer, IntPtr length, out bool isNull, out NativeException ex) =>
×
172
            {
×
173
                isNull = false;
×
174
                return NativeMethods.get_path(this, buffer, length, out ex);
×
175
            })!;
×
176
        }
177

178
        public ulong RegisterProgressNotifier(GCHandle managedHandle, ProgressDirection direction, ProgressMode mode)
179
        {
180
            var isStreaming = mode == ProgressMode.ReportIndefinitely;
2✔
181
            var token = NativeMethods.register_progress_notifier(this, GCHandle.ToIntPtr(managedHandle), direction, isStreaming, out var ex);
2✔
182
            ex.ThrowIfNecessary();
2✔
183
            return token;
2✔
184
        }
185

186
        public void UnregisterProgressNotifier(ulong token)
187
        {
188
            NativeMethods.unregister_progress_notifier(this, token, out var ex);
2✔
189
            ex.ThrowIfNecessary();
2✔
190
        }
2✔
191

192
        public void SubscribeNotifications(Session session)
193
        {
194
            Debug.Assert(!_notificationToken.HasValue, $"{nameof(_notificationToken)} must be null before subscribing.");
195

196
            var managedSessionHandle = GCHandle.Alloc(session, GCHandleType.Weak);
7✔
197
            var sessionPointer = GCHandle.ToIntPtr(managedSessionHandle);
7✔
198
            _notificationToken = NativeMethods.register_property_changed_callback(handle, sessionPointer, out var ex);
7✔
199
            ex.ThrowIfNecessary();
7✔
200
        }
7✔
201

202
        public void UnsubscribeNotifications()
203
        {
204
            if (_notificationToken.HasValue)
302✔
205
            {
206
                NativeMethods.unregister_property_changed_callback(handle, _notificationToken.Value, out var ex);
7✔
207
                _notificationToken = null;
7✔
208
                ex.ThrowIfNecessary();
7✔
209
            }
210
        }
302✔
211

212
        public async Task WaitAsync(ProgressDirection direction)
213
        {
214
            var tcs = new TaskCompletionSource();
215✔
215
            var tcsHandle = GCHandle.Alloc(tcs);
215✔
216

217
            try
218
            {
219
                NativeMethods.wait(this, GCHandle.ToIntPtr(tcsHandle), direction, out var ex);
215✔
220
                ex.ThrowIfNecessary();
215✔
221

222
                await tcs.Task;
215✔
223
            }
215✔
224
            finally
225
            {
226
                tcsHandle.Free();
215✔
227
            }
228
        }
215✔
229

230
        public IntPtr GetRawPointer()
231
        {
232
            return NativeMethods.get_raw_pointer(this);
14✔
233
        }
234

235
        public void ReportErrorForTesting(int errorCode, SessionErrorCategory errorCategory, string errorMessage, bool isFatal, ServerRequestsAction action)
236
        {
237
            NativeMethods.report_error_for_testing(this, errorCode, errorCategory, errorMessage, (IntPtr)errorMessage.Length, isFatal, (int)action);
1✔
238
        }
1✔
239

240
        public void Stop()
241
        {
242
            NativeMethods.stop(this, out var ex);
72✔
243
            ex.ThrowIfNecessary();
72✔
244
        }
72✔
245

246
        public void Start()
247
        {
248
            NativeMethods.start(this, out var ex);
46✔
249
            ex.ThrowIfNecessary();
46✔
250
        }
46✔
251

252
        /// <summary>
253
        /// Terminates the sync session and releases the Realm file it was using.
254
        /// </summary>
255
        public void ShutdownAndWait()
256
        {
257
            NativeMethods.shutdown_and_wait(this, out var ex);
4✔
258
            ex.ThrowIfNecessary();
4✔
259
        }
4✔
260

261
        public override void Unbind()
262
        {
263
            UnsubscribeNotifications();
297✔
264
            NativeMethods.destroy(handle);
297✔
265
        }
297✔
266

267
        [MonoPInvokeCallback(typeof(NativeMethods.SessionErrorCallback))]
268
        private static void HandleSessionError(IntPtr sessionHandlePtr, SyncError error, IntPtr managedSyncConfigurationBaseHandle)
269
        {
270
            try
271
            {
272
                // Filter out end of input, which the client seems to have started reporting
273
                if (error.error_code == (ErrorCode)1)
26!
274
                {
275
                    return;
×
276
                }
277

278
                using var handle = new SessionHandle(null, sessionHandlePtr);
26✔
279
                var session = new Session(handle);
26✔
280
                string messageString = error.message!;
26✔
281
                var syncConfigHandle = GCHandle.FromIntPtr(managedSyncConfigurationBaseHandle);
26✔
282
                var syncConfig = (SyncConfigurationBase)syncConfigHandle.Target!;
26✔
283

284
                if (error.is_client_reset)
26✔
285
                {
286
                    var userInfo = error.user_info_pairs.ToEnumerable().ToDictionary(kvp => (string)kvp.Key!, kvp => (string?)kvp.Value);
100✔
287
                    var clientResetEx = new ClientResetException(session.User.App, messageString, error.error_code, userInfo);
20✔
288

289
                    syncConfig.ClientResetHandler.ManualClientReset?.Invoke(clientResetEx);
20!
290
                    return;
20✔
291
                }
292

293
                SessionException exception;
294
                if (error.error_code == ErrorCode.CompensatingWrite)
6✔
295
                {
296
                    var compensatingWrites = error.compensating_writes
3✔
297
                        .ToEnumerable()
3✔
298
                        .Select(c => new CompensatingWriteInfo(c.object_name!, c.reason!, new RealmValue(c.primary_key)))
3✔
299
                        .ToArray();
3✔
300
                    exception = new CompensatingWriteException(messageString, compensatingWrites);
3✔
301
                }
302
                else
303
                {
304
                    exception = new SessionException(messageString, error.error_code);
3✔
305
                }
306

307
                exception.HelpLink = error.log_url;
6✔
308
                syncConfig.OnSessionError?.Invoke(session, exception);
6✔
309
            }
6✔
310
            catch (Exception ex)
×
311
            {
312
                Logger.Default.Log(LogLevel.Warn, $"An error has occurred while handling a session error: {ex}");
×
313
            }
×
314
        }
26✔
315

316
        [MonoPInvokeCallback(typeof(NativeMethods.NotifyBeforeClientReset))]
317
        private static IntPtr NotifyBeforeClientReset(IntPtr beforeFrozen, IntPtr managedSyncConfigurationHandle)
318
        {
319
            SyncConfigurationBase? syncConfig = null;
39✔
320

321
            try
322
            {
323
                var syncConfigHandle = GCHandle.FromIntPtr(managedSyncConfigurationHandle);
39✔
324
                syncConfig = (SyncConfigurationBase)syncConfigHandle.Target!;
39✔
325

326
                var cb = syncConfig.ClientResetHandler switch
39!
327
                {
39✔
328
                    DiscardUnsyncedChangesHandler handler => handler.OnBeforeReset,
13✔
329
                    RecoverUnsyncedChangesHandler handler => handler.OnBeforeReset,
12✔
330
                    RecoverOrDiscardUnsyncedChangesHandler handler => handler.OnBeforeReset,
14✔
331
                    _ => throw new NotSupportedException($"ClientResetHandlerBase of type {syncConfig.ClientResetHandler.GetType()} is not handled yet")
×
332
                };
39✔
333

334
                if (cb != null)
39✔
335
                {
336
                    var schema = syncConfig.Schema;
30✔
337
                    using var realmBefore = new Realm(new UnownedRealmHandle(beforeFrozen), syncConfig, schema);
30✔
338
                    cb.Invoke(realmBefore);
30✔
339
                }
340

341
                return IntPtr.Zero;
27✔
342
            }
343
            catch (Exception ex)
12✔
344
            {
345
                var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name;
12!
346
                Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnBeforeReset during a client reset: {ex}");
12✔
347

348
                var exHandle = GCHandle.Alloc(ex);
12✔
349
                return GCHandle.ToIntPtr(exHandle);
12✔
350
            }
351
        }
39✔
352

353
        [MonoPInvokeCallback(typeof(NativeMethods.NotifyAfterClientReset))]
354
        private static IntPtr NotifyAfterClientReset(IntPtr beforeFrozen, IntPtr after, IntPtr managedSyncConfigurationHandle, bool didRecover)
355
        {
356
            SyncConfigurationBase? syncConfig = null;
27✔
357

358
            try
359
            {
360
                var syncConfigHandle = GCHandle.FromIntPtr(managedSyncConfigurationHandle);
27✔
361
                syncConfig = (SyncConfigurationBase)syncConfigHandle.Target!;
27✔
362

363
                var cb = syncConfig.ClientResetHandler switch
27!
364
                {
27✔
365
                    DiscardUnsyncedChangesHandler handler => handler.OnAfterReset,
9✔
366
                    RecoverUnsyncedChangesHandler handler => handler.OnAfterReset,
8✔
367
                    RecoverOrDiscardUnsyncedChangesHandler handler => didRecover ? handler.OnAfterRecovery : handler.OnAfterDiscard,
10✔
368
                    _ => throw new NotSupportedException($"ClientResetHandlerBase of type {syncConfig.ClientResetHandler.GetType()} is not handled yet")
×
369
                };
27✔
370

371
                if (cb != null)
27✔
372
                {
373
                    var schema = syncConfig.Schema;
20✔
374
                    using var realmBefore = new Realm(new UnownedRealmHandle(beforeFrozen), syncConfig, schema);
20✔
375
                    using var realmAfter = new Realm(new UnownedRealmHandle(after), syncConfig, schema);
20✔
376
                    cb.Invoke(realmBefore, realmAfter);
20✔
377
                }
378

379
                return IntPtr.Zero;
21✔
380
            }
381
            catch (Exception ex)
6✔
382
            {
383
                var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name;
6!
384
                Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnAfterReset during a client reset: {ex}");
6✔
385

386
                var exHandle = GCHandle.Alloc(ex);
6✔
387
                return GCHandle.ToIntPtr(exHandle);
6✔
388
            }
389
        }
27✔
390

391
        [MonoPInvokeCallback(typeof(NativeMethods.SessionProgressCallback))]
392
        private static void HandleSessionProgress(IntPtr tokenPtr, ulong transferredBytes, ulong transferableBytes)
393
        {
394
            var token = (ProgressNotificationToken?)GCHandle.FromIntPtr(tokenPtr).Target;
17✔
395
            token?.Notify(transferredBytes, transferableBytes);
17!
396
        }
17✔
397

398
        [MonoPInvokeCallback(typeof(NativeMethods.SessionWaitCallback))]
399
        private static void HandleSessionWaitCallback(IntPtr taskCompletionSource, int error_code, PrimitiveValue message)
400
        {
401
            var handle = GCHandle.FromIntPtr(taskCompletionSource);
215✔
402
            var tcs = (TaskCompletionSource)handle.Target!;
215✔
403

404
            if (error_code == 0)
215!
405
            {
406
                tcs.TrySetResult();
215✔
407
            }
408
            else
409
            {
410
                var inner = new SessionException(message.AsString(), (ErrorCode)error_code);
×
411
                const string OuterMessage = "A system error occurred while waiting for completion. See InnerException for more details";
412
                tcs.TrySetException(new RealmException(OuterMessage, inner));
×
413
            }
414
        }
×
415

416
        [MonoPInvokeCallback(typeof(NativeMethods.SessionPropertyChangedCallback))]
417
        private static void HandleSessionPropertyChangedCallback(IntPtr managedSessionHandle, NotifiableProperty property)
418
        {
419
            try
420
            {
421
                if (managedSessionHandle == IntPtr.Zero)
8!
422
                {
423
                    return;
×
424
                }
425

426
                var propertyName = property switch
8!
427
                {
8✔
428
                    NotifiableProperty.ConnectionState => nameof(Session.ConnectionState),
8✔
429
                    _ => throw new NotSupportedException($"Unexpected notifiable property value: {property}")
×
430
                };
8✔
431
                var session = (Session)GCHandle.FromIntPtr(managedSessionHandle).Target!;
8✔
432
                if (session is null)
8!
433
                {
434
                    // We're taking a weak handle to the session, so it's possible that it's been collected
435
                    return;
×
436
                }
437

438
                System.Threading.ThreadPool.QueueUserWorkItem(_ =>
8✔
439
                {
8✔
440
                    session.RaisePropertyChanged(propertyName);
8✔
441
                });
16✔
442
            }
8✔
443
            catch (Exception ex)
×
444
            {
445
                Logger.Default.Log(LogLevel.Error, $"An error has occurred while raising a property changed event: {ex}");
×
446
            }
×
447
        }
8✔
448

449
        private enum NotifiableProperty : byte
450
        {
451
            ConnectionState = 0,
452
        }
453
    }
454
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc