Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b35dd86
bump to net7_0
barncastle May 27, 2023
31482fa
rename Requester to something more appropriate
barncastle May 27, 2023
91665e4
rename Options.Create more appropriately
barncastle May 27, 2023
297d7be
remove old endpoint
barncastle May 27, 2023
824ec09
prep models for new endpoint implementation
barncastle May 27, 2023
ce2a1be
replace JSON.net and reimplement endpoint structure
barncastle May 27, 2023
b310246
bump CommandLineParser
barncastle May 27, 2023
74da8f5
use new net7_0 regex generator
barncastle May 27, 2023
bd974a9
use HttpMethod properly
barncastle May 27, 2023
0fbe72c
minor refactoring + global usings
barncastle May 27, 2023
23c44c3
misc
barncastle May 28, 2023
baacdf1
fix delete method
barncastle May 28, 2023
5188387
pattern matching
barncastle May 28, 2023
c6b879d
minor SnakeCaseNamingPolicy performance increase
barncastle May 28, 2023
d54e180
refactor InstallEndpoint validation.. again
barncastle May 30, 2023
c4855bc
this is handled within BaseEndpoint
barncastle May 30, 2023
fe51db9
misc
barncastle May 30, 2023
9c6b795
misc
barncastle May 30, 2023
83a6492
fix
barncastle May 30, 2023
8e85935
add some requested specialist features
barncastle Jun 2, 2023
2eb7892
readonly variable naming conventions
barncastle Jun 5, 2023
e08695d
misc
barncastle Jun 5, 2023
f349fda
over engineer agent background tasks
barncastle Jun 9, 2025
eb78b85
inline enums
barncastle Jun 9, 2025
5872c18
update agent process code
barncastle Jun 9, 2025
35223f2
update to net8_0
barncastle Jun 9, 2025
91b27fb
switch to System.CommandLine
barncastle Jul 7, 2025
ae3d5c6
fix nullable warnings
barncastle Jul 7, 2025
2d23f50
minor argument tweaks
barncastle Jul 7, 2025
779c91a
fix potential memory leak
barncastle Jul 7, 2025
4dc8aa6
update readme
barncastle Aug 8, 2025
30dd1f7
merge net8_0
barncastle Aug 8, 2025
5fc9d01
update publish profile
barncastle Aug 8, 2025
dc80bcb
update install endpoint
barncastle Aug 29, 2025
3aa2d98
fix json serialization
barncastle Aug 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 57 additions & 29 deletions BNetInstaller/AgentApp.cs
Original file line number Diff line number Diff line change
@@ -1,61 +1,75 @@
using System;
using System.ComponentModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using BNetInstaller.Endpoints.Agent;
using BNetInstaller.Endpoints.Game;
using BNetInstaller.Endpoints.Install;
using BNetInstaller.Endpoints.Repair;
using BNetInstaller.Endpoints.Update;
using BNetInstaller.Endpoints.Version;

namespace BNetInstaller;

internal class AgentApp : IDisposable
internal sealed class AgentApp : IDisposable
{
public readonly AgentEndpoint AgentEndpoint;
public readonly InstallEndpoint InstallEndpoint;
public readonly UpdateEndpoint UpdateEndpoint;
public readonly RepairEndpoint RepairEndpoint;
public readonly GameEndpoint GameEndpoint;
public readonly VersionEndpoint VersionEndpoint;

private readonly string AgentPath;
private readonly int Port = 5050;

private Process Process;
private Requester Requester;
private readonly Process _process;
private readonly int _port;
private readonly AgentClient _client;

public AgentApp()
{
AgentPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Battle.net", "Agent", "Agent.exe");

if (!StartProcess())
if (!StartProcess(out _process, out _port))
{
Console.WriteLine("Please ensure Battle.net is installed and has recently been opened.");
Console.WriteLine("Please ensure Battle.net is installed and has recently been signed in to.");
Environment.Exit(0);
}

AgentEndpoint = new(Requester);
InstallEndpoint = new(Requester);
UpdateEndpoint = new(Requester);
RepairEndpoint = new(Requester);
GameEndpoint = new(Requester);
VersionEndpoint = new(Requester);
_client = new(_port);

AgentEndpoint = new(_client);
InstallEndpoint = new(_client);
UpdateEndpoint = new(_client);
RepairEndpoint = new(_client);
VersionEndpoint = new(_client);
}

private bool StartProcess()
private static bool StartProcess(out Process process, out int port)
{
if (!File.Exists(AgentPath))
(process, port) = (null, -1);

var agentPath = GetAgentPath();

if (!File.Exists(agentPath))
{
Console.WriteLine("Unable to find Agent.exe.");
return false;
}

try
{
Process = Process.Start(AgentPath, $"--port={Port}");
Requester = new Requester(Port);
process = Process.Start(new ProcessStartInfo(agentPath)
{
Arguments = "--internalclienttools",
UseShellExecute = true,
});

// detect listening port
while (process is { HasExited: false } && port == -1)
{
Thread.Sleep(250);
port = NativeMethods.GetProcessListeningPort(process.Id);
}

if (process is not { HasExited: false } || port == -1)
{
Console.WriteLine("Unable to connect to Agent.exe.");
return false;
}

return true;
}
catch (Win32Exception)
Expand All @@ -65,12 +79,26 @@ private bool StartProcess()
}
}

