Skip to content

Commit ddf72b6

Browse files
committed
[APIv1-Update4] Add Speed Limiter Service/Helper
1 parent 6987c7a commit ddf72b6

7 files changed

Lines changed: 254 additions & 22 deletions

SharedStatic.V1Ext.cs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,7 @@
1-
using Hi3Helper.Plugin.Core.DiscordPresence;
2-
using Hi3Helper.Plugin.Core.Management;
3-
using Hi3Helper.Plugin.Core.Utility;
4-
using System;
1+
using Hi3Helper.Plugin.Core.Management;
52

63
namespace Hi3Helper.Plugin.Core;
74

8-
public class SharedStaticV1Ext : SharedStatic
9-
{
10-
// Update1
11-
internal delegate HResult LaunchGameFromGameManagerAsyncDelegate(nint gameManagerP, nint pluginP, nint presetConfigP, nint printGameLogCallbackP, nint arguments, int argumentsLen, int runBoostedInt, int processPriorityInt, ref Guid cancelToken, out nint taskResult);
12-
internal delegate HResult WaitRunningGameAsyncDelegate(nint gameManagerP, nint pluginP, nint presetConfigP, ref Guid cancelToken, out nint taskResult);
13-
internal delegate HResult IsGameRunningDelegate(nint gameManagerP, nint presetConfigP, out int isGameRunning, out DateTime processStartTime);
14-
15-
// Update2
16-
internal unsafe delegate HResult GetCurrentDiscordPresenceInfoDelegate(void* presetConfigP,
17-
DiscordPresenceInfo** presenceInfoP);
18-
19-
// Update3
20-
internal delegate HResult StartResizableWindowHookAsyncDelegate(nint gameManagerP, nint presetConfigP, nint executableName, int executableNameLen, int height, int width, nint executableDirectory, int executableDirectoryLen, ref Guid cancelToken, out nint taskResult);
21-
}
22-
235
/// <summary>
246
/// Inherited <see cref="SharedStatic"/> with additional supports for API extensions which require call or property access to derived exports.
257
/// </summary>
@@ -42,6 +24,7 @@ static SharedStaticV1Ext()
4224
InitExtension_Update1Exports();
4325
InitExtension_Update2Exports();
4426
InitExtension_Update3Exports();
27+
InitExtension_Update4Exports();
4528
}
4629

4730
/// <summary>

SharedStatic.V1Ext_Update1.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Runtime.InteropServices;
99
using System.Threading;
1010
using System.Threading.Tasks;
11-
1211
using static Hi3Helper.Plugin.Core.Utility.GameManagerExtension;
1312

1413
#if !MANUALCOM
@@ -17,6 +16,35 @@
1716

1817
namespace Hi3Helper.Plugin.Core;
1918

