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:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user