Skip to content

Commit 0eba093

Browse files
committed
Reapply "Resilience (#34)"
This reverts commit fa984fe.
1 parent fa984fe commit 0eba093

54 files changed

Lines changed: 1096 additions & 578 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,57 @@ jobs:
313313
path: build/win/build/stalker-gamma-cli
314314
compression-level: 9
315315

316+
cli-windows-arm64:
317+
name: cli-windows-arm64
318+
runs-on: windows-11-arm
319+
outputs:
320+
version: ${{ steps.gitversion.outputs.SemVer }}
321+
env:
322+
APP_NAME: stalker-gamma-gui
323+
PROJECT_FOLDER: stalker-gamma-cli
324+
PROJECT_NAME: stalker-gamma-cli.csproj
325+
CONFIGURATION: Release
326+
steps:
327+
- name: Checkout
328+
uses: actions/checkout@v6
329+
with:
330+
fetch-depth: 0
331+
332+
- name: Run Csharpier
333+
run: |
334+
dotnet tool restore
335+
dotnet csharpier check .
336+
337+
- name: Install GitVersion
338+
uses: gittools/actions/gitversion/setup@v0
339+
with:
340+
versionSpec: "5.x"
341+
342+
- name: Determine Version
343+
id: gitversion
344+
uses: gittools/actions/gitversion/execute@v0
345+
346+
- name: Build CLI
347+
run: build/win/build.bat ${{ steps.gitversion.outputs.SemVer }} arm64
348+
349+
- name: Create Release
350+
uses: softprops/action-gh-release@v2.4.2
351+
if: github.ref_type == 'tag'
352+
with:
353+
files: stalker-gamma+win.arm64.zip
354+
token: ${{ secrets.STALKER_GAMMA_CLI_SECRET }}
355+
356+
- name: Upload
357+
uses: actions/upload-artifact@v4
358+
with:
359+
name: stalker-gamma+win.arm64
360+
path: build/win/build/stalker-gamma-cli
361+
compression-level: 9
362+
316363
update-aur:
317364
name: update-aur
318365
runs-on: ubuntu-22.04
319-
needs: [cli-ubuntu-arm64, cli-ubuntu-x64]
366+
needs: [update-chocolatey, cli-ubuntu-arm64, cli-ubuntu-x64]
320367
if: github.ref_type == 'tag'
321368
steps:
322369
- name: Checkout
@@ -345,7 +392,7 @@ jobs:
345392
update-chocolatey:
346393
name: update-chocolatey
347394
runs-on: windows-latest
348-
needs: [cli-windows]
395+
needs: [cli-windows, cli-windows-arm64]
349396
if: github.ref_type == 'tag'
350397
steps:
351398
- name: Checkout
@@ -360,7 +407,7 @@ jobs:
360407
update-homebrew:
361408
name: update-homebrew
362409
runs-on: ubuntu-22.04
363-
needs: [cli-macos-x64, cli-macos-arm64]
410+
needs: [update-chocolatey, cli-macos-x64, cli-macos-arm64]
364411
if: github.ref_type == 'tag'
365412
steps:
366413
- name: Checkout source repo

LibCurl/LibCurl.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<RootNamespace>curl_wrapper</RootNamespace>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
</PropertyGroup>
9+
</Project>

