Merge: source contract + JSON wire (no BinaryFormatter) + net8/x64, v4.11.4.1
Live-validated end-to-end on a stand-in pod (SecureConfig, install, launch). See branch commits for the full breakdown.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Packaged runnable console (dotnet publish output) + its zip
|
||||
/TeslaConsole-app/
|
||||
/TeslaConsole-app*.zip
|
||||
|
||||
# IDE / OS
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCopyright("Copyright © 2009")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: Guid("581ca4b6-a91c-4d24-b9b5-207f3b5da379")]
|
||||
[assembly: AssemblyFileVersion("4.11.3.0")]
|
||||
[assembly: AssemblyFileVersion("4.11.4.1")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: ComVisible(false)]
|
||||
[assembly: AssemblyTitle("Tesla Console")]
|
||||
[assembly: AssemblyDescription("All code and UI property of Virtual World Entertainment.\r\n\r\nDeveloped by Elsewhen Studios, LLC in association with VGCorps, LLC.\r\n\r\nElsewhen Studios and the Elsewhen Wormhole are trademarks of Elsewhen Studios, LLC\r\n\r\nIncludes the WeifenLuo DockingPane library. Copyright © 2007 Weifen Luo (email: weifenluo@yahoo.com). Licensed under the MIT License - details can be found in WeifenLuo.txt included in this installation.")]
|
||||
[assembly: AssemblyVersion("4.11.3.37076")]
|
||||
[assembly: AssemblyVersion("4.11.4.1")]
|
||||
|
||||
+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>
|
||||
|
||||
@@ -39,11 +39,15 @@ namespace TeslaConsole.DiffTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Loaded_Assemblies_Share_Identity()
|
||||
public void Loaded_Assemblies_Are_The_Expected_Builds()
|
||||
{
|
||||
// Both must really be TeslaConsole 4.11.3.37076 — guards against testing the wrong files.
|
||||
Assert.Equal(_fx.Original.AssemblyFullName, _fx.Recovered.AssemblyFullName);
|
||||
// The original is the untouched 4.11.3.37076 baseline; the recovered build is
|
||||
// the modernized line, intentionally bumped to 4.11.4.x (same assembly name,
|
||||
// new version). Both must be TeslaConsole — guards against testing the wrong files.
|
||||
Assert.Contains("TeslaConsole", _fx.Original.AssemblyFullName);
|
||||
Assert.Contains("TeslaConsole", _fx.Recovered.AssemblyFullName);
|
||||
Assert.Contains("4.11.3.37076", _fx.Original.AssemblyFullName);
|
||||
Assert.Contains("4.11.4.1", _fx.Recovered.AssemblyFullName);
|
||||
}
|
||||
|
||||
// ---- RPStrings.GetTimeString: mm:ss formatting with 0.5s rounding ----
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
@@ -37,11 +38,19 @@ namespace TeslaConsole.DiffTests
|
||||
string.Join("\n ", missing));
|
||||
}
|
||||
|
||||
// The recovered build is intentionally a newer assembly version than the
|
||||
// original baseline. A member signature that references a TeslaConsole type as a
|
||||
// generic argument embeds that type's assembly-qualified name — including the
|
||||
// version — so strip Version= stamps before comparing. We compare type NAMES,
|
||||
// not versions.
|
||||
private static string StripVersion(string sig)
|
||||
=> Regex.Replace(sig, @", Version=\d+\.\d+\.\d+\.\d+", "");
|
||||
|
||||
[Fact]
|
||||
public void Recovered_Exposes_All_Original_Public_Members()
|
||||
{
|
||||
var original = _fx.Original.GetPublicMemberSignatures();
|
||||
var recovered = _fx.Recovered.GetPublicMemberSignatures();
|
||||
var original = _fx.Original.GetPublicMemberSignatures().Select(StripVersion).ToArray();
|
||||
var recovered = _fx.Recovered.GetPublicMemberSignatures().Select(StripVersion).ToArray();
|
||||
|
||||
Assert.True(original.Length > 100,
|
||||
"Only " + original.Length + " public members enumerated from the original; " +
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
/_stage_svc/
|
||||
/_stage_agt/
|
||||
/TeslaLauncher/
|
||||
# zipped deployment package produced for transfer to a pod
|
||||
/TeslaLauncher-*.zip
|
||||
|
||||
# ── Runtime / installer state ────────────────────────────────────────────────
|
||||
# Generated by installutil / the service at runtime, not source
|
||||
|
||||
@@ -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>
|
||||
|
||||
+21
-12
@@ -1,6 +1,6 @@
|
||||
# TeslaLauncher
|
||||
|
||||
.NET 6 rewrite of the original Elsewhen Studios LLC software (Windows 2000 / .NET Framework 2.0).
|
||||
.NET 8 (win-x64, self-contained) rewrite of the original Elsewhen Studios LLC software (Windows 2000 / .NET Framework 2.0).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -8,7 +8,7 @@ TeslaLauncher has three components that work together:
|
||||
|
||||
### TeslaLauncherService (Windows Service, Session 0)
|
||||
- Runs at boot before any user logs in
|
||||
- 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 the Agent via Named Pipe (`TeslaLauncherIPC`)
|
||||
- Handles first-boot network configuration (SecureConfig)
|
||||
- Handles game file transfers from the Console (InstallProduct)
|
||||
@@ -29,7 +29,7 @@ TeslaLauncher has three components that work together:
|
||||
## Communication Flow
|
||||
|
||||
```
|
||||
TeslaConsole ──TCP 53290 (OFB + BinaryFormatter)──> TeslaLauncherService
|
||||
TeslaConsole ──TCP 53290 (OFB + framed JSON)──> TeslaLauncherService
|
||||
│
|
||||
Named Pipe (JSON)
|
||||
│
|
||||
@@ -42,10 +42,10 @@ TeslaConsole ──TCP 53290 (OFB + BinaryFormatter)──> TeslaLauncherService
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `TeslaLauncherService.cs` | Windows Service implementation |
|
||||
| `TeslaLauncherService.csproj` | Service project (net6.0-windows, x86, self-contained) |
|
||||
| `TeslaLauncherService.csproj` | Service project (net8.0-windows, x64, self-contained) |
|
||||
| `TeslaLauncherAgent.cs` | Userspace Agent implementation |
|
||||
| `TeslaLauncherAgent.csproj` | Agent project (WinForms, net6.0-windows, x86) |
|
||||
| `LaunchModels_Shared.cs` | Wire types (Tesla.Net) and IPC types (Tesla.Launcher.Shared) |
|
||||
| `TeslaLauncherAgent.csproj` | Agent project (WinForms, net8.0-windows, x64) |
|
||||
| `LaunchModels_Shared.cs` | Service↔Agent IPC types (Tesla.Launcher.Shared). Wire types (Tesla.Net) now come from `../Contract/Tesla.Contract.csproj` |
|
||||
| `SecureConfig.cs` | First-boot secure configuration protocol |
|
||||
| `build.bat` | Builds both components |
|
||||
| `install.bat` | Installs on a cockpit PC (run as Administrator) |
|
||||
@@ -53,16 +53,19 @@ TeslaConsole ──TCP 53290 (OFB + BinaryFormatter)──> TeslaLauncherService
|
||||
## Building
|
||||
|
||||
Requirements:
|
||||
- .NET 6 SDK
|
||||
- Internet access for NuGet restore
|
||||
- .NET 8 SDK
|
||||
- Internet access for NuGet restore (first build only)
|
||||
|
||||
```
|
||||
build.bat :: build both components
|
||||
build.bat :: build both components + assemble the package
|
||||
build.bat /service :: build Service only
|
||||
build.bat /agent :: build Agent only
|
||||
```
|
||||
|
||||
Output goes to `TeslaLauncher\` with `Service\` and `Agent\` subdirectories.
|
||||
Output goes to `TeslaLauncher\` with `Service\` and `Agent\` subdirectories plus
|
||||
`install.bat`. The projects are published in place (self-contained, single-file,
|
||||
win-x64) — they reference `../Contract`, so they cannot be staged into a temp
|
||||
folder. No .NET runtime is required on the target pod.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -105,6 +108,12 @@ The Console connects to each configured pod on TCP 53290 and can:
|
||||
|
||||
## Wire Protocol
|
||||
|
||||
The Console uses BinaryFormatter over OFB-encrypted TCP streams. All types in the `Tesla.Net` namespace are exact replicas of the original structs from `TeslaConsoleLaunchLib.dll` to maintain serialization compatibility.
|
||||
The Console talks to the Service with **length-prefixed System.Text.Json frames**
|
||||
over the OFB-encrypted TCP stream (dispatch by method name) — see
|
||||
`../Contract/PodRpcProtocol.cs`, shared by both ends. This replaced the original
|
||||
`BinaryFormatter` + serialized-`MethodBase` scheme. The `Tesla.Net` wire types now
|
||||
live in `../Contract/Tesla.Contract.csproj`, the single source of truth shared with
|
||||
the Console.
|
||||
|
||||
The Service-to-Agent IPC uses length-prefixed JSON over a Named Pipe, with flat types that avoid the nested struct layout of the wire format.
|
||||
The Service-to-Agent IPC uses length-prefixed JSON over a Named Pipe, with flat types
|
||||
that avoid the nested struct layout of the wire format.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -58,16 +58,22 @@ namespace Tesla.Launcher.Agent
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// First-boot check: if this machine is unconfigured (DHCP adapter),
|
||||
// show a waiting splash. The Service (Session 0) runs the full
|
||||
// SecureConfig protocol and applies the static IP.
|
||||
// When the splash detects configuration is complete it closes itself,
|
||||
// and we fall through to start the normal Agent with the pipe server.
|
||||
if (!IsMachineConfigured())
|
||||
// First-boot check: show the configuring splash (with the SecureConfig
|
||||
// Request ID + Passphrase) when the machine is unconfigured OR when
|
||||
// SecureConfig is actively running.
|
||||
//
|
||||
// The second condition is essential: during SecureConfig the Service
|
||||
// assigns a TEMPORARY static IP to the Ethernet adapter so it can
|
||||
// broadcast. That makes IsMachineConfigured() report true, so the DHCP
|
||||
// check ALONE would skip the splash and the codes would never reach the
|
||||
// screen — they'd only be in the log / on the COM2 plasma display. The
|
||||
// Service writes configuring.json *before* assigning that temp IP, so its
|
||||
// presence is the authoritative "SecureConfig in progress" signal.
|
||||
if (!IsMachineConfigured() || File.Exists(ConfiguringFilePath()))
|
||||
{
|
||||
ShowConfiguringWait(); // blocks until config detected or form closed
|
||||
ShowConfiguringWait(); // blocks until config completes or form closed
|
||||
|
||||
// If still not configured (user closed the splash manually), exit.
|
||||
// If still not configured (config failed / user closed splash), exit.
|
||||
if (!IsMachineConfigured())
|
||||
return;
|
||||
}
|
||||
@@ -109,6 +115,12 @@ namespace Tesla.Launcher.Agent
|
||||
return false; // all Ethernet adapters are DHCP or absent → needs configuration
|
||||
}
|
||||
|
||||
/// <summary>Shared file the Service writes (before assigning the temp IP) while
|
||||
/// SecureConfig is in progress; holds the RequestId/Passphrase for the splash.</summary>
|
||||
private static string ConfiguringFilePath() => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"TeslaLauncher", "configuring.json");
|
||||
|
||||
private static bool IsVirtualAdapter(NetworkInterface nic)
|
||||
{
|
||||
var desc = nic.Description ?? "";
|
||||
@@ -224,22 +236,24 @@ namespace Tesla.Launcher.Agent
|
||||
form.Controls.Add(lblHint);
|
||||
|
||||
string lastJson = null;
|
||||
bool codesLoaded = false;
|
||||
var cfgFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"TeslaLauncher", "configuring.json");
|
||||
bool sawFile = false;
|
||||
var cfgFile = ConfiguringFilePath();
|
||||
|
||||
// Poll every 2 s — read codes from the Service's shared file.
|
||||
// The file lifecycle:
|
||||
// 1. Service deletes stale file on startup
|
||||
// 2. Service writes fresh file with new codes
|
||||
// 2. Service writes fresh file with new codes (before the temp IP)
|
||||
// 3. Agent reads and displays codes
|
||||
// 4. Service deletes file when config is complete → splash closes
|
||||
// If the Agent started before the file appeared, it keeps waiting; it
|
||||
// closes once the file we saw is gone, or the machine becomes configured
|
||||
// (covers the case where config finished before we managed to read it).
|
||||
var timer = new System.Windows.Forms.Timer { Interval = 2000 };
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
if (File.Exists(cfgFile))
|
||||
{
|
||||
sawFile = true;
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(cfgFile);
|
||||
@@ -252,14 +266,14 @@ namespace Tesla.Launcher.Agent
|
||||
lblRequestId.Text = rid.GetString();
|
||||
if (root.TryGetProperty("Passphrase", out var pp))
|
||||
lblPassphrase.Text = pp.GetString();
|
||||
codesLoaded = true;
|
||||
}
|
||||
}
|
||||
catch { /* file may be mid-write, retry next tick */ }
|
||||
}
|
||||
else if (codesLoaded)
|
||||
else if (sawFile || IsMachineConfigured())
|
||||
{
|
||||
// Service deleted the file → configuration complete.
|
||||
// File gone after we saw it, or the machine is now configured →
|
||||
// SecureConfig is complete.
|
||||
timer.Stop();
|
||||
form.Close();
|
||||
}
|
||||
@@ -285,7 +299,8 @@ namespace Tesla.Launcher.Agent
|
||||
private void BuildTrayIcon()
|
||||
{
|
||||
_trayMenu = new ContextMenuStrip();
|
||||
_trayMenu.Items.Add("Tesla Launcher Agent", null, null).Enabled = false;
|
||||
var version = typeof(AgentApplication).Assembly.GetName().Version;
|
||||
_trayMenu.Items.Add($"Tesla Launcher Agent v{version}", null, null).Enabled = false;
|
||||
_trayMenu.Items.Add(new ToolStripSeparator());
|
||||
_trayMenu.Items.Add("Reload Config", null, (s, e) => LoadLaunchApps());
|
||||
_trayMenu.Items.Add("Kill All Apps", null, (s, e) => CmdKillAllApps());
|
||||
|
||||
@@ -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>
|
||||
<Version>4.11.4.1</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>
|
||||
|
||||
@@ -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,27 +403,14 @@ 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)
|
||||
{
|
||||
var p0 = parameters[0];
|
||||
var p0Type = p0?.GetType().FullName ?? "null";
|
||||
Guid progressId;
|
||||
Guid progressId = Guid.Empty;
|
||||
bool guidMatch = false;
|
||||
|
||||
if (p0 is Guid g)
|
||||
if (args.Count > 0 && args[0].ValueKind == JsonValueKind.String
|
||||
&& Guid.TryParse(args[0].GetString(), out var pg))
|
||||
{
|
||||
progressId = g;
|
||||
progressId = pg;
|
||||
guidMatch = true;
|
||||
}
|
||||
else if (p0 is string s && Guid.TryParse(s, out var gs))
|
||||
{
|
||||
progressId = gs;
|
||||
guidMatch = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
progressId = Guid.Empty;
|
||||
}
|
||||
|
||||
if (guidMatch)
|
||||
{
|
||||
@@ -464,7 +418,6 @@ namespace Tesla.Launcher.Service
|
||||
{
|
||||
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);
|
||||
@@ -472,18 +425,12 @@ namespace Tesla.Launcher.Service
|
||||
}
|
||||
}
|
||||
File.AppendAllText(diagPath,
|
||||
$"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (type={p0Type}, keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}");
|
||||
$"[{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: param[0] type={p0Type} value={p0} — NOT a Guid{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,18 +471,12 @@ namespace Tesla.Launcher.Service
|
||||
return callId;
|
||||
}
|
||||
|
||||
case "KillApp":
|
||||
case "KillAllOfType":
|
||||
case "KillAllApps":
|
||||
case "Shutdown":
|
||||
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)
|
||||
{
|
||||
// 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(),
|
||||
@@ -545,19 +486,30 @@ namespace Tesla.Launcher.Service
|
||||
Arguments = ld.Arguments,
|
||||
AutoRestart = ld.AutoRestart
|
||||
};
|
||||
parameters = new object[] { flat };
|
||||
var payloadJson = JsonSerializer.Serialize(new object[] { flat });
|
||||
await ForwardToAgentCoreAsync(functionName, launchKey: null, payloadJson, ct);
|
||||
return null;
|
||||
}
|
||||
await ForwardToAgentJsonAsync(functionName, parameters, ct);
|
||||
|
||||
case "KillApp":
|
||||
case "KillAllOfType":
|
||||
case "KillAllApps":
|
||||
case "Shutdown":
|
||||
case "ClearStore":
|
||||
case "RemoveApp":
|
||||
case "UninstallApp":
|
||||
{
|
||||
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>
|
||||
<Version>4.11.4.1</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>
|
||||
|
||||
|
||||
|
||||
+34
-87
@@ -1,36 +1,39 @@
|
||||
@echo off
|
||||
:: =============================================================================
|
||||
:: TeslaLauncher — Unified Build Script
|
||||
:: TeslaLauncher — Unified Build / Package Script
|
||||
:: =============================================================================
|
||||
:: Builds TeslaLauncherService.exe and TeslaLauncherAgent.exe (with integrated
|
||||
:: TeslaSecureConfiguration protocol) from source.
|
||||
:: Publishes TeslaLauncherService.exe and TeslaLauncherAgent.exe as
|
||||
:: self-contained single-file win-x64 executables (net8) and assembles the
|
||||
:: installable TeslaLauncher\ package next to install.bat.
|
||||
::
|
||||
:: Requirements:
|
||||
:: .NET 6 SDK https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
:: Internet access for NuGet restore (System.IO.Ports 7.0.0)
|
||||
:: .NET 8 SDK https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
:: Internet access for NuGet restore (first build only)
|
||||
::
|
||||
:: Usage:
|
||||
:: build.bat — build both components
|
||||
:: build.bat /service — build Service only
|
||||
:: build.bat /agent — build Agent only
|
||||
:: build.bat - build both components + package
|
||||
:: build.bat /service - build Service only
|
||||
:: build.bat /agent - build Agent only
|
||||
::
|
||||
:: Output:
|
||||
:: TeslaLauncher\
|
||||
:: Service\
|
||||
:: TeslaLauncherService.exe
|
||||
:: Agent\
|
||||
:: TeslaLauncherAgent.exe
|
||||
:: install.bat
|
||||
:: Service\TeslaLauncherService.exe
|
||||
:: Agent\TeslaLauncherAgent.exe
|
||||
:: [oalinst.exe, dx9201006\] (if vendored under assets\)
|
||||
::
|
||||
:: NOTE: the projects reference ..\Contract\Tesla.Contract.csproj, so they are
|
||||
:: published IN PLACE (not staged into a temp folder) — the project reference
|
||||
:: must be able to resolve relative to this directory.
|
||||
:: =============================================================================
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set ROOT=%~dp0
|
||||
set BUILD_DIR=%ROOT%TeslaLauncher
|
||||
set SVC_STAGE=%ROOT%_stage_svc
|
||||
set AGT_STAGE=%ROOT%_stage_agt
|
||||
set RID=win-x64
|
||||
|
||||
:: ── Parse arguments ───────────────────────────────────────────────────────────
|
||||
:: -- Parse arguments ----------------------------------------------------------
|
||||
set BUILD_SERVICE=1
|
||||
set BUILD_AGENT=1
|
||||
set QUIET=0
|
||||
@@ -43,17 +46,17 @@ for %%a in (%*) do (
|
||||
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo Tesla Launcher v0.1 — Unified Build
|
||||
echo Tesla Launcher v0.1 - Build ^& Package (net8, %RID%)
|
||||
echo Output : %BUILD_DIR%
|
||||
echo ============================================================
|
||||
echo.
|
||||
|
||||
:: ── Verify .NET SDK ───────────────────────────────────────────────────────────
|
||||
:: -- Verify .NET SDK ----------------------------------------------------------
|
||||
where dotnet >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ERROR: dotnet not found.
|
||||
echo Install the .NET 6 SDK from:
|
||||
echo https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
echo Install the .NET 8 SDK from:
|
||||
echo https://dotnet.microsoft.com/download/dotnet/8.0
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
)
|
||||
@@ -61,99 +64,63 @@ for /f "tokens=*" %%v in ('dotnet --version 2^>^&1') do set SDK_VER=%%v
|
||||
echo SDK : %SDK_VER%
|
||||
echo.
|
||||
|
||||
:: ── Verify source files ───────────────────────────────────────────────────────
|
||||
call :check_file "TeslaLauncherService.cs" || goto :err_missing
|
||||
call :check_file "TeslaLauncherService.csproj" || goto :err_missing
|
||||
call :check_file "TeslaLauncherAgent.cs" || goto :err_missing
|
||||
call :check_file "TeslaLauncherAgent.csproj" || goto :err_missing
|
||||
call :check_file "LaunchModels_Shared.cs" || goto :err_missing
|
||||
call :check_file "SecureConfig.cs" || goto :err_missing
|
||||
|
||||
:: ── Clean and prepare build output ───────────────────────────────────────────
|
||||
:: -- Clean prior package output ----------------------------------------------
|
||||
if exist "%BUILD_DIR%\Service" rmdir /s /q "%BUILD_DIR%\Service"
|
||||
if exist "%BUILD_DIR%\Agent" rmdir /s /q "%BUILD_DIR%\Agent"
|
||||
|
||||
:: ── Build Service ─────────────────────────────────────────────────────────────
|
||||
:: -- Build Service ------------------------------------------------------------
|
||||
if %BUILD_SERVICE%==0 goto :skip_service
|
||||
|
||||
echo [1/2] Building TeslaLauncherService...
|
||||
echo [1/2] Publishing TeslaLauncherService (Session 0 Windows Service)...
|
||||
echo.
|
||||
|
||||
if exist "%SVC_STAGE%" rmdir /s /q "%SVC_STAGE%"
|
||||
mkdir "%SVC_STAGE%"
|
||||
|
||||
copy /y "%ROOT%TeslaLauncherService.cs" "%SVC_STAGE%\" >nul
|
||||
copy /y "%ROOT%TeslaLauncherService.csproj" "%SVC_STAGE%\" >nul
|
||||
copy /y "%ROOT%LaunchModels_Shared.cs" "%SVC_STAGE%\" >nul
|
||||
copy /y "%ROOT%SecureConfig.cs" "%SVC_STAGE%\" >nul
|
||||
copy /y "%ROOT%app.ico" "%SVC_STAGE%\" >nul
|
||||
|
||||
dotnet publish "%SVC_STAGE%\TeslaLauncherService.csproj" ^
|
||||
dotnet publish "%ROOT%TeslaLauncherService.csproj" ^
|
||||
-c Release ^
|
||||
-r win-x86 ^
|
||||
-r %RID% ^
|
||||
--self-contained true ^
|
||||
-p:PublishSingleFile=true ^
|
||||
-o "%BUILD_DIR%\Service"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERROR: Service build failed.
|
||||
rmdir /s /q "%SVC_STAGE%" 2>nul
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rmdir /s /q "%SVC_STAGE%"
|
||||
echo.
|
||||
echo Service : %BUILD_DIR%\Service\TeslaLauncherService.exe
|
||||
echo.
|
||||
|
||||
:skip_service
|
||||
|
||||
:: ── Build Agent ───────────────────────────────────────────────────────────────
|
||||
:: -- Build Agent --------------------------------------------------------------
|
||||
if %BUILD_AGENT%==0 goto :skip_agent
|
||||
|
||||
if %BUILD_SERVICE%==1 (echo [2/2] Building TeslaLauncherAgent...) else (echo [1/1] Building TeslaLauncherAgent...)
|
||||
if %BUILD_SERVICE%==1 (echo [2/2] Publishing TeslaLauncherAgent...) else (echo [1/1] Publishing TeslaLauncherAgent...)
|
||||
echo.
|
||||
|
||||
if exist "%AGT_STAGE%" rmdir /s /q "%AGT_STAGE%"
|
||||
mkdir "%AGT_STAGE%"
|
||||
|
||||
copy /y "%ROOT%TeslaLauncherAgent.cs" "%AGT_STAGE%\" >nul
|
||||
copy /y "%ROOT%TeslaLauncherAgent.csproj" "%AGT_STAGE%\" >nul
|
||||
copy /y "%ROOT%LaunchModels_Shared.cs" "%AGT_STAGE%\" >nul
|
||||
copy /y "%ROOT%SecureConfig.cs" "%AGT_STAGE%\" >nul
|
||||
copy /y "%ROOT%app.ico" "%AGT_STAGE%\" >nul
|
||||
|
||||
dotnet publish "%AGT_STAGE%\TeslaLauncherAgent.csproj" ^
|
||||
dotnet publish "%ROOT%TeslaLauncherAgent.csproj" ^
|
||||
-c Release ^
|
||||
-r win-x86 ^
|
||||
-r %RID% ^
|
||||
--self-contained true ^
|
||||
-p:PublishSingleFile=true ^
|
||||
"-p:DefineConstants=WINFORMS" ^
|
||||
-o "%BUILD_DIR%\Agent"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERROR: Agent build failed.
|
||||
rmdir /s /q "%AGT_STAGE%" 2>nul
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
rmdir /s /q "%AGT_STAGE%"
|
||||
echo.
|
||||
echo Agent : %BUILD_DIR%\Agent\TeslaLauncherAgent.exe
|
||||
echo.
|
||||
|
||||
:skip_agent
|
||||
|
||||
:: ── Copy shared assets into build output ─────────────────────────────────────
|
||||
:: -- Copy shared assets into the package --------------------------------------
|
||||
if exist "%ROOT%install.bat" copy /y "%ROOT%install.bat" "%BUILD_DIR%\" >nul
|
||||
if exist "%ROOT%assets\oalinst.exe" copy /y "%ROOT%assets\oalinst.exe" "%BUILD_DIR%\" >nul
|
||||
if exist "%ROOT%assets\dx9201006\" xcopy /y /s /i /q "%ROOT%assets\dx9201006" "%BUILD_DIR%\dx9201006" >nul
|
||||
|
||||
:: ── Summary ───────────────────────────────────────────────────────────────────
|
||||
:: -- Summary ------------------------------------------------------------------
|
||||
echo ============================================================
|
||||
echo Build complete
|
||||
echo ============================================================
|
||||
@@ -162,28 +129,8 @@ if %BUILD_SERVICE%==1 echo Service : %BUILD_DIR%\Service\TeslaLauncherServic
|
||||
if %BUILD_AGENT%==1 echo Agent : %BUILD_DIR%\Agent\TeslaLauncherAgent.exe
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Copy the TeslaLauncher\ folder to each cockpit PC
|
||||
echo 1. Copy the TeslaLauncher\ folder to the pod (or stand-in) PC
|
||||
echo 2. Run TeslaLauncher\install.bat as Administrator
|
||||
echo.
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 0
|
||||
|
||||
:: ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
:check_file
|
||||
if not exist "%ROOT%%~1" (
|
||||
echo ERROR: Required source file not found: %~1
|
||||
exit /b 1
|
||||
)
|
||||
exit /b 0
|
||||
|
||||
:err_missing
|
||||
echo.
|
||||
echo Make sure all source files are in the same folder as build.bat:
|
||||
echo TeslaLauncherService.cs / .csproj
|
||||
echo TeslaLauncherAgent.cs / .csproj
|
||||
echo LaunchModels_Shared.cs
|
||||
echo SecureConfig.cs
|
||||
echo install.bat
|
||||
echo.
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
|
||||
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