// ============================================================================= // 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("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(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("Ping", now); public int LaunchApp(Guid launchKey) => Invoke("LaunchApp", launchKey); public LaunchPair[] GetLaunchableApps() => Invoke("GetLaunchableApps"); public Guid InitiateInstallProduct() => Invoke("InitiateInstallProduct"); public OutOfBandProgress GetOutOfBandProgress(Guid installId) => Invoke("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("GetLaunchedApps"); public LaunchData[] GetInstalledApps() => Invoke("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("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(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(); } } } }