Spike: net48-only launcher (evaluate size vs net8 self-contained)

Converts the Launcher Service + Agent from net8/win-x64 self-contained to net48
framework-dependent, and makes Tesla.Contract net48-only (drops multi-targeting).
Both consumers (Console + Launcher) are now a single TFM.

Code changes for net48 (the only net8/netstandard2.1 APIs in use):
- RandomNumberGenerator.Fill -> RandomNumberGenerator.Create().GetBytes (3x)
- TcpListener.AcceptTcpClientAsync(ct) -> AcceptTcpClientAsync() + stop-on-cancel
- byte[].AsSpan().SequenceEqual -> Linq SequenceEqual (no System.Memory) (2x)
- PipeStream.Write(byte[]) / WriteAsync(byte[],ct) -> explicit (buf,0,len[,ct])
- Math.Clamp -> Math.Max/Min
The generic host (Microsoft.Extensions.Hosting 8.x + UseWindowsService) runs on
net48 unchanged. build.bat/install.bat updated for the folder-of-DLLs deploy;
solution platform reverted x64 -> AnyCPU.

RESULT — package size: ~3.7 MB on disk / 1.58 MB zipped, vs ~213 MB / 91 MB for
the net8 self-contained build (~50-58x smaller). net48 ships in Win10/11 so no
runtime prerequisite. 73 tests green; NOT re-validated on a live pod.

