|
| 1 | +using System.Runtime.CompilerServices; |
| 2 | +using System.Runtime.InteropServices; |
| 3 | +using System.Threading; |
| 4 | +using System.Threading.Tasks; |
| 5 | +using Hi3Helper.Plugin.Core.Utility.Windows; |
| 6 | +using Microsoft.Win32.SafeHandles; |
| 7 | + |
| 8 | +namespace Hi3Helper.Plugin.Core.Utility; |
| 9 | + |
| 10 | +/// <summary> |
| 11 | +/// Provides a helper for using Speed Limiter Service. |
| 12 | +/// </summary> |
| 13 | +/// <remarks> |
| 14 | +/// In order to use the service, make sure the main application has registered the service's callback using RegisterSpeedThrottlerService export API. |
| 15 | +/// Once the service is successfully registered, you can call <see cref="AddBytesOrWaitAsync"/> method to pass the read bytes to the service and then throttle the speed for you. |
| 16 | +/// <br/><br/> |
| 17 | +/// Usage example on your code: |
| 18 | +/// <code> |
| 19 | +/// async Task ReadDataAsync(Stream inputStream, Stream outputStream, CancellationToken token) |
| 20 | +/// { |
| 21 | +/// byte[] buffer = new byte[8192]; |
| 22 | +/// int read; |
| 23 | +/// while ((read = await inputStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) |
| 24 | +/// { |
| 25 | +/// // Do anything... |
| 26 | +/// ... |
| 27 | +/// |
| 28 | +/// // Pass the read bytes to the speed limiter service, and wait if necessary until the service allows more bytes to be processed. |
| 29 | +/// await SpeedLimiterService.AddBytesOrWaitAsync(context, read, token); |
| 30 | +/// } |
| 31 | +/// } |
| 32 | +/// </code> |
| 33 | +/// </remarks> |
| 34 | +public static class SpeedLimiterService |
| 35 | +{ |
| 36 | + internal static unsafe delegate* unmanaged[Stdcall]<nint, long, nint, void**, int> AddBytesOrWaitAsyncCallback = |
| 37 | + null; |
| 38 | + |
| 39 | + /// <summary> |
| 40 | + /// Creates a context to be used for the speed limiter service. This context can be used into multiple instances or threads of your downloader. |
| 41 | + /// </summary> |
| 42 | + /// <returns></returns> |
| 43 | + public static unsafe nint CreateServiceContext() |
| 44 | + => (nint)Mem.Alloc<long>(2); // Context struct is 16 bytes in size. |
| 45 | + |
| 46 | + /// <summary> |
| 47 | + /// Free the speed limiter service context. |
| 48 | + /// </summary> |
| 49 | + /// <param name="context"></param> |
| 50 | + public static void FreeServiceContext(nint context) |
| 51 | + => context.Free(); |
| 52 | + |
| 53 | + /// <summary> |
| 54 | + /// Adds-up counter of the consumed bytes into the service, and await (throttle) if the target speed is already reached.<br/> |
| 55 | + /// If the service is not registered or the callback is not set, this method will simply return immediately without awaiting. |
| 56 | + /// </summary> |
| 57 | + /// <param name="context">The pointer of the service context.</param> |
| 58 | + /// <param name="readBytes">How many bytes consumed on current operation.</param> |
| 59 | + /// <param name="token">Cancellation token for the async operation.</param> |
| 60 | + [SkipLocalsInit] |
| 61 | + public static unsafe ValueTask AddBytesOrWaitAsync( |
| 62 | + nint context, |
| 63 | + long readBytes, |
| 64 | + CancellationToken token = default) |
| 65 | + { |
| 66 | + if (AddBytesOrWaitAsyncCallback == null) |
| 67 | + { |
| 68 | + return ValueTask.CompletedTask; |
| 69 | + } |
| 70 | + |
| 71 | + nint tokenHandle = token.WaitHandle.SafeWaitHandle.DangerousGetHandle(); |
| 72 | + nint asyncWaitHandle = nint.Zero; |
| 73 | + |
| 74 | + Unsafe.SkipInit(out int hr); |
| 75 | + fixed (nint* asyncWaitHandlePtr = &asyncWaitHandle) |
| 76 | + { |
| 77 | + hr = AddBytesOrWaitAsyncCallback( |
| 78 | + context, |
| 79 | + readBytes, |
| 80 | + tokenHandle, |
| 81 | + (void**)asyncWaitHandlePtr); |
| 82 | + } |
| 83 | + |
| 84 | + Marshal.ThrowExceptionForHR(hr); |
| 85 | + |
| 86 | + SafeWaitHandle safeHandle = new(asyncWaitHandle, false); |
| 87 | + WaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset) |
| 88 | + { |
| 89 | + SafeWaitHandle = safeHandle |
| 90 | + }; |
| 91 | + |
| 92 | + AsyncValueTaskMethodBuilder valueTaskCs = new(); |
| 93 | + ThreadPool.UnsafeRegisterWaitForSingleObject(waitHandle, |
| 94 | + DisposeWaitHandleCallback, |
| 95 | + null, |
| 96 | + -1, |
| 97 | + true); |
| 98 | + |
| 99 | + return valueTaskCs.Task; |
| 100 | + |
| 101 | + void DisposeWaitHandleCallback(object? state, bool isTimedOut) |
| 102 | + { |
| 103 | + safeHandle.Dispose(); |
| 104 | + waitHandle.Dispose(); |
| 105 | + |
| 106 | + if (asyncWaitHandle != nint.Zero) |
| 107 | + { |
| 108 | + _ = PInvoke.CloseHandle(asyncWaitHandle); |
| 109 | + } |
| 110 | + |
| 111 | + valueTaskCs.SetResult(); |
| 112 | + } |
| 113 | + } |
| 114 | +} |
0 commit comments