Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64

Make the Console<->Launcher system source-built and modern now that the console
is under our control and the WinXP-era pods are gone.

Contract extraction (Contract/Tesla.Contract.csproj):
- One multi-targeted (net48;net8.0-windows) source project for the RPC contract,
  replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net
  replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib.

SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj):
- net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto,
  RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll.

Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned):
- Console<->Launcher RPC is now length-prefixed System.Text.Json frames
  (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by
  method name. Deleted the SerializationBinder / MethodInfoProxy machinery.
- Console-local BinaryFormatter (Site config, mission replays) intentionally
  retained: local net48 file I/O, not the network surface.

Runtime modernization:
- Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are
  64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat)
  with SYSLIB0041 suppressed and documented.

Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB
ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON
round-trip of every request/response shape); removed the now-obsolete
BinaryFormatter byte-identity guard.

Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a
NuGet restore collision between the two Launcher projects sharing one folder.

NOT runtime-verified against a live pod.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-30 08:15:17 -05:00
co-authored by Claude Opus 4.8
parent 548550b312
commit b9d8027cf6
19 changed files with 2441 additions and 393 deletions
+23
View File
@@ -0,0 +1,23 @@
<Project>
<!-- TeslaLauncherService.csproj and TeslaLauncherAgent.csproj share this one
directory, so by default they also share obj\project.assets.json. NuGet
restore writes that single file per project, so whichever restores last
wins and the other compiles against the wrong package set (e.g. the
Service losing Microsoft.Extensions.Hosting). Give each project its own
intermediate directory so their restore outputs no longer collide.
(bin\ is safe to share — the two assemblies have distinct names.) -->
<PropertyGroup>
<BaseIntermediateOutputPath>obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
</PropertyGroup>
<!-- With per-project obj subdirs, each project would otherwise glob the OTHER
project's generated sources (AssemblyInfo.cs, etc.) out of the sibling
obj\ subtree, causing duplicate-attribute errors. The SDK only excludes a
project's own BaseIntermediateOutputPath by default, so exclude the whole
obj\ and bin\ trees here. The SDK appends its own defaults to this. -->
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);$(MSBuildProjectDirectory)\obj\**;$(MSBuildProjectDirectory)\bin\**</DefaultItemExcludes>
</PropertyGroup>
</Project>
+8 -97
View File
@@ -1,105 +1,16 @@
// =============================================================================
// TeslaLauncher — Shared Models
// TeslaLauncher — Shared IPC Models
// =============================================================================
// Wire types (Tesla.Net): BinaryFormatter-compatible replicas of the original
// structs from TeslaConsoleLaunchLib.dll. Field names, types, and order must
// match exactly for serialization compatibility.
// IPC types (Tesla.Launcher.Shared): JSON messages between the Windows Service
// and the user-session Agent over a Named Pipe. These never touch the Console
// TCP connection.
//
// IPC types (Tesla.Launcher.Shared): JSON messages between Service and Agent
// over Named Pipe. These never touch the Console TCP connection.
// The BinaryFormatter wire types (Tesla.Net: LaunchData, InvokeCommand, ...)
// formerly replicated here by hand now live in the shared Tesla.Contract project
// (assembly TeslaConsoleLaunchLib), referenced by the Service. This file is also
// compiled into the Agent, which uses only the IPC types below.
// =============================================================================
using System;
using System.Reflection;
using System.Runtime.Serialization;
namespace Tesla.Net
{
/// <summary>RPC command from TeslaConsole. Function is a MethodBase of ILauncherService.</summary>
[Serializable]
public struct InvokeCommand
{
public MethodBase Function;
public object[] Parameters;
}
/// <summary>RPC result returned to TeslaConsole.</summary>
[Serializable]
public struct InvokeResult
{
public object Result;
public Exception Exception;
public TimeSpan CallDuration;
}
/// <summary>App identifier + display name pair.</summary>
[Serializable]
public struct LaunchPair
{
public Guid LaunchKey;
public string DisplayName;
}
/// <summary>Describes one launchable simulation application.</summary>
[Serializable]
public struct LaunchData
{
public LaunchPair LaunchPair;
public string WorkingDirectory;
public string ExeFile;
public string Arguments;
public bool AutoRestart;
}
/// <summary>Tracks a currently running simulation process.</summary>
[Serializable]
public struct LaunchedAppData
{
public int ProcessId;
public Guid LaunchKey;
}
/// <summary>Complete pod state snapshot for FullUpdate RPC.</summary>
[Serializable]
public struct FullUpdateData
{
public LaunchData[] InstalledApps;
public LaunchedAppData[] LaunchedApps;
public float VolumeLevel;
}
/// <summary>Progress of an out-of-band product installation.</summary>
[Serializable]
public struct OutOfBandProgress
{
public int PercentComplete;
public string Status;
public bool IsCompleted;
}
/// <summary>Interface for BinaryFormatter MethodBase resolution. Signatures must match the original exactly.</summary>
public interface ILauncherService
{
float VolumeLevel { get; set; }
void ClearStore();
DateTime Ping(DateTime now);
Guid InitiateInstallProduct();
OutOfBandProgress GetOutOfBandProgress(Guid invokeCallId);
int LaunchApp(Guid launchKey);
LaunchPair[] GetLaunchableApps();
void KillApp(Guid launchKey, int processId);
void KillAllOfType(Guid launchKey);
void KillAllApps();
void Shutdown(bool restart);
LaunchedAppData[] GetLaunchedApps();
LaunchData[] GetInstalledApps();
void RemoveApp(Guid index);
void InstallApp(LaunchData data);
void UninstallApp(Guid launchKey);
FullUpdateData FullUpdate();
}
}
namespace Tesla.Launcher.Shared
{
/// <summary>Command forwarded from the Windows Service to the Userspace Agent.</summary>
+8 -1
View File
@@ -196,13 +196,20 @@ namespace TeslaSecureConfig
// ---- Crypto helpers ----
internal static class CryptoHelper
{
/// Derive AES key from passphrase using PBKDF2 with the hard-coded salt
/// Derive AES key from passphrase using PBKDF2 with the hard-coded salt.
/// COMPATIBILITY: this MUST use the SHA1-default Rfc2898DeriveBytes(string,
/// byte[], int) overload. The Console derives the same session key with the
/// identical SHA1-default PBKDF2 (Tesla.PodConfigurationServer.GenerateKeyFromPassphrase),
/// so switching to a SHA256 overload — as SYSLIB0041 suggests on net8 — would
/// silently break the secure-config key handshake. Do not "modernize" this.
#pragma warning disable SYSLIB0041 // SHA1-default PBKDF2 is required for Console wire compatibility
public static byte[] DeriveKey(string passphrase)
{
var pbkdf2 = new Rfc2898DeriveBytes(passphrase,
Proto.PassphraseSalt, Proto.Pbkdf2Iterations);
return pbkdf2.GetBytes(Proto.AesKeyBytes);
}
#pragma warning restore SYSLIB0041
/// Encrypt data with AES-256 (CBC mode)
public static byte[] Encrypt(byte[] data, byte[] key)
+4 -5
View File
@@ -1,19 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<OutputType>WinExe</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<WarningsAsErrors></WarningsAsErrors>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<Version>0.1.0</Version>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>TeslaLauncherAgent</AssemblyName>
<RootNamespace>Tesla.Launcher.Agent</RootNamespace>
<Platforms>x86</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<StartupObject>Tesla.Launcher.Agent.AgentApplication</StartupObject>
@@ -27,7 +26,7 @@
<ItemGroup>
<!-- Required by SecureConfig.cs for COM2/PlasmaIO serial output -->
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
</ItemGroup>
</Project>
+100 -259
View File
@@ -3,7 +3,7 @@
// =============================================================================
// Replaces TeslaLauncherService.exe (Elsewhen Studios LLC, .NET Framework 2.0).
// Runs in Session 0, auto-starts at boot before user login.
// Listens on TCP 53290 for OFB-encrypted BinaryFormatter RPC from TeslaConsole.
// Listens on TCP 53290 for OFB-encrypted framed-JSON RPC from TeslaConsole.
// Forwards commands to TeslaLauncherAgent (user session) via Named Pipe.
//
// On first boot (DHCP detected), runs SecureConfig to receive network
@@ -17,9 +17,6 @@ using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -32,8 +29,6 @@ using TeslaSecureConfig;
using Tesla.Launcher.Shared;
using Tesla.Net;
#pragma warning disable SYSLIB0011 // BinaryFormatter is required for Console wire compatibility
namespace Tesla.Launcher.Service
{
public class TeslaLauncherService : BackgroundService
@@ -92,11 +87,6 @@ namespace Tesla.Launcher.Service
_logger.LogInformation(
"Tesla Launcher Service starting on port {Port}", CONSOLE_PORT);
// Enable BinaryFormatter at runtime. Must also be set in .csproj.
// Both switches are required on .NET 6+.
AppContext.SetSwitch(
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true);
// ── Boot diagnostic — always written so every reboot is traceable ──
// Logs adapter state to podconf.log before IsMachineConfigured() decides,
// visible even if SecureConfig does not run.
@@ -170,8 +160,8 @@ namespace Tesla.Launcher.Service
"Console connected from {EP}", client.Client.RemoteEndPoint);
// Handle each Console connection on its own thread.
// BinaryFormatter.Deserialize is synchronous, so we
// offload to a thread-pool thread via Task.Run.
// Frame reads are synchronous, so we offload to a
// thread-pool thread via Task.Run.
var task = Task.Run(
() => HandleConsoleClient(client, stoppingToken),
stoppingToken);
@@ -247,88 +237,70 @@ namespace Tesla.Launcher.Service
}
Stream rpcStream = ofb;
var formatter = CreateFormatter();
var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
_logger.LogInformation("Console session started.");
try
{
while (!ct.IsCancellationRequested && client.Connected)
{
InvokeCommand command;
RpcRequest request;
try
{
MethodInfoProxy.LastResolvedName = null;
var obj = formatter.Deserialize(rpcStream);
command = (InvokeCommand)obj;
request = PodRpc.ReadRequest(rpcStream);
}
catch (IOException)
catch (EndOfStreamException)
{
_logger.LogInformation("Console disconnected (EOF).");
break;
}
catch (SerializationException ex)
when (ex.Message.Contains("End of Stream"))
catch (IOException ex)
{
_logger.LogInformation("Console disconnected (end of stream).");
break;
}
catch (SerializationException ex)
when (ex.InnerException is IOException)
{
// Socket timeout or reset — Console went idle or disconnected
_logger.LogInformation("Console disconnected (I/O: {Msg}).",
ex.InnerException.Message);
_logger.LogInformation("Console disconnected (I/O: {Msg}).", ex.Message);
break;
}
catch (Exception ex)
{
_logger.LogWarning("Deserialization error ({Type}): {Msg}",
_logger.LogWarning("Request read error ({Type}): {Msg}",
ex.GetType().Name, ex.Message);
File.AppendAllText(
Path.Combine(AppContext.BaseDirectory, "podconf.log"),
$"[{DateTime.Now:HH:mm:ss}] DESER ERROR: {ex}{Environment.NewLine}");
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] REQ ERROR: {ex}{Environment.NewLine}");
break;
}
// Fall back to ThreadStatic name if IObjectReference fixup
// didn't update the boxed struct field.
var funcName = command.Function?.Name
?? MethodInfoProxy.LastResolvedName ?? "???";
_logger.LogInformation(
"Command: {Func}({Args})", funcName,
command.Parameters != null
? string.Join(", ", command.Parameters) : "");
if (request == null) break;
var funcName = request.Method ?? "???";
IReadOnlyList<JsonElement> args = request.Args ?? new List<JsonElement>();
_logger.LogInformation("Command: {Func}({ArgCount} args)", funcName, args.Count);
var start = DateTime.UtcNow;
var result = new InvokeResult();
var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
object resultObj = null;
string error = null;
try
{
result.Result = await DispatchCommandAsync(
funcName, command.Parameters, ct);
resultObj = await DispatchCommandAsync(funcName, args, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing {Func}", funcName);
result.Exception = new Exception(ex.Message);
error = ex.Message;
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} ERROR: {ex.Message}{Environment.NewLine}");
}
result.CallDuration = DateTime.UtcNow - start;
if (result.Exception == null)
var dur = DateTime.UtcNow - start;
if (error == null)
{
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({result.CallDuration.TotalMilliseconds:F0}ms)" +
$"{(result.Result != null ? $" {result.Result}" : "")}{Environment.NewLine}");
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({dur.TotalMilliseconds:F0}ms)" +
$"{(resultObj != null ? $" {resultObj}" : "")}{Environment.NewLine}");
}
try
{
formatter.Serialize(rpcStream, result);
rpcStream.Flush();
PodRpc.WriteResponse(rpcStream, resultObj, error);
}
catch (IOException)
{
@@ -337,19 +309,10 @@ namespace Tesla.Launcher.Service
}
catch (Exception serEx)
{
// Result type not BinaryFormatter-serializable — send error instead
// Result type not JSON-serializable — send error instead.
_logger.LogWarning("Serialize failed for {Func}: {Msg}",
funcName, serEx.Message);
var fallback = new InvokeResult
{
Exception = new Exception($"Pod error: {serEx.Message}"),
CallDuration = result.CallDuration
};
try
{
formatter.Serialize(rpcStream, fallback);
rpcStream.Flush();
}
try { PodRpc.WriteResponse(rpcStream, null, $"Pod error: {serEx.Message}"); }
catch { break; }
}
@@ -359,8 +322,8 @@ namespace Tesla.Launcher.Service
// 8-byte Int64 file size + raw file bytes
// Then closes the connection (FIN).
if (funcName == "InitiateInstallProduct"
&& result.Exception == null
&& result.Result is Guid installGuid)
&& error == null
&& resultObj is Guid installGuid)
{
ReceiveInstallFile(rpcStream, installGuid, diagLog);
break; // connection done after file transfer
@@ -370,7 +333,6 @@ namespace Tesla.Launcher.Service
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error in Console handler");
var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] HANDLER ERROR: {ex.GetType().Name}: {ex.Message}{Environment.NewLine}" +
$"[{DateTime.Now:HH:mm:ss}] Inner: {ex.InnerException?.Message}{Environment.NewLine}");
@@ -380,19 +342,24 @@ namespace Tesla.Launcher.Service
}
// ── Command Dispatch ─────────────────────────────────────────────
// Arguments arrive as JSON elements (RpcRequest.Args); each case extracts
// the typed values it needs. The returned object is serialized straight
// into the RpcResponse.
private async Task<object> DispatchCommandAsync(
string functionName, object[] parameters, CancellationToken ct)
string functionName, IReadOnlyList<JsonElement> args, CancellationToken ct)
{
switch (functionName)
{
case "Ping":
// Echo back the DateTime parameter (Console measures round-trip)
return parameters?.Length > 0 ? parameters[0] : DateTime.Now;
return args.Count > 0 && args[0].ValueKind != JsonValueKind.Null
? args[0].GetDateTime()
: DateTime.Now;
case "GetLaunchableApps":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
var wire = FlatArrayToWire(json);
var pairs = new LaunchPair[wire.Length];
for (int i = 0; i < wire.Length; i++)
@@ -402,19 +369,19 @@ namespace Tesla.Launcher.Service
case "GetInstalledApps":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return FlatArrayToWire(json);
}
case "GetLaunchedApps":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return FlatLaunchedArrayToWire(json);
}
case "FullUpdate":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
if (json == null) return new FullUpdateData();
var flat = JsonSerializer.Deserialize<FlatFullUpdateData>(json);
if (flat == null) return new FullUpdateData();
@@ -436,54 +403,34 @@ namespace Tesla.Launcher.Service
// Handled directly in the Service — progress is tracked
// during file receive on the InitiateInstallProduct connection.
var diagPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
if (parameters?.Length > 0)
Guid progressId = Guid.Empty;
bool guidMatch = false;
if (args.Count > 0 && args[0].ValueKind == JsonValueKind.String
&& Guid.TryParse(args[0].GetString(), out var pg))
{
var p0 = parameters[0];
var p0Type = p0?.GetType().FullName ?? "null";
Guid progressId;
bool guidMatch = false;
progressId = pg;
guidMatch = true;
}
if (p0 is Guid g)
if (guidMatch)
{
lock (_installLock)
{
progressId = g;
guidMatch = true;
}
else if (p0 is string s && Guid.TryParse(s, out var gs))
{
progressId = gs;
guidMatch = true;
}
else
{
progressId = Guid.Empty;
}
if (guidMatch)
{
lock (_installLock)
if (_installProgress.TryGetValue(progressId, out var prog))
{
if (_installProgress.TryGetValue(progressId, out var prog))
{
// Log first poll and when status changes
_logger.LogDebug(
"GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}",
progressId, prog.PercentComplete, prog.Status, prog.IsCompleted);
return prog;
}
_logger.LogDebug(
"GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}",
progressId, prog.PercentComplete, prog.Status, prog.IsCompleted);
return prog;
}
File.AppendAllText(diagPath,
$"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (type={p0Type}, keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}");
}
else
{
File.AppendAllText(diagPath,
$"[{DateTime.Now:HH:mm:ss}] OOBP: param[0] type={p0Type} value={p0} — NOT a Guid{Environment.NewLine}");
}
File.AppendAllText(diagPath,
$"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}");
}
else
{
File.AppendAllText(diagPath,
$"[{DateTime.Now:HH:mm:ss}] OOBP: NO parameters{Environment.NewLine}");
$"[{DateTime.Now:HH:mm:ss}] OOBP: arg[0] not a Guid (kind={(args.Count > 0 ? args[0].ValueKind.ToString() : "none")}){Environment.NewLine}");
}
return new OutOfBandProgress
{
@@ -495,7 +442,7 @@ namespace Tesla.Launcher.Service
case "LaunchApp":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
if (json == null) return 0;
var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("ProcessId", out var pid))
@@ -505,7 +452,7 @@ namespace Tesla.Launcher.Service
case "get_VolumeLevel":
{
var json = await ForwardToAgentJsonAsync(functionName, parameters, ct);
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return json != null ? JsonSerializer.Deserialize<float>(json) : 1.0f;
}
@@ -524,6 +471,26 @@ namespace Tesla.Launcher.Service
return callId;
}
case "InstallApp":
{
// Flatten the wire LaunchData into the Agent's flat JSON format.
var ld = args.Count > 0
? args[0].Deserialize<LaunchData>(PodRpc.JsonOptions)
: default;
var flat = new
{
LaunchKey = ld.LaunchPair.LaunchKey.ToString(),
DisplayName = ld.LaunchPair.DisplayName,
WorkingDirectory = ld.WorkingDirectory,
ExeFile = ld.ExeFile,
Arguments = ld.Arguments,
AutoRestart = ld.AutoRestart
};
var payloadJson = JsonSerializer.Serialize(new object[] { flat });
await ForwardToAgentCoreAsync(functionName, launchKey: null, payloadJson, ct);
return null;
}
case "KillApp":
case "KillAllOfType":
case "KillAllApps":
@@ -531,33 +498,18 @@ namespace Tesla.Launcher.Service
case "ClearStore":
case "RemoveApp":
case "UninstallApp":
case "InstallApp":
{
// Flatten nested LaunchPair for Agent's flat JSON format
if (parameters?.Length > 0 && parameters[0] is LaunchData ld)
{
var flat = new
{
LaunchKey = ld.LaunchPair.LaunchKey.ToString(),
DisplayName = ld.LaunchPair.DisplayName,
WorkingDirectory = ld.WorkingDirectory,
ExeFile = ld.ExeFile,
Arguments = ld.Arguments,
AutoRestart = ld.AutoRestart
};
parameters = new object[] { flat };
}
await ForwardToAgentJsonAsync(functionName, parameters, ct);
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
}
case "set_VolumeLevel":
await ForwardToAgentJsonAsync(functionName, parameters, ct);
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
default:
_logger.LogWarning("Unknown command: {Func}", functionName);
await ForwardToAgentJsonAsync(functionName, parameters, ct);
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
}
}
@@ -882,30 +834,31 @@ namespace Tesla.Launcher.Service
}
}
// ── BinaryFormatter Setup ────────────────────────────────────────
private static BinaryFormatter CreateFormatter()
{
return new BinaryFormatter
{
Binder = new TeslaSerializationBinder()
};
}
// ── Named Pipe IPC ───────────────────────────────────────────────
private async Task<string> ForwardToAgentJsonAsync(
string functionName, object[] parameters, CancellationToken ct)
private Task<string> ForwardToAgentJsonAsync(
string functionName, IReadOnlyList<JsonElement> args, CancellationToken ct)
{
string launchKey = null;
if (parameters?.Length > 0 && parameters[0] is Guid guid)
if (args != null && args.Count > 0 && args[0].ValueKind == JsonValueKind.String
&& Guid.TryParse(args[0].GetString(), out var guid))
launchKey = guid.ToString();
string payloadJson = (args != null && args.Count > 0)
? JsonSerializer.Serialize(args)
: null;
return ForwardToAgentCoreAsync(functionName, launchKey, payloadJson, ct);
}
private async Task<string> ForwardToAgentCoreAsync(
string functionName, string launchKey, string payloadJson, CancellationToken ct)
{
var msg = new IpcMessage
{
Command = functionName.ToUpperInvariant(),
LaunchKey = launchKey,
PayloadJson = parameters?.Length > 0
? JsonSerializer.Serialize(parameters) : null
PayloadJson = payloadJson
};
using var pipe = new NamedPipeClientStream(
@@ -962,124 +915,12 @@ namespace Tesla.Launcher.Service
}
}
// ── Serialization Binder ─────────────────────────────────────────────
// BindToType: resolves Console-side type names to local Tesla.Net types.
// BindToName: writes "TeslaConsoleLaunchLib" as assembly for outgoing types.
internal sealed class TeslaSerializationBinder : SerializationBinder
{
private static readonly Assembly TeslaAssembly =
typeof(ILauncherService).Assembly;
public override Type BindToType(string assemblyName, string typeName)
{
// Intercept MemberInfoSerializationHolder — handles cross-runtime
// MethodBase deserialization (.NET Framework 2.0 → .NET 6).
if (typeName == "System.Reflection.MemberInfoSerializationHolder")
return typeof(MethodInfoProxy);
// Try to resolve Tesla.Net types from our assembly
var type = TeslaAssembly.GetType(typeName);
if (type != null) return type;
// Return null → default resolution (triggers AssemblyResolve if needed)
return null;
}
public override void BindToName(
Type serializedType, out string assemblyName, out string typeName)
{
if (serializedType.Namespace == "Tesla.Net")
{
// Console expects types from TeslaConsoleLaunchLib
assemblyName = "TeslaConsoleLaunchLib, Version=4.11.4.0, Culture=neutral, PublicKeyToken=null";
typeName = serializedType.FullName;
}
else
{
assemblyName = null;
typeName = null;
}
}
}
// ── MethodInfo Proxy ─────────────────────────────────────────────────
// Replaces MemberInfoSerializationHolder during deserialization.
// Resolves serialized MethodBase references to ILauncherService methods.
[Serializable]
internal sealed class MethodInfoProxy : ISerializable, IObjectReference
{
/// <summary>
/// Thread-static capture of the last deserialized method name.
/// IObjectReference fixup may fail to update fields inside boxed
/// value types (InvokeCommand is a struct), leaving Function null.
/// The handler reads this field as a fallback for the method name.
/// </summary>
[ThreadStatic]
internal static string LastResolvedName;
private readonly string _memberName;
private MethodInfoProxy(SerializationInfo info, StreamingContext ctx)
{
_memberName = info.GetString("Name");
LastResolvedName = _memberName;
}
public object GetRealObject(StreamingContext context)
{
// Look up the method on our ILauncherService interface.
// This handles both regular methods (Ping, LaunchApp, etc.)
// and property accessors (get_VolumeLevel, set_VolumeLevel).
var type = typeof(ILauncherService);
var method = type.GetMethod(_memberName,
BindingFlags.Public | BindingFlags.Instance);
if (method != null) return method;
// Property accessors: get_X / set_X
if (_memberName.StartsWith("get_") || _memberName.StartsWith("set_"))
{
var propName = _memberName.Substring(4);
var prop = type.GetProperty(propName,
BindingFlags.Public | BindingFlags.Instance);
if (prop != null)
{
var accessor = _memberName.StartsWith("get_")
? prop.GetMethod : prop.SetMethod;
if (accessor != null) return accessor;
}
}
throw new SerializationException(
$"Cannot resolve method '{_memberName}' on ILauncherService");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
throw new NotSupportedException("MethodInfoProxy is read-only.");
}
}
// ── Entry Point ──────────────────────────────────────────────────────
public class Program
{
public static void Main(string[] args)
{
AppContext.SetSwitch(
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true);
// Redirect TeslaConsoleLaunchLib assembly references to our assembly
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
{
var name = new AssemblyName(resolveArgs.Name);
if (string.Equals(name.Name, "TeslaConsoleLaunchLib",
StringComparison.OrdinalIgnoreCase))
return typeof(ILauncherService).Assembly;
return null;
};
Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
+17 -8
View File
@@ -1,18 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<WarningsAsErrors></WarningsAsErrors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
<Version>0.1.0</Version>
<ApplicationIcon>app.ico</ApplicationIcon>
<AssemblyName>TeslaLauncherService</AssemblyName>
<RootNamespace>Tesla.Launcher.Service</RootNamespace>
<Platforms>x86</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<WindowsService>true</WindowsService>
@@ -24,11 +23,21 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.*" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.*" />
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="6.*" />
<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.IO.Ports" Version="7.0.0" />
<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" />
</ItemGroup>