private static string GetAgentPath()
{
var agentDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Battle.net", "Agent");
var parentPath = Path.Combine(agentDirectory, "Agent.exe");
var parentVersion = 0;

// read parent Agent.exe version
if (File.Exists(parentPath))
parentVersion = FileVersionInfo.GetVersionInfo(parentPath).ProductPrivatePart;

// return expected child Agent path
return Path.Combine(agentDirectory, $"Agent.{parentVersion}", "Agent.exe");
}

public void Dispose()
{
if (Process?.HasExited == false)
Process.Kill();
if (_process?.HasExited == false)
_process.Kill();

Requester?.Dispose();
Process?.Dispose();
_client?.Dispose();
_process?.Dispose();
}
}
93 changes: 93 additions & 0 deletions BNetInstaller/AgentClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Diagnostics;

namespace BNetInstaller;

internal sealed class AgentClient : IDisposable
{
private readonly HttpClient _client;
private readonly JsonSerializerOptions _serializerOptions;

public AgentClient(int port)
{
_client = new();
_client.DefaultRequestHeaders.Add("User-Agent", "phoenix-agent/1.0");
_client.BaseAddress = new($"http://127.0.0.1:{port}");

_serializerOptions = new()
{
PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance,
};
}

public void SetAuthToken(string token)
{
_client.DefaultRequestHeaders.Add("Authorization", token);
}

public async Task<HttpResponseMessage> SendAsync(string endpoint, HttpMethod method, string content = null)
{
var request = new HttpRequestMessage(method, endpoint);

if (!string.IsNullOrEmpty(content))
request.Content = new StringContent(content);

var response = await _client.SendAsync(request);

if (!response.IsSuccessStatusCode)
await HandleRequestFailure(response, endpoint);

return response;
}

public async Task<HttpResponseMessage> SendAsync<T>(string endpoint, HttpMethod method, T payload = null) where T : class
{
if (payload == null)
return await SendAsync(endpoint, method);
else
return await SendAsync(endpoint, method, JsonSerializer.Serialize(payload, _serializerOptions));
}

private static async Task HandleRequestFailure(HttpResponseMessage response, string endpoint)
{
var statusCode = response.StatusCode;
var content = await response.Content.ReadAsStringAsync();
Debug.WriteLine($"{(int)statusCode} {statusCode}: {endpoint} {content}");
}

public void Dispose()
{
_client.Dispose();
}
}

file class SnakeCaseNamingPolicy : JsonNamingPolicy
{
public static readonly SnakeCaseNamingPolicy Instance = new();

public override string ConvertName(string name)
{
if (string.IsNullOrEmpty(name))
return string.Empty;

Span<char> input = stackalloc char[0x100];
Span<char> output = stackalloc char[0x100];

// lowercase the name
var inputLen = name.AsSpan().ToLowerInvariant(input);
var outputLen = 0;

for (var i = 0; i < inputLen; i++)
{
// prefix an underscore before any capitals
// excluding the initial character
if (name[i] is >= 'A' and <= 'Z' && i != 0)
output[outputLen++] = '_';

// write the lowercase character to the output
output[outputLen++] = input[i];
}

return new string(output[..outputLen]);
}
}
14 changes: 11 additions & 3 deletions BNetInstaller/BNetInstaller.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>warnings</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" />
</ItemGroup>

