Skip to content

Commit

Permalink
Switch from HttpListener to TcpListener to make calls from LAN possible
Browse files Browse the repository at this point in the history
  • Loading branch information
dichternebel committed Jan 6, 2024
1 parent 9bd5a4c commit a92d00b
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 132 deletions.
14 changes: 0 additions & 14 deletions App.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,6 @@
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<appSettings>
<add key="address" value="localhost" />
<add key="port" value="6969" />
<add key="ClientSettingsProvider.ServiceUri" value="" />
</appSettings>
<system.web>
<membership defaultProvider="ClientAuthenticationMembershipProvider">
<providers>
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
</providers>
</membership>
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
<providers>
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
</providers>
</roleManager>
</system.web>
</configuration>
143 changes: 48 additions & 95 deletions DataService.cs
Original file line number Diff line number Diff line change
@@ -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));
}

/// <summary>
/// Call this to stop the web server. It will not kill any requests currently being processed.
/// </summary>
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 { /* ¯\_(ツ)_/¯ */ }
}

/// <summary>
/// The main loop to handle requests into the HttpListener
/// </summary>
/// <returns></returns>
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.");
}
}

/// <summary>
/// Handle an incoming request
/// </summary>
/// <param name="context">The context of the incoming request</param>
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();
}
}
}
8 changes: 4 additions & 4 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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("")]

Expand All @@ -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")]
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 1 addition & 2 deletions TelemetryJsonService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}
Expand Down
27 changes: 12 additions & 15 deletions shared-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit a92d00b

Please sign in to comment.