Files
TeslaSuite/Launcher/TeslaLauncherService.cs
T
CydandClaude Opus 4.8 1e2ec12f11 Spike: net48-only launcher (evaluate size vs net8 self-contained)
Converts the Launcher Service + Agent from net8/win-x64 self-contained to net48
framework-dependent, and makes Tesla.Contract net48-only (drops multi-targeting).
Both consumers (Console + Launcher) are now a single TFM.

Code changes for net48 (the only net8/netstandard2.1 APIs in use):
- RandomNumberGenerator.Fill -> RandomNumberGenerator.Create().GetBytes (3x)
- TcpListener.AcceptTcpClientAsync(ct) -> AcceptTcpClientAsync() + stop-on-cancel
- byte[].AsSpan().SequenceEqual -> Linq SequenceEqual (no System.Memory) (2x)
- PipeStream.Write(byte[]) / WriteAsync(byte[],ct) -> explicit (buf,0,len[,ct])
- Math.Clamp -> Math.Max/Min
The generic host (Microsoft.Extensions.Hosting 8.x + UseWindowsService) runs on
net48 unchanged. build.bat/install.bat updated for the folder-of-DLLs deploy;
solution platform reverted x64 -> AnyCPU.

RESULT — package size: ~3.7 MB on disk / 1.58 MB zipped, vs ~213 MB / 91 MB for
the net8 self-contained build (~50-58x smaller). net48 ships in Win10/11 so no
runtime prerequisite. 73 tests green; NOT re-validated on a live pod.

Spike branch for evaluation — do not merge without a pod re-test.

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

954 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.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
// ---------------------------------------------------------------
/// <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...");
// 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<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), 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<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();
}
}
}