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:
+38
-9
@@ -20,20 +20,49 @@ it is otherwise a faithful decompilation, not a rewrite.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The console references five assemblies, vendored as binaries under `lib/`
|
||||
(copied from `assets/Tesla Console/`):
|
||||
The console references these assemblies. Most are vendored as binaries under
|
||||
`lib/` (copied from `assets/Tesla Console/`):
|
||||
|
||||
| Assembly | Origin |
|
||||
|----------|--------|
|
||||
| `WeifenLuo.WinFormsUI.Docking.dll` | Third-party docking UI (open source, see `assets/Tesla Console/WeifenLuo.txt`) |
|
||||
| `TeslaConsoleLaunchLib.dll` | Wire types / launch protocol (proprietary) |
|
||||
| `TeslaSecureConfiguration.dll` | First-boot secure config protocol (proprietary) |
|
||||
| `Munga Net.dll` | Networking helpers (proprietary) |
|
||||
| `BitmapLibrary.dll` | Plasma-display bitmap rendering (proprietary) |
|
||||
| `TeslaConsoleLaunchLib.dll` | Wire types / launch protocol — **now built from source** (see below) |
|
||||
| `TeslaSecureConfiguration.dll` | First-boot secure config protocol — **now built from source** (see below) |
|
||||
| `Munga Net.dll` | Managed C# client for the Red Planet game's Munga protocol (TCP 1501); manual binary serialization, no BinaryFormatter (proprietary, vendored) |
|
||||
| `BitmapLibrary.dll` | Plasma-display bitmap rendering (proprietary, vendored) |
|
||||
|
||||
These are still referenced as compiled binaries. They are also .NET 2.0 managed
|
||||
assemblies and could be decompiled to source later if full-source builds are
|
||||
needed.
|
||||
> The Red Planet game itself is C++ and lives in its own repo (`c:\vwe\rp411` /
|
||||
> `gitea.mysticmachines.com/VWE/RP411.git`). That source defines the Munga protocol
|
||||
> but is **not** a drop-in for the managed `Munga Net.dll` above, so it is not
|
||||
> vendored here.
|
||||
|
||||
Two of these are no longer vendored binaries — they are built from source and
|
||||
shared across the suite:
|
||||
|
||||
- `TeslaConsoleLaunchLib` ← `../Contract/Tesla.Contract.csproj`, a multi-targeted
|
||||
`net48;net8.0-windows` project: the single source of truth for the Console↔Launcher
|
||||
RPC contract (wire types, the `PodManagerConnection` client, and the framed-JSON
|
||||
`PodRpc` protocol), shared with the Launcher Service. The assembly keeps the
|
||||
`TeslaConsoleLaunchLib` name so the original-exe baseline still resolves in the
|
||||
differential tests; the wire no longer embeds assembly names (see RPC note below).
|
||||
- `TeslaSecureConfiguration` ← `../SecureConfig/Tesla.SecureConfig.csproj` (net48),
|
||||
the first-boot provisioning protocol (UDP beacons, OFB crypto, RSA key exchange).
|
||||
|
||||
The original `TeslaSecureConfiguration.dll` is retained under `lib/` as the baseline
|
||||
for the byte-identical crypto guard (`SecureConfigCompatTests`). The remaining
|
||||
vendored assemblies (`Munga Net`, `BitmapLibrary`) are .NET 2.0 managed and could be
|
||||
decompiled to source the same way if full-source builds are needed.
|
||||
|
||||
### Console ↔ Launcher RPC (no BinaryFormatter)
|
||||
|
||||
The pod-management channel (TCP 53290) runs **length-prefixed System.Text.Json**
|
||||
frames over the existing OFB-encrypted stream — see `Contract/PodRpcProtocol.cs`,
|
||||
shared verbatim by both ends. This replaced the original `BinaryFormatter` +
|
||||
serialized-`MethodBase` scheme (a remote-code-execution sink and the reason the
|
||||
Launcher was pinned to EOL .NET 6); dispatch is now by method-name string. The
|
||||
Launcher Service/Agent target **net8**. Note the Console still uses `BinaryFormatter`
|
||||
for *local* disk persistence (`Site` config, mission results) — that is local file
|
||||
I/O on net48, not the network surface, and is intentionally left alone.
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
@@ -53,17 +53,21 @@
|
||||
<Reference Include="WeifenLuo.WinFormsUI.Docking">
|
||||
<HintPath>lib\WeifenLuo.WinFormsUI.Docking.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="TeslaSecureConfiguration">
|
||||
<HintPath>lib\TeslaSecureConfiguration.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Munga Net">
|
||||
<HintPath>lib\Munga Net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="TeslaConsoleLaunchLib">
|
||||
<HintPath>lib\TeslaConsoleLaunchLib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="BitmapLibrary">
|
||||
<HintPath>lib\BitmapLibrary.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The wire contract (Tesla.Net) is now built from source. The project emits an
|
||||
assembly still named TeslaConsoleLaunchLib so the BinaryFormatter protocol is
|
||||
byte-identical to the old vendored lib\TeslaConsoleLaunchLib.dll. -->
|
||||
<ProjectReference Include="..\Contract\Tesla.Contract.csproj" />
|
||||
<!-- Secure-config provisioning (Tesla.PodConfigurationServer, OFBCryptoStream...),
|
||||
also built from source now, emitting assembly TeslaSecureConfiguration. -->
|
||||
<ProjectReference Include="..\SecureConfig\Tesla.SecureConfig.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Tesla.Net;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Exercises the framed JSON RPC protocol (PodRpc) that replaced the OFB +
|
||||
/// BinaryFormatter wire. Both the Console client (PodManagerConnection) and the
|
||||
/// Launcher Service share this exact code, so an in-process round-trip
|
||||
/// (encode request → decode → encode response → decode) validates the wire
|
||||
/// contract both ends depend on. The live console↔pod path still needs a pod;
|
||||
/// this covers the serialization, which is the part that can silently break.
|
||||
/// </summary>
|
||||
public class PodRpcProtocolTests
|
||||
{
|
||||
private static readonly Guid Key = new Guid("7D241B1F-AB6D-4e08-9C20-12294E743D94");
|
||||
|
||||
[Fact]
|
||||
public void Request_RoundTrips_Method_And_Args()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
PodRpc.WriteRequest(ms, "KillApp", new object[] { Key, 4242 });
|
||||
ms.Position = 0;
|
||||
|
||||
var req = PodRpc.ReadRequest(ms);
|
||||
Assert.Equal("KillApp", req.Method);
|
||||
Assert.Equal(2, req.Args.Count);
|
||||
Assert.Equal(Key, req.Args[0].GetGuid());
|
||||
Assert.Equal(4242, req.Args[1].GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Request_With_No_Args_RoundTrips()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
PodRpc.WriteRequest(ms, "FullUpdate", null);
|
||||
ms.Position = 0;
|
||||
|
||||
var req = PodRpc.ReadRequest(ms);
|
||||
Assert.Equal("FullUpdate", req.Method);
|
||||
Assert.Empty(req.Args);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Two_Frames_Read_Back_To_Back()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
PodRpc.WriteRequest(ms, "Ping", new object[] { new DateTime(2026, 6, 30, 12, 0, 0, DateTimeKind.Utc) });
|
||||
PodRpc.WriteRequest(ms, "KillAllApps", null);
|
||||
ms.Position = 0;
|
||||
|
||||
Assert.Equal("Ping", PodRpc.ReadRequest(ms).Method);
|
||||
Assert.Equal("KillAllApps", PodRpc.ReadRequest(ms).Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_Error_RoundTrips()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
PodRpc.WriteResponse(ms, null, "boom on the pod");
|
||||
ms.Position = 0;
|
||||
|
||||
var resp = PodRpc.ReadResponse(ms);
|
||||
Assert.Equal("boom on the pod", resp.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_Primitive_Results_RoundTrip()
|
||||
{
|
||||
Assert.Equal(99, RoundTripResult<int>(99));
|
||||
Assert.Equal(Key, RoundTripResult<Guid>(Key));
|
||||
Assert.Equal(0.5f, RoundTripResult<float>(0.5f));
|
||||
Assert.Equal(new DateTime(2026, 6, 30), RoundTripResult<DateTime>(new DateTime(2026, 6, 30)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_LaunchData_Array_RoundTrips_With_Fields()
|
||||
{
|
||||
var apps = new[]
|
||||
{
|
||||
new LaunchData
|
||||
{
|
||||
LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "Red Planet" },
|
||||
WorkingDirectory = @"C:\Games\RedPlanet",
|
||||
ExeFile = "RedPlanet.exe",
|
||||
Arguments = "-res 1024 768",
|
||||
AutoRestart = true,
|
||||
},
|
||||
};
|
||||
|
||||
var got = RoundTripResult<LaunchData[]>(apps);
|
||||
Assert.Single(got);
|
||||
Assert.Equal(Key, got[0].LaunchPair.LaunchKey);
|
||||
Assert.Equal("Red Planet", got[0].LaunchPair.DisplayName);
|
||||
Assert.Equal(@"C:\Games\RedPlanet", got[0].WorkingDirectory);
|
||||
Assert.Equal("RedPlanet.exe", got[0].ExeFile);
|
||||
Assert.Equal("-res 1024 768", got[0].Arguments);
|
||||
Assert.True(got[0].AutoRestart);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_FullUpdateData_RoundTrips()
|
||||
{
|
||||
var full = new FullUpdateData
|
||||
{
|
||||
InstalledApps = new[]
|
||||
{
|
||||
new LaunchData { LaunchPair = new LaunchPair { LaunchKey = Key, DisplayName = "RP" } },
|
||||
},
|
||||
LaunchedApps = new[] { new LaunchedAppData { LaunchKey = Key, ProcessId = 1234 } },
|
||||
VolumeLevel = 0.75f,
|
||||
};
|
||||
|
||||
var got = RoundTripResult<FullUpdateData>(full);
|
||||
Assert.Equal(0.75f, got.VolumeLevel);
|
||||
Assert.Equal("RP", got.InstalledApps[0].LaunchPair.DisplayName);
|
||||
Assert.Equal(1234, got.LaunchedApps[0].ProcessId);
|
||||
Assert.Equal(Key, got.LaunchedApps[0].LaunchKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Response_OutOfBandProgress_RoundTrips()
|
||||
{
|
||||
var p = new OutOfBandProgress { PercentComplete = 73, Status = "Extracting...", IsCompleted = false };
|
||||
var got = RoundTripResult<OutOfBandProgress>(p);
|
||||
Assert.Equal(73, got.PercentComplete);
|
||||
Assert.Equal("Extracting...", got.Status);
|
||||
Assert.False(got.IsCompleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Oversized_Frame_Length_Is_Rejected()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
// Craft a frame header claiming more than MaxFrameBytes.
|
||||
ms.Write(BitConverter.GetBytes(PodRpc.MaxFrameBytes + 1), 0, 4);
|
||||
ms.Position = 0;
|
||||
Assert.Throws<IOException>(() => PodRpc.ReadFrame(ms));
|
||||
}
|
||||
|
||||
// Serialize a result the way the Launcher does, read it the way the Console does.
|
||||
private static T RoundTripResult<T>(object result)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
PodRpc.WriteResponse(ms, result, null);
|
||||
ms.Position = 0;
|
||||
var resp = PodRpc.ReadResponse(ms);
|
||||
Assert.Null(resp.Error);
|
||||
return resp.Result.Deserialize<T>(PodRpc.JsonOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Proves the source-built secure-config library (SecureConfig/Tesla.SecureConfig,
|
||||
/// emitting assembly TeslaSecureConfiguration) is behaviourally identical to the
|
||||
/// original vendored TeslaSecureConfiguration.dll for the parts that define the
|
||||
/// over-the-wire provisioning format: the OFB keystream cipher and the
|
||||
/// BasicConfigResponse byte layout. Byte-identity against the original binary
|
||||
/// transitively guarantees compatibility with the pod-side counterpart (the
|
||||
/// Launcher's hand-written OFBDuplexStream), which was built against that binary.
|
||||
///
|
||||
/// The original is loaded out-of-band with Assembly.LoadFile (it shares identity
|
||||
/// with the source build); construction logic is identical reflection over both.
|
||||
/// </summary>
|
||||
public class SecureConfigCompatTests
|
||||
{
|
||||
private static readonly Assembly SourceCfg = typeof(Tesla.BasicConfigResponse).Assembly;
|
||||
private static readonly Assembly VendoredCfg = LoadVendored();
|
||||
|
||||
private static Assembly LoadVendored()
|
||||
{
|
||||
string path = Path.Combine(AssemblyPaths.RepoRoot, "lib", "TeslaSecureConfiguration.dll");
|
||||
Assert.True(File.Exists(path), "Vendored baseline not found: " + path);
|
||||
return Assembly.LoadFile(path);
|
||||
}
|
||||
|
||||
// Fixed, deterministic inputs so both assemblies see identical material.
|
||||
private static readonly byte[] Key = BuildBytes(32, i => (byte)(i * 7 + 1));
|
||||
private static readonly byte[] Iv = BuildBytes(16, i => (byte)(i * 13 + 5));
|
||||
private static readonly byte[] Plaintext =
|
||||
System.Text.Encoding.UTF8.GetBytes("The quick brown pod jumps over the lazy console. 0123456789!");
|
||||
|
||||
[Fact]
|
||||
public void Baseline_And_Source_Are_Distinct_Assemblies_With_Same_Identity()
|
||||
{
|
||||
Assert.NotEqual(SourceCfg.Location, VendoredCfg.Location);
|
||||
Assert.Equal(VendoredCfg.GetName().Name, SourceCfg.GetName().Name);
|
||||
Assert.Equal(VendoredCfg.GetName().Version, SourceCfg.GetName().Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OFBCryptoStream_Produces_Identical_Ciphertext()
|
||||
{
|
||||
byte[] fromVendored = Encrypt(VendoredCfg);
|
||||
byte[] fromSource = Encrypt(SourceCfg);
|
||||
Assert.Equal(fromVendored, fromSource);
|
||||
// Sanity: it actually encrypted (output differs from plaintext).
|
||||
Assert.NotEqual(Plaintext, fromSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OFBCryptoStream_RoundTrips()
|
||||
{
|
||||
byte[] cipher = Encrypt(SourceCfg);
|
||||
byte[] plain = Decrypt(SourceCfg, cipher);
|
||||
Assert.Equal(Plaintext, plain);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConfigResponse_ToBytes_Is_Identical()
|
||||
{
|
||||
Assert.Equal(ConfigToBytes(VendoredCfg), ConfigToBytes(SourceCfg));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicConfigResponse_RoundTrips_Across_Implementations()
|
||||
{
|
||||
// Bytes produced by the vendored impl must parse correctly in the source impl.
|
||||
byte[] bytes = ConfigToBytes(VendoredCfg);
|
||||
object parsed = Activator.CreateInstance(
|
||||
SourceCfg.GetType("Tesla.BasicConfigResponse", true), bytes);
|
||||
Assert.Equal("192.168.1.50", Prop(parsed, "Address").ToString());
|
||||
Assert.Equal("255.255.255.0", Prop(parsed, "Mask").ToString());
|
||||
Assert.Equal("192.168.1.1", Prop(parsed, "Gateway").ToString());
|
||||
Assert.Equal("8.8.8.8", Prop(parsed, "Dns").ToString());
|
||||
Assert.Equal("POD-01", Prop(parsed, "HostName"));
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private static byte[] Encrypt(Assembly asm)
|
||||
{
|
||||
using var aes = Rijndael.Create();
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Key = Key;
|
||||
var ms = new MemoryStream();
|
||||
var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Write, Iv);
|
||||
ofb.Write(Plaintext, 0, Plaintext.Length);
|
||||
ofb.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] Decrypt(Assembly asm, byte[] cipher)
|
||||
{
|
||||
using var aes = Rijndael.Create();
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Key = Key;
|
||||
var ms = new MemoryStream(cipher);
|
||||
var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Read, Iv);
|
||||
var outBuf = new byte[Plaintext.Length];
|
||||
int off = 0;
|
||||
while (off < outBuf.Length)
|
||||
{
|
||||
int n = ofb.Read(outBuf, off, outBuf.Length - off);
|
||||
if (n == 0) break;
|
||||
off += n;
|
||||
}
|
||||
return outBuf;
|
||||
}
|
||||
|
||||
private static Stream NewOfb(Assembly asm, Stream inner, ICryptoTransform t, CryptoStreamMode mode, byte[] iv)
|
||||
// OFBCryptoStream keeps the IV array by reference and reuses it as its
|
||||
// ping-pong keystream scratch buffer, so it MUST get its own copy — the
|
||||
// real callers always pass a freshly generated IV.
|
||||
=> (Stream)Activator.CreateInstance(
|
||||
asm.GetType("System.Security.Cryptography.OFBCryptoStream", true),
|
||||
inner, t, mode, (byte[])iv.Clone());
|
||||
|
||||
private static byte[] ConfigToBytes(Assembly asm)
|
||||
{
|
||||
Type t = asm.GetType("Tesla.BasicConfigResponse", true);
|
||||
object cfg = Activator.CreateInstance(t,
|
||||
IPAddress.Parse("192.168.1.50"),
|
||||
IPAddress.Parse("255.255.255.0"),
|
||||
IPAddress.Parse("192.168.1.1"),
|
||||
IPAddress.Parse("8.8.8.8"),
|
||||
"POD-01");
|
||||
return (byte[])t.GetMethod("ToBytes").Invoke(cfg, null);
|
||||
}
|
||||
|
||||
private static object Prop(object o, string name) => o.GetType().GetProperty(name).GetValue(o, null);
|
||||
|
||||
private static byte[] BuildBytes(int n, Func<int, byte> f)
|
||||
{
|
||||
var b = new byte[n];
|
||||
for (int i = 0; i < n; i++) b[i] = f(i);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,13 @@
|
||||
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
<!-- The source-built wire contract (emits TeslaConsoleLaunchLib.dll). Referenced
|
||||
directly so WireContractCompatTests can construct Tesla.Net types and compare
|
||||
their BinaryFormatter output against the original vendored DLL. -->
|
||||
<ProjectReference Include="..\..\..\Contract\Tesla.Contract.csproj" />
|
||||
<!-- The source-built secure-config (emits TeslaSecureConfiguration.dll), for
|
||||
SecureConfigCompatTests' byte-identity checks vs the original vendored DLL. -->
|
||||
<ProjectReference Include="..\..\..\SecureConfig\Tesla.SecureConfig.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- net48 only: consumed by the net48 Console and the net48 client half of
|
||||
Tesla.Contract. The pod side is the Launcher's own SecureConfig.cs. -->
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
|
||||
<!-- Emit an assembly named TeslaSecureConfiguration (v1.0.0.0) so it is a
|
||||
drop-in replacement for the original vendored binary. Unlike the wire
|
||||
contract this is not strictly required for compatibility (the provisioning
|
||||
protocol uses manual byte/AES/RSA serialization, not BinaryFormatter), but
|
||||
keeping the identity stable avoids surprising any name-based resolution. -->
|
||||
<AssemblyName>TeslaSecureConfiguration</AssemblyName>
|
||||
<RootNamespace>Tesla</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>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+20
-8
@@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "Lau
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "Launcher\TeslaLauncherAgent.csproj", "{916DCDA5-3379-4383-96F0-AC7B26FAF64E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\Tesla.Contract.csproj", "{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.SecureConfig", "SecureConfig\Tesla.SecureConfig.csproj", "{070A6093-6C46-4A5B-A119-47ED195530E1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -32,14 +36,22 @@ Global
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|x86
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|x86
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|x86
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|x86
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|x86
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|x86
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|x86
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|x86
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|x64
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|x64
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{070A6093-6C46-4A5B-A119-47ED195530E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{070A6093-6C46-4A5B-A119-47ED195530E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{070A6093-6C46-4A5B-A119-47ED195530E1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{27C769F3-F07F-456E-ACB7-F4A4A21AB6D1} = {91E92ADD-9F67-41E4-B43C-CCB7B2D95F15}
|
||||
|
||||
Reference in New Issue
Block a user