From a92d00bec4b3fd35bc2a3cb76e4ada2989761cb5 Mon Sep 17 00:00:00 2001 From: dichternebel Date: Sat, 6 Jan 2024 19:05:04 +0100 Subject: [PATCH] Switch from HttpListener to TcpListener to make calls from LAN possible --- App.config | 14 ---- DataService.cs | 143 +++++++++++++------------------------ Properties/AssemblyInfo.cs | 8 +-- README.md | 5 +- TelemetryJsonService.cs | 3 +- shared-template.js | 27 ++++--- 6 files changed, 68 insertions(+), 132 deletions(-) diff --git a/App.config b/App.config index 1156d13..8f575b3 100644 --- a/App.config +++ b/App.config @@ -4,20 +4,6 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DataService.cs b/DataService.cs index 3925ca8..8cbcc49 100644 --- a/DataService.cs +++ b/DataService.cs @@ -1,136 +1,89 @@ using System; using System.Configuration; +using System.Linq; using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; - namespace TelemetryJsonService { - // Source: http://www.gabescode.com/dotnet/2018/11/01/basic-HttpListener-web-service.html internal static class DataService { - private static volatile bool _keepGoing = true; - - private static HttpListener Listener; - private static Task _mainLoop; - - public static string Address { get; private set; } public static int Port { get; private set; } public static string JsonData { get; set; } public static void StartWebServer() { - if (_mainLoop != null && !_mainLoop.IsCompleted) return; - - Address = ConfigurationManager.AppSettings["address"]; Port = int.Parse(ConfigurationManager.AppSettings["port"]); - Listener = new HttpListener { Prefixes = { $"http://{Address}:{Port}/" } }; - _mainLoop = MainLoop(); + Task.Run(() => StartTcpListener(Port)); } - /// - /// Call this to stop the web server. It will not kill any requests currently being processed. - /// - public static void StopWebServer() + static async Task StartTcpListener(int port) { - _keepGoing = false; - lock (Listener) + TcpListener tcpListener = new TcpListener(IPAddress.Any, port); + tcpListener.Start(); + Console.WriteLine($"Listening on port {port}"); + + try { - //Use a lock so we don't kill a request that's currently being processed - Listener.Stop(); + while (true) + { + TcpClient client = await tcpListener.AcceptTcpClientAsync(); + _ = HandleClientAsync(client); + } } - try + catch (Exception ex) { - _mainLoop.Wait(); + Console.WriteLine($"Exception: {ex.Message}"); } - catch { /* ¯\_(ツ)_/¯ */ } - } - - /// - /// The main loop to handle requests into the HttpListener - /// - /// - private static async Task MainLoop() - { - Listener.Start(); - while (_keepGoing) + finally { - try - { - //GetContextAsync() returns when a new request come in - var context = await Listener.GetContextAsync(); - lock (Listener) - { - if (_keepGoing) ProcessRequest(context); - } - } - catch (Exception e) - { - if (e is HttpListenerException) return; //this gets thrown when the listener is stopped - //TODO: Log the exception - } + tcpListener.Stop(); + Console.WriteLine("Listener stopped."); } } /// /// Handle an incoming request /// - /// The context of the incoming request - private static void ProcessRequest(HttpListenerContext context) + static async Task HandleClientAsync(TcpClient tcpClient) { - using (var response = context.Response) + using (NetworkStream stream = tcpClient.GetStream()) { - try - { - var handled = false; - - // CORS - response.AppendHeader("Access-Control-Allow-Origin", "*"); + byte[] buffer = new byte[1024]; + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); + string request = Encoding.UTF8.GetString(buffer, 0, bytesRead); + Console.WriteLine($"Received request: {request}"); - switch (context.Request.Url.AbsolutePath) - { - // Define routes - case "/": - switch (context.Request.HttpMethod) - { - case "OPTIONS": - response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); - response.AddHeader("Access-Control-Allow-Methods", "OPTIONS, GET"); - response.AddHeader("Access-Control-Max-Age", "1728000"); - response.StatusCode = 200; - handled = true; - break; + // Parse the HTTP method + string httpMethod = "GET"; + string[] requestLines = request.Split('\n'); + string firstLine = requestLines.FirstOrDefault(); + string[] tokens = firstLine?.Split(' '); - case "GET": - response.ContentType = "application/json"; - var buffer = Encoding.UTF8.GetBytes(JsonData); - response.ContentLength64 = buffer.Length; - response.OutputStream.Write(buffer, 0, buffer.Length); - handled = true; - break; - - } - break; - } - if (!handled) - { - response.StatusCode = 404; - } - } - catch (Exception e) + if (tokens != null && tokens.Length >= 2) { - //Return the exception details to the client - response.StatusCode = 500; - response.ContentType = "application/json"; - var buffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(e)); - response.ContentLength64 = buffer.Length; - response.OutputStream.Write(buffer, 0, buffer.Length); + httpMethod = tokens[0]; + Console.WriteLine($"HTTP Method: {httpMethod}"); + } + + string responseText = "HTTP/1.1 200 OK\r\n" + + "Content-Type: application/json\r\n" + + "Access-Control-Allow-Origin: *\r\n" + // Allow requests from any origin + "Access-Control-Allow-Methods: GET, OPTIONS\r\n" + // Specify allowed methods + "Access-Control-Allow-Headers: Content-Type, Accept, X-Requested-With\r\n\r\n"; // Specify allowed headers - //TODO: Log the exception + if ( httpMethod == "GET" ) + { + responseText += JsonData; } + + byte[] responseBytes = Encoding.UTF8.GetBytes(responseText); + await stream.WriteAsync(responseBytes, 0, responseBytes.Length); } + + tcpClient.Close(); } } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index b65ac4c..2eaeebc 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -8,9 +8,9 @@ [assembly: AssemblyTitle("TelemetryJsonService")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] +[assembly: AssemblyCompany("dichternebel")] [assembly: AssemblyProduct("TelemetryJsonService")] -[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyCopyright("Copyright © dichternebel 2022")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.9.2.0")] -[assembly: AssemblyFileVersion("0.9.2.0")] +[assembly: AssemblyVersion("0.9.5.0")] +[assembly: AssemblyFileVersion("0.9.5.0")] diff --git a/README.md b/README.md index f28f72b..7c7d78f 100644 --- a/README.md +++ b/README.md @@ -59,14 +59,15 @@ If you want to create e.g. a **dashboard for ETS2** instead of overlays it might - Download the [SCS-SDK-plugin v.1.12.1](https://github.com/RenCloud/scs-sdk-plugin/releases/tag/V.1.12.1) and copy the `Win64\scs-telemetry.dll` into `[...]\SteamLibrary\steamapps\common\Euro Truck Simulator 2\bin\win_x64\plugins` - Download this thing from the [Releases](https://github.com//dichternebel/scs-telemetry-json-service/releases/latest/) section and extract it to wherever you want - Start the executable and keep it running (it's located to the system tray then) +- Please confirm the firewall exception if you want to access the service from other LAN devices ## Limitations - Must run on the same machine as your game ## Customization -- Change the used port and address in `TelemetryJsonService.exe.config` to match your needs +- Change the used port in `TelemetryJsonService.exe.config` to match your needs -## Using it in OBS +## Using overlays in OBS - Add two browser sources to OBS for the job and the status telemetry overlays - Change their width and height sizes filling your OBS resolution (not manually, go to the settings dialog of the browser source!) - change the source to **local file** and chose `[...]\scs-telemetry-json-service\overlays\overlay-job.html` and the other to `[...]\scs-telemetry-json-service\overlays\overlay-status.html` diff --git a/TelemetryJsonService.cs b/TelemetryJsonService.cs index 2ba7951..5f1b16c 100644 --- a/TelemetryJsonService.cs +++ b/TelemetryJsonService.cs @@ -27,7 +27,7 @@ public TelemetryJsonService() DataService.StartWebServer(); this.UpdateSharedJs(); - this.tbUrl.Text = $"http://{DataService.Address}:{DataService.Port}/"; + this.tbUrl.Text = $"http://localhost:{DataService.Port}/"; } catch (Exception ex) { @@ -74,7 +74,6 @@ private void UpdateSharedJs() resourceContent = reader.ReadToEnd(); } - resourceContent = resourceContent.Replace("{{address}}", DataService.Address); resourceContent = resourceContent.Replace("{{port}}", DataService.Port.ToString()); File.WriteAllText(Path.Combine(baseDir, "overlays/js", "shared.js"), resourceContent); } diff --git a/shared-template.js b/shared-template.js index 3554584..9985292 100644 --- a/shared-template.js +++ b/shared-template.js @@ -41,33 +41,30 @@ function setPollingInterval() { // Slow down polling interval when connection is lost function checkPollingInterval() { - if (retryCounter === 10 && timer === 100) { + if (retryCounter === 10 && timer === 1000) { isServiceConnected = false; - console.log("Too many connection errors: changing interval to 1sec..."); - timer = 1000; - setPollingInterval(); - } - if (retryCounter === 19 && timer === 1000) { - console.log( - "Too many connection errors: changing interval to 10secs and hiding '.game-connected'...", - ); + console.log("Too many connection errors: changing interval to 10sec and hiding '.game-connected'..."); timer = 10000; setPollingInterval(); - $(".game-connected").css({ - visibility: "hidden", + visibility: "hidden" }); - } else if (retryCounter === 0 && timer > 100) { + } + if (retryCounter === 19 && timer === 10000) { + console.log("Too many connection errors: changing interval to 30secs..."); + timer = 30000; + setPollingInterval(); + } else if (retryCounter === 0 && timer > 1000) { isServiceConnected = true; - console.log("We are back to business! Changing interval to 100ms..."); - timer = 100; + console.log("We are back to business! Changing interval to 1000ms..."); + timer = 1000; setPollingInterval(); } } // Run that creepy thing! function execute() { - $.getJSON("http://{{address}}:{{port}}/", function (json) { + $.getJSON("http://localhost:{{port}}/", function (json) { data = json; }) .done(function () {