// ============================================================================= // 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.Linq; 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 // --------------------------------------------------------------- /// /// 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). /// private const int CONSOLE_PORT = 53290; /// /// Named pipe connecting this service to TeslaLauncherAgent.exe. /// Must match PIPE_NAME in TeslaLauncherAgent.cs. /// private const string PIPE_NAME = "TeslaLauncherIPC"; /// /// 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. /// private const int AGENT_CONNECT_TIMEOUT_MS = 3000; // --------------------------------------------------------------- /// Path to session key file (original KeyStore format). private static readonly string KEY_FILE = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "TeslaLauncher", "TeslaKeyStore.key"); private readonly ILogger _logger; private TcpListener _listener; private readonly List _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 _installProgress = new(); private readonly object _installLock = new(); public TeslaLauncherService(ILogger 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..."); // net48's TcpListener has no CancellationToken overload for accept; // stop the listener on cancellation so the pending accept unblocks. using var stopReg = stoppingToken.Register(() => { try { _listener.Stop(); } catch { } }); while (!stoppingToken.IsCancellationRequested) { TcpClient client; try { client = await _listener.AcceptTcpClientAsync(); } catch (OperationCanceledException) { break; } catch (Exception) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { _logger.LogError(ex, "Error accepting Console connection"); continue; } _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); } } } 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]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(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.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 args = request.Args ?? new List(); _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 DispatchCommandAsync( string functionName, IReadOnlyList 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(json); if (flat == null) return new FullUpdateData(); var result2 = new FullUpdateData { InstalledApps = flat.InstalledApps != null ? Array.ConvertAll(flat.InstalledApps, FlatToWire) : Array.Empty(), LaunchedApps = flat.LaunchedApps != null ? Array.ConvertAll(flat.LaunchedApps, FlatLaunchedToWire) : Array.Empty(), 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(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(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": { await ForwardToAgentJsonAsync(functionName, args, ct); return null; } case "UninstallApp": { // The Agent kills + unregisters the app and returns the orphaned // product directory to remove (or null). The physical cleanup // (pre-uninstall.bat + delete) can exceed the Console's RPC timeout, // so respond immediately and clean up on a background thread. var json = await ForwardToAgentJsonAsync(functionName, args, ct); if (json != null) { try { using var doc = JsonDocument.Parse(json); if (doc.RootElement.TryGetProperty("CleanupDir", out var cd) && cd.ValueKind == JsonValueKind.String) { var dir = cd.GetString(); var logPath = Path.Combine(AppContext.BaseDirectory, "podconf.log"); _ = Task.Run(() => CleanupProductDirectory(dir, logPath)); } } catch (Exception ex) { _logger.LogWarning("UninstallApp cleanup parse failed: {Msg}", ex.Message); } } 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 0–50% 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 50–95% 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(_installProgress.Keys).ToArray(), g => g.ToString()); } } // ── Product Uninstall Cleanup ──────────────────────────────────── // Runs as SYSTEM (the Service) on a background thread after UninstallApp, // when the Agent reports the product directory is orphaned: run its // pre-uninstall.bat if it ships one (e.g. RIOJoy removes ViGEmBus + config), // then delete the C:\Games\ directory. private void CleanupProductDirectory(string productDir, string logPath) { void Log(string msg) => File.AppendAllText(logPath, $"[{DateTime.Now:HH:mm:ss}] {msg}{Environment.NewLine}"); if (string.IsNullOrWhiteSpace(productDir)) return; string full; try { full = Path.GetFullPath(productDir); } catch { return; } // Safety: only ever remove a proper subdirectory of C:\Games — never // C:\Games itself, never anything outside it. var gamesRoot = Path.GetFullPath(GAMES_DIR).TrimEnd('\\'); var target = full.TrimEnd('\\'); if (!target.StartsWith(gamesRoot + "\\", StringComparison.OrdinalIgnoreCase) || target.Equals(gamesRoot, StringComparison.OrdinalIgnoreCase)) { Log($"UNINSTALL: refusing to clean unsafe path '{full}'."); return; } if (!Directory.Exists(full)) { Log($"UNINSTALL: {full} already absent."); return; } // 1. Run pre-uninstall.bat if the product ships one (idempotent by design). var preUninstall = Path.Combine(full, "pre-uninstall.bat"); if (File.Exists(preUninstall)) { Log($"UNINSTALL: running {preUninstall}"); try { using var proc = System.Diagnostics.Process.Start( new System.Diagnostics.ProcessStartInfo { FileName = preUninstall, WorkingDirectory = full, UseShellExecute = false, CreateNoWindow = true }); proc?.WaitForExit(120_000); Log($"UNINSTALL: pre-uninstall.bat exit code {(proc != null ? proc.ExitCode.ToString() : "?")}"); } catch (Exception ex) { Log($"UNINSTALL: pre-uninstall.bat error: {ex.Message}"); } } // 2. Delete the product directory (retry briefly for lingering handles). for (int attempt = 1; attempt <= 3; attempt++) { try { Directory.Delete(full, recursive: true); Log($"UNINSTALL: removed {full}"); return; } catch (Exception ex) { if (attempt == 3) { Log($"UNINSTALL: could not remove {full}: {ex.Message}"); return; } Thread.Sleep(1000); } } } // ── 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(); var flat = JsonSerializer.Deserialize(json); if (flat == null) return Array.Empty(); return Array.ConvertAll(flat, FlatLaunchedToWire); } private static LaunchData[] FlatArrayToWire(string json) { if (json == null) return Array.Empty(); var flat = JsonSerializer.Deserialize(json); if (flat == null) return Array.Empty(); 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; } /// Reads exactly buf.Length bytes from stream; throws IOException on EOF. 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 ForwardToAgentJsonAsync( string functionName, IReadOnlyList 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 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), 0, 4, ct); await pipe.WriteAsync(reqBytes, 0, reqBytes.Length, 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( 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(); }) .ConfigureLogging(logging => { logging.AddEventLog(settings => { settings.SourceName = "Tesla Application Launcher"; }); }) .Build() .Run(); } } }