Spike branch for evaluation — do not merge without a pod re-test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-30 11:46:00 -05:00
co-authored by Claude Opus 4.8
parent d873b653c7
commit 1e2ec12f11
9 changed files with 79 additions and 73 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Multi-targeted: net48 for the Console, net8.0-windows for the Launcher Service. -->
<TargetFrameworks>net48;net8.0-windows</TargetFrameworks>
<!-- net48 only: both consumers (Console and Launcher) target .NET Framework 4.8.
(Was multi-targeted net48;net8.0-windows while the Launcher was on net8.) -->
<TargetFramework>net48</TargetFramework>
<!-- CRITICAL: the output assembly MUST be named TeslaConsoleLaunchLib at
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
+4 -3
View File
@@ -21,6 +21,7 @@ using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Text;
using System.Security.Cryptography;
using System.Linq;
using System.Threading;
using System.Diagnostics;
using Microsoft.Win32;
@@ -813,7 +814,7 @@ namespace TeslaSecureConfig
TcpReadExact(netStream, consoleIv);
var podIv = new byte[16];
RandomNumberGenerator.Fill(podIv);
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(podIv);
netStream.Write(podIv, 0, 16);
netStream.Flush();
@@ -836,7 +837,7 @@ namespace TeslaSecureConfig
var confReply = new byte[4];
TcpReadExact(ofb, confReply);
if (!confReply.AsSpan().SequenceEqual(confMsg))
if (!confReply.SequenceEqual(confMsg))
{
Log($"CONF mismatch: got [{BitConverter.ToString(confReply)}], expected CONF — key/IV mismatch.");
return;
@@ -1188,7 +1189,7 @@ namespace TeslaSecureConfig
private static string GenerateRandomString(int length)
{
var buf = new byte[length];
RandomNumberGenerator.Fill(buf);
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(buf);
var sb = new System.Text.StringBuilder(length);
for (int i = 0; i < length; i++)
sb.Append(Proto.Alphabet[buf[i] % Proto.Alphabet.Length]);
+3 -3
View File
@@ -422,8 +422,8 @@ namespace Tesla.Launcher.Agent
}
var resBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(response));
pipe.Write(BitConverter.GetBytes(resBytes.Length));
pipe.Write(resBytes);
pipe.Write(BitConverter.GetBytes(resBytes.Length), 0, 4);
pipe.Write(resBytes, 0, resBytes.Length);
pipe.Flush();
}
@@ -672,7 +672,7 @@ namespace Tesla.Launcher.Agent
var scalar = parms[0].ValueKind == JsonValueKind.Number
? (float)parms[0].GetDouble()
: throw new Exception("Invalid volume value");
SetMasterVolume(Math.Clamp(scalar, 0f, 1f));
SetMasterVolume(Math.Max(0f, Math.Min(1f, scalar)));
return null;
}
+5 -7
View File
@@ -1,20 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net48</TargetFramework>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<WarningsAsErrors></WarningsAsErrors>
<Version>4.11.4.1</Version>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>TeslaLauncherAgent</AssemblyName>
<RootNamespace>Tesla.Launcher.Agent</RootNamespace>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<StartupObject>Tesla.Launcher.Agent.AgentApplication</StartupObject>
</PropertyGroup>
@@ -25,8 +22,9 @@
</ItemGroup>
<ItemGroup>
<!-- Required by SecureConfig.cs for COM2/PlasmaIO serial output -->
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<!-- Agent parses configuring.json + the IPC JSON; built-in on net6, a package on net48. -->
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>
+29 -20
View File
@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
@@ -151,31 +152,39 @@ namespace Tesla.Launcher.Service
_listener.Start();
_logger.LogInformation("Listening. Waiting for Console connections...");
// net48's TcpListener has no CancellationToken overload for accept;
// stop the listener on cancellation so the pending accept unblocks.
using var stopReg = stoppingToken.Register(() => { try { _listener.Stop(); } catch { } });
while (!stoppingToken.IsCancellationRequested)
{
TcpClient client;
try
{
var client = await _listener.AcceptTcpClientAsync(stoppingToken);
_logger.LogInformation(
"Console connected from {EP}", client.Client.RemoteEndPoint);
// Handle each Console connection on its own thread.
// Frame reads are synchronous, so we offload to a
// thread-pool thread via Task.Run.
var task = Task.Run(
() => HandleConsoleClient(client, stoppingToken),
stoppingToken);
lock (_taskLock)
{
_clientTasks.Add(task);
_clientTasks.RemoveAll(t => t.IsCompleted);
}
client = await _listener.AcceptTcpClientAsync();
}
catch (OperationCanceledException) { break; }
catch (Exception) when (stoppingToken.IsCancellationRequested) { break; }
catch (Exception ex)
{
_logger.LogError(ex, "Error accepting Console connection");
continue;
}
_logger.LogInformation(
"Console connected from {EP}", client.Client.RemoteEndPoint);
// Handle each Console connection on its own thread.
// Frame reads are synchronous, so we offload to a
// thread-pool thread via Task.Run.
var task = Task.Run(
() => HandleConsoleClient(client, stoppingToken),
stoppingToken);
lock (_taskLock)
{
_clientTasks.Add(task);
_clientTasks.RemoveAll(t => t.IsCompleted);
}
}
}
@@ -209,7 +218,7 @@ namespace Tesla.Launcher.Service
// Generate and send our IV
var podIv = new byte[16];
RandomNumberGenerator.Fill(podIv);
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(podIv);
netStream.Write(podIv, 0, 16);
netStream.Flush();
@@ -230,7 +239,7 @@ namespace Tesla.Launcher.Service
_logger.LogWarning("CONF read failed: {Msg}", ex.Message);
return;
}
if (!confReply.AsSpan().SequenceEqual(confMsg))
if (!confReply.SequenceEqual(confMsg))
{
_logger.LogWarning("CONF mismatch — session key mismatch, dropping connection.");
return;
@@ -879,8 +888,8 @@ namespace Tesla.Launcher.Service
// Send IpcMessage
var reqBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg));
await pipe.WriteAsync(BitConverter.GetBytes(reqBytes.Length), ct);
await pipe.WriteAsync(reqBytes, ct);
await pipe.WriteAsync(BitConverter.GetBytes(reqBytes.Length), 0, 4, ct);
await pipe.WriteAsync(reqBytes, 0, reqBytes.Length, ct);
await pipe.FlushAsync(ct);
// Read IpcResponse
+17 -16
View File
@@ -1,20 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net48</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<WarningsAsErrors></WarningsAsErrors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>4.11.4.1</Version>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>TeslaLauncherService</AssemblyName>
<RootNamespace>Tesla.Launcher.Service</RootNamespace>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<WindowsService>true</WindowsService>
</PropertyGroup>
<ItemGroup>
@@ -23,22 +20,26 @@
</ItemGroup>
<ItemGroup>
<!-- net48 reference assemblies + GAC refs the framework-dependent build needs. -->
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.ServiceProcess" />
</ItemGroup>
<ItemGroup>
<!-- Generic host on .NET Framework: Microsoft.Extensions.Hosting(.WindowsServices)
8.x ship a net462 target, so they run on net48 unchanged. -->
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="8.*" />
<!-- Required by SecureConfig.cs for COM2/PlasmaIO serial output -->
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<!-- COM2 / PlasmaIO serial output in SecureConfig.cs -->
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Tesla.Net wire types now come from the shared contract (net6 build),
replacing the hand-maintained replica in LaunchModels_Shared.cs.
SetPlatform pins the reference to AnyCPU: this project is x86, the
contract is platform-neutral, and without the pin MSBuild's dynamic
platform negotiation corrupts this project's restore (one-shot
`dotnet build` of the solution then fails to resolve its packages). -->
<ProjectReference Include="..\Contract\Tesla.Contract.csproj" SetPlatform="Platform=AnyCPU" />
<ProjectReference Include="..\Contract\Tesla.Contract.csproj" />
</ItemGroup>
</Project>
+5 -11
View File
@@ -3,11 +3,12 @@
:: TeslaLauncher — Unified Build / Package Script
:: =============================================================================
:: Publishes TeslaLauncherService.exe and TeslaLauncherAgent.exe as
:: self-contained single-file win-x64 executables (net8) and assembles the
:: installable TeslaLauncher\ package next to install.bat.
:: framework-dependent net48 builds and assembles the installable TeslaLauncher\
:: package next to install.bat. The target pod needs .NET Framework 4.8 (built
:: into Windows 10/11) — there is no bundled runtime, so the package is small.
::
:: Requirements:
:: .NET 8 SDK https://dotnet.microsoft.com/download/dotnet/8.0
:: .NET SDK (6.0+) to drive the build https://dotnet.microsoft.com/download
:: Internet access for NuGet restore (first build only)
::
:: Usage:
@@ -31,7 +32,6 @@ setlocal enabledelayedexpansion
set ROOT=%~dp0
set BUILD_DIR=%ROOT%TeslaLauncher
set RID=win-x64
:: -- Parse arguments ----------------------------------------------------------
set BUILD_SERVICE=1
@@ -46,7 +46,7 @@ for %%a in (%*) do (
echo.
echo ============================================================
echo Tesla Launcher v0.1 - Build ^& Package (net8, %RID%)
echo Tesla Launcher v0.1 - Build ^& Package (net48, framework-dependent)
echo Output : %BUILD_DIR%
echo ============================================================
echo.
@@ -75,9 +75,6 @@ echo [1/2] Publishing TeslaLauncherService (Session 0 Windows Service)...
echo.
dotnet publish "%ROOT%TeslaLauncherService.csproj" ^
-c Release ^
-r %RID% ^
--self-contained true ^
-p:PublishSingleFile=true ^
-o "%BUILD_DIR%\Service"
if errorlevel 1 (
echo.
@@ -98,9 +95,6 @@ if %BUILD_SERVICE%==1 (echo [2/2] Publishing TeslaLauncherAgent...) else (echo [
echo.
dotnet publish "%ROOT%TeslaLauncherAgent.csproj" ^
-c Release ^
-r %RID% ^
--self-contained true ^
-p:PublishSingleFile=true ^
"-p:DefineConstants=WINFORMS" ^
-o "%BUILD_DIR%\Agent"
if errorlevel 1 (
+5 -3
View File
@@ -120,9 +120,11 @@ echo C:\Games (Users: modify access)
:: ── STEP 2: Copy files ────────────────────────────────────────────────────────
echo [2/8] Copying files...
copy /Y "%SERVICE_SRC%\TeslaLauncherService.exe" "%INSTALL_DIR%\" >nul
copy /Y "%AGENT_SRC%\TeslaLauncherAgent.exe" "%INSTALL_DIR%\" >nul
copy /Y "%AGENT_SRC%\*.dll" "%INSTALL_DIR%\" >nul 2>&1
:: net48 framework-dependent build: each folder holds the exe + its dependency
:: DLLs + .config. Copy both folders into the install dir (shared DLLs overwrite
:: identically; the two .exe.config files coexist).
copy /Y "%SERVICE_SRC%\*.*" "%INSTALL_DIR%\" >nul
copy /Y "%AGENT_SRC%\*.*" "%INSTALL_DIR%\" >nul
:: Install DirectX June 2010 runtime (required by games)
if exist "%ROOT%\dx9201006\DXSETUP.exe" (
+8 -8
View File
@@ -36,14 +36,14 @@ Global
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.Build.0 = Release|Any CPU
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|x64
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|x64
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|x64
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|x64
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|x64
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|x64
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|x64
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|x64
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|Any CPU
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|Any CPU
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU