Files
TeslaSuite/Launcher/TeslaLauncherService.cs
T
CydandClaude Opus 4.8 b9d8027cf6 Extract shared contract, drop BinaryFormatter wire, modernize to net8/x64
Make the Console<->Launcher system source-built and modern now that the console
is under our control and the WinXP-era pods are gone.

Contract extraction (Contract/Tesla.Contract.csproj):
- One multi-targeted (net48;net8.0-windows) source project for the RPC contract,
  replacing the vendored TeslaConsoleLaunchLib.dll and the hand-synced Tesla.Net
  replica in Launcher/LaunchModels_Shared.cs. Emits assembly TeslaConsoleLaunchLib.

SecureConfig extraction (SecureConfig/Tesla.SecureConfig.csproj):
- net48 source of the first-boot provisioning protocol (UDP beacons, OFB crypto,
  RSA key exchange), replacing the vendored TeslaSecureConfiguration.dll.

Remove BinaryFormatter from the wire (RCE sink + the reason net6 was pinned):
- Console<->Launcher RPC is now length-prefixed System.Text.Json frames
  (Contract/PodRpcProtocol.cs) over the unchanged OFB transport; dispatch by
  method name. Deleted the SerializationBinder / MethodInfoProxy machinery.
- Console-local BinaryFormatter (Site config, mission replays) intentionally
  retained: local net48 file I/O, not the network surface.

Runtime modernization:
- Launcher Service + Agent: net6 -> net8, win-x86 -> win-x64 (all pods are
  64-bit Win10). Kept the SHA1-default PBKDF2 (Console key-derivation compat)
  with SYSLIB0041 suppressed and documented.

Tests: differential suite now 73 green. Added SecureConfigCompatTests (OFB
ciphertext byte-identical to the vendored DLL) and PodRpcProtocolTests (JSON
round-trip of every request/response shape); removed the now-obsolete
BinaryFormatter byte-identity guard.

Build hygiene: per-project obj dirs (Launcher/Directory.Build.props) fix a
NuGet restore collision between the two Launcher projects sharing one folder.

NOT runtime-verified against a live pod.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:15:17 -05:00

945 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// TeslaLauncher — Windows Service (Session 0)
// =============================================================================
// 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 framed-JSON RPC from TeslaConsole.
// Forwards commands to TeslaLauncherAgent (user session) via Named Pipe.
//
// On first boot (DHCP detected), runs SecureConfig to receive network
// configuration from the Console and establish the session key.
// =============================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TeslaSecureConfig;
using Tesla.Launcher.Shared;
using Tesla.Net;
namespace Tesla.Launcher.Service
{
public class TeslaLauncherService : BackgroundService
{
// ---------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------
/// <summary>
/// TCP port the Console connects to for RPC commands.
/// Original: ManagePort = 53290 (confirmed from decompiled TeslaLauncherService.exe).
/// Each connection is wrapped in OFB encryption via NegotiateCryptoStreams
/// using the session key from SecureConfig (TeslaKeyStore.key).
/// </summary>
private const int CONSOLE_PORT = 53290;
/// <summary>
/// Named pipe connecting this service to TeslaLauncherAgent.exe.
/// Must match PIPE_NAME in TeslaLauncherAgent.cs.
/// </summary>
private const string PIPE_NAME = "TeslaLauncherIPC";
/// <summary>
/// Seconds to wait for the Agent to connect on the Named Pipe.
/// If the Agent isn't running (user not yet logged in), we return an error.
/// </summary>
private const int AGENT_CONNECT_TIMEOUT_MS = 3000;
// ---------------------------------------------------------------
/// <summary>Path to session key file (original KeyStore format).</summary>
private static readonly string KEY_FILE = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"TeslaLauncher", "TeslaKeyStore.key");
private readonly ILogger<TeslaLauncherService> _logger;
private TcpListener _listener;
private readonly List<Task> _clientTasks = new();
private readonly object _taskLock = new();
private byte[] _sessionKey;
// Install product — file transfer progress tracking.
// InitiateInstallProduct creates a Guid; the Console then streams the
// game archive on the SAME OFB connection. GetOutOfBandProgress polls
// this dictionary from a separate connection.
private readonly Dictionary<Guid, OutOfBandProgress> _installProgress = new();
private readonly object _installLock = new();
public TeslaLauncherService(ILogger<TeslaLauncherService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Tesla Launcher Service starting on port {Port}", CONSOLE_PORT);
// ── 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.
var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
void StartupLog(string msg) => File.AppendAllText(logPath,
$"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
StartupLog("=== Service starting ===");
foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{
if (nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Ethernet) continue;
if (nic.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue;
try
{
var ipv4 = nic.GetIPProperties().GetIPv4Properties();
StartupLog($" NIC [{nic.Name}] DHCP={ipv4.IsDhcpEnabled} Virtual={IsVirtualAdapter(nic)}");
}
catch { StartupLog($" NIC [{nic.Name}] (no IPv4 props)"); }
}
// ── First-boot secure configuration ──────────────────────────────
// If the Ethernet adapter is still on DHCP the cockpit has not yet
// been configured. Run PodSecureConfigurator here in Session 0:
// • broadcasts the RQST beacon (MAC + RequestId)
// • displays passphrase on COM2 / PlasmaIO
// • receives UDP RPLY with AES-encrypted network configuration
// • applies netsh IP settings and hostname
// • completes TCP handshake with Console + RSA key exchange
// • saves 32-byte session key to TeslaKeyStore.key
// After SecureConfig, fall through to the normal command listener —
// the original Service does the same (ExecutionMode after Configure).
if (!IsMachineConfigured())
{
StartupLog("IsMachineConfigured=false → running SecureConfig");
_logger.LogInformation("Machine is unconfigured (DHCP). Running SecureConfiguration.");
await Task.Run(() => RunSecureConfiguration(stoppingToken), stoppingToken);
// Load key that was saved during SecureConfig
_sessionKey = LoadSessionKey();
if (_sessionKey == null)
{
_logger.LogWarning("SecureConfig completed but no session key was saved. Cannot start command listener.");
return;
}
StartupLog("SecureConfig complete → starting command listener on port 53290");
}
else
{
StartupLog("IsMachineConfigured=true → loading session key");
_sessionKey = LoadSessionKey();
if (_sessionKey == null)
{
_logger.LogWarning("Machine is configured but TeslaKeyStore.key not found. Reboot with DHCP to re-run SecureConfig.");
StartupLog("ERROR: TeslaKeyStore.key not found — cannot start command listener");
return;
}
StartupLog($"Session key loaded ({_sessionKey.Length} bytes) → starting normal service");
}
_logger.LogInformation("Starting command listener on port {Port}.", CONSOLE_PORT);
try
{
_listener = new TcpListener(IPAddress.Any, CONSOLE_PORT);
_listener.Start();
_logger.LogInformation("Listening. Waiting for Console connections...");
while (!stoppingToken.IsCancellationRequested)
{
try
{
var client = await _listener.AcceptTcpClientAsync(stoppingToken);
_logger.LogInformation(
"Console connected from {EP}", client.Client.RemoteEndPoint);
// Handle each Console connection on its own thread.
// Frame reads are synchronous, so we offload to a
// thread-pool thread via Task.Run.
var task = Task.Run(
() => HandleConsoleClient(client, stoppingToken),
stoppingToken);
lock (_taskLock)
{
_clientTasks.Add(task);
_clientTasks.RemoveAll(t => t.IsCompleted);
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
_logger.LogError(ex, "Error accepting Console connection");
}
}
}
finally
{
_listener?.Stop();
_logger.LogInformation("Tesla Launcher Service stopped.");
}
}
// ── Console Connection Handler ───────────────────────────────────
private async Task HandleConsoleClient(TcpClient tcpClient, CancellationToken ct)
{
using var client = tcpClient;
using var netStream = client.GetStream();
netStream.WriteTimeout = 10_000;
netStream.ReadTimeout = 30_000;
// ── OFB crypto negotiation ────────────────────────────────────
// Both sides exchange 16-byte IVs, then verify "CONF" handshake.
// Read Console's IV
var consoleIv = new byte[16];
try { ReadExactSync(netStream, consoleIv); }
catch (Exception ex)
{
_logger.LogWarning("Failed reading Console IV: {Msg}", ex.Message);
return;
}
// Generate and send our IV
var podIv = new byte[16];
RandomNumberGenerator.Fill(podIv);
netStream.Write(podIv, 0, 16);
netStream.Flush();
// Create OFB duplex stream — lives for the entire session
using var ofb = new OFBDuplexStream(netStream, _sessionKey,
writeIv: consoleIv, // pod→console (other's IV)
readIv: podIv); // console→pod (own IV)
// CONF handshake — verify both sides have the same session key
var confMsg = Encoding.UTF8.GetBytes("CONF");
ofb.Write(confMsg, 0, confMsg.Length);
ofb.Flush();
var confReply = new byte[4];
try { ReadExactSync(ofb, confReply); }
catch (Exception ex)
{
_logger.LogWarning("CONF read failed: {Msg}", ex.Message);
return;
}
if (!confReply.AsSpan().SequenceEqual(confMsg))
{
_logger.LogWarning("CONF mismatch — session key mismatch, dropping connection.");
return;
}
Stream rpcStream = ofb;
var diagLog = Path.Combine(AppContext.BaseDirectory, "podconf.log");
_logger.LogInformation("Console session started.");
try
{
while (!ct.IsCancellationRequested && client.Connected)
{
RpcRequest request;
try
{
request = PodRpc.ReadRequest(rpcStream);
}
catch (EndOfStreamException)
{
_logger.LogInformation("Console disconnected (EOF).");
break;
}
catch (IOException ex)
{
_logger.LogInformation("Console disconnected (I/O: {Msg}).", ex.Message);
break;
}
catch (Exception ex)
{
_logger.LogWarning("Request read error ({Type}): {Msg}",
ex.GetType().Name, ex.Message);
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] REQ ERROR: {ex}{Environment.NewLine}");
break;
}
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;
object resultObj = null;
string error = null;
try
{
resultObj = await DispatchCommandAsync(funcName, args, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing {Func}", funcName);
error = ex.Message;
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} ERROR: {ex.Message}{Environment.NewLine}");
}
var dur = DateTime.UtcNow - start;
if (error == null)
{
File.AppendAllText(diagLog,
$"[{DateTime.Now:HH:mm:ss}] CMD {funcName} OK ({dur.TotalMilliseconds:F0}ms)" +
$"{(resultObj != null ? $" {resultObj}" : "")}{Environment.NewLine}");
}
try
{
PodRpc.WriteResponse(rpcStream, resultObj, error);
}
catch (IOException)
{
_logger.LogInformation("Console disconnected while writing response.");
break;
}
catch (Exception serEx)
{
// Result type not JSON-serializable — send error instead.
_logger.LogWarning("Serialize failed for {Func}: {Msg}",
funcName, serEx.Message);
try { PodRpc.WriteResponse(rpcStream, null, $"Pod error: {serEx.Message}"); }
catch { break; }
}
// ── File receive after InitiateInstallProduct ────────────
// The Console streams the game archive on this same OFB
// connection immediately after receiving the Guid response:
// 8-byte Int64 file size + raw file bytes
// Then closes the connection (FIN).
if (funcName == "InitiateInstallProduct"
&& error == null
&& resultObj is Guid installGuid)
{
ReceiveInstallFile(rpcStream, installGuid, diagLog);
break; // connection done after file transfer
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error in Console handler");
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}");
}
_logger.LogInformation("Console session ended.");
}
// ── 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, IReadOnlyList<JsonElement> args, CancellationToken ct)
{
switch (functionName)
{
case "Ping":
// Echo back the DateTime parameter (Console measures round-trip)
return args.Count > 0 && args[0].ValueKind != JsonValueKind.Null
? args[0].GetDateTime()
: DateTime.Now;
case "GetLaunchableApps":
{
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++)
pairs[i] = wire[i].LaunchPair;
return pairs;
}
case "GetInstalledApps":
{
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return FlatArrayToWire(json);
}
case "GetLaunchedApps":
{
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return FlatLaunchedArrayToWire(json);
}
case "FullUpdate":
{
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();
var result2 = new FullUpdateData
{
InstalledApps = flat.InstalledApps != null
? Array.ConvertAll(flat.InstalledApps, FlatToWire)
: Array.Empty<LaunchData>(),
LaunchedApps = flat.LaunchedApps != null
? Array.ConvertAll(flat.LaunchedApps, FlatLaunchedToWire)
: Array.Empty<LaunchedAppData>(),
VolumeLevel = flat.VolumeLevel
};
return result2;
}
case "GetOutOfBandProgress":
{
// Handled directly in the Service — progress is tracked
// during file receive on the InitiateInstallProduct connection.
var diagPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
Guid progressId = Guid.Empty;
bool guidMatch = false;
if (args.Count > 0 && args[0].ValueKind == JsonValueKind.String
&& Guid.TryParse(args[0].GetString(), out var pg))
{
progressId = pg;
guidMatch = true;
}
if (guidMatch)
{
lock (_installLock)
{
if (_installProgress.TryGetValue(progressId, out var prog))
{
_logger.LogDebug(
"GetOutOfBandProgress({Id}): {Pct}% {Status} done={Done}",
progressId, prog.PercentComplete, prog.Status, prog.IsCompleted);
return prog;
}
}
File.AppendAllText(diagPath,
$"[{DateTime.Now:HH:mm:ss}] OOBP: Guid {progressId} NOT FOUND in _installProgress (keys={string.Join(",", GetProgressKeys())}){Environment.NewLine}");
}
else
{
File.AppendAllText(diagPath,
$"[{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
{
PercentComplete = 0,
Status = "Unknown call ID",
IsCompleted = true
};
}
case "LaunchApp":
{
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))
return pid.GetInt32();
return 0;
}
case "get_VolumeLevel":
{
var json = await ForwardToAgentJsonAsync(functionName, args, ct);
return json != null ? JsonSerializer.Deserialize<float>(json) : 1.0f;
}
case "InitiateInstallProduct":
{
var callId = Guid.NewGuid();
lock (_installLock)
{
_installProgress[callId] = new OutOfBandProgress
{
PercentComplete = 0,
Status = "Waiting for file...",
IsCompleted = false
};
}
return callId;
}
case "InstallApp":
{
// Flatten the wire LaunchData into the Agent's flat JSON format.
var ld = args.Count > 0
? args[0].Deserialize<LaunchData>(PodRpc.JsonOptions)
: default;
var flat = new
{
LaunchKey = ld.LaunchPair.LaunchKey.ToString(),
DisplayName = ld.LaunchPair.DisplayName,
WorkingDirectory = ld.WorkingDirectory,
ExeFile = ld.ExeFile,
Arguments = ld.Arguments,
AutoRestart = ld.AutoRestart
};
var payloadJson = JsonSerializer.Serialize(new object[] { flat });
await ForwardToAgentCoreAsync(functionName, launchKey: null, payloadJson, ct);
return null;
}
case "KillApp":
case "KillAllOfType":
case "KillAllApps":
case "Shutdown":
case "ClearStore":
case "RemoveApp":
case "UninstallApp":
{
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
}
case "set_VolumeLevel":
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
default:
_logger.LogWarning("Unknown command: {Func}", functionName);
await ForwardToAgentJsonAsync(functionName, args, ct);
return null;
}
}
// ── Install Product File Receive ─────────────────────────────────
private const string GAMES_DIR = @"C:\Games";
private void ReceiveInstallFile(Stream stream, Guid callId, string logPath)
{
void Log(string msg) => File.AppendAllText(logPath,
$"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
string tempZip = null;
try
{
// ── Phase 1: Receive file ────────────────────────────────
var sizeBuf = new byte[8];
ReadExactSync(stream, sizeBuf);
long fileSize = BitConverter.ToInt64(sizeBuf, 0);
Log($"INSTALL {callId:N}: receiving {fileSize:N0} bytes...");
UpdateProgress(callId, 0, "Receiving file...");
var dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"TeslaLauncher");
Directory.CreateDirectory(dataDir);
tempZip = Path.Combine(dataDir, $"install_{callId:N}.zip");
using (var fs = File.Create(tempZip))
{
var buffer = new byte[65536];
long received = 0;
while (received < fileSize)
{
var toRead = (int)Math.Min(buffer.Length, fileSize - received);
var n = stream.Read(buffer, 0, toRead);
if (n == 0) throw new IOException("Connection closed during file transfer.");
fs.Write(buffer, 0, n);
received += n;
// Report receive progress as 050%
int pct = (int)(received * 50 / fileSize);
UpdateProgress(callId, pct, "Receiving file...");
}
}
Log($"INSTALL {callId:N}: file received ({fileSize:N0} bytes)");
// ── Phase 2: Extract to C:\Games\ ────────────────────────
UpdateProgress(callId, 50, "Extracting...");
Directory.CreateDirectory(GAMES_DIR);
using (var zip = ZipFile.OpenRead(tempZip))
{
int total = zip.Entries.Count;
int done = 0;
foreach (var entry in zip.Entries)
{
var destPath = Path.GetFullPath(
Path.Combine(GAMES_DIR, entry.FullName));
// Zip-slip protection
if (!destPath.StartsWith(GAMES_DIR, StringComparison.OrdinalIgnoreCase))
continue;
if (string.IsNullOrEmpty(entry.Name))
{
// Directory entry
Directory.CreateDirectory(destPath);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
entry.ExtractToFile(destPath, overwrite: true);
}
done++;
// Report extract progress as 5095%
int pct = 50 + (done * 45 / Math.Max(total, 1));
UpdateProgress(callId, pct, "Extracting...");
}
}
Log($"INSTALL {callId:N}: extracted to {GAMES_DIR}");
// ── Phase 3: Run postinstall.bat if present ──────────────
var postInstall = Path.Combine(GAMES_DIR, "postinstall.bat");
if (File.Exists(postInstall))
{
UpdateProgress(callId, 96, "Running postinstall...");
Log($"INSTALL {callId:N}: running postinstall.bat");
try
{
using var proc = System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo
{
FileName = postInstall,
WorkingDirectory = GAMES_DIR,
UseShellExecute = false,
CreateNoWindow = true
});
proc?.WaitForExit(60_000); // 60 s timeout
}
catch (Exception ex)
{
Log($"INSTALL {callId:N}: postinstall.bat error: {ex.Message}");
}
try { File.Delete(postInstall); } catch { }
Log($"INSTALL {callId:N}: postinstall.bat completed and removed");
}
// ── Done ─────────────────────────────────────────────────
// Console checks PercentComplete == 99 to break out of its
// 3-iteration install loop (InstallProductWorker in PodInfo).
// Using 100 causes the Console to retry all 3 times.
UpdateProgress(callId, 99, "Complete", isCompleted: true);
Log($"INSTALL {callId:N}: install complete");
}
catch (Exception ex)
{
UpdateProgress(callId, 0, $"Failed: {ex.Message}", isCompleted: true);
Log($"INSTALL {callId:N}: FAILED: {ex.Message}");
_logger.LogError(ex, "Install {Id} failed", callId);
}
finally
{
// Clean up temp zip
try { if (tempZip != null) File.Delete(tempZip); } catch { }
}
}
private string[] GetProgressKeys()
{
lock (_installLock)
{
return Array.ConvertAll(
new List<Guid>(_installProgress.Keys).ToArray(),
g => g.ToString());
}
}
// ── Flat ↔ Wire Conversion ────────────────────────────────────────
private class FlatLaunchData
{
public string LaunchKey { get; set; }
public string DisplayName { get; set; }
public string WorkingDirectory { get; set; }
public string ExeFile { get; set; }
public string Arguments { get; set; }
public bool AutoRestart { get; set; }
}
private class FlatLaunchedAppData
{
public string LaunchKey { get; set; }
public int ProcessId { get; set; }
}
private class FlatFullUpdateData
{
public FlatLaunchData[] InstalledApps { get; set; }
public FlatLaunchedAppData[] LaunchedApps { get; set; }
public float VolumeLevel { get; set; }
}
private static LaunchData FlatToWire(FlatLaunchData f)
{
return new LaunchData
{
LaunchPair = new LaunchPair
{
LaunchKey = Guid.TryParse(f.LaunchKey, out var g) ? g : Guid.Empty,
DisplayName = f.DisplayName
},
WorkingDirectory = f.WorkingDirectory,
ExeFile = f.ExeFile,
Arguments = f.Arguments,
AutoRestart = f.AutoRestart
};
}
private static LaunchedAppData FlatLaunchedToWire(FlatLaunchedAppData f)
{
return new LaunchedAppData
{
LaunchKey = Guid.TryParse(f.LaunchKey, out var g) ? g : Guid.Empty,
ProcessId = f.ProcessId
};
}
private static LaunchedAppData[] FlatLaunchedArrayToWire(string json)
{
if (json == null) return Array.Empty<LaunchedAppData>();
var flat = JsonSerializer.Deserialize<FlatLaunchedAppData[]>(json);
if (flat == null) return Array.Empty<LaunchedAppData>();
return Array.ConvertAll(flat, FlatLaunchedToWire);
}
private static LaunchData[] FlatArrayToWire(string json)
{
if (json == null) return Array.Empty<LaunchData>();
var flat = JsonSerializer.Deserialize<FlatLaunchData[]>(json);
if (flat == null) return Array.Empty<LaunchData>();
var result = new LaunchData[flat.Length];
for (int i = 0; i < flat.Length; i++)
result[i] = FlatToWire(flat[i]);
return result;
}
private void UpdateProgress(Guid callId, int pct, string status, bool isCompleted = false)
{
lock (_installLock)
{
_installProgress[callId] = new OutOfBandProgress
{
PercentComplete = pct,
Status = status,
IsCompleted = isCompleted
};
}
}
// ── Secure Configuration ─────────────────────────────────────────
private static bool IsVirtualAdapter(System.Net.NetworkInformation.NetworkInterface nic)
{
var desc = nic.Description ?? ""; var name = nic.Name ?? "";
string[] markers = { "Virtual", "Hyper-V", "VMware", "VirtualBox",
"Loopback", "Tunnel", "Miniport", "Wi-Fi Direct", "Bluetooth" };
foreach (var m in markers)
if (desc.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0 ||
name.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0) return true;
var mac = nic.GetPhysicalAddress().GetAddressBytes();
if (mac.Length == 6 && (mac[0] & 0x02) != 0) return true;
return false;
}
private static bool IsMachineConfigured()
{
foreach (var nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
{
if (nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Ethernet) continue;
if (nic.OperationalStatus != System.Net.NetworkInformation.OperationalStatus.Up) continue;
if (IsVirtualAdapter(nic)) continue;
try
{
var ipv4 = nic.GetIPProperties().GetIPv4Properties();
if (!ipv4.IsDhcpEnabled) return true; // static = configured
}
catch (System.Net.NetworkInformation.NetworkInformationException) { }
}
return false;
}
private void RunSecureConfiguration(CancellationToken ct)
{
var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log");
void Log(string msg) => File.AppendAllText(logPath,
$"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}");
try
{
using var configurator = new PodSecureConfigurator(
adapterName: null,
logger: msg => { Log(msg); _logger.LogInformation(msg); });
// 10-minute timeout — rebroadcasts automatically every 2 s
var config = configurator.Configure(timeoutMs: 600_000);
if (config == null)
{
_logger.LogWarning(
"SecureConfiguration timed out. Machine will need a reboot to retry.");
return;
}
_logger.LogInformation("SecureConfiguration complete.");
}
catch (Exception ex)
{
_logger.LogError(ex, "SecureConfiguration failed");
}
}
// ── Session Key ──────────────────────────────────────────────────
private static byte[] LoadSessionKey()
{
if (!File.Exists(KEY_FILE)) return null;
using var fs = File.Open(KEY_FILE, FileMode.Open, FileAccess.Read, FileShare.Read);
var len = fs.ReadByte();
if (len <= 0) return null;
var key = new byte[len];
int off = 0;
while (off < key.Length)
{
int n = fs.Read(key, off, key.Length - off);
if (n == 0) break;
off += n;
}
return off == key.Length ? key : null;
}
/// <summary>Reads exactly buf.Length bytes from stream; throws IOException on EOF.</summary>
private static void ReadExactSync(Stream stream, byte[] buf)
{
int off = 0;
while (off < buf.Length)
{
int n = stream.Read(buf, off, buf.Length - off);
if (n == 0) throw new IOException("Connection closed before all bytes received.");
off += n;
}
}
// ── Named Pipe IPC ───────────────────────────────────────────────
private Task<string> ForwardToAgentJsonAsync(
string functionName, IReadOnlyList<JsonElement> args, CancellationToken ct)
{
string launchKey = null;
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 = payloadJson
};
using var pipe = new NamedPipeClientStream(
".", PIPE_NAME, PipeDirection.InOut, PipeOptions.Asynchronous);
try
{
await pipe.ConnectAsync(AGENT_CONNECT_TIMEOUT_MS, ct);
}
catch (TimeoutException)
{
_logger.LogWarning(
"Agent pipe timeout — is TeslaLauncherAgent.exe running?");
throw new Exception(
"Cockpit agent is not available. " +
"Ensure TeslaLauncherAgent.exe is running in the user session.");
}
// Send IpcMessage
var reqBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(msg));
await pipe.WriteAsync(BitConverter.GetBytes(reqBytes.Length), ct);
await pipe.WriteAsync(reqBytes, ct);
await pipe.FlushAsync(ct);
// Read IpcResponse
var lenBuf = new byte[4];
await ReadExactAsync(pipe, lenBuf, ct);
var resBuf = new byte[BitConverter.ToInt32(lenBuf, 0)];
await ReadExactAsync(pipe, resBuf, ct);
var response = JsonSerializer.Deserialize<IpcResponse>(
Encoding.UTF8.GetString(resBuf));
if (response == null)
throw new Exception("Null response from Agent.");
if (!response.Success)
throw new Exception(response.Message ?? "Agent returned failure.");
if (response.Data is System.Text.Json.JsonElement je)
return je.GetRawText();
return response.Data?.ToString();
}
private static async Task ReadExactAsync(Stream stream, byte[] buf, CancellationToken ct)
{
var off = 0;
while (off < buf.Length)
{
var n = await stream.ReadAsync(buf, off, buf.Length - off, ct);
if (n == 0) throw new IOException("Pipe closed before all bytes were read.");
off += n;
}
}
}
// ── Entry Point ──────────────────────────────────────────────────────
public class Program
{
public static void Main(string[] args)
{
Host.CreateDefaultBuilder(args)
.UseWindowsService(options =>
{
options.ServiceName = "Tesla Application Launcher";
})
.ConfigureServices(services =>
{
services.AddHostedService<TeslaLauncherService>();
})
.ConfigureLogging(logging =>
{
logging.AddEventLog(settings =>
{
settings.SourceName = "Tesla Application Launcher";
});
})
.Build()
.Run();
}
}
}