<ItemGroup>
<Using Include="BNetInstaller.Models" />
<Using Include="System.Text.Json" />
<Using Include="System.Text.Json.Nodes" />
</ItemGroup>

</Project>
9 changes: 0 additions & 9 deletions BNetInstaller/Constants/HttpVerb.cs

This file was deleted.

19 changes: 0 additions & 19 deletions BNetInstaller/Constants/Locale.cs

This file was deleted.

7 changes: 0 additions & 7 deletions BNetInstaller/Constants/Mode.cs

This file was deleted.

31 changes: 6 additions & 25 deletions BNetInstaller/Endpoints/Agent/AgentEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
using System;
using System.Threading.Tasks;
using BNetInstaller.Constants;
using Newtonsoft.Json.Linq;
namespace BNetInstaller.Endpoints.Agent;

namespace BNetInstaller.Endpoints.Agent;

internal class AgentEndpoint : BaseEndpoint
internal sealed class AgentEndpoint(AgentClient client) : BaseEndpoint<NullModel>("agent", client)
{
public AgentEndpoint(Requester requester) : base("agent", requester)
{
}

public async Task Delete()
protected override void ValidateResponse(JsonNode response, string content)
{
await Requester.SendAsync(Endpoint, HttpVerb.DELETE);
}

public async Task<JToken> Get()
{
using var response = await Requester.SendAsync(Endpoint, HttpVerb.GET);
return await Deserialize(response);
}
base.ValidateResponse(response, content);

protected override void ValidateResponse(JToken response, string content)
{
var token = response.Value<string>("authorization");
var token = response["authorization"]?.GetValue<string>();

if (string.IsNullOrEmpty(token))
throw new Exception("Agent Error: Unable to authenticate.", new(content));

Requester.SetAuthorization(token);
base.ValidateResponse(response, content);
Client.SetAuthToken(token);
}
}
51 changes: 35 additions & 16 deletions BNetInstaller/Endpoints/BaseEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace BNetInstaller.Endpoints;

namespace BNetInstaller.Endpoints;

internal abstract class BaseEndpoint
internal abstract class BaseEndpoint<T>(string endpoint, AgentClient client) where T : class, IModel, new()
{
public string Endpoint { get; }
public string Endpoint { get; } = endpoint;
public T Model { get; } = new();

protected AgentClient Client { get; } = client;

public virtual async Task<JsonNode> Get()
{
using var response = await Client.SendAsync(Endpoint, HttpMethod.Get);
return await Deserialize(response);
}

public virtual async Task<JsonNode> Post()
{
if (Model is NullModel)
return default;

using var response = await Client.SendAsync(Endpoint, HttpMethod.Post, Model);
return await Deserialize(response);
}

protected Requester Requester { get; }
public virtual async Task<JsonNode> Put()
{
if (Model is NullModel)
return default;

using var response = await Client.SendAsync(Endpoint, HttpMethod.Put, Model);
return await Deserialize(response);
}

protected BaseEndpoint(string endpoint, Requester requester)
public virtual async Task Delete()
{
Endpoint = endpoint;
Requester = requester;
await Client.SendAsync(Endpoint, HttpMethod.Delete);
}

protected async Task<JToken> Deserialize(HttpResponseMessage response)
protected async Task<JsonNode> Deserialize(HttpResponseMessage response)
{
var content = await response.Content.ReadAsStringAsync();
var result = JToken.Parse(content);
var result = JsonNode.Parse(content);
ValidateResponse(result, content);
return result;
}

protected virtual void ValidateResponse(JToken response, string content)
protected virtual void ValidateResponse(JsonNode response, string content)
{
var errorCode = response.Value<float?>("error");
var errorCode = response["error"]?.GetValue<float?>();
if (errorCode > 0)
throw new Exception($"Agent Error: {errorCode}", new(content));
}
Expand Down
Loading