1
+ #nullable enable
2
+ using System ;
3
+ using System . Collections . Generic ;
4
+ using System . Diagnostics ;
5
+ using System . Linq ;
6
+ using System . Runtime . ExceptionServices ;
7
+ using System . Threading ;
8
+ using Avalonia . Logging ;
9
+ using Avalonia . Threading ;
10
+ using static Avalonia . X11 . Interop . Glib ;
11
+ namespace Avalonia . X11 . Dispatching ;
12
+
13
+ internal class GlibDispatcherImpl :
14
+ IDispatcherImplWithExplicitBackgroundProcessing ,
15
+ IControlledDispatcherImpl
16
+ {
17
+ /*
18
+ GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
19
+ are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing,
20
+ background jobs are executed strictly after user input processing.
21
+
22
+ GLib has numeric priorities that are used in the following way:
23
+ -100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK
24
+ 0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add
25
+ 100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts
26
+ 110 Resize/layout operations (GTK)
27
+ 120 Render operations (GTK)
28
+ 200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources
29
+
30
+ So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render
31
+
32
+ So, to map our model to GLib we do the following:
33
+ - foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs)
34
+ - X11 socket is polled with G_PRIORITY_DEFAULT, all X11 events are read until socket is empty,
35
+ we also group input events at that stage (this matches our epoll-based dispatcher)
36
+ - background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK
37
+ foreground jobs
38
+
39
+ Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since
40
+ - g_main_context_pending doesn't accept max_priority argument
41
+ - even if it did, that would still involve a syscall to the kernel to poll for fds anyway
42
+
43
+ So we just report that we don't support pending input query and let the dispatcher to
44
+ call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job.
45
+ Background jobs are expected to be relatively expensive to execute since on Windows
46
+ MsgWaitForMultipleObjectsEx results isn't really free too.
47
+
48
+ For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using
49
+ g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job,
50
+ it actually adds an always-triggered source that would be called before other sources with lower priority.
51
+
52
+ For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests
53
+ an update
54
+
55
+ Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs
56
+ whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio
57
+ Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly
58
+ and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs
59
+
60
+ Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions,
61
+ so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers
62
+ that try to propagate those to the nearest run loop frame that was initiated by Avalonia.
63
+
64
+ If there is no such frame, we have no choice but to log/swallow those
65
+ */
66
+
67
+ private readonly AvaloniaX11Platform _platform ;
68
+
69
+ // Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an
70
+ // inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that
71
+ // the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop
72
+ // to a different thread
73
+ private readonly Thread _mainThread = Thread . CurrentThread ;
74
+
75
+ private readonly X11EventDispatcher _x11Events ;
76
+ private bool _signaled ;
77
+ private bool _signaledSourceAdded ;
78
+ private readonly object _signalLock = new ( ) ;
79
+ private readonly Stack < ManagedLoopFrame > _runLoopStack = new ( ) ;
80
+
81
+ private readonly Stopwatch _stopwatch = Stopwatch . StartNew ( ) ;
82
+ private uint ? _glibTimerSourceTag ;
83
+
84
+ public GlibDispatcherImpl ( AvaloniaX11Platform platform )
85
+ {
86
+ _platform = platform ;
87
+ _x11Events = new X11EventDispatcher ( platform ) ;
88
+ var unixFdId = g_unix_fd_add_full ( G_PRIORITY_DEFAULT , _x11Events . Fd , GIOCondition . G_IO_IN ,
89
+ X11SourceCallback ) ;
90
+ // We can trigger a nested event loop when handling X11 events, so we need to mark the source as recursive
91
+ var unixFdSource = g_main_context_find_source_by_id ( IntPtr . Zero , unixFdId ) ;
92
+ g_source_set_can_recurse ( unixFdSource , 1 ) ;
93
+ }
94
+
95
+ public bool CurrentThreadIsLoopThread => _mainThread == Thread . CurrentThread ;
96
+
97
+ public event Action ? Signaled ;
98
+ public void Signal ( )
99
+ {
100
+ lock ( _signalLock )
101
+ {
102
+ if ( _signaled )
103
+ return ;
104
+ _signaled = true ;
105
+ if ( _signaledSourceAdded )
106
+ return ;
107
+ _signaledSourceAdded = true ;
108
+ }
109
+ g_idle_add_full ( G_PRIORITY_DEFAULT - 1 , SignalSourceCallback ) ;
110
+ }
111
+
112
+ private void CheckSignaled ( )
113
+ {
114
+ lock ( _signalLock )
115
+ {
116
+ if ( ! _signaled )
117
+ return ;
118
+ _signaled = false ;
119
+ }
120
+
121
+ try
122
+ {
123
+ Signaled ? . Invoke ( ) ;
124
+ }
125
+ catch ( Exception e )
126
+ {
127
+ HandleException ( e ) ;
128
+ }
129
+ _x11Events . Flush ( ) ;
130
+ }
131
+
132
+ private bool SignalSourceCallback ( )
133
+ {
134
+ lock ( _signalLock )
135
+ {
136
+ _signaledSourceAdded = false ;
137
+ }
138
+ CheckSignaled ( ) ;
139
+ return false ;
140
+ }
141
+
142
+ public event Action ? Timer ;
143
+ public long Now => _stopwatch . ElapsedMilliseconds ;
144
+
145
+ public void UpdateTimer ( long ? dueTimeInMs )
146
+ {
147
+ if ( _glibTimerSourceTag . HasValue )
148
+ {
149
+ g_source_remove ( _glibTimerSourceTag . Value ) ;
150
+ _glibTimerSourceTag = null ;
151
+ }
152
+
153
+ if ( dueTimeInMs == null )
154
+ return ;
155
+
156
+ var interval = ( uint ) Math . Max ( 0 , ( int ) Math . Min ( int . MaxValue , dueTimeInMs . Value - Now ) ) ;
157
+ _glibTimerSourceTag = g_timeout_add_once ( interval , TimerCallback ) ;
158
+ }
159
+
160
+ private void TimerCallback ( )
161
+ {
162
+ try
163
+ {
164
+ Timer ? . Invoke ( ) ;
165
+ }
166
+ catch ( Exception e )
167
+ {
168
+ HandleException ( e ) ;
169
+ }
170
+ _x11Events . Flush ( ) ;
171
+ }
172
+
173
+ public event Action ? ReadyForBackgroundProcessing ;
174
+
175
+ public void RequestBackgroundProcessing ( ) =>
176
+ g_idle_add_once ( ( ) => ReadyForBackgroundProcessing ? . Invoke ( ) ) ;
177
+
178
+ public bool CanQueryPendingInput => false ;
179
+ public bool HasPendingInput => _platform . EventGrouperDispatchQueue . HasJobs || _x11Events . IsPending ;
180
+
181
+ private bool X11SourceCallback ( int i , GIOCondition gioCondition )
182
+ {
183
+ CheckSignaled ( ) ;
184
+ var token = _runLoopStack . Count > 0 ? _runLoopStack . Peek ( ) . Cancelled : CancellationToken . None ;
185
+ try
186
+ {
187
+ // Completely drain X11 socket while we are at it
188
+ while ( _x11Events . IsPending )
189
+ {
190
+ // If we don't actually drain our X11 socket, GLib _will_ call us again even if
191
+ // we request the run loop to quit
192
+ _x11Events . DispatchX11Events ( CancellationToken . None ) ;
193
+ if ( ! token . IsCancellationRequested )
194
+ {
195
+ while ( _platform . EventGrouperDispatchQueue . HasJobs )
196
+ {
197
+ CheckSignaled ( ) ;
198
+ _platform . EventGrouperDispatchQueue . DispatchNext ( ) ;
199
+ }
200
+
201
+ _x11Events . Flush ( ) ;
202
+ }
203
+ }
204
+ }
205
+ catch ( Exception e )
206
+ {
207
+ HandleException ( e ) ;
208
+ }
209
+
210
+ return true ;
211
+ }
212
+
213
+ public void RunLoop ( CancellationToken token )
214
+ {
215
+ if ( token . IsCancellationRequested )
216
+ return ;
217
+
218
+ using var loop = new ManagedLoopFrame ( token ) ;
219
+ _runLoopStack . Push ( loop ) ;
220
+ loop . Run ( ) ;
221
+ _runLoopStack . Pop ( ) ;
222
+
223
+ // Propagate any managed exceptions that we've captured from this frame
224
+ if ( loop . Exceptions . Count == 1 )
225
+ loop . Exceptions [ 0 ] . Throw ( ) ;
226
+ else if ( loop . Exceptions . Count > 1 )
227
+ throw new AggregateException ( loop . Exceptions . Select ( x => x . SourceException ) ) ;
228
+ }
229
+
230
+ void HandleException ( Exception e )
231
+ {
232
+ if ( _runLoopStack . Count > 0 )
233
+ {
234
+ var frame = _runLoopStack . Peek ( ) ;
235
+ frame . Exceptions . Add ( ExceptionDispatchInfo . Capture ( e ) ) ;
236
+ frame . Stop ( ) ;
237
+ }
238
+ else
239
+ {
240
+ var externalLogger = _platform . Options . ExterinalGLibMainLoopExceptionLogger ;
241
+ if ( externalLogger != null )
242
+ externalLogger . Invoke ( e ) ;
243
+ else
244
+ Logger . TryGet ( LogEventLevel . Error , LogArea . Control )
245
+ ? . Log ( "Dispatcher" , "Unhandled exception: {exception}" , e ) ;
246
+ }
247
+ }
248
+
249
+ private class ManagedLoopFrame : IDisposable
250
+ {
251
+ private readonly CancellationToken _externalToken ;
252
+ private CancellationTokenSource ? _internalTokenSource ;
253
+ public CancellationToken Cancelled { get ; private set ; }
254
+
255
+ private readonly IntPtr _loop = g_main_loop_new ( IntPtr . Zero , 1 ) ;
256
+ public List < ExceptionDispatchInfo > Exceptions { get ; } = new ( ) ;
257
+ private readonly object _destroyLock = new ( ) ;
258
+ private bool _disposed ;
259
+
260
+ public ManagedLoopFrame ( CancellationToken token )
261
+ {
262
+ _externalToken = token ;
263
+ }
264
+
265
+ public void Stop ( )
266
+ {
267
+ try
268
+ {
269
+ _internalTokenSource ? . Cancel ( ) ;
270
+ }
271
+ catch
272
+ {
273
+ // Ignore
274
+ }
275
+ }
276
+
277
+ public void Run ( )
278
+ {
279
+ if ( _externalToken . IsCancellationRequested )
280
+ return ;
281
+ using ( _internalTokenSource = new ( ) )
282
+ using ( var composite =
283
+ CancellationTokenSource . CreateLinkedTokenSource ( _externalToken , _internalTokenSource . Token ) )
284
+ {
285
+ Cancelled = composite . Token ;
286
+ using ( Cancelled . Register ( ( ) =>
287
+ {
288
+ lock ( _destroyLock )
289
+ {
290
+ if ( _disposed )
291
+ return ;
292
+ g_main_loop_quit ( _loop ) ;
293
+ }
294
+ } ) )
295
+ {
296
+ g_main_loop_run ( _loop ) ;
297
+ }
298
+ }
299
+ }
300
+
301
+ public void Dispose ( )
302
+ {
303
+ lock ( _destroyLock )
304
+ {
305
+ if ( _disposed )
306
+ return ;
307
+ _disposed = true ;
308
+ g_main_loop_unref ( _loop ) ;
309
+ }
310
+ }
311
+ }
312
+
313
+ }
0 commit comments