From dff43288e215ba3554ecde1ecf751ffdebfd6c30 Mon Sep 17 00:00:00 2001 From: meelstorm Date: Fri, 17 Jul 2026 17:44:05 +0000 Subject: [PATCH] feat(backend): wire ASP.NET startup with WebSocket endpoint and DI --- Backend/LanderConfig.cs | 34 +++++++++++++++++++++++++++ Backend/Program.cs | 51 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 Backend/LanderConfig.cs diff --git a/Backend/LanderConfig.cs b/Backend/LanderConfig.cs new file mode 100644 index 0000000..3dd57bb --- /dev/null +++ b/Backend/LanderConfig.cs @@ -0,0 +1,34 @@ +namespace Backend; + +public sealed class LanderConfig +{ + public string CliPath { get; set; } = "publish/GameCli/GameCli"; + public string ModelPath { get; set; } = "models/ppo_lander.onnx"; +} + +/// +/// Walks up from to find the repo root +/// (marker file: GameCli.sln) and resolves the configured paths against it. +/// +public static class PathResolver +{ + public static string RepoRoot() + { + var dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "GameCli.sln"))) return dir; + var parent = Directory.GetParent(dir); + if (parent is null) break; + dir = parent.FullName; + } + throw new InvalidOperationException( + "could not locate repo root (no GameCli.sln found in any ancestor)"); + } + + public static string Resolve(string relativeOrAbsolute) + { + if (Path.IsPathRooted(relativeOrAbsolute)) return relativeOrAbsolute; + return Path.GetFullPath(Path.Combine(RepoRoot(), relativeOrAbsolute)); + } +} diff --git a/Backend/Program.cs b/Backend/Program.cs index f0282f0..a965806 100644 --- a/Backend/Program.cs +++ b/Backend/Program.cs @@ -1,5 +1,52 @@ -// Placeholder. Rewritten in Task 6. +using System.Net.WebSockets; +using Backend; + var builder = WebApplication.CreateBuilder(args); + +// -------- Config -------- +builder.Services.Configure(builder.Configuration.GetSection("Lander")); + +// -------- Singletons -------- +builder.Services.AddSingleton(sp => +{ + var cfg = sp.GetRequiredService>().Value; + var absolute = PathResolver.Resolve(cfg.ModelPath); + return new PolicyRunner(absolute); +}); + +builder.Services.AddLogging(); + var app = builder.Build(); -app.MapGet("/", () => "GameCli Backend placeholder"); +app.UseWebSockets(); + +// -------- Health -------- +app.MapGet("/", () => "GameCli Backend"); + +// -------- WebSocket endpoint -------- +app.Map("/ws/game", async (HttpContext ctx, + PolicyRunner policy, + Microsoft.Extensions.Options.IOptions cfg, + ILoggerFactory loggerFactory) => +{ + if (!ctx.WebSockets.IsWebSocketRequest) + { + ctx.Response.StatusCode = 400; + return; + } + using var socket = await ctx.WebSockets.AcceptWebSocketAsync(); + var cliPath = PathResolver.Resolve(cfg.Value.CliPath); + var proc = new GameProcess(cliPath); + var logger = loggerFactory.CreateLogger(); + await using var session = new GameSession(socket, proc, policy, logger); + try + { + await session.RunAsync(ctx.RequestAborted); + } + catch (WebSocketException) { /* client disconnected */ } + catch (OperationCanceledException) { /* shutdown */ } +}); + app.Run(); + +// -------- Public program class so Backend.Tests can use WebApplicationFactory -------- +public partial class Program { }