19+
public partial class SharedStaticV1Ext
20+
{
21+
// Update1
22+
internal delegate HResult LaunchGameFromGameManagerAsyncDelegate(
23+
nint gameManagerP,
24+
nint pluginP,
25+
nint presetConfigP,
26+
nint printGameLogCallbackP,
27+
nint arguments,
28+
int argumentsLen,
29+
int runBoostedInt,
30+
int processPriorityInt,
31+
ref Guid cancelToken,
32+
out nint taskResult);
33+
34+
internal delegate HResult WaitRunningGameAsyncDelegate(
35+
nint gameManagerP,
36+
nint pluginP,
37+
nint presetConfigP,
38+
ref Guid cancelToken,
39+
out nint taskResult);
40+
41+
internal delegate HResult IsGameRunningDelegate(
42+
nint gameManagerP,
43+
nint presetConfigP,
44+
out int isGameRunning,
45+
out DateTime processStartTime);
46+
}
47+
2048
public partial class SharedStaticV1Ext<T>
2149
{
2250
private static void InitExtension_Update1Exports()

SharedStatic.V1Ext_Update2.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99

1010
namespace Hi3Helper.Plugin.Core;
1111

12+
public partial class SharedStaticV1Ext
13+
{
14+
// Update2
15+
internal unsafe delegate HResult GetCurrentDiscordPresenceInfoDelegate(
16+
void* presetConfigP,
17+
DiscordPresenceInfo** presenceInfoP);
18+
}
19+
1220
public partial class SharedStaticV1Ext<T>
1321
{
1422
private static unsafe void InitExtension_Update2Exports()

SharedStatic.V1Ext_Update3.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616

1717
namespace Hi3Helper.Plugin.Core;
1818

19+
public partial class SharedStaticV1Ext : SharedStatic
20+
{
21+
// Update3
22+
internal delegate HResult StartResizableWindowHookAsyncDelegate(
23+
nint gameManagerP,
24+
nint presetConfigP,
25+
nint executableName,
26+
int executableNameLen,
27+
int height,
28+
int width,
29+
nint executableDirectory,
30+
int executableDirectoryLen,
31+
ref Guid cancelToken,
32+
out nint taskResult);
33+
}
34+
1935
public partial class SharedStaticV1Ext<T>
2036
{
2137
private static void InitExtension_Update3Exports()
@@ -35,7 +51,7 @@ private static void InitExtension_Update3Exports()
3551
#region ABI Proxies
3652
/// <summary>
3753
/// This method is an ABI proxy function between the PInvoke Export and the actual plugin's method.<br/>
38-
/// See the documentation for <see cref="SharedStaticV1Ext{T}.StartResizableWindowHookAsync(RunGameFromGameManagerContext, string?, int, int, string?, CancellationToken)"/> method for more information.
54+
/// See the documentation for <see cref="SharedStaticV1Ext{T}.StartResizableWindowHookAsync(nint, nint, nint, int, int, int, nint, int, ref Guid, out nint)"/> method for more information.
3955
/// </summary>
4056
private static unsafe HResult StartResizableWindowHookAsync(nint gameManagerP,
4157
nint presetConfigP,

SharedStatic.V1Ext_Update4.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using System.Threading.Tasks;
4+
using Hi3Helper.Plugin.Core.Utility;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Hi3Helper.Plugin.Core;
8+
9+
public partial class SharedStaticV1Ext
10+
{
11+
// Update4
12+
/// <summary>
13+
/// Registers the function of the Speed Throttler Service from the Main Application.
14+
/// To disable the speed throttler service, set the <paramref name="addBytesOrWaitAsyncCallback"/> to <see cref="nint.Zero"/>.
15+
/// </summary>
16+
/// <param name="addBytesOrWaitAsyncCallback">The address of the speed throttler service callback</param>
17+
internal delegate HResult RegisterSpeedThrottlerServiceDelegate(nint addBytesOrWaitAsyncCallback);
18+
}
19+
20+
public partial class SharedStaticV1Ext<T>
21+
{
22+
private static void InitExtension_Update4Exports()
23+
{
24+
/* ----------------------------------------------------------------------
25+
* Update 4 Feature Sets
26+
* ----------------------------------------------------------------------
27+
* This feature sets includes the following feature:
28+
* - Download Speed Limiter Service
29+
* - RegisterSpeedThrottlerService
30+
*/
31+
32+
// -> Plugin Speed Throttler Service Installer
33+
TryRegisterApiExport<RegisterSpeedThrottlerServiceDelegate>("RegisterSpeedThrottlerService", RegisterSpeedThrottlerService);
34+
}
35+
36+
#region ABI Proxies
37+
/// <summary>
38+
/// This method is an ABI proxy and installer for the Speed Throttler Service functionality. To use Speed Throttler Service functionalities, Use <see cref="SpeedLimiterService"/> instead.
39+
/// </summary>
40+
private static unsafe HResult RegisterSpeedThrottlerService(nint addBytesOrWaitAsyncCallback)
41+
{
42+
SpeedLimiterService.AddBytesOrWaitAsyncCallback = (delegate* unmanaged[Stdcall]<nint, long, nint, void**, int>)addBytesOrWaitAsyncCallback;
43+
if (addBytesOrWaitAsyncCallback == nint.Zero)
44+
{
45+
SpeedLimiterService.AddBytesOrWaitAsyncCallback = null;
46+
InstanceLogger.LogTrace("[RegisterSpeedThrottlerService] Speed Throttler Service has been uninstalled");
47+
48+
return HResult.Ok;
49+
}
50+
51+
// Test the delegate first before registering it.
52+
nint contextP = SpeedLimiterService.CreateServiceContext();
53+
try
54+
{
55+
// Try call the increment function with 16 bytes of load, then check for exception
56+
#pragma warning disable CA2012
57+
ValueTask task = SpeedLimiterService.AddBytesOrWaitAsync(contextP, 16);
58+
#pragma warning restore CA2012
59+
if (task is { IsCompleted: true, IsFaulted: false } ||
60+
task.IsCompletedSuccessfully)
61+
{
62+
InstanceLogger.LogTrace("[RegisterSpeedThrottlerService] Speed Throttler Service has been installed. Service's callback is located at address: 0x{Ptr:x8}", addBytesOrWaitAsyncCallback);
63+
return HResult.Ok;
64+
}
65+
66+
// Try block and await if task is still going.
67+
task.GetAwaiter().GetResult();
68+
69+
// If nothing blown up, return OK.
70+
return HResult.Ok;
71+
}
72+
catch (Exception ex)
73+
{
74+
SpeedLimiterService.AddBytesOrWaitAsyncCallback = null; // Reset the callback
75+
return Marshal.GetHRForException(ex);
76+
}
77+
finally
78+
{
79+
SpeedLimiterService.FreeServiceContext(contextP);
80+
}
81+
}
82+
#endregion
83+
}

SharedStatic.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ static unsafe SharedStatic()
103103
internal static string? ProxyPassword;
104104

105105
public static string PluginLocaleCode { get; internal set; } = "en-us";
106-
public static readonly GameVersion LibraryStandardVersion = new(0, 1, 3, 0);
106+
public static readonly GameVersion LibraryStandardVersion = new(0, 1, 4, 0);
107107
public static readonly ILogger InstanceLogger = new SharedLogger();
108108

109109
#if DEBUG

Utility/SpeedLimiterService.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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

Comments
 (0)