Skip to content

Commit 3eaab50

Browse files
committed
Use Unfucked.HTTP; #8: Program crashes on socket exception when setting qBittorrent listening port
1 parent 6cbbc0d commit 3eaab50

8 files changed

Lines changed: 187 additions & 60 deletions

File tree

PortForwardingService/PortForwardingService.csproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@
2828
</PropertyGroup>
2929
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
3030
<PlatformTarget>AnyCPU</PlatformTarget>
31-
<DebugType>pdbonly</DebugType>
31+
<DebugType>embedded</DebugType>
3232
<Optimize>true</Optimize>
3333
<OutputPath>bin\Release\</OutputPath>
3434
<DefineConstants>TRACE</DefineConstants>
3535
<ErrorReport>prompt</ErrorReport>
3636
<WarningLevel>4</WarningLevel>
37+
<DebugSymbols>true</DebugSymbols>
3738
</PropertyGroup>
3839
<PropertyGroup>
3940
<StartupObject />
@@ -83,8 +84,11 @@
8384
<PackageReference Include="KoKo">
8485
<Version>2.2.0</Version>
8586
</PackageReference>
86-
<PackageReference Include="Newtonsoft.Json">
87-
<Version>13.0.3</Version>
87+
<PackageReference Include="System.Net.Http.Json">
88+
<Version>9.0.4</Version>
89+
</PackageReference>
90+
<PackageReference Include="Unfucked.HTTP">
91+
<Version>0.0.1-beta.3</Version>
8892
</PackageReference>
8993
</ItemGroup>
9094
<ItemGroup />

