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,177 @@
|
||||
// =============================================================================
|
||||
// Tesla.Contract — Mock RPC client (net48 only)
|
||||
// =============================================================================
|
||||
// Reconstructed verbatim from TeslaConsoleLaunchLib.dll. An offline test/dev
|
||||
// stand-in for PodManagerConnection. Not referenced by the shipping Console UI,
|
||||
// but retained for parity with the original assembly surface.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
public class MockPodManagerConnection : IPodManagerConnection, ILauncherService
|
||||
{
|
||||
public static readonly Guid RPAppGuid = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94");
|
||||
|
||||
public static readonly Guid RPMRAppGuid = new Guid("8F71D5C2-38E4-413c-8E22-88CAD08774D2");
|
||||
|
||||
public static readonly Guid RPLCAppGuid = new Guid("57A0B3C2-D5CF-46d6-ABED-A8F4A26AB086");
|
||||
|
||||
public static readonly Guid MW4AppGuid = new Guid("CC8500ED-A653-45a7-BEF8-C332D30371A6");
|
||||
|
||||
public static readonly Guid MW4LCAppGuid = new Guid("8EE93A6C-F16A-49be-B867-37FAE9087FFF");
|
||||
|
||||
private bool mIsOpen;
|
||||
|
||||
private List<LaunchedAppData> mLaunchedApps = new List<LaunchedAppData>();
|
||||
|
||||
public bool IsOpen => mIsOpen;
|
||||
|
||||
public float VolumeLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public void Open(IPEndPoint remoteEndPoint, byte[] secureKey)
|
||||
{
|
||||
Thread.Sleep(5000);
|
||||
mIsOpen = true;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Thread.Sleep(5000);
|
||||
mIsOpen = false;
|
||||
}
|
||||
|
||||
public Guid InstallProduct(string filename)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void ClearStore()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public DateTime Ping(DateTime now)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Guid InitiateInstallProduct()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public OutOfBandProgress GetOutOfBandProgress(Guid invokeCallId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public int LaunchApp(Guid launchKey)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public LaunchPair[] GetLaunchableApps()
|
||||
{
|
||||
return new LaunchPair[2]
|
||||
{
|
||||
new LaunchPair
|
||||
{
|
||||
DisplayName = "BattleTech Firestorm",
|
||||
LaunchKey = MW4AppGuid
|
||||
},
|
||||
new LaunchPair
|
||||
{
|
||||
DisplayName = "Red Planet",
|
||||
LaunchKey = RPAppGuid
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void KillApp(Guid launchKey, int processId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void KillAllOfType(Guid launchKey)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void KillAllApps()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Shutdown(bool restart)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public LaunchedAppData[] GetLaunchedApps()
|
||||
{
|
||||
return mLaunchedApps.ToArray();
|
||||
}
|
||||
|
||||
public LaunchData[] GetInstalledApps()
|
||||
{
|
||||
return new LaunchData[2]
|
||||
{
|
||||
new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair
|
||||
{
|
||||
DisplayName = "BattleTech Firestorm",
|
||||
LaunchKey = MW4AppGuid
|
||||
}
|
||||
},
|
||||
new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair
|
||||
{
|
||||
DisplayName = "Red Planet",
|
||||
LaunchKey = RPAppGuid
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void RemoveApp(Guid index)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InstallApp(LaunchData data)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void UninstallApp(Guid launchKey)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public FullUpdateData FullUpdate()
|
||||
{
|
||||
FullUpdateData result = default(FullUpdateData);
|
||||
result.InstalledApps = GetInstalledApps();
|
||||
result.LaunchedApps = GetLaunchedApps();
|
||||
result.VolumeLevel = VolumeLevel;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// =============================================================================
|
||||
// Tesla.Contract — Console-side RPC client (net48 only)
|
||||
// =============================================================================
|
||||
// Opens an OFB-encrypted TCP connection to the pod (port 53290) and dispatches
|
||||
// ILauncherService calls as framed JSON RpcRequest / RpcResponse pairs (see
|
||||
// PodRpcProtocol.cs). Replaces the original serialized-MethodBase + BinaryFormatter
|
||||
// scheme; the OFB transport (PodConfigurationServer.NegotiateCryptoStreams) is
|
||||
// unchanged.
|
||||
//
|
||||
// Depends on Tesla.PodConfigurationServer (Tesla.SecureConfig) for the crypto
|
||||
// handshake, so it is compiled for net48 only. The Launcher is the server end.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
public class PodManagerConnection : IPodManagerConnection, ILauncherService, IDisposable
|
||||
{
|
||||
private TcpClient mOpenClient;
|
||||
|
||||
private object mCommSyncRoot = new object();
|
||||
|
||||
protected Stream mUnderlyingOutStream;
|
||||
|
||||
private BufferedStream mReader;
|
||||
|
||||
private BufferedStream mWriter;
|
||||
|
||||
private IPEndPoint mEndPoint;
|
||||
|
||||
private byte[] mKey;
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mOpenClient == null || mOpenClient.Client == null || mWriter == null || mReader == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return mOpenClient.Connected;
|
||||
}
|
||||
}
|
||||
|
||||
protected object SyncRoot => mCommSyncRoot;
|
||||
|
||||
public float VolumeLevel
|
||||
{
|
||||
get => Invoke<float>("get_VolumeLevel");
|
||||
set => InvokeVoid("set_VolumeLevel", value);
|
||||
}
|
||||
|
||||
public void Open(IPEndPoint remoteEndPoint, byte[] secureKey)
|
||||
{
|
||||
mEndPoint = remoteEndPoint;
|
||||
mKey = secureKey;
|
||||
mOpenClient = new TcpClient();
|
||||
mOpenClient.SendTimeout = 10000;
|
||||
mOpenClient.ReceiveTimeout = 10000;
|
||||
mOpenClient.Connect(remoteEndPoint);
|
||||
if (!PodConfigurationServer.NegotiateCryptoStreams(mOpenClient.GetStream(), secureKey, out var outStream, out var inStream))
|
||||
{
|
||||
mOpenClient.Close();
|
||||
throw new IOException("Error connecting to pod.");
|
||||
}
|
||||
mUnderlyingOutStream = outStream;
|
||||
mWriter = new BufferedStream(outStream);
|
||||
mReader = new BufferedStream(inStream);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
mWriter.Close();
|
||||
mReader.Close();
|
||||
mOpenClient.Client.Close();
|
||||
mOpenClient = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public Guid InstallProduct(string filepath)
|
||||
{
|
||||
if (!File.Exists(filepath))
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
FileStream outOfBandData = File.OpenRead(filepath);
|
||||
Guid result = Guid.Empty;
|
||||
try
|
||||
{
|
||||
result = OutOfBandInvocation.Invoke(mEndPoint, mKey, "InitiateInstallProduct", new object[0], outOfBandData);
|
||||
return result;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// ── RPC core ─────────────────────────────────────────────────────────
|
||||
// Writes a framed JSON request and (optionally) reads the framed response,
|
||||
// deserializing its Result into the caller's expected type.
|
||||
|
||||
protected object InvokeCore(string method, bool waitForResponse, Type resultType, object[] parameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (mCommSyncRoot)
|
||||
{
|
||||
PodRpc.WriteRequest(mWriter, method, parameters);
|
||||
if (!waitForResponse)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
RpcResponse response = PodRpc.ReadResponse(mReader);
|
||||
if (response.Error != null)
|
||||
{
|
||||
throw new Exception("Server function threw an exception: " + response.Error);
|
||||
}
|
||||
if (resultType == null
|
||||
|| response.Result.ValueKind == JsonValueKind.Null
|
||||
|| response.Result.ValueKind == JsonValueKind.Undefined)
|
||||
{
|
||||
return resultType != null && resultType.IsValueType
|
||||
? Activator.CreateInstance(resultType)
|
||||
: null;
|
||||
}
|
||||
return response.Result.Deserialize(resultType, PodRpc.JsonOptions);
|
||||
}
|
||||
}
|
||||
catch (IOException innerException)
|
||||
{
|
||||
throw new TeslaConsole.ConnectionLostException("Connection Lost", innerException);
|
||||
}
|
||||
}
|
||||
|
||||
protected T Invoke<T>(string method, params object[] parameters)
|
||||
=> (T)InvokeCore(method, waitForResponse: true, typeof(T), parameters);
|
||||
|
||||
protected void InvokeVoid(string method, params object[] parameters)
|
||||
=> InvokeCore(method, waitForResponse: true, null, parameters);
|
||||
|
||||
protected void InvokeOneWay(string method, params object[] parameters)
|
||||
=> InvokeCore(method, waitForResponse: false, null, parameters);
|
||||
|
||||
// ── ILauncherService ─────────────────────────────────────────────────
|
||||
|
||||
public void ClearStore() => InvokeOneWay("ClearStore");
|
||||
|
||||
public DateTime Ping(DateTime now) => Invoke<DateTime>("Ping", now);
|
||||
|
||||
public int LaunchApp(Guid launchKey) => Invoke<int>("LaunchApp", launchKey);
|
||||
|
||||
public LaunchPair[] GetLaunchableApps() => Invoke<LaunchPair[]>("GetLaunchableApps");
|
||||
|
||||
public Guid InitiateInstallProduct() => Invoke<Guid>("InitiateInstallProduct");
|
||||
|
||||
public OutOfBandProgress GetOutOfBandProgress(Guid installId)
|
||||
=> Invoke<OutOfBandProgress>("GetOutOfBandProgress", installId);
|
||||
|
||||
public void KillApp(Guid launchKey, int processId)
|
||||
=> InvokeVoid("KillApp", launchKey, processId);
|
||||
|
||||
public void KillAllOfType(Guid launchKey) => InvokeVoid("KillAllOfType", launchKey);
|
||||
|
||||
public void KillAllApps() => InvokeVoid("KillAllApps");
|
||||
|
||||
public void Shutdown(bool doRestart) => InvokeVoid("Shutdown", doRestart);
|
||||
|
||||
public LaunchedAppData[] GetLaunchedApps() => Invoke<LaunchedAppData[]>("GetLaunchedApps");
|
||||
|
||||
public LaunchData[] GetInstalledApps() => Invoke<LaunchData[]>("GetInstalledApps");
|
||||
|
||||
public void RemoveApp(Guid index) => InvokeVoid("RemoveApp", index);
|
||||
|
||||
public void InstallApp(LaunchData data) => InvokeVoid("InstallApp", data);
|
||||
|
||||
public void UninstallApp(Guid launchKey) => InvokeVoid("UninstallApp", launchKey);
|
||||
|
||||
public FullUpdateData FullUpdate() => Invoke<FullUpdateData>("FullUpdate");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public class OutOfBandInvocation : PodManagerConnection
|
||||
{
|
||||
private Stream mOutOfBandData;
|
||||
|
||||
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData)
|
||||
{
|
||||
return Invoke(target, key, methodName, parameters, outOfBandData, null);
|
||||
}
|
||||
|
||||
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outofBandData)
|
||||
{
|
||||
return Invoke(target, key, method.Name, parameters, outofBandData, null);
|
||||
}
|
||||
|
||||
public static Guid Invoke(IPEndPoint target, byte[] key, MethodBase method, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
|
||||
{
|
||||
return Invoke(target, key, method.Name, parameters, outOfBandData, progressCallback);
|
||||
}
|
||||
|
||||
public static Guid Invoke(IPEndPoint target, byte[] key, string methodName, object[] parameters, Stream outOfBandData, OutOfBandProgressChanged progressCallback)
|
||||
{
|
||||
OutOfBandInvocation outOfBandInvocation = new OutOfBandInvocation(target, key);
|
||||
// progressCallback is vestigial: install progress is polled via
|
||||
// GetOutOfBandProgress, never pushed. Kept on the signature for surface parity.
|
||||
_ = progressCallback;
|
||||
if (outOfBandData != null)
|
||||
{
|
||||
outOfBandInvocation.mOutOfBandData = outOfBandData;
|
||||
}
|
||||
Guid result = outOfBandInvocation.Invoke<Guid>(methodName, parameters ?? new object[0]);
|
||||
ThreadPool.QueueUserWorkItem(AppendOutOfBandData, outOfBandInvocation);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected static void AppendOutOfBandData(object state)
|
||||
{
|
||||
OutOfBandInvocation outOfBandInvocation = (OutOfBandInvocation)state;
|
||||
using (outOfBandInvocation)
|
||||
{
|
||||
try
|
||||
{
|
||||
outOfBandInvocation.WriteOutOfBandData();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected OutOfBandInvocation(IPEndPoint target, byte[] key)
|
||||
{
|
||||
Open(target, key);
|
||||
}
|
||||
|
||||
protected void WriteOutOfBandData()
|
||||
{
|
||||
lock (base.SyncRoot)
|
||||
{
|
||||
int num = 8192;
|
||||
byte[] buffer = new byte[num];
|
||||
long num2 = mOutOfBandData.Length;
|
||||
mUnderlyingOutStream.Write(BitConverter.GetBytes(num2), 0, 8);
|
||||
while (num2 > num)
|
||||
{
|
||||
mOutOfBandData.Read(buffer, 0, num);
|
||||
mUnderlyingOutStream.Write(buffer, 0, num);
|
||||
num2 -= num;
|
||||
}
|
||||
if (num2 > 0)
|
||||
{
|
||||
mOutOfBandData.Read(buffer, 0, (int)num2);
|
||||
mUnderlyingOutStream.Write(buffer, 0, (int)num2);
|
||||
}
|
||||
mUnderlyingOutStream.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// =============================================================================
|
||||
// Tesla.Contract — Pod RPC protocol (Console <-> Launcher Service)
|
||||
// =============================================================================
|
||||
// The framed, JSON-based replacement for the old OFB-encrypted BinaryFormatter
|
||||
// RPC. Runs ON TOP of the same OFB-encrypted stream (NegotiateCryptoStreams is
|
||||
// unchanged); only the per-message serialization changed.
|
||||
//
|
||||
// Wire format, after the OFB/CONF handshake:
|
||||
// request : [4-byte little-endian length][UTF-8 JSON RpcRequest]
|
||||
// response: [4-byte little-endian length][UTF-8 JSON RpcResponse]
|
||||
// The InitiateInstallProduct out-of-band file transfer (8-byte length + raw
|
||||
// bytes) is unchanged and still follows its response frame on the same stream.
|
||||
//
|
||||
// Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase
|
||||
// + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this
|
||||
// one file, so the request/response shape cannot drift.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
/// <summary>One RPC call: a method name plus its arguments as JSON elements.</summary>
|
||||
public sealed class RpcRequest
|
||||
{
|
||||
public string Method { get; set; }
|
||||
public List<JsonElement> Args { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
||||
public sealed class RpcResponse
|
||||
{
|
||||
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
|
||||
public string Error { get; set; } // null on success
|
||||
}
|
||||
|
||||
/// <summary>Framing + (de)serialization shared by the Console client and the
|
||||
/// Launcher Service. Synchronous to match the existing stream usage.</summary>
|
||||
public static class PodRpc
|
||||
{
|
||||
/// <summary>Upper bound on a single JSON frame (the bulk install payload is
|
||||
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
|
||||
public const int MaxFrameBytes = 16 * 1024 * 1024;
|
||||
|
||||
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
||||
// FIELDS, which System.Text.Json ignores unless this is set.
|
||||
IncludeFields = true,
|
||||
};
|
||||
|
||||
// ── Framing ──────────────────────────────────────────────────────────
|
||||
|
||||
public static void WriteFrame(Stream stream, byte[] payload)
|
||||
{
|
||||
if (payload.Length > MaxFrameBytes)
|
||||
throw new IOException($"RPC frame too large ({payload.Length} bytes).");
|
||||
var len = BitConverter.GetBytes(payload.Length); // little-endian on x86/x64
|
||||
stream.Write(len, 0, 4);
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
public static byte[] ReadFrame(Stream stream)
|
||||
{
|
||||
var lenBuf = ReadExact(stream, 4);
|
||||
int len = BitConverter.ToInt32(lenBuf, 0);
|
||||
if (len < 0 || len > MaxFrameBytes)
|
||||
throw new IOException($"RPC frame length out of range ({len}).");
|
||||
return ReadExact(stream, len);
|
||||
}
|
||||
|
||||
private static byte[] ReadExact(Stream stream, int count)
|
||||
{
|
||||
var buf = new byte[count];
|
||||
int off = 0;
|
||||
while (off < count)
|
||||
{
|
||||
int n = stream.Read(buf, off, count - off);
|
||||
if (n == 0) throw new EndOfStreamException("Connection closed mid-frame.");
|
||||
off += n;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ── Request ──────────────────────────────────────────────────────────
|
||||
|
||||
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||
{
|
||||
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
||||
if (args != null)
|
||||
foreach (var a in args)
|
||||
req.Args.Add(JsonSerializer.SerializeToElement(a, JsonOptions));
|
||||
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(req, JsonOptions));
|
||||
}
|
||||
|
||||
public static RpcRequest ReadRequest(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
|
||||
public static void WriteResponse(Stream stream, object result, string error)
|
||||
{
|
||||
var resp = new RpcResponse
|
||||
{
|
||||
// Always a valid element (JSON null when there is no result): a
|
||||
// default(JsonElement) is ValueKind.Undefined and is not serializable.
|
||||
Result = JsonSerializer.SerializeToElement(error == null ? result : null, JsonOptions),
|
||||
Error = error,
|
||||
};
|
||||
WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(resp, JsonOptions));
|
||||
}
|
||||
|
||||
public static RpcResponse ReadResponse(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<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>
|
||||
|
||||
<!-- CRITICAL: the output assembly MUST be named TeslaConsoleLaunchLib at
|
||||
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
|
||||
stream and the Console resolves the wire types by that simple name, so
|
||||
renaming the assembly would change the protocol. This is a pure
|
||||
refactor that keeps the bytes identical. -->
|
||||
<AssemblyName>TeslaConsoleLaunchLib</AssemblyName>
|
||||
<RootNamespace>Tesla.Net</RootNamespace>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<Version>1.0.0.0</Version>
|
||||
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- Decompiled-style source: designer-ish fields, BinaryFormatter on net6. -->
|
||||
<NoWarn>$(NoWarn);SYSLIB0011</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- net48 reference assemblies so the project builds without a full targeting pack,
|
||||
plus System.Text.Json (built into the net8 shared framework, a package on net48). -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- The TCP/OFB client (Client/**) is net48-only: it depends on the crypto-stream
|
||||
handshake in TeslaSecureConfiguration.dll. The Launcher (net6) is the SERVER
|
||||
end of this protocol and never references these classes, so they are excluded
|
||||
from the net6.0-windows build (which carries only the wire data types). -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
||||
<Compile Remove="Client\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<!-- Source-built secure-config (PodConfigurationServer.NegotiateCryptoStreams),
|
||||
emitting assembly TeslaSecureConfiguration. net48-only, same as Client/**. -->
|
||||
<ProjectReference Include="..\SecureConfig\Tesla.SecureConfig.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,176 @@
|
||||
// =============================================================================
|
||||
// Tesla.Contract — Shared RPC Wire Contract (assembly: TeslaConsoleLaunchLib)
|
||||
// =============================================================================
|
||||
// Single source of truth for the Console <-> Launcher protocol. Previously this
|
||||
// lived in two hand-synced places: the vendored binary
|
||||
// Console/lib/TeslaConsoleLaunchLib.dll (consumed by the Console) and a manual
|
||||
// replica in Launcher/LaunchModels_Shared.cs (consumed by the Launcher Service).
|
||||
//
|
||||
// The wire types below are reconstructed verbatim from the original
|
||||
// TeslaConsoleLaunchLib.dll. Field NAMES, TYPES and ORDER are load-bearing:
|
||||
// they are serialized with BinaryFormatter and must match byte-for-byte or the
|
||||
// protocol breaks silently at runtime. The byte-identical guarantee is asserted
|
||||
// by WireContractCompatTests in the differential suite.
|
||||
//
|
||||
// This file is compiled for BOTH net48 (Console) and net6.0-windows (Launcher).
|
||||
// The TCP/OFB/BinaryFormatter client (PodManagerConnection) is net48-only and
|
||||
// lives under Client/ — it depends on TeslaSecureConfiguration.dll.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
/// <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>Progress of an out-of-band product installation.</summary>
|
||||
[Serializable]
|
||||
public struct OutOfBandProgress
|
||||
{
|
||||
public int PercentComplete;
|
||||
|
||||
public string Status;
|
||||
|
||||
public bool IsCompleted;
|
||||
}
|
||||
|
||||
/// <summary>Complete pod state snapshot for the FullUpdate RPC.</summary>
|
||||
[Serializable]
|
||||
public struct FullUpdateData
|
||||
{
|
||||
public LaunchData[] InstalledApps;
|
||||
|
||||
public LaunchedAppData[] LaunchedApps;
|
||||
|
||||
public float VolumeLevel;
|
||||
}
|
||||
|
||||
/// <summary>RPC command from the Console. Function is a MethodBase of ILauncherService.</summary>
|
||||
[Serializable]
|
||||
public struct InvokeCommand
|
||||
{
|
||||
public MethodBase Function;
|
||||
|
||||
public object[] Parameters;
|
||||
}
|
||||
|
||||
/// <summary>RPC result returned to the Console.</summary>
|
||||
[Serializable]
|
||||
public struct InvokeResult
|
||||
{
|
||||
public object Result;
|
||||
|
||||
public Exception Exception;
|
||||
|
||||
public TimeSpan CallDuration;
|
||||
}
|
||||
|
||||
public delegate void OutOfBandProgressChanged(Guid outOfBandCallId, OutOfBandProgress progress);
|
||||
|
||||
/// <summary>Server-side RPC surface. Method signatures are resolved by name from a
|
||||
/// serialized MethodBase, so they 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();
|
||||
}
|
||||
|
||||
/// <summary>Console-side connection: ILauncherService plus connection management
|
||||
/// and the out-of-band InstallProduct file transfer.</summary>
|
||||
public interface IPodManagerConnection : ILauncherService
|
||||
{
|
||||
bool IsOpen { get; }
|
||||
|
||||
void Open(IPEndPoint remoteEndPoint, byte[] secureKey);
|
||||
|
||||
void Close();
|
||||
|
||||
Guid InstallProduct(string filename);
|
||||
}
|
||||
}
|
||||
|
||||
namespace TeslaConsole
|
||||
{
|
||||
/// <summary>Thrown by the Console-side connection when the pod link drops.</summary>
|
||||
[Serializable]
|
||||
public class ConnectionLostException : Exception
|
||||
{
|
||||
public ConnectionLostException()
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionLostException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionLostException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user