// ============================================================================= // TeslaLauncher — XP11 single-binary launcher (tray app, user session) // ============================================================================= // One userland process replaces the TeslaLauncherService (Session 0) + // TeslaLauncherAgent (tray) pair. The split existed only to work around // Vista+ Session 0 isolation; running everything in the auto-logged-in user // session needs no service, no named pipe, and no flat<->wire conversions. // The original Elsewhen software was likewise a single binary (a service — // which could still touch the desktop on Win2k/XP). // // Targets net40 so the SAME exe runs on Windows XP SP3 (newest framework XP // can install) and on Windows 10/11 (net40 loads in-place on the 4.8 runtime). // // Responsibilities: // - Listen on TCP 53290 for OFB-encrypted framed-JSON RPC from TeslaConsole // - First boot (DHCP): run SecureConfig, show Request ID + Passphrase // - Install products (receive zip, extract to C:\Games, postinstall.bat) // - Launch/kill/watch simulation apps, volume, LaunchApps.xml registry // // Command line: // /skipconfig skip the DHCP SecureConfig gate (bench testing) // /port:NNNN listen on a non-standard port (bench testing) // ============================================================================= using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.IO.IsolatedStorage; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Windows.Forms; using Newtonsoft.Json.Linq; using TeslaSecureConfig; using Wire = Tesla.Net; namespace Tesla.Launcher { public class LauncherApplication : Form { // ── Configuration ───────────────────────────────────────────────────── private const int DEFAULT_PORT = 53290; private const string GAMES_DIR = @"C:\Games"; // CommonApplicationData differs by OS (XP: ...\All Users\Application Data, // Vista+: C:\ProgramData) — never hardcode C:\ProgramData here. private static readonly string DATA_DIR = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "TeslaLauncher"); private static readonly string CONFIG_FILE = Path.Combine(DATA_DIR, "LaunchApps.xml"); private static readonly string KEY_FILE = Path.Combine(DATA_DIR, "TeslaKeyStore.key"); private static readonly string LOG_FILE = Path.Combine(DATA_DIR, "podconf.log"); private static bool _skipConfigGate; private static int _port = DEFAULT_PORT; // ── State ───────────────────────────────────────────────────────────── private NotifyIcon _trayIcon; private ContextMenuStrip _trayMenu; private List _launchApps = new List(); private readonly Dictionary> _runningProcesses = new Dictionary>(); private readonly object _processLock = new object(); private volatile bool _stopping; private readonly Dictionary _installProgress = new Dictionary(); private readonly object _installLock = new object(); private byte[] _sessionKey; private TcpListener _listener; // ── Entry point ─────────────────────────────────────────────────────── [STAThread] public static void Main(string[] args) { bool createdNew; using (new Mutex(true, "TeslaLauncherSingleInstance", out createdNew)) { if (!createdNew) return; // already running foreach (var a in args ?? new string[0]) { if (string.Equals(a, "/skipconfig", StringComparison.OrdinalIgnoreCase)) _skipConfigGate = true; else if (a.StartsWith("/port:", StringComparison.OrdinalIgnoreCase)) { int p; if (int.TryParse(a.Substring(6), out p) && p > 0 && p < 65536) _port = p; } } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LauncherApplication()); } } public LauncherApplication() { ShowInTaskbar = false; WindowState = FormWindowState.Minimized; Opacity = 0; BuildTrayIcon(); LoadLaunchApps(); var orchestrator = new Thread(OrchestratorLoop) { IsBackground = true, Name = "Orchestrator" }; orchestrator.Start(); } // ── Orchestrator: SecureConfig gate → session key → command listener ── private void OrchestratorLoop() { Log("=== Launcher starting (single-binary) ==="); TryRegisterApplicationRestart(); LogNicState(); if (!_skipConfigGate && !IsMachineConfigured()) { Log("IsMachineConfigured=false -> running SecureConfig"); SetTrayStatus("Configuring..."); RunSecureConfiguration(); } _sessionKey = LoadSessionKey(); if (_sessionKey == null) { Log("ERROR: TeslaKeyStore.key not found - cannot start command listener. " + "Reboot with DHCP to re-run SecureConfig."); SetTrayStatus("Not configured (no session key)"); return; } Log(string.Format("Session key loaded ({0} bytes) -> starting command listener on port {1}", _sessionKey.Length, _port)); ListenerLoop(); } private void RunSecureConfiguration() { try { using (var configurator = new PodSecureConfigurator( adapterName: null, logger: Log)) { // 10-minute timeout — rebroadcasts automatically every 2 s. var config = configurator.Configure(timeoutMs: 600_000); if (config == null) { Log("SecureConfiguration timed out or failed. Reboot to retry."); SetTrayStatus("Configuration failed - reboot to retry"); return; } Log("SecureConfiguration complete."); } } catch (Exception ex) { Log("SecureConfiguration error: " + ex.Message); } } // ── TCP command listener (Console connections) ──────────────────────── private void ListenerLoop() { try { _listener = new TcpListener(IPAddress.Any, _port); _listener.Start(); } catch (SocketException ex) { Log(string.Format("Cannot listen on port {0}: {1}", _port, ex.Message)); SetTrayStatus("Port " + _port + " unavailable"); return; } SetTrayStatus(_launchApps.Count + " apps configured"); Log("Listening. Waiting for Console connections..."); while (!_stopping) { TcpClient client; try { client = _listener.AcceptTcpClient(); } catch (Exception) { if (_stopping) break; Thread.Sleep(500); continue; } Log("Console connected from " + client.Client.RemoteEndPoint); var t = new Thread(() => HandleConsoleClient(client)) { IsBackground = true, Name = "ConsoleClient" }; t.Start(); } try { _listener.Stop(); } catch { } Log("Command listener stopped."); } private void HandleConsoleClient(TcpClient tcpClient) { try { using (tcpClient) using (var netStream = tcpClient.GetStream()) { netStream.WriteTimeout = 10_000; netStream.ReadTimeout = 30_000; // ── OFB crypto negotiation ──────────────────────────────── // Both sides exchange 16-byte IVs, then verify "CONF". var consoleIv = new byte[16]; ReadExactSync(netStream, consoleIv); var podIv = new byte[16]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(podIv); netStream.Write(podIv, 0, 16); netStream.Flush(); using (var ofb = new OFBDuplexStream(netStream, _sessionKey, writeIv: consoleIv, // pod→console (other's IV) readIv: podIv)) // console→pod (own IV) { var confMsg = Encoding.UTF8.GetBytes("CONF"); ofb.Write(confMsg, 0, confMsg.Length); ofb.Flush(); var confReply = new byte[4]; ReadExactSync(ofb, confReply); if (!confReply.SequenceEqual(confMsg)) { Log("CONF mismatch - session key mismatch, dropping connection."); return; } RpcSessionLoop(ofb, tcpClient); } } } catch (IOException) { /* console went away mid-handshake */ } catch (Exception ex) { Log(string.Format("Console handler error ({0}): {1}", ex.GetType().Name, ex.Message)); } Log("Console session ended."); } private void RpcSessionLoop(Stream rpcStream, TcpClient client) { while (!_stopping && client.Connected) { Wire.RpcRequest request; try { request = Wire.PodRpc.ReadRequest(rpcStream); } catch (EndOfStreamException) { break; } catch (IOException) { break; } catch (Exception ex) { Log(string.Format("Request read error ({0}): {1}", ex.GetType().Name, ex.Message)); break; } if (request == null) break; var funcName = request.Method ?? "???"; var args = request.Args ?? new List(); var start = DateTime.UtcNow; object resultObj = null; string error = null; try { resultObj = DispatchCommand(funcName, args); } catch (Exception ex) { error = ex.Message; Log(string.Format("CMD {0} ERROR: {1}", funcName, ex.Message)); } if (error == null) Log(string.Format("CMD {0} OK ({1:F0}ms)", funcName, (DateTime.UtcNow - start).TotalMilliseconds)); try { Wire.PodRpc.WriteResponse(rpcStream, resultObj, error); } catch (IOException) { break; } catch (Exception serEx) { Log(string.Format("Serialize failed for {0}: {1}", funcName, serEx.Message)); try { Wire.PodRpc.WriteResponse(rpcStream, null, "Pod error: " + serEx.Message); } catch { break; } } // ── File receive after InitiateInstallProduct ──────────────── // The Console streams the game archive on this same OFB stream // right after the Guid response: 8-byte Int64 size + raw bytes. if (funcName == "InitiateInstallProduct" && error == null && resultObj is Guid) { ReceiveInstallFile(rpcStream, (Guid)resultObj); break; // connection done after file transfer } } } // ── Command dispatch ────────────────────────────────────────────────── // Arguments arrive as JTokens (Newtonsoft leg of PodRpc); results are // wire types serialized straight into the response — no IPC hop. private object DispatchCommand(string functionName, List args) { switch (functionName) { case "Ping": // Echo the DateTime argument byte-identically (the request // reader keeps date strings raw for exactly this purpose). return HasArg(args, 0) ? (object)args[0] : DateTime.Now; case "GetLaunchableApps": { lock (_processLock) return _launchApps.Select(a => new Wire.LaunchPair { LaunchKey = ParseGuid(a.LaunchKey), DisplayName = a.DisplayName }).ToArray(); } case "GetInstalledApps": lock (_processLock) return _launchApps.Select(ToWire).ToArray(); case "GetLaunchedApps": return SnapshotLaunchedApps(); case "FullUpdate": { var launched = SnapshotLaunchedApps(); lock (_processLock) return new Wire.FullUpdateData { InstalledApps = _launchApps.Select(ToWire).ToArray(), LaunchedApps = launched, VolumeLevel = GetMasterVolume() }; } case "get_VolumeLevel": return GetMasterVolume(); case "set_VolumeLevel": SetMasterVolume(Math.Max(0f, Math.Min(1f, ArgFloat(args, 0, 1.0f)))); return null; case "LaunchApp": return CmdLaunchApp(ArgGuid(args, 0)); case "KillApp": CmdKillApp(ArgGuid(args, 0), ArgIntOrNull(args, 1)); return null; case "KillAllOfType": CmdKillAllOfType(ArgGuid(args, 0)); return null; case "KillAllApps": CmdKillAllApps(); return null; case "Shutdown": CmdShutdown(ArgBool(args, 0)); return null; case "ClearStore": CmdClearStore(); return null; case "RemoveApp": CmdRemoveApp(ArgGuid(args, 0)); return null; case "InstallApp": { if (!HasArg(args, 0)) throw new Exception("InstallApp requires LaunchData"); CmdInstallApp(args[0].ToObject()); return null; } case "UninstallApp": CmdUninstallApp(ArgGuid(args, 0)); return null; case "InitiateInstallProduct": { var callId = Guid.NewGuid(); lock (_installLock) _installProgress[callId] = new Wire.OutOfBandProgress { PercentComplete = 0, Status = "Waiting for file...", IsCompleted = false }; return callId; } case "GetOutOfBandProgress": { var id = ArgGuid(args, 0); lock (_installLock) { Wire.OutOfBandProgress prog; if (_installProgress.TryGetValue(id, out prog)) return prog; } return new Wire.OutOfBandProgress { PercentComplete = 0, Status = "Unknown call ID", IsCompleted = true }; } default: throw new Exception("Unknown command: " + functionName); } } // ── Command implementations ─────────────────────────────────────────── private int CmdLaunchApp(Guid launchKey) { var key = launchKey.ToString(); var app = FindApp(key); if (app == null) throw new Exception("No app configured for key '" + key + "'"); // A launch entry can be registered (pushed from the Console) before its // game files are installed. Fail cleanly with an actionable message. if (string.IsNullOrEmpty(app.ExeFile) || !File.Exists(app.ExeFile)) throw new Exception(string.Format( "Cannot launch '{0}': executable not found at '{1}'. The product may be " + "registered but not yet installed on this pod.", app.DisplayName ?? key, app.ExeFile)); var psi = new ProcessStartInfo { FileName = app.ExeFile, Arguments = app.Arguments ?? "", WorkingDirectory = string.IsNullOrEmpty(app.WorkingDirectory) ? Path.GetDirectoryName(app.ExeFile) : app.WorkingDirectory, UseShellExecute = false }; var process = Process.Start(psi); if (process == null) throw new Exception("Process.Start returned null for '" + app.ExeFile + "'"); lock (_processLock) { List procs; if (!_runningProcesses.TryGetValue(app.LaunchKey, out procs)) _runningProcesses[app.LaunchKey] = procs = new List(); procs.Add(process); } if (app.AutoRestart) StartWatcher(app, process); SetTrayStatus("Running: " + (app.DisplayName ?? key)); return process.Id; } private void CmdKillApp(Guid launchKey, int? pid) { var key = FindKeyString(launchKey); lock (_processLock) { List procs; if (key == null || !_runningProcesses.TryGetValue(key, out procs)) return; var toKill = pid.HasValue ? procs.Where(p => p.Id == pid.Value).ToList() : procs.ToList(); foreach (var p in toKill) { try { if (!p.HasExited) p.Kill(); } catch { } procs.Remove(p); } if (procs.Count == 0) _runningProcesses.Remove(key); } } private void CmdKillAllOfType(Guid launchKey) { var key = FindKeyString(launchKey); lock (_processLock) { List procs; if (key == null || !_runningProcesses.TryGetValue(key, out procs)) return; foreach (var p in procs) try { if (!p.HasExited) p.Kill(); } catch { } _runningProcesses.Remove(key); } } private void CmdKillAllApps() { lock (_processLock) { foreach (var kvp in _runningProcesses) foreach (var p in kvp.Value) try { if (!p.HasExited) p.Kill(); } catch { } _runningProcesses.Clear(); } SetTrayStatus("All apps stopped"); } private void CmdShutdown(bool restart) { CmdKillAllApps(); var flag = restart ? "/r" : "/s"; new Thread(() => { Thread.Sleep(2000); Process.Start("shutdown", flag + " /t 0"); }) { IsBackground = true }.Start(); } private void CmdClearStore() { var store = IsolatedStorageFile.GetMachineStoreForAssembly(); foreach (var file in store.GetFileNames("*")) store.DeleteFile(file); LoadLaunchApps(); } private void CmdRemoveApp(Guid launchKey) { var key = launchKey.ToString(); lock (_processLock) _launchApps.RemoveAll(a => string.Equals(a.LaunchKey, key, StringComparison.OrdinalIgnoreCase)); SaveLaunchApps(); } private void CmdInstallApp(Wire.LaunchData data) { var entry = FromWire(data); lock (_processLock) { var existing = _launchApps.FindIndex(a => string.Equals(a.LaunchKey, entry.LaunchKey, StringComparison.OrdinalIgnoreCase)); if (existing >= 0) _launchApps[existing] = entry; else _launchApps.Add(entry); } SaveLaunchApps(); } private void CmdUninstallApp(Guid launchKey) { CmdKillAllOfType(launchKey); var key = launchKey.ToString(); LaunchData removed; string cleanupDir = null; lock (_processLock) { removed = _launchApps.FirstOrDefault(a => string.Equals(a.LaunchKey, key, StringComparison.OrdinalIgnoreCase)); _launchApps.RemoveAll(a => string.Equals(a.LaunchKey, key, StringComparison.OrdinalIgnoreCase)); // If the removed app's product directory under C:\Games is no longer // used by any remaining entry, clean it up (pre-uninstall.bat + delete). // The orphan check keeps a product with several launch entries sharing // one folder (e.g. Red Planet's GameClient/LC/MR) intact until its // LAST entry is removed. if (removed != null) { var dir = ProductDirUnderGames(removed.WorkingDirectory) ?? ProductDirUnderGames(removed.ExeFile); if (dir != null && !_launchApps.Any(a => string.Equals( ProductDirUnderGames(a.WorkingDirectory) ?? ProductDirUnderGames(a.ExeFile), dir, StringComparison.OrdinalIgnoreCase))) cleanupDir = dir; } } SaveLaunchApps(); // Physical cleanup can exceed the Console's RPC timeout — background it. if (cleanupDir != null) new Thread(() => CleanupProductDirectory(cleanupDir)) { IsBackground = true }.Start(); } // ── Install product: file receive + extract ────────────────────────── private void ReceiveInstallFile(Stream stream, Guid callId) { string tempZip = null; try { // ── Phase 1: Receive file ──────────────────────────────────── var sizeBuf = new byte[8]; ReadExactSync(stream, sizeBuf); long fileSize = BitConverter.ToInt64(sizeBuf, 0); Log(string.Format("INSTALL {0:N}: receiving {1:N0} bytes...", callId, fileSize)); UpdateProgress(callId, 0, "Receiving file..."); Directory.CreateDirectory(DATA_DIR); tempZip = Path.Combine(DATA_DIR, string.Format("install_{0:N}.zip", callId)); 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; UpdateProgress(callId, (int)(received * 50 / fileSize), "Receiving file..."); } } Log(string.Format("INSTALL {0:N}: file received ({1:N0} bytes)", callId, fileSize)); // ── Phase 2: Extract to C:\Games (MiniZip: net40 has no ZipFile) ─ UpdateProgress(callId, 50, "Extracting..."); Directory.CreateDirectory(GAMES_DIR); MiniZip.ExtractToDirectory(tempZip, GAMES_DIR, (done, total) => UpdateProgress(callId, 50 + done * 45 / Math.Max(total, 1), "Extracting...")); Log(string.Format("INSTALL {0:N}: extracted to {1}", callId, 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(string.Format("INSTALL {0:N}: running postinstall.bat", callId)); try { using (var proc = Process.Start(new ProcessStartInfo { FileName = postInstall, WorkingDirectory = GAMES_DIR, UseShellExecute = false, CreateNoWindow = true })) { if (proc != null) proc.WaitForExit(60_000); } } catch (Exception ex) { Log(string.Format("INSTALL {0:N}: postinstall.bat error: {1}", callId, ex.Message)); } try { File.Delete(postInstall); } catch { } } // Console checks PercentComplete == 99 to break out of its // 3-iteration install loop (InstallProductWorker in PodInfo). UpdateProgress(callId, 99, "Complete", isCompleted: true); Log(string.Format("INSTALL {0:N}: install complete", callId)); } catch (Exception ex) { UpdateProgress(callId, 0, "Failed: " + ex.Message, isCompleted: true); Log(string.Format("INSTALL {0:N}: FAILED: {1}", callId, ex.Message)); } finally { try { if (tempZip != null) File.Delete(tempZip); } catch { } } } private void UpdateProgress(Guid callId, int pct, string status, bool isCompleted = false) { lock (_installLock) _installProgress[callId] = new Wire.OutOfBandProgress { PercentComplete = pct, Status = status, IsCompleted = isCompleted }; } // ── Product uninstall cleanup ───────────────────────────────────────── // Run the product's pre-uninstall.bat if it ships one (e.g. RIOJoy removes // ViGEmBus + config), then delete the C:\Games\ directory. private void CleanupProductDirectory(string productDir) { if (string.IsNullOrEmpty(productDir)) return; string full; try { full = Path.GetFullPath(productDir); } catch { return; } // Safety: only ever remove a proper subdirectory of C:\Games. 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; } var preUninstall = Path.Combine(full, "pre-uninstall.bat"); if (File.Exists(preUninstall)) { Log("UNINSTALL: running " + preUninstall); try { using (var proc = Process.Start(new ProcessStartInfo { FileName = preUninstall, WorkingDirectory = full, UseShellExecute = false, CreateNoWindow = true })) { if (proc != null) { proc.WaitForExit(120_000); Log("UNINSTALL: pre-uninstall.bat exit code " + proc.ExitCode); } } } catch (Exception ex) { Log("UNINSTALL: pre-uninstall.bat error: " + ex.Message); } } 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); } } } /// The immediate child of C:\Games that contains /// (e.g. C:\Games\RIOJoy\app\x.exe → C:\Games\RIOJoy), or null if not under C:\Games. private static string ProductDirUnderGames(string path) { if (string.IsNullOrEmpty(path)) return null; string full; try { full = Path.GetFullPath(path); } catch { return null; } const string games = @"C:\Games\"; if (!full.StartsWith(games, StringComparison.OrdinalIgnoreCase)) return null; var seg = full.Substring(games.Length).Split('\\', '/')[0]; return string.IsNullOrEmpty(seg) ? null : games + seg; } // ── Process tracking helpers ────────────────────────────────────────── private Wire.LaunchedAppData[] SnapshotLaunchedApps() { var result = new List(); lock (_processLock) { foreach (var kvp in _runningProcesses) { kvp.Value.RemoveAll(p => p.HasExited); foreach (var p in kvp.Value) result.Add(new Wire.LaunchedAppData { LaunchKey = ParseGuid(kvp.Key), ProcessId = p.Id }); } foreach (var key in _runningProcesses.Keys .Where(k => _runningProcesses[k].Count == 0).ToList()) _runningProcesses.Remove(key); } return result.ToArray(); } private LaunchData FindApp(string launchKey) { lock (_processLock) return _launchApps.FirstOrDefault(a => string.Equals(a.LaunchKey, launchKey, StringComparison.OrdinalIgnoreCase)); } /// Maps a wire Guid to the tracked key string (which preserves the /// exact casing used when the process was launched). private string FindKeyString(Guid launchKey) { var key = launchKey.ToString(); lock (_processLock) { foreach (var k in _runningProcesses.Keys) if (string.Equals(k, key, StringComparison.OrdinalIgnoreCase)) return k; } return key; } private void StartWatcher(LaunchData app, Process process) { new Thread(() => { process.WaitForExit(); if (_stopping) return; bool stillTracked; lock (_processLock) { List procs; stillTracked = _runningProcesses.TryGetValue(app.LaunchKey, out procs) && procs.Contains(process); } if (!stillTracked) return; Thread.Sleep(2000); try { CmdLaunchApp(ParseGuid(app.LaunchKey)); } catch { } }) { IsBackground = true, Name = "Watcher-" + app.LaunchKey }.Start(); } // ── LaunchApps.xml ──────────────────────────────────────────────────── private void LoadLaunchApps() { try { if (!File.Exists(CONFIG_FILE)) { lock (_processLock) _launchApps = new List(); SetTrayStatus("Config not found"); return; } var xml = new System.Xml.Serialization.XmlSerializer(typeof(List)); using (var reader = File.OpenRead(CONFIG_FILE)) { var apps = (List)xml.Deserialize(reader) ?? new List(); lock (_processLock) _launchApps = apps; } SetTrayStatus(_launchApps.Count + " apps configured"); } catch (Exception ex) { SetTrayStatus("Config error: " + ex.Message); } } private void SaveLaunchApps() { try { var xml = new System.Xml.Serialization.XmlSerializer(typeof(List)); Directory.CreateDirectory(Path.GetDirectoryName(CONFIG_FILE)); List snapshot; lock (_processLock) snapshot = new List(_launchApps); using (var writer = File.CreateText(CONFIG_FILE)) xml.Serialize(writer, snapshot); } catch (Exception ex) { Log("SaveLaunchApps error: " + ex.Message); } } private static Wire.LaunchData ToWire(LaunchData e) { return new Wire.LaunchData { LaunchPair = new Wire.LaunchPair { LaunchKey = ParseGuid(e.LaunchKey), DisplayName = e.DisplayName }, WorkingDirectory = e.WorkingDirectory, ExeFile = e.ExeFile, Arguments = e.Arguments, AutoRestart = e.AutoRestart }; } private static LaunchData FromWire(Wire.LaunchData d) { return new LaunchData { LaunchKey = d.LaunchPair.LaunchKey.ToString(), DisplayName = d.LaunchPair.DisplayName, WorkingDirectory = d.WorkingDirectory, ExeFile = d.ExeFile, Arguments = d.Arguments, AutoRestart = d.AutoRestart }; } // ── Volume control ──────────────────────────────────────────────────── // The Console stores/retrieves volume as a float (0.0–1.0 scalar). We // cache the exact value the Console sent so get returns the same value // without device roundtrip quantization error. // // Setter chain: nircmd.exe (legacy, works everywhere incl. XP) // → CoreAudio (Vista+) → winmm waveOutSetVolume (XP fallback). private float _cachedVolumeScalar = 1.0f; private float GetMasterVolume() { return _cachedVolumeScalar; } private void SetMasterVolume(float scalar) { _cachedVolumeScalar = scalar; var nircmd = Path.Combine(GAMES_DIR, "nircmd.exe"); if (File.Exists(nircmd)) { try { var p = Process.Start(nircmd, "setsysvolume " + (int)(scalar * 65535)); if (p != null) p.Dispose(); return; } catch { /* fall through to API */ } } if (Environment.OSVersion.Version.Major >= 6) { try { CoreAudio.SetMasterScalar(scalar); return; } catch { /* fall through */ } } WinMmVolume.SetMasterScalar(scalar); } // ── 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; } } // ── Machine-configured check (same rule as SecureConfig) ───────────── private static bool IsMachineConfigured() { foreach (var nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.NetworkInterfaceType != NetworkInterfaceType.Ethernet) continue; if (nic.OperationalStatus != OperationalStatus.Up) continue; if (IsVirtualAdapter(nic)) continue; try { var ipv4 = nic.GetIPProperties().GetIPv4Properties(); if (!ipv4.IsDhcpEnabled) return true; // static = configured } catch (NetworkInformationException) { } } return false; } private static bool IsVirtualAdapter(NetworkInterface nic) { var desc = nic.Description ?? ""; var name = nic.Name ?? ""; string[] markers = { "Virtual", "Hyper-V", "VMware", "VirtualBox", "Loopback", "Tunnel", "Miniport", "Wi-Fi Direct", "Bluetooth", "WAN Miniport", "Microsoft Kernel Debug" }; 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; // locally administered return false; } private void LogNicState() { foreach (var nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.NetworkInterfaceType != NetworkInterfaceType.Ethernet) continue; if (nic.OperationalStatus != OperationalStatus.Up) continue; try { var ipv4 = nic.GetIPProperties().GetIPv4Properties(); Log(string.Format(" NIC [{0}] DHCP={1} Virtual={2}", nic.Name, ipv4.IsDhcpEnabled, IsVirtualAdapter(nic))); } catch { Log(string.Format(" NIC [{0}] (no IPv4 props)", nic.Name)); } } } // ── Tray UI ─────────────────────────────────────────────────────────── private void BuildTrayIcon() { _trayMenu = new ContextMenuStrip(); var version = typeof(LauncherApplication).Assembly.GetName().Version; _trayMenu.Items.Add("Tesla Launcher 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()); _trayMenu.Items.Add(new ToolStripSeparator()); _trayMenu.Items.Add("Exit", null, (s, e) => ExitLauncher()); _trayIcon = new NotifyIcon { Text = "Tesla Launcher", Icon = SystemIcons.Application, ContextMenuStrip = _trayMenu, Visible = true }; } private void ExitLauncher() { _stopping = true; try { if (_listener != null) _listener.Stop(); } catch { } _trayIcon.Visible = false; Application.Exit(); } private void SetTrayStatus(string status) { if (_trayIcon == null) return; var text = "Tesla Launcher: " + status; if (text.Length > 63) text = text.Substring(0, 63); // NotifyIcon.Text limit try { _trayIcon.Text = text; } catch { } } protected override void OnLoad(EventArgs e) { base.OnLoad(e); Visible = false; } protected override void Dispose(bool disposing) { if (disposing) { if (_trayIcon != null) _trayIcon.Dispose(); if (_trayMenu != null) _trayMenu.Dispose(); } base.Dispose(disposing); } // ── Argument helpers (JTokens from the Newtonsoft PodRpc leg) ───────── private static bool HasArg(List args, int i) => args != null && args.Count > i && args[i] != null && args[i].Type != JTokenType.Null; private static Guid ArgGuid(List args, int i) { if (!HasArg(args, i)) return Guid.Empty; Guid g; return Guid.TryParse(args[i].Value(), out g) ? g : Guid.Empty; } private static int? ArgIntOrNull(List args, int i) { if (!HasArg(args, i)) return null; var t = args[i]; if (t.Type == JTokenType.Integer || t.Type == JTokenType.Float) return t.Value(); return null; } private static bool ArgBool(List args, int i) => HasArg(args, i) && args[i].Type == JTokenType.Boolean && args[i].Value(); private static float ArgFloat(List args, int i, float fallback) { if (!HasArg(args, i)) return fallback; var t = args[i]; if (t.Type == JTokenType.Integer || t.Type == JTokenType.Float) return t.Value(); return fallback; } private static Guid ParseGuid(string s) { Guid g; return s != null && Guid.TryParse(s, out g) ? g : Guid.Empty; } // ── Misc helpers ────────────────────────────────────────────────────── /// 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; } } private static readonly object _logLock = new object(); private static void Log(string msg) { try { lock (_logLock) { Directory.CreateDirectory(DATA_DIR); File.AppendAllText(LOG_FILE, string.Format("[{0:HH:mm:ss}] {1}{2}", DateTime.Now, msg, Environment.NewLine)); } } catch { /* logging must never take the launcher down */ } } // WER relaunches the app after a crash/hang (Vista+; no-op on XP). [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern int RegisterApplicationRestart(string commandLine, int flags); private static void TryRegisterApplicationRestart() { if (Environment.OSVersion.Version.Major < 6) return; try { RegisterApplicationRestart(null, 0); } catch { } } } // ── Launcher-local data type ────────────────────────────────────────────── // Loaded from LaunchApps.xml via XmlSerializer. The CLASS NAME and property // names are load-bearing: they define the XML element names, and existing // pods already have LaunchApps.xml files written by the two-process Agent // with this exact shape. (The wire struct is Tesla.Net.LaunchData.) [Serializable] public class LaunchData { 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; } } // ── Windows Core Audio API — minimal COM interop (Vista+) ───────────────── // No external dependencies. Vtable slot order matches the SDK headers exactly. // Methods we don't call are declared as void stubs to preserve vtable offsets. // NOT available on XP — callers must gate on OS version. internal static class CoreAudio { internal static void SetMasterScalar(float scalar) { var ep = GetEndpointVolume(); try { var ctx = Guid.Empty; ep.SetMasterVolumeLevelScalar(scalar, ref ctx); } finally { Marshal.ReleaseComObject(ep); } } private static IAudioEndpointVolume GetEndpointVolume() { var enumerator = (IMMDeviceEnumerator)new MMAudioEnumeratorComClass(); try { IMMDevice device; enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out device); try { var iid = typeof(IAudioEndpointVolume).GUID; object obj; device.Activate(ref iid, 23 /*CLSCTX_ALL*/, IntPtr.Zero, out obj); return (IAudioEndpointVolume)obj; } finally { Marshal.ReleaseComObject(device); } } finally { Marshal.ReleaseComObject(enumerator); } } } [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] internal class MMAudioEnumeratorComClass { } [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IMMDeviceEnumerator { void _unused_EnumAudioEndpoints(); // slot 0 — not used [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint); } [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IMMDevice { [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object interfacePointer); } // Vtable order (after IUnknown): RegisterControlChangeNotify(0), // UnregisterControlChangeNotify(1), GetChannelCount(2), // SetMasterVolumeLevel(3), SetMasterVolumeLevelScalar(4), // GetMasterVolumeLevel(5), GetMasterVolumeLevelScalar(6) [Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IAudioEndpointVolume { void _unused_RegisterControlChangeNotify(); // slot 0 void _unused_UnregisterControlChangeNotify(); // slot 1 void _unused_GetChannelCount(); // slot 2 [PreserveSig] int SetMasterVolumeLevel(float levelDB, ref Guid ctx); [PreserveSig] int SetMasterVolumeLevelScalar(float level, ref Guid ctx); [PreserveSig] int GetMasterVolumeLevel(out float levelDB); [PreserveSig] int GetMasterVolumeLevelScalar(out float level); } // ── winmm wave-out volume (XP fallback) ─────────────────────────────────── // waveOutSetVolume with device -1 sets the wave mixer level of the default // device: low 16 bits = left channel, high 16 bits = right channel. internal static class WinMmVolume { [DllImport("winmm.dll")] private static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume); internal static void SetMasterScalar(float scalar) { uint level = (uint)(Math.Max(0f, Math.Min(1f, scalar)) * 0xFFFF); try { waveOutSetVolume(IntPtr.Zero, (level << 16) | level); } catch { } } } }