PortForwardingService/Properties/AssemblyInfo.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[assembly: AssemblyConfiguration("")]
1010
[assembly: AssemblyCompany("Ben Hutchison")]
1111
[assembly: AssemblyProduct("PortForwardingService")]
12-
[assembly: AssemblyCopyright(2024 Ben Hutchison")]
12+
[assembly: AssemblyCopyright(2025 Ben Hutchison")]
1313
[assembly: AssemblyTrademark("")]
1414
[assembly: AssemblyCulture("")]
1515

@@ -31,5 +31,5 @@
3131
// You can specify all the values or you can default the Build and Revision Numbers
3232
// by using the '*' as shown below:
3333
// [assembly: AssemblyVersion("1.0.*")]
34-
[assembly: AssemblyVersion("1.1.0.0")]
35-
[assembly: AssemblyFileVersion("1.1.0.0")]
34+
[assembly: AssemblyVersion("1.2.0.0")]
35+
[assembly: AssemblyFileVersion("1.2.0.0")]

PortForwardingService/packages.lock.json

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,126 @@
1414
"resolved": "2.2.0",
1515
"contentHash": "8sgJYZINKgsuRYlvuuiXw+3GmheMd7XZO11ve9czTrHwd0rVIRSY0kLPQOkT3kxwILhEkNFXEK44pJy9hDF9Ww=="
1616
},
17-
"Newtonsoft.Json": {
17+
"System.Net.Http.Json": {
1818
"type": "Direct",
19-
"requested": "[13.0.3, )",
20-
"resolved": "13.0.3",
21-
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
19+
"requested": "[9.0.4, )",
20+
"resolved": "9.0.4",
21+
"contentHash": "t34SoSx8l/O5kv7IwYXENjQOTpVKFkH3ydZejcZCsNsymthJrdCguvHFcbP3ayeBOXdm4Bq8bhmVGCgr0RXIQg==",
22+
"dependencies": {
23+
"System.Buffers": "4.5.1",
24+
"System.Memory": "4.5.5",
25+
"System.Text.Json": "9.0.4",
26+
"System.Threading.Tasks.Extensions": "4.5.4"
27+
}
28+
},
29+
"Unfucked.HTTP": {
30+
"type": "Direct",
31+
"requested": "[0.0.1-beta.3, )",
32+
"resolved": "0.0.1-beta.3",
33+
"contentHash": "JMXFWAEEOBS3XrGnDoSKuDZeQnrg13HsV4AhAG7XbD6iFhB991bCE5rsKQtGRB5956o7DgYqwHw30gZOvcqJRQ==",
34+
"dependencies": {
35+
"System.Text.Json": "8.0.5",
36+
"Unfucked": "0.0.1-beta.2"
37+
}
38+
},
39+
"Microsoft.Bcl.AsyncInterfaces": {
40+
"type": "Transitive",
41+
"resolved": "9.0.4",
42+
"contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==",
43+
"dependencies": {
44+
"System.Threading.Tasks.Extensions": "4.5.4"
45+
}
46+
},
47+
"System.Buffers": {
48+
"type": "Transitive",
49+
"resolved": "4.5.1",
50+
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
51+
},
52+
"System.Collections.Immutable": {
53+
"type": "Transitive",
54+
"resolved": "9.0.4",
55+
"contentHash": "wfm2NgK22MmBe5qJjp52qzpkeDZKb4l9LbdubhZSehY1z4LS+lld6R+B+UQNb2AZRHu/QJlHxEUcRst5hIEejg==",
56+
"dependencies": {
57+
"System.Memory": "4.5.5",
58+
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
59+
}
60+
},
61+
"System.IO.Pipelines": {
62+
"type": "Transitive",
63+
"resolved": "9.0.4",
64+
"contentHash": "luF2Xba+lTe2GOoNQdZLe8q7K6s7nSpWZl9jIwWNMszN4/Yv0lmxk9HISgMmwdyZ83i3UhAGXaSY9o6IJBUuuA==",
65+
"dependencies": {
66+
"System.Buffers": "4.5.1",
67+
"System.Memory": "4.5.5",
68+
"System.Threading.Tasks.Extensions": "4.5.4"
69+
}
70+
},
71+
"System.Memory": {
72+
"type": "Transitive",
73+
"resolved": "4.5.5",
74+
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
75+
"dependencies": {
76+
"System.Buffers": "4.5.1",
77+
"System.Numerics.Vectors": "4.5.0",
78+
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
79+
}
80+
},
81+
"System.Numerics.Vectors": {
82+
"type": "Transitive",
83+
"resolved": "4.5.0",
84+
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
85+
},
86+
"System.Runtime.CompilerServices.Unsafe": {
87+
"type": "Transitive",
88+
"resolved": "6.0.0",
89+
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
90+
},
91+
"System.Text.Encodings.Web": {
92+
"type": "Transitive",
93+
"resolved": "9.0.4",
94+
"contentHash": "V+5cCPpk1S2ngekUs9nDrQLHGiWFZMg8BthADQr+Fwi59a8DdHFu26S2oi9Bfgv+d67bqmkPqctJXMEXiimXUg==",
95+
"dependencies": {
96+
"System.Buffers": "4.5.1",
97+
"System.Memory": "4.5.5",
98+
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
99+
}
100+
},
101+
"System.Text.Json": {
102+
"type": "Transitive",
103+
"resolved": "9.0.4",
104+
"contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==",
105+
"dependencies": {
106+
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
107+
"System.Buffers": "4.5.1",
108+
"System.IO.Pipelines": "9.0.4",
109+
"System.Memory": "4.5.5",
110+
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
111+
"System.Text.Encodings.Web": "9.0.4",
112+
"System.Threading.Tasks.Extensions": "4.5.4",
113+
"System.ValueTuple": "4.5.0"
114+
}
115+
},
116+
"System.Threading.Tasks.Extensions": {
117+
"type": "Transitive",
118+
"resolved": "4.5.4",
119+
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
120+
"dependencies": {
121+
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
122+
}
123+
},
124+
"System.ValueTuple": {
125+
"type": "Transitive",
126+
"resolved": "4.5.0",
127+
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
128+
},
129+
"Unfucked": {
130+
"type": "Transitive",
131+
"resolved": "0.0.1-beta.2",
132+
"contentHash": "J8VhdV4QyfN40LO+m5tCa4O1lmpQXPB5rK2fmcJ/uN+y0VGldzjVNJnUxe/sPntPCxWuFc6OOjjPF9f6GpYOMQ==",
133+
"dependencies": {
134+
"Microsoft.Bcl.AsyncInterfaces": "9.0.4",
135+
"System.Collections.Immutable": "9.0.4"
136+
}
22137
}
23138
},
24139
".NETFramework,Version=v4.8/win": {},
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#nullable enable
22

3-
using Newtonsoft.Json;
3+
using System.Text.Json.Serialization;
44

55
namespace PortForwardingService.qBittorrent.Data;
66

77
// Other preferences are excluded
88
// See full list at https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-preferences
99
internal class Preferences {
1010

11-
[JsonProperty("listen_port")]
11+
[JsonPropertyName("listen_port")]
1212
public ushort listeningPort { get; set; }
1313

1414
}

PortForwardingService/qBittorrent/Data/TransferInfo.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
#nullable enable
2-
using Newtonsoft.Json;
2+
3+
using System.Text.Json.Serialization;
34

45
namespace PortForwardingService.qBittorrent.Data;
56

67
internal class TransferInfo {
78

8-
[JsonProperty("connection_status")]
9+
[JsonPropertyName("connection_status")]
910
public ConnectionStatus connectionStatus { get; set; }
1011

11-
[JsonProperty("dht_nodes")]
12+
[JsonPropertyName("dht_nodes")]
1213
public uint dhtNodes { get; set; }
1314

14-
[JsonProperty("dl_info_data")]
15+
[JsonPropertyName("dl_info_data")]
1516
public ulong dlInfoData { get; set; }
1617

17-
[JsonProperty("dl_info_speed")]
18+
[JsonPropertyName("dl_info_speed")]
1819
public uint dlInfoSpeed { get; set; }
1920

20-
[JsonProperty("dl_rate_limit")]
21+
[JsonPropertyName("dl_rate_limit")]
2122
public uint dlRateLimit { get; set; }
2223

23-
[JsonProperty("up_info_data")]
24+
[JsonPropertyName("up_info_data")]
2425
public ulong upInfoData { get; set; }
2526

26-
[JsonProperty("up_info_speed")]
27+
[JsonPropertyName("up_info_speed")]
2728
public uint upInfoSpeed { get; set; }
2829

29-
[JsonProperty("up_rate_limit")]
30+
[JsonPropertyName("up_rate_limit")]
3031
public uint upRateLimit { get; set; }
3132

3233
internal enum ConnectionStatus {

PortForwardingService/qBittorrent/ListeningPortEditors/WebApiListeningPortEditor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace PortForwardingService.qBittorrent.ListeningPortEditors;
1010
internal class WebApiListeningPortEditor(QbittorrentClient client): ListeningPortEditor {
1111

1212
public async Task setListeningPort(ushort listeningPort) {
13-
await client.send(HttpMethod.Post, "app/setPreferences", new Preferences { listeningPort = listeningPort });
13+
(await client.send(HttpMethod.Post, "app/setPreferences", new Preferences { listeningPort = listeningPort })).Dispose();
1414

1515
Console.WriteLine($"Set qBittorrent listening port to {listeningPort} using Web API.");
1616
}

PortForwardingService/qBittorrent/QbittorrentClient.cs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,53 @@
11
#nullable enable
22

3-
using Newtonsoft.Json;
4-
using Newtonsoft.Json.Converters;
5-
using Newtonsoft.Json.Serialization;
63
using System;
74
using System.Collections.Generic;
8-
using System.IO;
95
using System.Net.Http;
10-
using System.Text;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
118
using System.Threading.Tasks;
9+
using Unfucked;
10+
using Unfucked.HTTP;
11+
using Unfucked.HTTP.Config;
1212

1313
namespace PortForwardingService.qBittorrent;
1414

1515
public class QbittorrentClient: IDisposable {
1616

17-
private readonly HttpClient httpClient = new();
18-
private readonly Uri baseUri = new("http://localhost:8080/api/v2/");
19-
private readonly JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { Converters = { new StringEnumConverter(new SnakeCaseNamingStrategy()) } });
17+
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } };
18+
19+
private readonly HttpClient httpClient = new UnfuckedHttpClient { Timeout = TimeSpan.FromSeconds(5) };
20+
private readonly WebTarget api;
21+
22+
public QbittorrentClient() {
23+
api = httpClient
24+
.Property(PropertyKey.JsonSerializerOptions, JsonOptions)
25+
.Target(new UrlBuilder("http", "localhost", 8080).Path("/api/v2"));
26+
}
2027

2128
/// <summary>
2229
/// Send an HTTP request to the qBittorrent JSON REST API and receive a response.
2330
/// </summary>
24-
/// <param name="method">HTTP verb to send</param>
31+
/// <param name="verb">HTTP verb to send</param>
2532
/// <param name="apiMethodSubPath">request URL path after the <c>/api/v2/</c>, such as <c>app/setPreferences</c></param>
2633
/// <param name="requestBody">optional object to be serialized to JSON and passed in the JSON form field, or <c>null</c> to not send a request body</param>
2734
/// <returns>the HTTP response</returns>
2835
/// <exception cref="HttpRequestException">if the response status code is ≥400</exception>
29-
public async Task<HttpResponseMessage> send(HttpMethod method, string apiMethodSubPath, object? requestBody = null) {
30-
FormUrlEncodedContent? formBody = (method == HttpMethod.Post || method == HttpMethod.Put) && requestBody != null ?
31-
new FormUrlEncodedContent([
32-
new KeyValuePair<string, string>("json", JsonConvert.SerializeObject(requestBody))
33-
]) : null;
34-
35-
HttpResponseMessage response = await httpClient.SendAsync(new HttpRequestMessage(method, new Uri(baseUri, apiMethodSubPath.TrimStart('/'))) { Content = formBody });
36-
response.EnsureSuccessStatusCode();
37-
return response;
38-
}
36+
public async Task<HttpResponseMessage> send(HttpMethod verb, string apiMethodSubPath, object? requestBody = null) =>
37+
await api.Path(sanitizeSubpath(apiMethodSubPath)).Send(verb, createBody(verb, requestBody));
3938

4039
/// <inheritdoc cref="send"/>
4140
/// <returns>deserialized response body</returns>
4241
/// <typeparam name="T">the type to deserialize from the response JSON body</typeparam>
43-
public async Task<T?> send<T>(HttpMethod method, string apiMethodSubPath, object? requestBody = null) {
44-
HttpResponseMessage response = await send(method, apiMethodSubPath, requestBody);
45-
using StreamReader streamReader = new(await response.Content.ReadAsStreamAsync(), Encoding.UTF8);
46-
using JsonTextReader jsonTextReader = new(streamReader);
47-
return jsonSerializer.Deserialize<T>(jsonTextReader);
48-
}
42+
public async Task<T?> send<T>(HttpMethod verb, string apiMethodSubPath, object? requestBody = null) =>
43+
await api.Path(sanitizeSubpath(apiMethodSubPath)).Send<T>(verb, createBody(verb, requestBody));
44+
45+
private static string sanitizeSubpath(string apiMethodSubPath) => apiMethodSubPath.TrimStart('/');
46+
47+
private static FormUrlEncodedContent? createBody(HttpMethod verb, object? requestBody) =>
48+
(verb == HttpMethod.Post || verb == HttpMethod.Put) && requestBody != null ? new FormUrlEncodedContent([
49+
new KeyValuePair<string, string>("json", JsonSerializer.Serialize(requestBody, JsonOptions)) // that's right, it's JSON inside form URL-encoding
50+
]) : null;
4951

5052
public void Dispose() {
5153
httpClient.Dispose();

PortForwardingService/qBittorrent/QbittorrentManager.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Net.Http;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12+
using Unfucked;
1213

1314
namespace PortForwardingService.qBittorrent;
1415

@@ -51,19 +52,23 @@ private bool isQbittorrentRunning() {
5152
public void listenForSocketErrors() {
5253
timer = new Timer(async _ => {
5354
if (isQbittorrentRunning()) {
54-
TransferInfo? transferInfo = await qbittorrentClient.send<TransferInfo>(HttpMethod.Get, "transfer/info");
55-
56-
if (transferInfo?.connectionStatus is not (TransferInfo.ConnectionStatus.CONNECTED or null) && piaForwardedPortMonitor.forwardedPort.Value is { } correctListeningPort) {
57-
Console.WriteLine(
58-
$"qBittorrent connection state is {transferInfo.connectionStatus} instead of {TransferInfo.ConnectionStatus.CONNECTED}, which means it likely failed to listen on the given IP address and port.");
59-
ushort temporaryListeningPort = (ushort) (correctListeningPort < ushort.MaxValue ? correctListeningPort + 1 : correctListeningPort - 1);
60-
Console.WriteLine($"Setting qBittorrent listening port to {temporaryListeningPort} and then to {correctListeningPort} in order to try to fix the socket listening error.");
61-
62-
await webApiListeningPortEditor.setListeningPort(temporaryListeningPort);
63-
await Task.Delay(TimeSpan.FromSeconds(3));
64-
await webApiListeningPortEditor.setListeningPort(correctListeningPort);
65-
} else {
66-
Console.WriteLine($"qBittorrent connection status is {transferInfo?.connectionStatus}, which does not indicate a socket error, ignoring.");
55+
try {
56+
TransferInfo? transferInfo = await qbittorrentClient.send<TransferInfo>(HttpMethod.Get, "transfer/info");
57+
58+
if (transferInfo?.connectionStatus is not (TransferInfo.ConnectionStatus.CONNECTED or null) && piaForwardedPortMonitor.forwardedPort.Value is { } correctListeningPort) {
59+
Console.WriteLine(
60+
$"qBittorrent connection state is {transferInfo.connectionStatus} instead of {TransferInfo.ConnectionStatus.CONNECTED}, which means it likely failed to listen on the given IP address and port.");
61+
ushort temporaryListeningPort = (ushort) (correctListeningPort < ushort.MaxValue ? correctListeningPort + 1 : correctListeningPort - 1);
62+
Console.WriteLine($"Setting qBittorrent listening port to {temporaryListeningPort} and then to {correctListeningPort} in order to try to fix the socket listening error.");
63+
64+
await webApiListeningPortEditor.setListeningPort(temporaryListeningPort);
65+
await Task.Delay(TimeSpan.FromSeconds(3));
66+
await webApiListeningPortEditor.setListeningPort(correctListeningPort);
67+
} else {
68+
Console.WriteLine($"qBittorrent connection status is {transferInfo?.connectionStatus}, which does not indicate a socket error, ignoring.");
69+
}
70+
} catch (HttpRequestException e) {
71+
Console.WriteLine("Failed to check qBittorrent connection state due to error: " + e.MessageChain());
6772
}
6873
} else {
6974
Console.WriteLine("qBittorrent is not running, not checking for socket errors.");

0 commit comments

Comments
 (0)