LibCurlImpersonate/CurlHttp.cs

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
using System.Runtime.InteropServices;
2+
using System.Text;
3+
4+
namespace LibCurlImpersonate;
5+
6+
public class CurlHttp : IDisposable
7+
{
8+
static CurlHttp() => LibCurl.curl_global_init(LibCurl.CURL_GLOBAL_DEFAULT);
9+
10+
public static IEnumerable<string> FetchLines(
11+
string url,
12+
Action<double>? onSpeed = null,
13+
bool http3 = false,
14+
Action<string>? onHttpVersion = null,
15+
CancellationToken ct = default
16+
)
17+
{
18+
using var reader = new StringReader(Fetch(url, onSpeed, http3, onHttpVersion, ct));
19+
while (reader.ReadLine() is { } line)
20+
yield return line;
21+
}
22+
23+
public static string Fetch(
24+
string url,
25+
Action<double>? onSpeed = null,
26+
bool http3 = false,
27+
Action<string>? onHttpVersion = null,
28+
CancellationToken ct = default
29+
)
30+
{
31+
IntPtr handle = LibCurl.curl_easy_init();
32+
if (handle == IntPtr.Zero)
33+
throw new InvalidOperationException("curl_easy_init failed");
34+
35+
var sb = new StringBuilder();
36+
var writePin = GCHandle.Alloc(sb);
37+
var xferPin = InstallXferCallback(handle, ct, onSpeed: onSpeed);
38+
try
39+
{
40+
LibCurl.curl_easy_impersonate(handle, Impersonation, 1);
41+
LibCurl.curl_easy_setopt_str(handle, LibCurl.CURLOPT_ACCEPT_ENCODING, "");
42+
LibCurl.curl_easy_setopt_str(handle, LibCurl.CURLOPT_URL, url);
43+
LibCurl.curl_easy_setopt_cb(handle, LibCurl.CURLOPT_WRITEFUNCTION, OnWrite);
44+
LibCurl.curl_easy_setopt_ptr(
45+
handle,
46+
LibCurl.CURLOPT_WRITEDATA,
47+
GCHandle.ToIntPtr(writePin)
48+
);
49+
LibCurl.curl_easy_setopt_long(handle, LibCurl.CURLOPT_FOLLOWLOCATION, 1L);
50+
LibCurl.curl_easy_setopt_str(handle, LibCurl.CURLOPT_CAINFO, "cacert.pem");
51+
if (http3)
52+
LibCurl.curl_easy_setopt_long(
53+
handle,
54+
LibCurl.CURLOPT_HTTP_VERSION,
55+
LibCurl.CURL_HTTP_VERSION_3
56+
);
57+
58+
int code = LibCurl.curl_easy_perform(handle);
59+
if (code == LibCurl.CURLE_ABORTED_BY_CALLBACK)
60+
ct.ThrowIfCancellationRequested();
61+
if (code != LibCurl.CURLE_OK)
62+
throw new InvalidOperationException(
63+
$"curl_easy_perform returned error code {code}"
64+
);
65+
66+
ReportHttpVersion(handle, onHttpVersion);
67+
return sb.ToString();
68+
}
69+
finally
70+
{
71+
writePin.Free();
72+
if (xferPin.IsAllocated)
73+
xferPin.Free();
74+
LibCurl.curl_easy_cleanup(handle);
75+
}
76+
}
77+
78+
public static Dictionary<string, string> GetHeaders(
79+
string url,
80+
bool http3 = false,
81+
Action<string>? onHttpVersion = null,
82+
CancellationToken ct = default
83+
)
84+
{
85+
IntPtr handle = LibCurl.curl_easy_init();
86+
if (handle == IntPtr.Zero)
87+
throw new InvalidOperationException("curl_easy_init failed");
88+
89+
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
90+
var headerPin = GCHandle.Alloc(headers);
91+
var xferPin = InstallXferCallback(handle, ct);
92+
try
93+
{
94+
LibCurl.curl_easy_impersonate(handle, Impersonation, 1);
95+
LibCurl.curl_easy_setopt_str(handle, LibCurl.CURLOPT_URL, url);
96+
LibCurl.curl_easy_setopt_cb(handle, LibCurl.CURLOPT_HEADERFUNCTION, OnHeader);
97+
LibCurl.curl_easy_setopt_ptr(
98+
handle,
99+
LibCurl.CURLOPT_HEADERDATA,
100+
GCHandle.ToIntPtr(headerPin)
101+
);
102+
LibCurl.curl_easy_setopt_long(handle, LibCurl.CURLOPT_NOBODY, 1L);
103+
LibCurl.curl_easy_setopt_str(handle, LibCurl.CURLOPT_CAINFO, "cacert.pem");
104+
if (http3)
105+
LibCurl.curl_easy_setopt_long(
106+
handle,
107+
LibCurl.CURLOPT_HTTP_VERSION,
108+
LibCurl.CURL_HTTP_VERSION_3
109+
);
110+
111+
int code = LibCurl.curl_easy_perform(handle);
112+
if (code == LibCurl.CURLE_ABORTED_BY_CALLBACK)
113+
ct.ThrowIfCancellationRequested();
114+
if (code != LibCurl.CURLE_OK)
115+
throw new InvalidOperationException(
116+
$"curl_easy_perform returned error code {code}"
117+
);
118+
119+
ReportHttpVersion(handle, onHttpVersion);
120+
return headers;
121+
}
122+
finally
123+
{
124+
headerPin.Free();
125+
if (xferPin.IsAllocated)
126+
xferPin.Free();
127+
LibCurl.curl_easy_cleanup(handle);
128+
}
129+
}
130+
131+
private static void ReportHttpVersion(IntPtr handle, Action<string>? cb)
132+
{
133+
if (cb is null)
134+
return;
135+
LibCurl.curl_easy_getinfo_long(handle, LibCurl.CURLINFO_HTTP_VERSION, out long v);
136+
cb(
137+
v switch
138+
{
139+
10 => "HTTP/1.0",
140+
11 => "HTTP/1.1",
141+
20 => "HTTP/2",
142+
30 => "HTTP/3",
143+
_ => $"HTTP/?",
144+
}
145+
);
146+
}
147+
148+
// Installs CURLOPT_XFERINFOFUNCTION for cancellation and optional progress reporting.
149+
// Skipped entirely when neither is needed to avoid the NOPROGRESS overhead.
150+
private static GCHandle InstallXferCallback(
151+
IntPtr handle,
152+
CancellationToken ct,
153+
Action<double>? onProgress = null,
154+
Action<double>? onSpeed = null
155+
)
156+
{
157+
if (!ct.CanBeCanceled && onProgress is null && onSpeed is null)
158+
return default;
159+
160+
LibCurl.XferInfoCallback cb = (_, total, now, _, _) =>
161+
{
162+
if (ct.IsCancellationRequested)
163+
return 1;
164+
if (onProgress is not null && total > 0)
165+
onProgress(now / (double)total);
166+
if (onSpeed is not null)
167+
{
168+
LibCurl.curl_easy_getinfo_long(
169+
handle,
170+
LibCurl.CURLINFO_SPEED_DOWNLOAD_T,
171+
out long bps
172+
);
173+
onSpeed(bps);
174+
}
175+
return 0;
176+
};
177+
var pin = GCHandle.Alloc(cb);
178+
LibCurl.curl_easy_setopt_long(handle, LibCurl.CURLOPT_NOPROGRESS, 0L);
179+
LibCurl.curl_easy_setopt_xcb(handle, LibCurl.CURLOPT_XFERINFOFUNCTION, cb);
180+
return pin;
181+
}
182+
183+
private static nuint OnWrite(IntPtr data, nuint size, nuint nmemb, IntPtr userdata)
184+
{
185+
nuint total = size * nmemb;
186+
var sb = (StringBuilder)GCHandle.FromIntPtr(userdata).Target!;
187+
sb.Append(Marshal.PtrToStringUTF8(data, (int)total));
188+
return total;
189+
}
190+
191+
private static unsafe nuint OnWriteFile(IntPtr data, nuint size, nuint nmemb, IntPtr userdata)
192+
{
193+
nuint total = size * nmemb;
194+
var fs = (FileStream)GCHandle.FromIntPtr(userdata).Target!;
195+
fs.Write(new ReadOnlySpan<byte>((void*)data, (int)total));
196+
return total;
197+
}
198+
199+
private static nuint OnHeader(IntPtr data, nuint size, nuint nmemb, IntPtr userdata)
200+
{
201+
nuint total = size * nmemb;
202+
var line = Marshal.PtrToStringUTF8(data, (int)total)?.Trim();
203+
if (string.IsNullOrEmpty(line) || line.StartsWith("HTTP/"))
204+
return total;
205+
206+
int colon = line.IndexOf(':');
207+
if (colon > 0)
208+
{
209+
var headers = (Dictionary<string, string>)GCHandle.FromIntPtr(userdata).Target!;
210+
headers[line[..colon].Trim()] = line[(colon + 1)..].Trim();
211+
}
212+
return total;
213+
}
214+
215+
public void Dispose()
216+
{
217+
LibCurl.curl_global_cleanup();
218+
}
219+
220+
private static readonly HashSet<string> Impersonations =
221+
[
222+
"chrome145",
223+
"chrome142",
224+
// "firefox147",
225+
"safari2601",
226+
];
227+
228+
// choose an impersonation randomly to be used for this session's requests to moddb
229+
private static readonly string Impersonation = Impersonations.ElementAt(
230+
Random.Shared.Next(Impersonations.Count)
231+
);
232+
}

0 commit comments

Comments
 (0)