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
+177
View File
@@ -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;
}
}
}
+276
View File
@@ -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();
}
}
}
}
+120
View File
@@ -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);
}
}
+47
View File
@@ -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>
+176
View File
@@ -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)
{
}
}
}