XP11: single net40 binary runs on XP SP3 through Windows 11
Merge TeslaLauncherService (Session 0) + TeslaLauncherAgent (tray) into one userland TeslaLauncher.exe. The split existed only to work around Vista+ Session 0 isolation; running everything in the auto-logged-in admin session needs no service, no named pipe, and no flat<->wire conversion layer. The original Elsewhen software was likewise a single binary. net40 is the newest framework XP SP3 can install, and net40 assemblies load in-place on the 4.8 runtime in Win10/11 -- one exe covers both. - Contract: multi-target net48;net40. The net40 leg of PodRpcProtocol uses Newtonsoft.Json (STJ has no net40 target); JSON is shape-identical on the wire, and the request reader keeps date strings raw so Ping echoes byte-identically. Console keeps the untouched net48/STJ leg. - MiniZip.cs: central-directory ZIP extractor (stored/deflate/ZIP64) since net40 has no ZipFile; zip-slip guarded. - SecureConfig: RSACryptoServiceProvider instead of RSA.Create (net46+), no leaveOpen BinaryWriter/Reader, struct instead of ValueTuple, and no SetCompatibleTextRenderingDefault on the passcode thread (the merged app already has a form). netsh "interface ip" syntax was already XP-correct. - Volume: nircmd -> CoreAudio (Vista+) -> winmm waveOutSetVolume (XP). - Paths: CommonApplicationData resolved per-OS (XP has no C:\ProgramData); launcher log moved next to the key/config in the data dir. - install.bat: dual-OS (cacls/icacls, netsh firewall/advfirewall, dism and UAC/notification steps skipped on XP, .NET 4.0 redist check, XP DHCP reset via netsh); no service registration -- HKLM Run key + auto-login on both; RegisterApplicationRestart supplies crash-restart on Vista+. - Bench switches: /skipconfig, /port:NNNN. Verified: Console + diff suite (103 tests) still green on the net48 leg; E2E smoke test drove the net40 launcher over real TCP with the Console's own PodManagerConnection (STJ client vs Newtonsoft server) -- Ping echo, volume, app registry, launch/kill/watch, InstallProduct zip transfer + extract, and uninstall orphan cleanup: 17/17 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,22 @@
|
||||
// one file, so the request/response shape cannot drift.
|
||||
// =============================================================================
|
||||
|
||||
// NET40 (XP11 single-binary launcher): System.Text.Json has no net40 target, so
|
||||
// this leg serializes with Newtonsoft.Json instead. Same framing, same JSON shape
|
||||
// (PascalCase member names, fields included, Guids as strings, ISO-8601 dates) —
|
||||
// an STJ client and a Newtonsoft server interoperate byte-compatibly on the wire.
|
||||
// Arguments surface as JToken here instead of JsonElement.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
#if NET40
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
#else
|
||||
using System.Text.Json;
|
||||
#endif
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
@@ -27,13 +39,21 @@ namespace Tesla.Net
|
||||
public sealed class RpcRequest
|
||||
{
|
||||
public string Method { get; set; }
|
||||
#if NET40
|
||||
public List<JToken> Args { get; set; }
|
||||
#else
|
||||
public List<JsonElement> Args { get; set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
||||
public sealed class RpcResponse
|
||||
{
|
||||
#if NET40
|
||||
public JToken Result { get; set; } // JSON null for void / null
|
||||
#else
|
||||
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
|
||||
#endif
|
||||
public string Error { get; set; } // null on success
|
||||
}
|
||||
|
||||
@@ -45,12 +65,18 @@ namespace Tesla.Net
|
||||
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
|
||||
public const int MaxFrameBytes = 16 * 1024 * 1024;
|
||||
|
||||
#if NET40
|
||||
// Newtonsoft serializes public fields of the wire types by default, and
|
||||
// writes ISO-8601 dates / string Guids like STJ — no special options needed.
|
||||
public static readonly JsonSerializer JsonOptions = JsonSerializer.CreateDefault();
|
||||
#else
|
||||
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
||||
// FIELDS, which System.Text.Json ignores unless this is set.
|
||||
IncludeFields = true,
|
||||
};
|
||||
#endif
|
||||
|
||||
// ── Framing ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -88,6 +114,25 @@ namespace Tesla.Net
|
||||
|
||||
// ── Request ──────────────────────────────────────────────────────────
|
||||
|
||||
#if NET40
|
||||
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||
{
|
||||
var req = new RpcRequest { Method = method, Args = new List<JToken>() };
|
||||
if (args != null)
|
||||
foreach (var a in args)
|
||||
req.Args.Add(a == null ? JValue.CreateNull() : JToken.FromObject(a, JsonOptions));
|
||||
WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req)));
|
||||
}
|
||||
|
||||
public static RpcRequest ReadRequest(Stream stream)
|
||||
=> JsonConvert.DeserializeObject<RpcRequest>(
|
||||
Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings);
|
||||
|
||||
// Keep date-looking strings as raw strings so an echoed argument (Ping)
|
||||
// goes back byte-identical instead of reformatted through DateTime.
|
||||
private static readonly JsonSerializerSettings ReadSettings =
|
||||
new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
|
||||
#else
|
||||
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||
{
|
||||
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
||||
@@ -99,9 +144,26 @@ namespace Tesla.Net
|
||||
|
||||
public static RpcRequest ReadRequest(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
||||
#endif
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
|
||||
#if NET40
|
||||
public static void WriteResponse(Stream stream, object result, string error)
|
||||
{
|
||||
object payload = error == null ? result : null;
|
||||
var resp = new RpcResponse
|
||||
{
|
||||
Result = payload == null ? JValue.CreateNull() : JToken.FromObject(payload, JsonOptions),
|
||||
Error = error,
|
||||
};
|
||||
WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(resp)));
|
||||
}
|
||||
|
||||
public static RpcResponse ReadResponse(Stream stream)
|
||||
=> JsonConvert.DeserializeObject<RpcResponse>(
|
||||
Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings);
|
||||
#else
|
||||
public static void WriteResponse(Stream stream, object result, string error)
|
||||
{
|
||||
var resp = new RpcResponse
|
||||
@@ -116,5 +178,6 @@ namespace Tesla.Net
|
||||
|
||||
public static RpcResponse ReadResponse(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- net48 only: both consumers (Console and Launcher) target .NET Framework 4.8.
|
||||
(Was multi-targeted net48;net8.0-windows while the Launcher was on net8.) -->
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<!-- net48: the Console (and the client/crypto stack under Client/**).
|
||||
net40: the XP11 single-binary Launcher — the oldest framework installable
|
||||
on Windows XP SP3, and net40 assemblies run in-place on the 4.8 runtime,
|
||||
so ONE launcher binary covers XP SP3 through Windows 11. -->
|
||||
<TargetFrameworks>net48;net40</TargetFrameworks>
|
||||
|
||||
<!-- CRITICAL: the output assembly MUST be named TeslaConsoleLaunchLib at
|
||||
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
|
||||
@@ -24,17 +26,27 @@
|
||||
<NoWarn>$(NoWarn);SYSLIB0011</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- net48 reference assemblies so the project builds without a full targeting pack,
|
||||
plus System.Text.Json (built into the net8 shared framework, a package on net48). -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<!-- .NET Framework reference assemblies so the project builds without a full
|
||||
targeting pack (resolves per-TFM, covers net40 and net48). -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- JSON serializer per leg. System.Text.Json has no net40 target, so the
|
||||
net40 leg of PodRpcProtocol.cs uses Newtonsoft.Json (which still ships
|
||||
lib/net40). The JSON bytes on the wire are shape-identical; the
|
||||
XpWireCompatTests exercise STJ-client <-> Newtonsoft-server for real. -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- The TCP/OFB client (Client/**) is net48-only: it depends on the crypto-stream
|
||||
handshake in TeslaSecureConfiguration.dll. The Launcher (net6) is the SERVER
|
||||
end of this protocol and never references these classes, so they are excluded
|
||||
from the net6.0-windows build (which carries only the wire data types). -->
|
||||
handshake in TeslaSecureConfiguration.dll. The Launcher is the SERVER end of
|
||||
this protocol and never references these classes, so the net40 leg carries
|
||||
only the wire data types + PodRpc framing. -->
|
||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
||||
<Compile Remove="Client\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// =============================================================================
|
||||
// TeslaLauncher — Shared IPC Models
|
||||
// =============================================================================
|
||||
// IPC types (Tesla.Launcher.Shared): JSON messages between the Windows Service
|
||||
// and the user-session Agent over a Named Pipe. These never touch the Console
|
||||
// TCP connection.
|
||||
//
|
||||
// The BinaryFormatter wire types (Tesla.Net: LaunchData, InvokeCommand, ...)
|
||||
// formerly replicated here by hand now live in the shared Tesla.Contract project
|
||||
// (assembly TeslaConsoleLaunchLib), referenced by the Service. This file is also
|
||||
// compiled into the Agent, which uses only the IPC types below.
|
||||
// =============================================================================
|
||||
|
||||
namespace Tesla.Launcher.Shared
|
||||
{
|
||||
/// <summary>Command forwarded from the Windows Service to the Userspace Agent.</summary>
|
||||
public sealed class IpcMessage
|
||||
{
|
||||
public string Command { get; set; }
|
||||
public string LaunchKey { get; set; }
|
||||
public string PayloadJson { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Response from the Userspace Agent back to the Windows Service.</summary>
|
||||
public sealed class IpcResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
public object Data { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// =============================================================================
|
||||
// TeslaLauncher — MiniZip
|
||||
// =============================================================================
|
||||
// Minimal ZIP extractor for net40: System.IO.Compression.ZipFile/ZipArchive are
|
||||
// net45+, so they do not exist on the XP-compatible framework. This reads the
|
||||
// archive via the central directory and supports exactly what the Console's
|
||||
// install packages use:
|
||||
// - compression methods 0 (stored) and 8 (deflate; DeflateStream is net20+)
|
||||
// - UTF-8 (general-purpose flag bit 11) or ANSI/CP437 entry names
|
||||
// - ZIP64 sizes/offsets/entry counts (archives > 4 GB or > 65535 entries)
|
||||
// CRC is not verified — archives arrive from our own Console over the
|
||||
// OFB-encrypted link, and the install already fails loudly on truncation.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace Tesla.Launcher
|
||||
{
|
||||
internal static class MiniZip
|
||||
{
|
||||
/// <summary>Extracts every entry of <paramref name="zipPath"/> under
|
||||
/// <paramref name="destDir"/> (existing files overwritten), reporting
|
||||
/// (entriesDone, entriesTotal) after each entry. Entries that would
|
||||
/// escape the destination directory (zip-slip) are skipped.</summary>
|
||||
public static void ExtractToDirectory(string zipPath, string destDir, Action<int, int> onEntry)
|
||||
{
|
||||
using (var fs = new FileStream(zipPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
var entries = ReadCentralDirectory(fs);
|
||||
var destRoot = Path.GetFullPath(destDir).TrimEnd('\\');
|
||||
int done = 0;
|
||||
foreach (var e in entries)
|
||||
{
|
||||
ExtractEntry(fs, e, destRoot);
|
||||
done++;
|
||||
if (onEntry != null) onEntry(done, entries.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Entry
|
||||
{
|
||||
public string Name;
|
||||
public int Method;
|
||||
public long CompressedSize;
|
||||
public long LocalHeaderOffset;
|
||||
}
|
||||
|
||||
// ── Central directory ─────────────────────────────────────────────────
|
||||
|
||||
private static List<Entry> ReadCentralDirectory(FileStream fs)
|
||||
{
|
||||
// End-of-central-directory record: fixed 22 bytes + up to 64K comment.
|
||||
long fileLen = fs.Length;
|
||||
int maxScan = (int)Math.Min(fileLen, 22 + 65535);
|
||||
var tail = new byte[maxScan];
|
||||
fs.Seek(fileLen - maxScan, SeekOrigin.Begin);
|
||||
ReadExact(fs, tail, maxScan);
|
||||
|
||||
int eocd = -1;
|
||||
for (int i = maxScan - 22; i >= 0; i--)
|
||||
{
|
||||
if (tail[i] == 0x50 && tail[i + 1] == 0x4B &&
|
||||
tail[i + 2] == 0x05 && tail[i + 3] == 0x06) { eocd = i; break; }
|
||||
}
|
||||
if (eocd < 0) throw new IOException("Not a ZIP archive (no end-of-central-directory record).");
|
||||
|
||||
long eocdPos = fileLen - maxScan + eocd;
|
||||
long totalEntries = BitConverter.ToUInt16(tail, eocd + 10);
|
||||
long cdOffset = BitConverter.ToUInt32(tail, eocd + 16);
|
||||
|
||||
// ZIP64: any maxed-out field means the real values live in the
|
||||
// ZIP64 EOCD record, found via the locator 20 bytes before EOCD.
|
||||
if (totalEntries == 0xFFFF || cdOffset == 0xFFFFFFFF)
|
||||
{
|
||||
long locPos = eocdPos - 20;
|
||||
if (locPos >= 0)
|
||||
{
|
||||
var loc = new byte[20];
|
||||
fs.Seek(locPos, SeekOrigin.Begin);
|
||||
ReadExact(fs, loc, 20);
|
||||
if (loc[0] == 0x50 && loc[1] == 0x4B && loc[2] == 0x06 && loc[3] == 0x07)
|
||||
{
|
||||
long z64Pos = BitConverter.ToInt64(loc, 8);
|
||||
var z64 = new byte[56];
|
||||
fs.Seek(z64Pos, SeekOrigin.Begin);
|
||||
ReadExact(fs, z64, 56);
|
||||
if (!(z64[0] == 0x50 && z64[1] == 0x4B && z64[2] == 0x06 && z64[3] == 0x06))
|
||||
throw new IOException("Corrupt ZIP64 end-of-central-directory record.");
|
||||
totalEntries = BitConverter.ToInt64(z64, 32);
|
||||
cdOffset = BitConverter.ToInt64(z64, 48);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var list = new List<Entry>((int)totalEntries);
|
||||
fs.Seek(cdOffset, SeekOrigin.Begin);
|
||||
var br = new BinaryReader(fs); // not disposed — would close fs
|
||||
|
||||
for (long i = 0; i < totalEntries; i++)
|
||||
{
|
||||
if (br.ReadUInt32() != 0x02014B50)
|
||||
throw new IOException("Corrupt central directory (bad entry signature).");
|
||||
|
||||
br.ReadUInt16(); // version made by
|
||||
br.ReadUInt16(); // version needed
|
||||
ushort flags = br.ReadUInt16();
|
||||
ushort method = br.ReadUInt16();
|
||||
br.ReadUInt32(); // DOS mod time/date
|
||||
br.ReadUInt32(); // CRC-32
|
||||
long comp = br.ReadUInt32();
|
||||
long uncomp = br.ReadUInt32();
|
||||
ushort nameLen = br.ReadUInt16();
|
||||
ushort extraLen = br.ReadUInt16();
|
||||
ushort commentLen = br.ReadUInt16();
|
||||
br.ReadUInt16(); // disk number start
|
||||
br.ReadUInt16(); // internal attributes
|
||||
br.ReadUInt32(); // external attributes
|
||||
long lho = br.ReadUInt32();
|
||||
|
||||
byte[] nameBytes = br.ReadBytes(nameLen);
|
||||
byte[] extra = br.ReadBytes(extraLen);
|
||||
if (commentLen > 0) br.ReadBytes(commentLen);
|
||||
|
||||
// ZIP64 extended-information extra field (id 0x0001): 8-byte
|
||||
// values present, in order, only for headers that were 0xFFFFFFFF.
|
||||
int p = 0;
|
||||
while (p + 4 <= extra.Length)
|
||||
{
|
||||
ushort id = BitConverter.ToUInt16(extra, p);
|
||||
ushort sz = BitConverter.ToUInt16(extra, p + 2);
|
||||
if (id == 0x0001)
|
||||
{
|
||||
int q = p + 4;
|
||||
if (uncomp == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { uncomp = BitConverter.ToInt64(extra, q); q += 8; }
|
||||
if (comp == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { comp = BitConverter.ToInt64(extra, q); q += 8; }
|
||||
if (lho == 0xFFFFFFFF && q + 8 <= p + 4 + sz) { lho = BitConverter.ToInt64(extra, q); }
|
||||
}
|
||||
p += 4 + sz;
|
||||
}
|
||||
|
||||
bool utf8 = (flags & 0x0800) != 0;
|
||||
var name = (utf8 ? Encoding.UTF8 : Encoding.Default).GetString(nameBytes);
|
||||
|
||||
list.Add(new Entry
|
||||
{
|
||||
Name = name,
|
||||
Method = method,
|
||||
CompressedSize = comp,
|
||||
LocalHeaderOffset = lho
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ── Entry extraction ──────────────────────────────────────────────────
|
||||
|
||||
private static void ExtractEntry(FileStream fs, Entry e, string destRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(e.Name)) return;
|
||||
|
||||
string destPath;
|
||||
try { destPath = Path.GetFullPath(Path.Combine(destRoot, e.Name.Replace('/', '\\'))); }
|
||||
catch { return; } // illegal characters in name — skip
|
||||
|
||||
// Zip-slip protection: never write outside the destination root.
|
||||
if (!destPath.StartsWith(destRoot + "\\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !destPath.Equals(destRoot, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (e.Name.EndsWith("/") || e.Name.EndsWith("\\"))
|
||||
{
|
||||
Directory.CreateDirectory(destPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Local header: its name/extra lengths can differ from the central
|
||||
// directory's, so read them from the local header itself.
|
||||
fs.Seek(e.LocalHeaderOffset, SeekOrigin.Begin);
|
||||
var lh = new byte[30];
|
||||
ReadExact(fs, lh, 30);
|
||||
if (!(lh[0] == 0x50 && lh[1] == 0x4B && lh[2] == 0x03 && lh[3] == 0x04))
|
||||
throw new IOException("Corrupt local header for entry '" + e.Name + "'.");
|
||||
int lNameLen = BitConverter.ToUInt16(lh, 26);
|
||||
int lExtraLen = BitConverter.ToUInt16(lh, 28);
|
||||
fs.Seek(lNameLen + lExtraLen, SeekOrigin.Current);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destPath));
|
||||
using (var outFs = File.Create(destPath))
|
||||
{
|
||||
if (e.Method == 0) // stored
|
||||
{
|
||||
CopyExactly(fs, outFs, e.CompressedSize);
|
||||
}
|
||||
else if (e.Method == 8) // deflate
|
||||
{
|
||||
var limited = new LimitedReadStream(fs, e.CompressedSize);
|
||||
using (var inflate = new DeflateStream(limited, CompressionMode.Decompress, leaveOpen: true))
|
||||
inflate.CopyTo(outFs);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException(string.Format(
|
||||
"Unsupported compression method {0} for entry '{1}'.", e.Method, e.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyExactly(Stream src, Stream dst, long count)
|
||||
{
|
||||
var buffer = new byte[65536];
|
||||
while (count > 0)
|
||||
{
|
||||
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, count));
|
||||
if (n == 0) throw new IOException("Unexpected end of archive.");
|
||||
dst.Write(buffer, 0, n);
|
||||
count -= n;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadExact(Stream stream, byte[] buf, int count)
|
||||
{
|
||||
int off = 0;
|
||||
while (off < count)
|
||||
{
|
||||
int n = stream.Read(buf, off, count - off);
|
||||
if (n == 0) throw new IOException("Unexpected end of archive.");
|
||||
off += n;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Caps reads at N bytes of the underlying stream so DeflateStream's
|
||||
/// read-ahead cannot consume the next entry's bytes. Does not own the inner
|
||||
/// stream.</summary>
|
||||
private sealed class LimitedReadStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private long _remaining;
|
||||
|
||||
public LimitedReadStream(Stream inner, long limit)
|
||||
{
|
||||
_inner = inner;
|
||||
_remaining = limit;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_remaining <= 0) return 0;
|
||||
int n = _inner.Read(buffer, offset, (int)Math.Min(count, _remaining));
|
||||
_remaining -= n;
|
||||
return n;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanWrite => false;
|
||||
public override bool CanSeek => false;
|
||||
public override void Flush() { }
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-56
@@ -1,23 +1,33 @@
|
||||
# TeslaLauncher
|
||||
# TeslaLauncher (XP11 — single binary)
|
||||
|
||||
.NET Framework 4.8 (framework-dependent) rewrite of the original Elsewhen Studios LLC software (Windows 2000 / .NET Framework 2.0). net48 ships in Windows 10/11, so the pod needs no separate runtime install and the package stays small (~3.7 MB).
|
||||
.NET Framework **4.0** (framework-dependent) rewrite of the original Elsewhen Studios LLC
|
||||
software. One `TeslaLauncher.exe` runs on **Windows XP SP3 through Windows 11**:
|
||||
net40 is the newest framework XP can install, and net40 assemblies load in-place on the
|
||||
4.8 runtime built into Windows 10/11. XP pods need the .NET 4.0 redistributable
|
||||
(installed automatically by `install.bat` when `dotnet40\` is in the package);
|
||||
Win10/11 pods need nothing extra.
|
||||
|
||||
## Architecture
|
||||
|
||||
TeslaLauncher has three components that work together:
|
||||
One userland tray application — no Windows Service, no IPC.
|
||||
|
||||
### TeslaLauncherService (Windows Service, Session 0)
|
||||
- Runs at boot before any user logs in
|
||||
The original Elsewhen software was a single service (on Win2k/XP a service could still
|
||||
touch the desktop). The modern rewrite split it into Service + Agent purely to work
|
||||
around Vista+ **Session 0 isolation**. XP11 closes the loop: everything runs in the
|
||||
auto-logged-in kiosk session, where the desktop, audio, and game processes live anyway.
|
||||
|
||||
### TeslaLauncher (WinForms tray app, user session)
|
||||
- Listens on **TCP 53290** for OFB-encrypted framed-JSON RPC from TeslaConsole
|
||||
- Forwards commands to the Agent via Named Pipe (`TeslaLauncherIPC`)
|
||||
- Handles first-boot network configuration (SecureConfig)
|
||||
- Handles game file transfers from the Console (InstallProduct)
|
||||
- Handles first-boot network configuration (SecureConfig) and shows the
|
||||
Request ID / Passphrase on screen + COM2 plasma
|
||||
- Handles game file transfers from the Console (InstallProduct → `C:\Games`,
|
||||
`postinstall.bat`, `pre-uninstall.bat` on uninstall)
|
||||
- Launches/kills/watches simulation apps, controls volume, manages `LaunchApps.xml`
|
||||
- Registers with WER for restart-after-crash (Vista+; no-op on XP)
|
||||
|
||||
### TeslaLauncherAgent (WinForms tray app, user session)
|
||||
- Runs in the logged-in user's desktop session
|
||||
- Executes commands that require desktop access: launching/killing apps, volume control
|
||||
- Manages `LaunchApps.xml` (installed games registry)
|
||||
- On first boot, displays SecureConfig Request ID and Passphrase
|
||||
Requires the kiosk account (`Firestorm`) to be in **Administrators** — SecureConfig's
|
||||
`netsh`/hostname writes and product `postinstall.bat` driver installs need the admin
|
||||
token (UAC is disabled by the installer on modern Windows, so no prompts).
|
||||
|
||||
### SecureConfig (first-boot protocol)
|
||||
- Assigns a temporary IP and broadcasts a UDP beacon so the Console can discover the pod
|
||||
@@ -26,29 +36,25 @@ TeslaLauncher has three components that work together:
|
||||
- TCP handshake establishes an OFB-encrypted session with RSA key exchange
|
||||
- Session key is saved for all subsequent Console connections
|
||||
|
||||
Uses the old-style `netsh interface ip` commands throughout — they update the live
|
||||
TCP/IP stack immediately and are the only form XP understands.
|
||||
|
||||
## Communication Flow
|
||||
|
||||
```
|
||||
TeslaConsole ──TCP 53290 (OFB + framed JSON)──> TeslaLauncherService
|
||||
│
|
||||
Named Pipe (JSON)
|
||||
│
|
||||
v
|
||||
TeslaLauncherAgent
|
||||
TeslaConsole ──TCP 53290 (OFB + framed JSON)──> TeslaLauncher.exe (user session)
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `TeslaLauncherService.cs` | Windows Service implementation |
|
||||
| `TeslaLauncherService.csproj` | Service project (net48, generic host + Windows service) |
|
||||
| `TeslaLauncherAgent.cs` | Userspace Agent implementation |
|
||||
| `TeslaLauncherAgent.csproj` | Agent project (WinForms, net48) |
|
||||
| `LaunchModels_Shared.cs` | Service↔Agent IPC types (Tesla.Launcher.Shared). Wire types (Tesla.Net) now come from `../Contract/Tesla.Contract.csproj` |
|
||||
| `SecureConfig.cs` | First-boot secure configuration protocol |
|
||||
| `build.bat` | Builds both components |
|
||||
| `install.bat` | Installs on a cockpit PC (run as Administrator) |
|
||||
| `TeslaLauncher.cs` | The whole launcher: tray, TCP listener, RPC dispatch, install, processes, volume |
|
||||
| `TeslaLauncher.csproj` | net40 WinForms exe project |
|
||||
| `MiniZip.cs` | Central-directory ZIP extractor (net40 has no `ZipFile`; stored + deflate + ZIP64) |
|
||||
| `SecureConfig.cs` | First-boot secure configuration protocol + OFB duplex stream |
|
||||
| `build.bat` | Builds + assembles the package |
|
||||
| `install.bat` | Dual-OS installer (XP SP3 and Win10/11 code paths; run as Administrator) |
|
||||
|
||||
## Building
|
||||
|
||||
@@ -57,34 +63,47 @@ Requirements:
|
||||
- Internet access for NuGet restore (first build only)
|
||||
|
||||
```
|
||||
build.bat :: build both components + assemble the package
|
||||
build.bat /service :: build Service only
|
||||
build.bat /agent :: build Agent only
|
||||
build.bat :: build + assemble the package
|
||||
```
|
||||
|
||||
Output goes to `dist\TeslaLauncher\` (with `Service\` and `Agent\` subdirectories plus
|
||||
`install.bat`) and `dist\TeslaLauncher-podpkg.zip`, mirroring `Console\dist\`. The projects
|
||||
are published in place (framework-dependent net48) — they reference `../Contract`, so they
|
||||
cannot be staged into a temp folder. Each folder holds the exe plus its dependency DLLs;
|
||||
the target pod needs only .NET Framework 4.8 (built into Windows 10/11), no bundled runtime.
|
||||
Output goes to `dist\TeslaLauncher\` (with `App\` plus `install.bat` and redist
|
||||
folders) and `dist\TeslaLauncher-podpkg.zip`. The project is published in place
|
||||
(framework-dependent net40) — it references `../Contract`, so it cannot be staged
|
||||
into a temp folder. `App\` holds the exe plus `Newtonsoft.Json.dll` and
|
||||
`TeslaConsoleLaunchLib.dll` (the net40 leg of the shared contract).
|
||||
|
||||
### Bench-testing switches
|
||||
|
||||
```
|
||||
TeslaLauncher.exe /skipconfig :: skip the DHCP SecureConfig gate
|
||||
TeslaLauncher.exe /port:53291 :: listen on a non-standard port
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the `TeslaLauncher\` folder to each cockpit PC
|
||||
1. Copy the `TeslaLauncher\` folder to each cockpit PC (XP SP3 or Win10/11)
|
||||
2. Run `TeslaLauncher\install.bat` as Administrator
|
||||
|
||||
The installer:
|
||||
- Registers the Service (delayed auto-start)
|
||||
- Configures the Agent for auto-login startup
|
||||
- Installs OpenAL and DirectX runtimes
|
||||
- Enables SMB1 file sharing
|
||||
- Creates `C:\Games` with appropriate permissions
|
||||
- Resets network adapters to DHCP for SecureConfig
|
||||
The installer detects the OS and branches where the tooling differs:
|
||||
|
||||
| Step | XP SP3 | Windows 10/11 |
|
||||
|------|--------|---------------|
|
||||
| .NET | installs 4.0 redist from `dotnet40\` if missing | 4.8 built in — nothing |
|
||||
| ACLs | `cacls` | `icacls` |
|
||||
| Firewall off | `netsh firewall` | `netsh advfirewall` |
|
||||
| SMB1 / DirectPlay | native — skipped | `dism /Enable-Feature` |
|
||||
| DHCP reset | `netsh interface ip set address ... dhcp` | PowerShell `Set-NetIPInterface` |
|
||||
| Notifications / UAC | n/a | policy keys + `EnableLUA=0` |
|
||||
| UltraVNC | `UltraVNC_x86_Setup.exe` (if bundled) | `UltraVNC_x64_Setup.exe` |
|
||||
|
||||
Common to both: auto-login (`Firestorm`), HKLM Run key for the launcher
|
||||
(**no service registration**), `C:\Games` + data dir creation, shares, workgroup,
|
||||
power settings, reboot.
|
||||
|
||||
## First Boot
|
||||
|
||||
1. Cockpit boots with DHCP (unconfigured state)
|
||||
2. Service runs SecureConfig: broadcasts beacon, displays codes on screen
|
||||
1. Cockpit boots with DHCP (unconfigured state), auto-logs into the kiosk account
|
||||
2. Launcher runs SecureConfig: broadcasts beacon, displays codes on screen + plasma
|
||||
3. Console operator sees the pod's Request ID and enters the Passphrase
|
||||
4. Console sends encrypted network configuration
|
||||
5. Pod applies the configuration and is ready for normal operation
|
||||
@@ -100,21 +119,27 @@ The Console connects to each configured pod on TCP 53290 and can:
|
||||
|
||||
## Key Paths
|
||||
|
||||
`<CommonAppData>` is `C:\ProgramData` on Vista+, and
|
||||
`C:\Documents and Settings\All Users\Application Data` on XP — the launcher and
|
||||
installer both resolve it per-OS; nothing hardcodes `C:\ProgramData` anymore.
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `C:\ProgramData\TeslaLauncher\TeslaKeyStore.key` | Session key (32 bytes) |
|
||||
| `C:\ProgramData\TeslaLauncher\LaunchApps.xml` | Installed games registry |
|
||||
| `C:\ProgramData\TeslaLauncher\configuring.json` | Transient: SecureConfig codes for Agent display |
|
||||
| `<CommonAppData>\TeslaLauncher\TeslaKeyStore.key` | Session key (32 bytes) |
|
||||
| `<CommonAppData>\TeslaLauncher\LaunchApps.xml` | Installed games registry (same XML shape as the two-process Agent wrote) |
|
||||
| `<CommonAppData>\TeslaLauncher\podconf.log` | Launcher log (was next to the exe pre-XP11) |
|
||||
| `<CommonAppData>\TeslaLauncher\configuring.json` | Transient: SecureConfig codes (kept for external diagnostics) |
|
||||
| `C:\Games\` | Game installation directory |
|
||||
|
||||
## Wire Protocol
|
||||
|
||||
The Console talks to the Service with **length-prefixed System.Text.Json frames**
|
||||
over the OFB-encrypted TCP stream (dispatch by method name) — see
|
||||
`../Contract/PodRpcProtocol.cs`, shared by both ends. This replaced the original
|
||||
`BinaryFormatter` + serialized-`MethodBase` scheme. The `Tesla.Net` wire types now
|
||||
live in `../Contract/Tesla.Contract.csproj`, the single source of truth shared with
|
||||
the Console.
|
||||
The Console talks to the launcher with **length-prefixed JSON frames** over the
|
||||
OFB-encrypted TCP stream (dispatch by method name) — see
|
||||
`../Contract/PodRpcProtocol.cs`, shared by both ends. The Contract multi-targets
|
||||
`net48;net40`: the Console's net48 leg serializes with System.Text.Json, the
|
||||
launcher's net40 leg with Newtonsoft.Json (STJ has no net40 target). The JSON is
|
||||
shape-identical on the wire; the request reader keeps date strings raw so a Ping
|
||||
echo returns byte-identical.
|
||||
|
||||
The Service-to-Agent IPC uses length-prefixed JSON over a Named Pipe, with flat types
|
||||
that avoid the nested struct layout of the wire format.
|
||||
Volume on XP falls back from CoreAudio (Vista+) to `nircmd.exe` / winmm
|
||||
`waveOutSetVolume`.
|
||||
|
||||
+36
-24
@@ -416,7 +416,10 @@ namespace TeslaSecureConfig
|
||||
}
|
||||
else
|
||||
{
|
||||
(_adapterIndex, _adapterId, _adapterName) = FindFirstEthernetAdapter();
|
||||
var adapter = FindFirstEthernetAdapter();
|
||||
_adapterIndex = adapter.Index;
|
||||
_adapterId = adapter.Id;
|
||||
_adapterName = adapter.Name;
|
||||
}
|
||||
_log = logger ?? (s => Debug.WriteLine(s));
|
||||
}
|
||||
@@ -845,23 +848,23 @@ namespace TeslaSecureConfig
|
||||
Log("Secure connection to console negotiated.");
|
||||
|
||||
// ── 4. RSA key exchange ─────────────────────────────────
|
||||
using var rsa = RSA.Create(Proto.RsaKeySize);
|
||||
// net40: RSA.Create(int) and RSAEncryptionPadding are net46+.
|
||||
// RSACryptoServiceProvider.Decrypt(data, false) is the same
|
||||
// PKCS#1 v1.5 padding, and works on XP SP3's CAPI.
|
||||
using var rsa = new RSACryptoServiceProvider(Proto.RsaKeySize);
|
||||
|
||||
Log("Sending console final key.");
|
||||
using (var bw = new BinaryWriter(ofb, Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
bw.Write(rsa.ToXmlString(false)); // public key only
|
||||
bw.Flush();
|
||||
}
|
||||
// net40 BinaryWriter/Reader have no leaveOpen overload; not
|
||||
// disposing them keeps the OFB stream open, which is the intent.
|
||||
var bw = new BinaryWriter(ofb, Encoding.UTF8);
|
||||
bw.Write(rsa.ToXmlString(false)); // public key only
|
||||
bw.Flush();
|
||||
|
||||
Log("Receiving console key.");
|
||||
byte[] sessionKey;
|
||||
using (var br = new BinaryReader(ofb, Encoding.UTF8, leaveOpen: true))
|
||||
{
|
||||
int encLen = br.ReadInt32();
|
||||
byte[] enc = br.ReadBytes(encLen);
|
||||
sessionKey = rsa.Decrypt(enc, RSAEncryptionPadding.Pkcs1);
|
||||
}
|
||||
var br = new BinaryReader(ofb, Encoding.UTF8);
|
||||
int encLen = br.ReadInt32();
|
||||
byte[] enc = br.ReadBytes(encLen);
|
||||
byte[] sessionKey = rsa.Decrypt(enc, false); // PKCS#1 v1.5
|
||||
Log($"Console key received ({sessionKey.Length} bytes).");
|
||||
|
||||
// Store session key — used for OFB on the management port (53290)
|
||||
@@ -945,11 +948,12 @@ namespace TeslaSecureConfig
|
||||
_plasma.WriteLine("Passphrase: {0}", _passphrase);
|
||||
|
||||
#if WINFORMS
|
||||
// WinForms dialog — only shown when running as the Agent (user session)
|
||||
// WinForms dialog on its own STA thread. Do NOT call
|
||||
// SetCompatibleTextRenderingDefault here: the single-binary launcher
|
||||
// already created its tray form, and calling it after any control
|
||||
// exists throws InvalidOperationException.
|
||||
var t = new Thread(() =>
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
_displayForm = new PasscodeDisplayForm(_requestId, _passphrase);
|
||||
_displayForm.ShowDialog();
|
||||
})
|
||||
@@ -1129,10 +1133,18 @@ namespace TeslaSecureConfig
|
||||
Log($"Warning: target IP {targetIp} not on any local subnet — keeping [{_adapterIndex}] {_adapterName}");
|
||||
}
|
||||
|
||||
// Returns (IPv4 interface index, adapter GUID).
|
||||
// Index is used with `netsh interface ipv4 ... interface=N` (avoids name-quoting issues).
|
||||
// GUID is used for direct registry writes to persist static IP across reboots.
|
||||
private static (int index, string id, string name) FindFirstEthernetAdapter()
|
||||
// IPv4 interface index + adapter GUID + display name (a struct instead of a
|
||||
// ValueTuple — net40 has no System.ValueTuple).
|
||||
// Index avoids netsh name-quoting issues; GUID is used for direct registry
|
||||
// writes to persist the static IP across reboots.
|
||||
private struct AdapterInfo
|
||||
{
|
||||
public int Index;
|
||||
public string Id;
|
||||
public string Name;
|
||||
}
|
||||
|
||||
private static AdapterInfo FindFirstEthernetAdapter()
|
||||
{
|
||||
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
|
||||
{
|
||||
@@ -1150,7 +1162,7 @@ namespace TeslaSecureConfig
|
||||
{
|
||||
int idx = nic.GetIPProperties().GetIPv4Properties().Index;
|
||||
Log2($"Selected physical adapter: [{idx}] {nic.Name} / {nic.Description}");
|
||||
return (idx, nic.Id, nic.Name);
|
||||
return new AdapterInfo { Index = idx, Id = nic.Id, Name = nic.Name };
|
||||
}
|
||||
catch (NetworkInformationException) { }
|
||||
}
|
||||
@@ -1165,7 +1177,7 @@ namespace TeslaSecureConfig
|
||||
string.Equals(nic.Description, displayName, StringComparison.OrdinalIgnoreCase))
|
||||
return nic.Id;
|
||||
}
|
||||
return FindFirstEthernetAdapter().id; // .name ignored in fallback
|
||||
return FindFirstEthernetAdapter().Id; // Name ignored in fallback
|
||||
}
|
||||
|
||||
// Find index for a named adapter (fallback when caller passes a display name)
|
||||
@@ -1181,7 +1193,7 @@ namespace TeslaSecureConfig
|
||||
}
|
||||
}
|
||||
// Fall back to auto-detect if name not matched
|
||||
return FindFirstEthernetAdapter().index; // .name ignored in fallback
|
||||
return FindFirstEthernetAdapter().Index; // Name ignored in fallback
|
||||
}
|
||||
|
||||
// --- Random string generator ---
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ===========================================================================
|
||||
XP11 single-binary launcher.
|
||||
|
||||
TargetFramework net40 is deliberate: it is the newest .NET Framework that
|
||||
installs on Windows XP SP3, and net40 assemblies load in-place on the 4.8
|
||||
runtime that ships in Windows 10/11 — so this ONE exe covers XP SP3
|
||||
through Windows 11. Everything net45+ is off-limits here:
|
||||
- System.Text.Json -> Newtonsoft.Json (net40 leg of Tesla.Contract)
|
||||
- ZipFile/ZipArchive -> MiniZip.cs
|
||||
- async/await, Task.Run -> threads
|
||||
- RSA.Create(int) -> RSACryptoServiceProvider
|
||||
=========================================================================== -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net40</TargetFramework>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>4.11.4.1</Version>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>TeslaLauncher</AssemblyName>
|
||||
<RootNamespace>Tesla.Launcher</RootNamespace>
|
||||
<StartupObject>Tesla.Launcher.LauncherApplication</StartupObject>
|
||||
<!-- SecureConfig.cs shows the PasscodeDisplayForm in-process. -->
|
||||
<DefineConstants>$(DefineConstants);WINFORMS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<!-- WinForms via plain framework references: UseWindowsForms is not wired up
|
||||
for net40, and all UI here is code-built (no designer). -->
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Drawing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Contract\Tesla.Contract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -2,9 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "TeslaLauncherAgent.csproj", "{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "TeslaLauncherService.csproj", "{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncher", "TeslaLauncher.csproj", "{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -16,10 +14,6 @@ Global
|
||||
{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,989 +0,0 @@
|
||||
// =============================================================================
|
||||
// TeslaLauncher — Userspace Agent
|
||||
// =============================================================================
|
||||
// Runs as a WinForms tray application in the logged-in user's desktop session.
|
||||
// On first boot (unconfigured machine), displays a splash with the SecureConfig
|
||||
// Request ID and Passphrase so the operator can read them to the console.
|
||||
// On subsequent boots, listens on a Named Pipe for commands from
|
||||
// TeslaLauncherService and manages simulation applications.
|
||||
// =============================================================================
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.IO.IsolatedStorage;
|
||||
using System.IO.Pipes;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Tesla.Launcher.Shared;
|
||||
|
||||
namespace Tesla.Launcher.Agent
|
||||
{
|
||||
public class AgentApplication : Form
|
||||
{
|
||||
// ── Configuration ─────────────────────────────────────────────────────
|
||||
private const string PIPE_NAME = "TeslaLauncherIPC";
|
||||
private const string CONFIG_FILE = @"C:\ProgramData\TeslaLauncher\LaunchApps.xml";
|
||||
private const string GAMES_DIR = @"C:\Games";
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
private NotifyIcon _trayIcon;
|
||||
private ContextMenuStrip _trayMenu;
|
||||
|
||||
private List<LaunchData> _launchApps = new();
|
||||
private readonly Dictionary<string, List<Process>> _runningProcesses = new();
|
||||
private readonly object _processLock = new();
|
||||
private readonly List<Thread> _watcherThreads = new();
|
||||
private bool _stopping = false;
|
||||
|
||||
private readonly Dictionary<string, OutOfBandProgress> _installProgress = new();
|
||||
private readonly object _installLock = new();
|
||||
|
||||
private Thread _pipeThread;
|
||||
private CancellationTokenSource _cts = new();
|
||||
|
||||
// ── Entry point ───────────────────────────────────────────────────────
|
||||
|
||||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// First-boot check: show the configuring splash (with the SecureConfig
|
||||
// Request ID + Passphrase) when the machine is unconfigured OR when
|
||||
// SecureConfig is actively running.
|
||||
//
|
||||
// The second condition is essential: during SecureConfig the Service
|
||||
// assigns a TEMPORARY static IP to the Ethernet adapter so it can
|
||||
// broadcast. That makes IsMachineConfigured() report true, so the DHCP
|
||||
// check ALONE would skip the splash and the codes would never reach the
|
||||
// screen — they'd only be in the log / on the COM2 plasma display. The
|
||||
// Service writes configuring.json *before* assigning that temp IP, so its
|
||||
// presence is the authoritative "SecureConfig in progress" signal.
|
||||
if (!IsMachineConfigured() || File.Exists(ConfiguringFilePath()))
|
||||
{
|
||||
ShowConfiguringWait(); // blocks until config completes or form closed
|
||||
|
||||
// If still not configured (config failed / user closed splash), exit.
|
||||
if (!IsMachineConfigured())
|
||||
return;
|
||||
}
|
||||
|
||||
Application.Run(new AgentApplication());
|
||||
}
|
||||
|
||||
// ── Secure Configuration ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when the cockpit has already been configured —
|
||||
/// detected by the Ethernet adapter having a static IP assignment.
|
||||
/// SecureConfiguration writes the permanent address via netsh with
|
||||
/// no DHCP, so a static adapter means configuration is complete.
|
||||
/// A DHCP adapter (or no adapter) means we are on a fresh machine
|
||||
/// and need to run the SecureConfiguration protocol.
|
||||
/// </summary>
|
||||
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();
|
||||
// IsDhcpEnabled == false → static IP → already configured
|
||||
if (!ipv4.IsDhcpEnabled)
|
||||
return true;
|
||||
}
|
||||
catch (NetworkInformationException)
|
||||
{
|
||||
// GetIPv4Properties() throws if IPv4 is not configured at all;
|
||||
// treat that the same as DHCP (i.e. not yet configured).
|
||||
}
|
||||
}
|
||||
return false; // all Ethernet adapters are DHCP or absent → needs configuration
|
||||
}
|
||||
|
||||
/// <summary>Shared file the Service writes (before assigning the temp IP) while
|
||||
/// SecureConfig is in progress; holds the RequestId/Passphrase for the splash.</summary>
|
||||
private static string ConfiguringFilePath() => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"TeslaLauncher", "configuring.json");
|
||||
|
||||
private static bool IsVirtualAdapter(NetworkInterface nic)
|
||||
{
|
||||
var desc = nic.Description ?? "";
|
||||
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 MAC
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ShowConfiguringWait()
|
||||
{
|
||||
// The Service handles SecureConfig in Session 0. The Agent shows
|
||||
// a splash with the RequestId and Passphrase for the operator.
|
||||
using var form = new Form
|
||||
{
|
||||
Text = "Tesla Cockpit Configuration",
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
StartPosition = FormStartPosition.CenterScreen,
|
||||
ClientSize = new System.Drawing.Size(480, 260),
|
||||
MaximizeBox = false,
|
||||
MinimizeBox = false,
|
||||
TopMost = true,
|
||||
};
|
||||
|
||||
var lblStatus = new Label
|
||||
{
|
||||
Text = "This cockpit is being configured.\r\n" +
|
||||
"Configuration will complete automatically.",
|
||||
AutoSize = false,
|
||||
Dock = DockStyle.Top,
|
||||
Height = 50,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Arial", 10f),
|
||||
Padding = new Padding(12, 8, 12, 0),
|
||||
};
|
||||
form.Controls.Add(lblStatus);
|
||||
|
||||
var lblRequestIdTitle = new Label
|
||||
{
|
||||
Text = "Request ID:",
|
||||
AutoSize = false,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Arial", 11f),
|
||||
Top = 60,
|
||||
Left = 0,
|
||||
Width = 480,
|
||||
Height = 25,
|
||||
};
|
||||
form.Controls.Add(lblRequestIdTitle);
|
||||
|
||||
var lblRequestId = new Label
|
||||
{
|
||||
Text = "waiting...",
|
||||
AutoSize = false,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Consolas", 28f, System.Drawing.FontStyle.Bold),
|
||||
ForeColor = System.Drawing.Color.DarkBlue,
|
||||
Top = 85,
|
||||
Left = 0,
|
||||
Width = 480,
|
||||
Height = 45,
|
||||
};
|
||||
form.Controls.Add(lblRequestId);
|
||||
|
||||
var lblPassphraseTitle = new Label
|
||||
{
|
||||
Text = "Passphrase:",
|
||||
AutoSize = false,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Arial", 11f),
|
||||
Top = 140,
|
||||
Left = 0,
|
||||
Width = 480,
|
||||
Height = 25,
|
||||
};
|
||||
form.Controls.Add(lblPassphraseTitle);
|
||||
|
||||
var lblPassphrase = new Label
|
||||
{
|
||||
Text = "waiting...",
|
||||
AutoSize = false,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Consolas", 28f, System.Drawing.FontStyle.Bold),
|
||||
ForeColor = System.Drawing.Color.DarkRed,
|
||||
Top = 165,
|
||||
Left = 0,
|
||||
Width = 480,
|
||||
Height = 45,
|
||||
};
|
||||
form.Controls.Add(lblPassphrase);
|
||||
|
||||
var lblHint = new Label
|
||||
{
|
||||
Text = "Read the Passphrase to the console operator.",
|
||||
AutoSize = false,
|
||||
TextAlign = System.Drawing.ContentAlignment.MiddleCenter,
|
||||
Font = new System.Drawing.Font("Arial", 9f, System.Drawing.FontStyle.Italic),
|
||||
ForeColor = System.Drawing.Color.Gray,
|
||||
Top = 220,
|
||||
Left = 0,
|
||||
Width = 480,
|
||||
Height = 25,
|
||||
};
|
||||
form.Controls.Add(lblHint);
|
||||
|
||||
string lastJson = null;
|
||||
bool sawFile = false;
|
||||
var cfgFile = ConfiguringFilePath();
|
||||
|
||||
// Poll every 2 s — read codes from the Service's shared file.
|
||||
// The file lifecycle:
|
||||
// 1. Service deletes stale file on startup
|
||||
// 2. Service writes fresh file with new codes (before the temp IP)
|
||||
// 3. Agent reads and displays codes
|
||||
// 4. Service deletes file when config is complete → splash closes
|
||||
// If the Agent started before the file appeared, it keeps waiting; it
|
||||
// closes once the file we saw is gone, or the machine becomes configured
|
||||
// (covers the case where config finished before we managed to read it).
|
||||
var timer = new System.Windows.Forms.Timer { Interval = 2000 };
|
||||
timer.Tick += (s, e) =>
|
||||
{
|
||||
if (File.Exists(cfgFile))
|
||||
{
|
||||
sawFile = true;
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(cfgFile);
|
||||
if (json != lastJson)
|
||||
{
|
||||
lastJson = json;
|
||||
var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("RequestId", out var rid))
|
||||
lblRequestId.Text = rid.GetString();
|
||||
if (root.TryGetProperty("Passphrase", out var pp))
|
||||
lblPassphrase.Text = pp.GetString();
|
||||
}
|
||||
}
|
||||
catch { /* file may be mid-write, retry next tick */ }
|
||||
}
|
||||
else if (sawFile || IsMachineConfigured())
|
||||
{
|
||||
// File gone after we saw it, or the machine is now configured →
|
||||
// SecureConfig is complete.
|
||||
timer.Stop();
|
||||
form.Close();
|
||||
}
|
||||
};
|
||||
timer.Start();
|
||||
|
||||
Application.Run(form);
|
||||
}
|
||||
|
||||
// ── Form / tray setup ─────────────────────────────────────────────────
|
||||
|
||||
public AgentApplication()
|
||||
{
|
||||
ShowInTaskbar = false;
|
||||
WindowState = FormWindowState.Minimized;
|
||||
Opacity = 0;
|
||||
|
||||
BuildTrayIcon();
|
||||
LoadLaunchApps();
|
||||
StartPipeServer();
|
||||
}
|
||||
|
||||
private void BuildTrayIcon()
|
||||
{
|
||||
_trayMenu = new ContextMenuStrip();
|
||||
var version = typeof(AgentApplication).Assembly.GetName().Version;
|
||||
_trayMenu.Items.Add($"Tesla Launcher Agent v{version}", null, null).Enabled = false;
|
||||
_trayMenu.Items.Add(new ToolStripSeparator());
|
||||
_trayMenu.Items.Add("Reload Config", null, (s, e) => LoadLaunchApps());
|
||||
_trayMenu.Items.Add("Kill All Apps", null, (s, e) => CmdKillAllApps());
|
||||
_trayMenu.Items.Add(new ToolStripSeparator());
|
||||
_trayMenu.Items.Add("Exit", null, (s, e) => ExitAgent());
|
||||
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Text = "Tesla Launcher Agent",
|
||||
Icon = SystemIcons.Application,
|
||||
ContextMenuStrip = _trayMenu,
|
||||
Visible = true
|
||||
};
|
||||
}
|
||||
|
||||
private void ExitAgent()
|
||||
{
|
||||
_stopping = true;
|
||||
_cts.Cancel();
|
||||
_trayIcon.Visible = false;
|
||||
Application.Exit();
|
||||
}
|
||||
|
||||
// ── LaunchApps.xml ────────────────────────────────────────────────────
|
||||
|
||||
private void LoadLaunchApps()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(CONFIG_FILE))
|
||||
{
|
||||
_launchApps = new List<LaunchData>();
|
||||
SetTrayStatus("Config not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var xml = new System.Xml.Serialization.XmlSerializer(typeof(List<LaunchData>));
|
||||
using var reader = File.OpenRead(CONFIG_FILE);
|
||||
_launchApps = (List<LaunchData>)xml.Deserialize(reader)
|
||||
?? new List<LaunchData>();
|
||||
|
||||
SetTrayStatus($"{_launchApps.Count} apps configured");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetTrayStatus($"Config error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLaunchApps()
|
||||
{
|
||||
var xml = new System.Xml.Serialization.XmlSerializer(typeof(List<LaunchData>));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(CONFIG_FILE)!);
|
||||
using var writer = File.CreateText(CONFIG_FILE);
|
||||
xml.Serialize(writer, _launchApps);
|
||||
}
|
||||
|
||||
private void SetTrayStatus(string status)
|
||||
{
|
||||
if (_trayIcon != null) _trayIcon.Text = $"Tesla Launcher: {status}";
|
||||
}
|
||||
|
||||
// ── Named Pipe Server ─────────────────────────────────────────────────
|
||||
|
||||
private void StartPipeServer()
|
||||
{
|
||||
_pipeThread = new Thread(PipeServerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "TeslaLauncherPipeServer"
|
||||
};
|
||||
_pipeThread.Start();
|
||||
}
|
||||
|
||||
private void PipeServerLoop()
|
||||
{
|
||||
var ct = _cts.Token;
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var server = new NamedPipeServerStream(
|
||||
PIPE_NAME,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
server.WaitForConnectionAsync(ct).GetAwaiter().GetResult();
|
||||
HandlePipeRequest(server);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception)
|
||||
{
|
||||
if (!ct.IsCancellationRequested) Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePipeRequest(NamedPipeServerStream pipe)
|
||||
{
|
||||
IpcResponse response;
|
||||
try
|
||||
{
|
||||
var lenBuf = new byte[4];
|
||||
ReadExact(pipe, lenBuf);
|
||||
var reqBuf = new byte[BitConverter.ToInt32(lenBuf, 0)];
|
||||
ReadExact(pipe, reqBuf);
|
||||
|
||||
var msg = JsonSerializer.Deserialize<IpcMessage>(
|
||||
Encoding.UTF8.GetString(reqBuf));
|
||||
|
||||
response = ExecuteCommand(msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response = new IpcResponse { Success = false, Message = ex.Message };
|
||||
}
|
||||
|
||||
var resBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(response));
|
||||
pipe.Write(BitConverter.GetBytes(resBytes.Length), 0, 4);
|
||||
pipe.Write(resBytes, 0, resBytes.Length);
|
||||
pipe.Flush();
|
||||
}
|
||||
|
||||
private static void ReadExact(Stream stream, byte[] buf)
|
||||
{
|
||||
var off = 0;
|
||||
while (off < buf.Length)
|
||||
{
|
||||
var n = stream.Read(buf, off, buf.Length - off);
|
||||
if (n == 0) throw new IOException("Pipe closed.");
|
||||
off += n;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Dispatcher ────────────────────────────────────────────────
|
||||
|
||||
private IpcResponse ExecuteCommand(IpcMessage msg)
|
||||
{
|
||||
if (msg == null) return Fail("Null message received.");
|
||||
|
||||
try
|
||||
{
|
||||
object result = msg.Command switch
|
||||
{
|
||||
"PING" => (object)null,
|
||||
"CLEARSTORE" => CmdClearStore(),
|
||||
"LAUNCHAPP" => CmdLaunchApp(msg),
|
||||
"KILLAPP" => CmdKillApp(msg),
|
||||
"KILLALLOFTYPE" => CmdKillAllOfType(msg),
|
||||
"KILLALLAPPS" => CmdKillAllApps(),
|
||||
"GETLAUNCHEDAPPS" => CmdGetLaunchedApps(),
|
||||
"GETLAUNCHABLEAPPS" => CmdGetLaunchableApps(),
|
||||
"GETINSTALLEDAPPS" => CmdGetLaunchableApps(),
|
||||
"REMOVEAPP" => CmdRemoveApp(msg),
|
||||
"INSTALLAPP" => CmdInstallApp(msg),
|
||||
"UNINSTALLAPP" => CmdUninstallApp(msg),
|
||||
"GET_VOLUMELEVEL" => CmdGetVolumeLevel(),
|
||||
"SET_VOLUMELEVEL" => CmdSetVolumeLevel(msg),
|
||||
"FULLUPDATE" => CmdFullUpdate(),
|
||||
"INITIATEINSTALLPRODUCT" => CmdInitiateInstallProduct(msg),
|
||||
"GETOUTOFBANDPROGRESS" => CmdGetOutOfBandProgress(msg),
|
||||
"SHUTDOWN" => CmdShutdown(msg),
|
||||
_ => throw new Exception($"Unknown command: {msg.Command}")
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Implementations ───────────────────────────────────────────
|
||||
|
||||
private object CmdClearStore()
|
||||
{
|
||||
var store = IsolatedStorageFile.GetMachineStoreForAssembly();
|
||||
foreach (var file in store.GetFileNames("*"))
|
||||
store.DeleteFile(file);
|
||||
LoadLaunchApps();
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdLaunchApp(IpcMessage msg)
|
||||
{
|
||||
var key = msg.LaunchKey
|
||||
?? throw new Exception("LaunchApp requires a LaunchKey");
|
||||
|
||||
var app = FindApp(key)
|
||||
?? 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 rather than
|
||||
// surfacing a raw Win32 "file not found" from Process.Start.
|
||||
if (string.IsNullOrWhiteSpace(app.ExeFile) || !File.Exists(app.ExeFile))
|
||||
throw new Exception(
|
||||
$"Cannot launch '{app.DisplayName ?? key}': executable not found at " +
|
||||
$"'{app.ExeFile}'. The product may be registered but not yet installed on this pod.");
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = app.ExeFile,
|
||||
Arguments = app.Arguments ?? "",
|
||||
WorkingDirectory = app.WorkingDirectory ?? Path.GetDirectoryName(app.ExeFile),
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
var process = Process.Start(psi)
|
||||
?? throw new Exception($"Process.Start returned null for '{app.ExeFile}'");
|
||||
|
||||
lock (_processLock)
|
||||
{
|
||||
if (!_runningProcesses.ContainsKey(key))
|
||||
_runningProcesses[key] = new List<Process>();
|
||||
_runningProcesses[key].Add(process);
|
||||
}
|
||||
|
||||
if (app.AutoRestart) StartWatcher(app, process);
|
||||
|
||||
SetTrayStatus($"Running: {app.DisplayName ?? key}");
|
||||
return new LaunchedAppData { LaunchKey = key, ProcessId = process.Id };
|
||||
}
|
||||
|
||||
private object CmdKillApp(IpcMessage msg)
|
||||
{
|
||||
var key = msg.LaunchKey;
|
||||
int? pid = null;
|
||||
|
||||
if (msg.PayloadJson != null)
|
||||
{
|
||||
var parms = JsonSerializer.Deserialize<object[]>(msg.PayloadJson);
|
||||
if (parms?.Length > 1 && parms[1] is JsonElement je)
|
||||
pid = je.ValueKind == JsonValueKind.Number ? je.GetInt32() : (int?)null;
|
||||
}
|
||||
|
||||
lock (_processLock)
|
||||
{
|
||||
if (!_runningProcesses.TryGetValue(key ?? "", out var procs)) return null;
|
||||
|
||||
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 ?? "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdKillAllOfType(IpcMessage msg)
|
||||
{
|
||||
var key = msg.LaunchKey ?? throw new Exception("KillAllOfType requires LaunchKey");
|
||||
|
||||
lock (_processLock)
|
||||
{
|
||||
if (_runningProcesses.TryGetValue(key, out var procs))
|
||||
{
|
||||
foreach (var p in procs)
|
||||
try { if (!p.HasExited) p.Kill(); } catch { }
|
||||
_runningProcesses.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private object 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");
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdGetLaunchedApps()
|
||||
{
|
||||
var result = new List<LaunchedAppData>();
|
||||
|
||||
lock (_processLock)
|
||||
{
|
||||
foreach (var kvp in _runningProcesses)
|
||||
{
|
||||
kvp.Value.RemoveAll(p => p.HasExited);
|
||||
|
||||
foreach (var p in kvp.Value)
|
||||
result.Add(new LaunchedAppData
|
||||
{
|
||||
LaunchKey = 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 object CmdGetLaunchableApps() => _launchApps.ToArray();
|
||||
|
||||
private object CmdRemoveApp(IpcMessage msg)
|
||||
{
|
||||
var key = msg.LaunchKey ?? throw new Exception("RemoveApp requires LaunchKey");
|
||||
_launchApps.RemoveAll(a => a.LaunchKey == key);
|
||||
SaveLaunchApps();
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdInstallApp(IpcMessage msg)
|
||||
{
|
||||
if (msg.PayloadJson == null)
|
||||
throw new Exception("InstallApp requires PayloadJson");
|
||||
|
||||
var parms = JsonSerializer.Deserialize<JsonElement[]>(msg.PayloadJson);
|
||||
if (parms == null || parms.Length == 0)
|
||||
throw new Exception("InstallApp: no parameters");
|
||||
|
||||
var appData = JsonSerializer.Deserialize<LaunchData>(parms[0].GetRawText())
|
||||
?? throw new Exception("InstallApp: could not deserialize LaunchData");
|
||||
|
||||
var existing = _launchApps.FindIndex(a => a.LaunchKey == appData.LaunchKey);
|
||||
if (existing >= 0) _launchApps[existing] = appData;
|
||||
else _launchApps.Add(appData);
|
||||
|
||||
SaveLaunchApps();
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdUninstallApp(IpcMessage msg)
|
||||
{
|
||||
var key = msg.LaunchKey ?? throw new Exception("UninstallApp requires LaunchKey");
|
||||
CmdKillAllOfType(msg);
|
||||
|
||||
var removed = _launchApps.FirstOrDefault(a => a.LaunchKey == key);
|
||||
_launchApps.RemoveAll(a => a.LaunchKey == key);
|
||||
SaveLaunchApps();
|
||||
|
||||
// If the removed app's product directory under C:\Games is no longer used by
|
||||
// any remaining registered entry, tell the Service (which runs as SYSTEM) to
|
||||
// run its pre-uninstall.bat and delete the directory. 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.
|
||||
string cleanupDir = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return new { CleanupDir = cleanupDir };
|
||||
}
|
||||
|
||||
/// <summary>The immediate child of C:\Games that contains <paramref name="path"/>
|
||||
/// (e.g. C:\Games\RIOJoy\app\x.exe → C:\Games\RIOJoy), or null if not under C:\Games.</summary>
|
||||
private static string ProductDirUnderGames(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(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;
|
||||
}
|
||||
|
||||
private object CmdGetVolumeLevel()
|
||||
{
|
||||
return GetMasterVolume();
|
||||
}
|
||||
|
||||
private object CmdSetVolumeLevel(IpcMessage msg)
|
||||
{
|
||||
if (msg.PayloadJson == null)
|
||||
throw new Exception("set_VolumeLevel requires parameters");
|
||||
|
||||
var parms = JsonSerializer.Deserialize<JsonElement[]>(msg.PayloadJson);
|
||||
if (parms == null || parms.Length == 0)
|
||||
throw new Exception("Invalid volume value");
|
||||
var scalar = parms[0].ValueKind == JsonValueKind.Number
|
||||
? (float)parms[0].GetDouble()
|
||||
: throw new Exception("Invalid volume value");
|
||||
SetMasterVolume(Math.Max(0f, Math.Min(1f, scalar)));
|
||||
return null;
|
||||
}
|
||||
|
||||
private object CmdFullUpdate() =>
|
||||
new FullUpdateData
|
||||
{
|
||||
InstalledApps = _launchApps.ToArray(),
|
||||
LaunchedApps = (LaunchedAppData[])CmdGetLaunchedApps(),
|
||||
VolumeLevel = GetMasterVolume()
|
||||
};
|
||||
|
||||
private object CmdInitiateInstallProduct(IpcMessage msg)
|
||||
{
|
||||
// ILauncherService.InitiateInstallProduct() takes no parameters.
|
||||
// It just returns a tracking Guid. The actual game registration
|
||||
// happens via InstallApp(LaunchData) in a separate RPC call.
|
||||
var callId = Guid.NewGuid().ToString("N");
|
||||
var progress = new OutOfBandProgress
|
||||
{
|
||||
PercentComplete = 100,
|
||||
Status = "Complete",
|
||||
IsCompleted = true
|
||||
};
|
||||
|
||||
lock (_installLock) _installProgress[callId] = progress;
|
||||
|
||||
return callId;
|
||||
}
|
||||
|
||||
private object CmdGetOutOfBandProgress(IpcMessage msg)
|
||||
{
|
||||
var callId = msg.PayloadJson != null
|
||||
? JsonSerializer.Deserialize<JsonElement[]>(msg.PayloadJson)?[0].GetString()
|
||||
: null;
|
||||
|
||||
if (callId == null)
|
||||
throw new Exception("GetOutOfBandProgress requires a callId");
|
||||
|
||||
lock (_installLock)
|
||||
{
|
||||
if (_installProgress.TryGetValue(callId, out var prog)) return prog;
|
||||
}
|
||||
|
||||
return new OutOfBandProgress
|
||||
{
|
||||
PercentComplete = 0,
|
||||
Status = "Unknown call ID",
|
||||
IsCompleted = true
|
||||
};
|
||||
}
|
||||
|
||||
private object CmdShutdown(IpcMessage msg)
|
||||
{
|
||||
bool doRestart = false;
|
||||
if (msg.PayloadJson != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parms = JsonSerializer.Deserialize<JsonElement[]>(msg.PayloadJson);
|
||||
if (parms?.Length > 0 && parms[0].ValueKind == JsonValueKind.True)
|
||||
doRestart = true;
|
||||
}
|
||||
catch { /* default to shutdown */ }
|
||||
}
|
||||
|
||||
CmdKillAllApps();
|
||||
var flag = doRestart ? "/r" : "/s";
|
||||
new Thread(() =>
|
||||
{
|
||||
Thread.Sleep(2000);
|
||||
Process.Start("shutdown", $"{flag} /t 0");
|
||||
}) { IsBackground = true }.Start();
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private LaunchData FindApp(string launchKey) =>
|
||||
_launchApps.FirstOrDefault(a =>
|
||||
string.Equals(a.LaunchKey, launchKey, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private void StartWatcher(LaunchData app, Process process)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
process.WaitForExit();
|
||||
if (_stopping) return;
|
||||
|
||||
bool stillTracked;
|
||||
lock (_processLock)
|
||||
{
|
||||
stillTracked = _runningProcesses.TryGetValue(
|
||||
app.LaunchKey, out var procs) && procs.Contains(process);
|
||||
}
|
||||
|
||||
if (!stillTracked) return;
|
||||
|
||||
Thread.Sleep(2000);
|
||||
try { CmdLaunchApp(new IpcMessage { LaunchKey = app.LaunchKey }); }
|
||||
catch { }
|
||||
})
|
||||
{ IsBackground = true, Name = $"Watcher-{app.LaunchKey}" }.Start();
|
||||
}
|
||||
|
||||
// ── 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 CoreAudio roundtrip quantization error.
|
||||
|
||||
private float _cachedVolumeScalar = 1.0f;
|
||||
|
||||
private float GetMasterVolume()
|
||||
{
|
||||
return _cachedVolumeScalar;
|
||||
}
|
||||
|
||||
private void SetMasterVolume(float scalar)
|
||||
{
|
||||
_cachedVolumeScalar = scalar;
|
||||
|
||||
// Try nircmd.exe first (legacy compatibility)
|
||||
var nircmd = Path.Combine(GAMES_DIR, "nircmd.exe");
|
||||
if (File.Exists(nircmd))
|
||||
{
|
||||
Process.Start(nircmd, $"setsysvolume {(int)(scalar * 65535)}")?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows Core Audio API fallback (Vista+, no external dependencies)
|
||||
CoreAudio.SetMasterScalar(scalar);
|
||||
}
|
||||
|
||||
// ── Response helpers ──────────────────────────────────────────────────
|
||||
|
||||
private static IpcResponse Ok(object data = null) => new() { Success = true, Data = data };
|
||||
private static IpcResponse Fail(string msg) => new() { Success = false, Message = msg };
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
Visible = false;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) { _trayIcon?.Dispose(); _trayMenu?.Dispose(); }
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Agent-local data types ────────────────────────────────────────────────
|
||||
// These are used internally by the Agent (XML config, process management).
|
||||
// They are NOT the BinaryFormatter wire types (Tesla.Net namespace).
|
||||
|
||||
/// <summary>
|
||||
/// Agent-internal representation of a launchable app.
|
||||
/// Loaded from LaunchApps.xml via XmlSerializer.
|
||||
/// </summary>
|
||||
[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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent-internal tracking of a running process.
|
||||
/// Returned via JSON IPC (string LaunchKey, not Guid).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class LaunchedAppData
|
||||
{
|
||||
public string LaunchKey { get; set; }
|
||||
public int ProcessId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent-internal full state snapshot.
|
||||
/// Returned via JSON IPC for the FullUpdate command.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FullUpdateData
|
||||
{
|
||||
public LaunchData[] InstalledApps { get; set; }
|
||||
public LaunchedAppData[] LaunchedApps { get; set; }
|
||||
public float VolumeLevel { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OutOfBandProgress
|
||||
{
|
||||
public int PercentComplete { get; set; }
|
||||
public string Status { get; set; }
|
||||
public bool IsCompleted { 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.
|
||||
|
||||
internal static class CoreAudio
|
||||
{
|
||||
internal static float GetMasterScalar()
|
||||
{
|
||||
var ep = GetEndpointVolume();
|
||||
try { ep.GetMasterVolumeLevelScalar(out float v); return v; }
|
||||
finally { ReleaseAll(ep); }
|
||||
}
|
||||
|
||||
internal static void SetMasterScalar(float scalar)
|
||||
{
|
||||
var ep = GetEndpointVolume();
|
||||
try { var ctx = Guid.Empty; ep.SetMasterVolumeLevelScalar(scalar, ref ctx); }
|
||||
finally { ReleaseAll(ep); }
|
||||
}
|
||||
|
||||
private static IAudioEndpointVolume GetEndpointVolume()
|
||||
{
|
||||
var enumerator = (IMMDeviceEnumerator)new MMAudioEnumeratorComClass();
|
||||
try
|
||||
{
|
||||
enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out var device);
|
||||
try
|
||||
{
|
||||
var iid = typeof(IAudioEndpointVolume).GUID;
|
||||
device.Activate(ref iid, 23 /*CLSCTX_ALL*/, IntPtr.Zero, out var obj);
|
||||
return (IAudioEndpointVolume)obj;
|
||||
}
|
||||
finally { Marshal.ReleaseComObject(device); }
|
||||
}
|
||||
finally { Marshal.ReleaseComObject(enumerator); }
|
||||
}
|
||||
|
||||
private static void ReleaseAll(object obj)
|
||||
{
|
||||
if (obj != null) Marshal.ReleaseComObject(obj);
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<WarningsAsErrors></WarningsAsErrors>
|
||||
<Version>4.11.4.1</Version>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>TeslaLauncherAgent</AssemblyName>
|
||||
<RootNamespace>Tesla.Launcher.Agent</RootNamespace>
|
||||
<StartupObject>Tesla.Launcher.Agent.AgentApplication</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Exclude service-only source files — they belong to TeslaLauncherService.csproj -->
|
||||
<Compile Remove="TeslaLauncherService.cs" />
|
||||
<Compile Remove="SecureConfig.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<!-- Agent parses configuring.json + the IPC JSON; built-in on net6, a package on net48. -->
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<WarningsAsErrors></WarningsAsErrors>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>4.11.4.1</Version>
|
||||
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||
<AssemblyName>TeslaLauncherService</AssemblyName>
|
||||
<RootNamespace>Tesla.Launcher.Service</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Exclude the Agent source — it belongs to TeslaLauncherAgent.csproj only -->
|
||||
<Compile Remove="TeslaLauncherAgent.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- net48 reference assemblies + GAC refs the framework-dependent build needs. -->
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Generic host on .NET Framework: Microsoft.Extensions.Hosting(.WindowsServices)
|
||||
8.x ship a net462 target, so they run on net48 unchanged. -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="8.*" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<!-- COM2 / PlasmaIO serial output in SecureConfig.cs -->
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Contract\Tesla.Contract.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+32
-58
@@ -1,55 +1,52 @@
|
||||
@echo off
|
||||
:: =============================================================================
|
||||
:: TeslaLauncher — Unified Build / Package Script
|
||||
:: TeslaLauncher - Unified Build / Package Script (XP11 single binary)
|
||||
:: =============================================================================
|
||||
:: Publishes TeslaLauncherService.exe and TeslaLauncherAgent.exe as
|
||||
:: framework-dependent net48 builds and assembles the installable TeslaLauncher\
|
||||
:: package next to install.bat. The target pod needs .NET Framework 4.8 (built
|
||||
:: into Windows 10/11) — there is no bundled runtime, so the package is small.
|
||||
:: Publishes TeslaLauncher.exe as a framework-dependent net40 build and
|
||||
:: assembles the installable TeslaLauncher\ package next to install.bat.
|
||||
::
|
||||
:: net40 is deliberate: it is the newest .NET Framework that installs on
|
||||
:: Windows XP SP3, and net40 assemblies run in-place on the 4.8 runtime built
|
||||
:: into Windows 10/11 - so this ONE exe covers XP SP3 through Windows 11.
|
||||
:: - XP SP3 pods additionally need the .NET Framework 4.0 redistributable
|
||||
:: (dotNetFx40_Full_x86_x64.exe); drop it into assets\dotnet40\ to have
|
||||
:: install.bat run it automatically when missing.
|
||||
:: - Windows 10/11 pods need nothing extra.
|
||||
::
|
||||
:: Requirements:
|
||||
:: .NET SDK (6.0+) to drive the build https://dotnet.microsoft.com/download
|
||||
:: Internet access for NuGet restore (first build only)
|
||||
::
|
||||
:: Usage:
|
||||
:: build.bat - build both components + package
|
||||
:: build.bat /service - build Service only
|
||||
:: build.bat /agent - build Agent only
|
||||
:: build.bat - build + package
|
||||
:: build.bat /q - no pause on exit
|
||||
::
|
||||
:: Output (under dist\, parity with Console\dist\):
|
||||
:: dist\TeslaLauncher\
|
||||
:: install.bat
|
||||
:: Service\TeslaLauncherService.exe
|
||||
:: Agent\TeslaLauncherAgent.exe
|
||||
:: dx9201006\ openal\ UltraVNC\ (redist, mirrored from assets\)
|
||||
:: dist\TeslaLauncher-podpkg.zip (the deployable package)
|
||||
:: App\TeslaLauncher.exe (+ Newtonsoft.Json.dll, TeslaConsoleLaunchLib.dll)
|
||||
:: dx9201006\ openal\ UltraVNC\ dotnet40\ (redist, mirrored from assets\)
|
||||
:: dist\TeslaLauncher-podpkg.zip (the deployable package)
|
||||
::
|
||||
:: NOTE: the projects reference ..\Contract\Tesla.Contract.csproj, so they are
|
||||
:: published IN PLACE (not staged into a temp folder) — the project reference
|
||||
:: NOTE: the project references ..\Contract\Tesla.Contract.csproj, so it is
|
||||
:: published IN PLACE (not staged into a temp folder) - the project reference
|
||||
:: must be able to resolve relative to this directory.
|
||||
:: =============================================================================
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set ROOT=%~dp0
|
||||
:: Package under dist\ (parity with Console\dist\).
|
||||
set BUILD_DIR=%ROOT%dist\TeslaLauncher
|
||||
set ZIP=%ROOT%dist\TeslaLauncher-podpkg.zip
|
||||
|
||||
:: -- Parse arguments ----------------------------------------------------------
|
||||
set BUILD_SERVICE=1
|
||||
set BUILD_AGENT=1
|
||||
set QUIET=0
|
||||
|
||||
for %%a in (%*) do (
|
||||
if /i "%%~a"=="/service" set BUILD_AGENT=0
|
||||
if /i "%%~a"=="/agent" set BUILD_SERVICE=0
|
||||
if /i "%%~a"=="/q" set QUIET=1
|
||||
if /i "%%~a"=="/q" set QUIET=1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo Tesla Launcher v4.11.4.1 - Build ^& Package (net48, framework-dependent)
|
||||
echo Tesla Launcher v4.11.4.1 - Build ^& Package (net40 single binary)
|
||||
echo Output : %BUILD_DIR%
|
||||
echo ============================================================
|
||||
echo.
|
||||
@@ -68,60 +65,38 @@ echo SDK : %SDK_VER%
|
||||
echo.
|
||||
|
||||
:: -- Clean prior package output ----------------------------------------------
|
||||
if exist "%BUILD_DIR%\App" rmdir /s /q "%BUILD_DIR%\App"
|
||||
if exist "%BUILD_DIR%\Service" rmdir /s /q "%BUILD_DIR%\Service"
|
||||
if exist "%BUILD_DIR%\Agent" rmdir /s /q "%BUILD_DIR%\Agent"
|
||||
if exist "%BUILD_DIR%\dx9201006" rmdir /s /q "%BUILD_DIR%\dx9201006"
|
||||
if exist "%BUILD_DIR%\openal" rmdir /s /q "%BUILD_DIR%\openal"
|
||||
if exist "%BUILD_DIR%\UltraVNC" rmdir /s /q "%BUILD_DIR%\UltraVNC"
|
||||
if exist "%BUILD_DIR%\dotnet40" rmdir /s /q "%BUILD_DIR%\dotnet40"
|
||||
|
||||
:: -- Build Service ------------------------------------------------------------
|
||||
if %BUILD_SERVICE%==0 goto :skip_service
|
||||
|
||||
echo [1/2] Publishing TeslaLauncherService (Session 0 Windows Service)...
|
||||
:: -- Build --------------------------------------------------------------------
|
||||
echo [1/1] Publishing TeslaLauncher (net40 single binary)...
|
||||
echo.
|
||||
dotnet publish "%ROOT%TeslaLauncherService.csproj" ^
|
||||
dotnet publish "%ROOT%TeslaLauncher.csproj" ^
|
||||
-c Release ^
|
||||
-o "%BUILD_DIR%\Service"
|
||||
-o "%BUILD_DIR%\App"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERROR: Service build failed.
|
||||
echo ERROR: build failed.
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo Service : %BUILD_DIR%\Service\TeslaLauncherService.exe
|
||||
echo Launcher : %BUILD_DIR%\App\TeslaLauncher.exe
|
||||
echo.
|
||||
|
||||
:skip_service
|
||||
|
||||
:: -- Build Agent --------------------------------------------------------------
|
||||
if %BUILD_AGENT%==0 goto :skip_agent
|
||||
|
||||
if %BUILD_SERVICE%==1 (echo [2/2] Publishing TeslaLauncherAgent...) else (echo [1/1] Publishing TeslaLauncherAgent...)
|
||||
echo.
|
||||
dotnet publish "%ROOT%TeslaLauncherAgent.csproj" ^
|
||||
-c Release ^
|
||||
"-p:DefineConstants=WINFORMS" ^
|
||||
-o "%BUILD_DIR%\Agent"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo ERROR: Agent build failed.
|
||||
if "%QUIET%"=="0" pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo Agent : %BUILD_DIR%\Agent\TeslaLauncherAgent.exe
|
||||
echo.
|
||||
|
||||
:skip_agent
|
||||
|
||||
:: -- Copy shared assets into the package --------------------------------------
|
||||
:: Mirror each redist folder under assets\ into the package root so install.bat's
|
||||
:: paths (%ROOT%\dx9201006\, %ROOT%\openal\, %ROOT%\UltraVNC\) resolve.
|
||||
:: paths (%ROOT%\dx9201006\ etc.) resolve.
|
||||
if exist "%ROOT%install.bat" copy /y "%ROOT%install.bat" "%BUILD_DIR%\" >nul
|
||||
if exist "%ROOT%assets\dx9201006" xcopy /y /s /i /q "%ROOT%assets\dx9201006" "%BUILD_DIR%\dx9201006" >nul
|
||||
if exist "%ROOT%assets\openal" xcopy /y /s /i /q "%ROOT%assets\openal" "%BUILD_DIR%\openal" >nul
|
||||
if exist "%ROOT%assets\UltraVNC" xcopy /y /s /i /q "%ROOT%assets\UltraVNC" "%BUILD_DIR%\UltraVNC" >nul
|
||||
if exist "%ROOT%assets\dotnet40" xcopy /y /s /i /q "%ROOT%assets\dotnet40" "%BUILD_DIR%\dotnet40" >nul
|
||||
|
||||
:: -- Zip the package ----------------------------------------------------------
|
||||
echo Zipping package...
|
||||
@@ -133,14 +108,13 @@ echo ============================================================
|
||||
echo Build complete
|
||||
echo ============================================================
|
||||
echo.
|
||||
if %BUILD_SERVICE%==1 echo Service : %BUILD_DIR%\Service\TeslaLauncherService.exe
|
||||
if %BUILD_AGENT%==1 echo Agent : %BUILD_DIR%\Agent\TeslaLauncherAgent.exe
|
||||
echo Launcher : %BUILD_DIR%\App\TeslaLauncher.exe
|
||||
echo.
|
||||
echo Package : %BUILD_DIR%
|
||||
echo Zip : %ZIP%
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Copy TeslaLauncher-podpkg.zip to the pod (or stand-in) PC and extract it
|
||||
echo 1. Copy TeslaLauncher-podpkg.zip to the pod (XP SP3 or Win10/11) and extract
|
||||
echo 2. Run TeslaLauncher\install.bat as Administrator
|
||||
echo.
|
||||
if "%QUIET%"=="0" pause
|
||||
|
||||
+245
-242
@@ -1,40 +1,53 @@
|
||||
@echo off
|
||||
:: =============================================================================
|
||||
:: TeslaLauncher Modern — Installation Script
|
||||
:: TeslaLauncher Modern - Installation Script (XP11 single binary, dual-OS)
|
||||
:: =============================================================================
|
||||
:: Run as Administrator on each cockpit PC.
|
||||
:: Run as Administrator on each cockpit PC. Works on Windows XP SP3 and on
|
||||
:: Windows 10/11 - one binary, one installer, two OS code paths where the
|
||||
:: tooling differs (cacls/icacls, netsh firewall/advfirewall, no dism on XP...).
|
||||
:: Must be run from the TeslaLauncher\ folder produced by build.bat:
|
||||
::
|
||||
:: TeslaLauncher\
|
||||
:: install.bat <- this file
|
||||
:: Service\TeslaLauncherService.exe
|
||||
:: Agent\TeslaLauncherAgent.exe
|
||||
:: App\TeslaLauncher.exe (net40 single binary)
|
||||
:: dx9201006\DXSETUP.exe (DirectX June 2010 redist)
|
||||
:: openal\oalinst.exe (OpenAL redist)
|
||||
:: UltraVNC\UltraVNC_x64_Setup.exe (UltraVNC server + UltraVNC.inf)
|
||||
:: UltraVNC\UltraVNC_x64_Setup.exe (UltraVNC server, Win10/11)
|
||||
:: UltraVNC\UltraVNC_x86_Setup.exe (optional: UltraVNC for XP)
|
||||
:: dotnet40\dotNetFx40_Full_x86_x64.exe (optional: .NET 4.0 for XP)
|
||||
:: =============================================================================
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ============================================================
|
||||
echo Tesla Launcher v4.11.4.1 - Installation
|
||||
echo Tesla Launcher v4.11.4.1 - Installation (single binary)
|
||||
echo ============================================================
|
||||
echo.
|
||||
|
||||
:: ── Paths ─────────────────────────────────────────────────────────────────────
|
||||
:: -- OS detection --------------------------------------------------------------
|
||||
:: NT 5.x = XP / Server 2003 era. Everything else is treated as modern Windows.
|
||||
set ISXP=0
|
||||
ver | findstr /r /c:"Version 5\." >nul && set ISXP=1
|
||||
if "%ISXP%"=="1" (echo OS : Windows XP era ^(NT 5.x^)) else (echo OS : Windows Vista or later)
|
||||
echo.
|
||||
|
||||
:: -- Paths ----------------------------------------------------------------------
|
||||
set ROOT=%~dp0
|
||||
set SERVICE_SRC=%ROOT%Service
|
||||
set AGENT_SRC=%ROOT%Agent
|
||||
set INSTALL_DIR=C:\Program Files\TeslaLauncher
|
||||
set DATA_DIR=C:\ProgramData\TeslaLauncher
|
||||
set SERVICE_EXE=%INSTALL_DIR%\TeslaLauncherService.exe
|
||||
set AGENT_EXE=%INSTALL_DIR%\TeslaLauncherAgent.exe
|
||||
set SERVICE_NAME=Tesla Application Launcher
|
||||
set SERVICE_DISPLAY=Tesla Application Launcher
|
||||
set APP_SRC=%ROOT%App
|
||||
set INSTALL_DIR=%ProgramFiles%\TeslaLauncher
|
||||
set LAUNCHER_EXE=%INSTALL_DIR%\TeslaLauncher.exe
|
||||
set CONSOLE_PORT=53290
|
||||
|
||||
:: CommonApplicationData differs by OS. The launcher resolves it via
|
||||
:: Environment.GetFolderPath; these must match what it computes.
|
||||
if "%ISXP%"=="1" (
|
||||
set DATA_DIR=%ALLUSERSPROFILE%\Application Data\TeslaLauncher
|
||||
) else (
|
||||
set DATA_DIR=%ALLUSERSPROFILE%\TeslaLauncher
|
||||
)
|
||||
|
||||
:: Auto-login account for the cockpit (kiosk). Plain-text password in the
|
||||
:: registry is by design here — closed network, single-purpose PC.
|
||||
:: registry is by design here - closed network, single-purpose PC.
|
||||
set AUTOLOGIN_USER=Firestorm
|
||||
set AUTOLOGIN_PASS=thor6
|
||||
|
||||
@@ -42,139 +55,161 @@ set AUTOLOGIN_PASS=thor6
|
||||
:: the mw4files / c shares browse cleanly. Change once per site if needed.
|
||||
set WORKGROUP=Tesla
|
||||
|
||||
:: ── Admin check ───────────────────────────────────────────────────────────────
|
||||
:: -- Admin check -----------------------------------------------------------------
|
||||
net session >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: This script must be run as Administrator.
|
||||
pause & exit /b 1
|
||||
)
|
||||
|
||||
:: ── Verify source files ───────────────────────────────────────────────────────
|
||||
if not exist "%SERVICE_SRC%\TeslaLauncherService.exe" (
|
||||
echo ERROR: TeslaLauncherService.exe not found in %SERVICE_SRC%
|
||||
echo Run build.bat first, then run install.bat from the TeslaLauncher\ folder.
|
||||
pause & exit /b 1
|
||||
)
|
||||
if not exist "%AGENT_SRC%\TeslaLauncherAgent.exe" (
|
||||
echo ERROR: TeslaLauncherAgent.exe not found in %AGENT_SRC%
|
||||
:: -- Verify source files ---------------------------------------------------------
|
||||
if not exist "%APP_SRC%\TeslaLauncher.exe" (
|
||||
echo ERROR: TeslaLauncher.exe not found in %APP_SRC%
|
||||
echo Run build.bat first, then run install.bat from the TeslaLauncher\ folder.
|
||||
pause & exit /b 1
|
||||
)
|
||||
|
||||
:: ── Detect and clean up existing installation ─────────────────────────────────
|
||||
set EXISTING=0
|
||||
if exist "%SERVICE_EXE%" set EXISTING=1
|
||||
if exist "%AGENT_EXE%" set EXISTING=1
|
||||
|
||||
if "%EXISTING%"=="1" (
|
||||
echo Existing installation detected. Cleaning up...
|
||||
echo.
|
||||
|
||||
:: Stop and kill the Agent process
|
||||
taskkill /F /IM TeslaLauncherAgent.exe >nul 2>&1
|
||||
echo Agent process stopped.
|
||||
|
||||
:: Stop and remove the service, wait for it to fully stop
|
||||
sc stop "%SERVICE_NAME%" >nul 2>&1
|
||||
sc stop "Tesla Application Launcher" >nul 2>&1
|
||||
timeout /t 3 /nobreak >nul
|
||||
sc delete "%SERVICE_NAME%" >nul 2>&1
|
||||
sc delete "Tesla Application Launcher" >nul 2>&1
|
||||
timeout /t 2 /nobreak >nul
|
||||
echo Service stopped and removed.
|
||||
|
||||
:: Delete old executables, logs, and session key so SecureConfig re-runs
|
||||
del /F /Q "%SERVICE_EXE%" >nul 2>&1
|
||||
del /F /Q "%AGENT_EXE%" >nul 2>&1
|
||||
del /F /Q "%INSTALL_DIR%\podconf.log" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\TeslaKeyStore.key" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\LaunchApps.xml" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\configuring.json" >nul 2>&1
|
||||
echo Old executables, logs, session key, and app config deleted.
|
||||
|
||||
:: Reset all physical Ethernet adapters to DHCP so the SecureConfig
|
||||
:: protocol triggers correctly on the next boot.
|
||||
:: We must REMOVE the static IP address entry first, then enable DHCP.
|
||||
:: Set-NetIPInterface -Dhcp Enabled alone only flips the DHCP flag but
|
||||
:: leaves the static address in the registry, so IsMachineConfigured()
|
||||
:: still sees a non-DHCP adapter on the next boot and skips SecureConfig.
|
||||
echo Resetting network adapters to DHCP...
|
||||
powershell -NoProfile -Command "Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | ForEach-Object { $n=$_.Name; Get-NetIPAddress -InterfaceAlias $n -AddressFamily IPv4 -EA 0 | Where-Object {$_.PrefixOrigin -ne 'WellKnown' -and $_.PrefixOrigin -ne 'Dhcp'} | Remove-NetIPAddress -Confirm:$false -EA 0; Set-NetIPInterface -InterfaceAlias $n -Dhcp Enabled -EA 0 }; Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | Set-DnsClientServerAddress -ResetServerAddresses" >nul 2>&1
|
||||
echo Network adapters reset to DHCP.
|
||||
echo.
|
||||
:: -- .NET Framework 4.0 check (XP ships no .NET; Win10/11 has 4.8 built in) ------
|
||||
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" /v Install >nul 2>&1
|
||||
if not errorlevel 1 goto :dotnet_ok
|
||||
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Client" /v Install >nul 2>&1
|
||||
if not errorlevel 1 goto :dotnet_ok
|
||||
if exist "%ROOT%dotnet40\dotNetFx40_Full_x86_x64.exe" (
|
||||
echo .NET Framework 4.0 not found - installing it now ^(takes a few minutes^)...
|
||||
"%ROOT%dotnet40\dotNetFx40_Full_x86_x64.exe" /q /norestart
|
||||
echo .NET Framework 4.0 installed.
|
||||
) else (
|
||||
:: No existing install — still remove any stale service registration
|
||||
sc stop "%SERVICE_NAME%" >nul 2>&1
|
||||
sc stop "Tesla Application Launcher" >nul 2>&1
|
||||
sc delete "%SERVICE_NAME%" >nul 2>&1
|
||||
sc delete "Tesla Application Launcher" >nul 2>&1
|
||||
timeout /t 2 /nobreak >nul
|
||||
echo ERROR: .NET Framework 4.0 is not installed and dotnet40\ redist is not
|
||||
echo in this package. Install dotNetFx40_Full_x86_x64.exe first.
|
||||
pause & exit /b 1
|
||||
)
|
||||
:dotnet_ok
|
||||
|
||||
:: ── STEP 1: Create directories ────────────────────────────────────────────────
|
||||
echo [1/8] Creating directories...
|
||||
:: -- Detect and clean up existing installation -----------------------------------
|
||||
echo Cleaning up any existing installation...
|
||||
|
||||
:: Stop launcher / legacy agent processes
|
||||
taskkill /F /IM TeslaLauncher.exe >nul 2>&1
|
||||
taskkill /F /IM TeslaLauncherAgent.exe >nul 2>&1
|
||||
|
||||
:: Remove the legacy two-process service registration (pre-XP11 installs)
|
||||
sc stop "Tesla Application Launcher" >nul 2>&1
|
||||
ping -n 4 127.0.0.1 >nul
|
||||
sc delete "Tesla Application Launcher" >nul 2>&1
|
||||
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v TeslaLauncherAgent /f >nul 2>&1
|
||||
|
||||
:: Delete old executables, logs, and session key so SecureConfig re-runs
|
||||
if exist "%LAUNCHER_EXE%" (
|
||||
del /F /Q "%INSTALL_DIR%\*.exe" >nul 2>&1
|
||||
del /F /Q "%INSTALL_DIR%\*.dll" >nul 2>&1
|
||||
del /F /Q "%INSTALL_DIR%\podconf.log" >nul 2>&1
|
||||
)
|
||||
del /F /Q "%INSTALL_DIR%\TeslaLauncherService.exe" >nul 2>&1
|
||||
del /F /Q "%INSTALL_DIR%\TeslaLauncherAgent.exe" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\TeslaKeyStore.key" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\LaunchApps.xml" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\configuring.json" >nul 2>&1
|
||||
del /F /Q "%DATA_DIR%\podconf.log" >nul 2>&1
|
||||
echo Old executables, logs, session key, and app config deleted.
|
||||
|
||||
:: Reset all Ethernet adapters to DHCP so the SecureConfig protocol triggers
|
||||
:: on the next boot. The static address entry must be REMOVED, not just the
|
||||
:: DHCP flag flipped, or IsMachineConfigured() still sees a static adapter.
|
||||
echo Resetting network adapters to DHCP...
|
||||
if "%ISXP%"=="1" (
|
||||
rem XP: netsh "interface ip set address <name> dhcp" clears the static entry.
|
||||
rem Enumerate interface names from "netsh interface show interface" (col 4+).
|
||||
for /f "skip=3 tokens=4*" %%i in ('netsh interface show interface') do (
|
||||
if "%%j"=="" (
|
||||
netsh interface ip set address "%%i" dhcp >nul 2>&1
|
||||
netsh interface ip set dns "%%i" dhcp >nul 2>&1
|
||||
) else (
|
||||
netsh interface ip set address "%%i %%j" dhcp >nul 2>&1
|
||||
netsh interface ip set dns "%%i %%j" dhcp >nul 2>&1
|
||||
)
|
||||
)
|
||||
) else (
|
||||
powershell -NoProfile -Command "Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | ForEach-Object { $n=$_.Name; Get-NetIPAddress -InterfaceAlias $n -AddressFamily IPv4 -EA 0 | Where-Object {$_.PrefixOrigin -ne 'WellKnown' -and $_.PrefixOrigin -ne 'Dhcp'} | Remove-NetIPAddress -Confirm:$false -EA 0; Set-NetIPInterface -InterfaceAlias $n -Dhcp Enabled -EA 0 }; Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | Set-DnsClientServerAddress -ResetServerAddresses" >nul 2>&1
|
||||
)
|
||||
echo Network adapters reset to DHCP.
|
||||
echo.
|
||||
|
||||
:: -- STEP 1: Create directories ---------------------------------------------------
|
||||
echo [1/7] Creating directories...
|
||||
if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%"
|
||||
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
|
||||
if not exist "C:\Games" mkdir "C:\Games"
|
||||
|
||||
:: Grant all users modify access to the data and games directories so
|
||||
:: the Service and Agent can write files (LaunchApps.xml, game installs).
|
||||
icacls "%DATA_DIR%" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
||||
icacls "C:\Games" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
||||
:: Grant Users modify access to the data and games directories so the launcher
|
||||
:: can write files (LaunchApps.xml, session key, game installs) from any account.
|
||||
if "%ISXP%"=="1" (
|
||||
cacls "%DATA_DIR%" /T /E /G Users:C >nul 2>&1
|
||||
cacls "C:\Games" /T /E /G Users:C >nul 2>&1
|
||||
) else (
|
||||
icacls "%DATA_DIR%" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
||||
icacls "C:\Games" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
||||
)
|
||||
echo %INSTALL_DIR%
|
||||
echo %DATA_DIR% (Users: modify access)
|
||||
echo C:\Games (Users: modify access)
|
||||
|
||||
:: ── STEP 2: Copy files ────────────────────────────────────────────────────────
|
||||
echo [2/8] Copying files...
|
||||
:: net48 framework-dependent build: each folder holds the exe + its dependency
|
||||
:: DLLs + .config. Copy both folders into the install dir (shared DLLs overwrite
|
||||
:: identically; the two .exe.config files coexist).
|
||||
copy /Y "%SERVICE_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
||||
copy /Y "%AGENT_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
||||
:: -- STEP 2: Copy files ------------------------------------------------------------
|
||||
echo [2/7] Copying files...
|
||||
:: net40 framework-dependent build: the folder holds the exe + its dependency
|
||||
:: DLLs (Newtonsoft.Json, TeslaConsoleLaunchLib) + .config.
|
||||
copy /Y "%APP_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
||||
|
||||
:: Install DirectX June 2010 runtime (required by games)
|
||||
if exist "%ROOT%\dx9201006\DXSETUP.exe" (
|
||||
:: Install DirectX June 2010 runtime (required by games; supports XP and Win10/11)
|
||||
if exist "%ROOT%dx9201006\DXSETUP.exe" (
|
||||
echo Installing DirectX runtime...
|
||||
"%ROOT%\dx9201006\DXSETUP.exe" /silent
|
||||
"%ROOT%dx9201006\DXSETUP.exe" /silent
|
||||
echo DirectX installed.
|
||||
)
|
||||
|
||||
:: Install OpenAL runtime (required by game audio)
|
||||
if exist "%ROOT%\openal\oalinst.exe" (
|
||||
if exist "%ROOT%openal\oalinst.exe" (
|
||||
echo Installing OpenAL runtime...
|
||||
"%ROOT%\openal\oalinst.exe" /s
|
||||
"%ROOT%openal\oalinst.exe" /s
|
||||
echo OpenAL installed.
|
||||
)
|
||||
|
||||
:: Install UltraVNC server (remote diagnostics). The Inno installer lays down
|
||||
:: the files and (via /loadinf) sets the install dir + VNC password; we then
|
||||
:: register and start the service EXPLICITLY with winvnc.exe. Doing the service
|
||||
:: step ourselves means a missing/incomplete "install service" task in the
|
||||
:: answer file can't leave the pod with the files present but nothing listening.
|
||||
set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x64_Setup.exe
|
||||
set UVNC_DIR=C:\Program Files\uvnc bvba\UltraVNC
|
||||
if exist "%UVNC_SETUP%" (
|
||||
:: Install UltraVNC server (remote diagnostics). x64 build for modern Windows;
|
||||
:: XP uses the x86 build when the package carries one.
|
||||
set UVNC_SETUP=
|
||||
if "%ISXP%"=="1" (
|
||||
if exist "%ROOT%UltraVNC\UltraVNC_x86_Setup.exe" set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x86_Setup.exe
|
||||
) else (
|
||||
if exist "%ROOT%UltraVNC\UltraVNC_x64_Setup.exe" set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x64_Setup.exe
|
||||
)
|
||||
if defined UVNC_SETUP (
|
||||
echo Installing UltraVNC server...
|
||||
start "" /wait "%UVNC_SETUP%" /verysilent /norestart /loadinf="%ROOT%UltraVNC\UltraVNC.inf"
|
||||
)
|
||||
) else (
|
||||
if "%ISXP%"=="1" echo UltraVNC x86 setup not in package - skipped on XP.
|
||||
)
|
||||
|
||||
:: Enable SMB1 client AND server (required by game networking). The pod both
|
||||
:: reaches SMB1 shares (client) and hosts the mw4files / c shares below (server),
|
||||
:: and Win10/11 ship both SMB1 sub-features off by default — so enable all three
|
||||
:: (umbrella + client + server); each child pulls in the umbrella via /All.
|
||||
echo Enabling SMB1 file sharing (client + server)...
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol /All /NoRestart >nul 2>&1
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Client /All /NoRestart >nul 2>&1
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Server /All /NoRestart >nul 2>&1
|
||||
echo SMB1 client + server enabled (reboot required to take effect).
|
||||
:: SMB1 + DirectPlay: native on XP; must be re-enabled on Win10/11.
|
||||
if "%ISXP%"=="0" (
|
||||
echo Enabling SMB1 file sharing ^(client + server^)...
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol /All /NoRestart >nul 2>&1
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Client /All /NoRestart >nul 2>&1
|
||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Server /All /NoRestart >nul 2>&1
|
||||
echo SMB1 client + server enabled ^(reboot required to take effect^).
|
||||
echo Enabling DirectPlay...
|
||||
dism /Online /Enable-Feature /FeatureName:DirectPlay /All /NoRestart >nul 2>&1
|
||||
echo DirectPlay enabled.
|
||||
) else (
|
||||
echo SMB1 and DirectPlay are native on XP - nothing to enable.
|
||||
)
|
||||
|
||||
:: Create and share game-data folders (closed network — open to Everyone).
|
||||
:: Create and share game-data folders (closed network - open to Everyone).
|
||||
echo Creating network shares...
|
||||
if not exist "C:\mw4files" mkdir "C:\mw4files"
|
||||
:: Grant Everyone modify on the NTFS side so the share grant is effective.
|
||||
icacls "C:\mw4files" /grant *S-1-1-0:(OI)(CI)M /T >nul 2>&1
|
||||
:: Recreate shares idempotently (net share fails if the name already exists).
|
||||
if "%ISXP%"=="1" (
|
||||
cacls "C:\mw4files" /T /E /G Everyone:C >nul 2>&1
|
||||
) else (
|
||||
icacls "C:\mw4files" /grant *S-1-1-0:(OI)(CI)M /T >nul 2>&1
|
||||
)
|
||||
net share mw4files /delete >nul 2>&1
|
||||
net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1
|
||||
echo "Share \\%COMPUTERNAME%\mw4files -> C:\mw4files (Everyone: Full)"
|
||||
@@ -182,146 +217,114 @@ net share c /delete >nul 2>&1
|
||||
net share c=C:\ /grant:Everyone,FULL >nul 2>&1
|
||||
echo "Share \\%COMPUTERNAME%\c -> C:\ (Everyone: Full)"
|
||||
|
||||
:: Join the site workgroup so every pod shares one browse list. Add-Computer
|
||||
:: throws if the machine is already in that workgroup, so the error is ignored.
|
||||
:: Takes effect on the reboot at the end of this script (same as the hostname
|
||||
:: SecureConfig applies on first boot).
|
||||
:: Join the site workgroup so every pod shares one browse list.
|
||||
echo Joining workgroup "%WORKGROUP%"...
|
||||
powershell -NoProfile -Command "try { Add-Computer -WorkgroupName '%WORKGROUP%' -Force -ErrorAction Stop } catch { }" >nul 2>&1
|
||||
if "%ISXP%"=="1" (
|
||||
wmic computersystem where "name='%COMPUTERNAME%'" call joindomainorworkgroup name="%WORKGROUP%" >nul 2>&1
|
||||
) else (
|
||||
powershell -NoProfile -Command "try { Add-Computer -WorkgroupName '%WORKGROUP%' -Force -ErrorAction Stop } catch { }" >nul 2>&1
|
||||
)
|
||||
echo Workgroup set to "%WORKGROUP%" (effective after reboot).
|
||||
|
||||
:: Enable DirectPlay (required by older games)
|
||||
echo Enabling DirectPlay...
|
||||
dism /Online /Enable-Feature /FeatureName:DirectPlay /All /NoRestart >nul 2>&1
|
||||
echo DirectPlay enabled.
|
||||
echo Done.
|
||||
|
||||
:: ── STEP 3: Install Windows Service ──────────────────────────────────────────
|
||||
echo [3/8] Installing Windows Service...
|
||||
sc create "%SERVICE_NAME%" ^
|
||||
binPath= "\"%SERVICE_EXE%\"" ^
|
||||
DisplayName= "%SERVICE_DISPLAY%" ^
|
||||
start= delayed-auto ^
|
||||
obj= LocalSystem ^
|
||||
type= own
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: Failed to register Windows Service.
|
||||
pause & exit /b 1
|
||||
)
|
||||
|
||||
sc description "%SERVICE_NAME%" ^
|
||||
"Tesla Application Launcher - accepts TeslaConsole connections and controls simulation software"
|
||||
sc failure "%SERVICE_NAME%" ^
|
||||
reset= 86400 actions= restart/5000/restart/10000/restart/30000
|
||||
|
||||
:: Delay auto-start by 20 seconds to let the network stack settle
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Services\Tesla Application Launcher" ^
|
||||
/v AutoStartDelay /t REG_DWORD /d 20000 /f >nul
|
||||
|
||||
:: Set the global delayed-auto-start delay to 20 seconds (default is 120s)
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Control" ^
|
||||
/v AutoStartDelay /t REG_DWORD /d 20000 /f >nul
|
||||
|
||||
echo Service registered: "%SERVICE_NAME%" (20s per-service delay, 20s global delay)
|
||||
|
||||
:: ── STEP 4: Disable Windows Firewall (closed network) ────────────────────────
|
||||
echo.
|
||||
echo [4/8] Disabling Windows Firewall (closed network)...
|
||||
:: Disable firewall on all profiles — pods run on a closed network
|
||||
netsh advfirewall set allprofiles state off
|
||||
echo Windows Firewall disabled on all profiles.
|
||||
|
||||
:: ── STEP 5: Power settings (max performance, no sleep) ──────────────────────
|
||||
echo.
|
||||
echo [5/8] Configuring power settings...
|
||||
|
||||
:: Activate Ultimate Performance plan (hidden by default, GUID is well-known)
|
||||
powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 >nul 2>&1
|
||||
for /f "tokens=4" %%g in ('powercfg -list ^| findstr /i "Ultimate"') do (
|
||||
powercfg -setactive %%g
|
||||
)
|
||||
echo Ultimate Performance power plan activated.
|
||||
|
||||
:: Disable all sleep/hibernate/standby timeouts (AC power)
|
||||
powercfg -change -standby-timeout-ac 0
|
||||
powercfg -change -hibernate-timeout-ac 0
|
||||
powercfg -change -monitor-timeout-ac 0
|
||||
powercfg -change -disk-timeout-ac 0
|
||||
powercfg -h off >nul 2>&1
|
||||
echo Sleep, hibernate, monitor timeout, and disk timeout disabled.
|
||||
|
||||
:: ── STEP 6: Disable notifications ───────────────────────────────────────────
|
||||
echo.
|
||||
echo [6/8] Disabling notifications...
|
||||
:: These are machine-wide Group Policy keys (HKLM) so they apply to EVERY user,
|
||||
:: including the auto-login kiosk account. Per-user HKCU tweaks would only touch
|
||||
:: the admin running this installer, not the account the pod actually runs as.
|
||||
|
||||
:: Remove Action Center / Notification Center entirely
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" ^
|
||||
/v DisableNotificationCenter /t REG_DWORD /d 1 /f >nul
|
||||
|
||||
:: Kill all toast/app notifications (desktop and lock screen) and cloud toasts
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoToastApplicationNotification /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoToastApplicationNotificationOnLockScreen /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoCloudApplicationNotification /t REG_DWORD /d 1 /f >nul
|
||||
|
||||
:: Suppress tips, suggestions, Spotlight, and other consumer content pop-ups
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableSoftLanding /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableWindowsSpotlightFeatures /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableWindowsConsumerFeatures /t REG_DWORD /d 1 /f >nul
|
||||
|
||||
:: Silence Security & Maintenance (formerly Action Center) health notifications
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows Defender Security Center\Notifications" ^
|
||||
/v DisableNotifications /t REG_DWORD /d 1 /f >nul
|
||||
echo All Windows notifications disabled (machine-wide).
|
||||
|
||||
:: ── STEP 7: Disable UAC ─────────────────────────────────────────────────────
|
||||
echo.
|
||||
echo [7/8] Disabling UAC...
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v EnableLUA /t REG_DWORD /d 0 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul
|
||||
echo UAC disabled (takes effect after reboot).
|
||||
|
||||
:: ── STEP 8: Configure Agent auto-start and auto-login ────────────────────────
|
||||
echo.
|
||||
echo [8/8] Configuring Agent auto-start and auto-login...
|
||||
|
||||
:: -- STEP 3: Launcher auto-start (no Windows Service on XP11) ----------------------
|
||||
echo [3/7] Configuring launcher auto-start...
|
||||
:: The single binary runs in the auto-logged-in user session. Restart-after-crash
|
||||
:: comes from RegisterApplicationRestart (Vista+) inside the launcher itself.
|
||||
set AUTORUN_KEY=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
||||
reg add "%AUTORUN_KEY%" /v "TeslaLauncherAgent" /t REG_SZ ^
|
||||
/d "\"%AGENT_EXE%\"" /f >nul
|
||||
echo Added TeslaLauncherAgent to HKLM\...\Run.
|
||||
reg add "%AUTORUN_KEY%" /v "TeslaLauncher" /t REG_SZ ^
|
||||
/d "\"%LAUNCHER_EXE%\"" /f >nul
|
||||
echo Added TeslaLauncher to HKLM\...\Run (no service - single userland binary).
|
||||
|
||||
:: Auto-login: Winlogon reads these on every boot. AutoAdminLogon=1 tells
|
||||
:: Winlogon to sign the DefaultUserName in with DefaultPassword without a
|
||||
:: prompt. DefaultDomainName="." means the local machine account.
|
||||
:: -- STEP 4: Disable Windows Firewall (closed network) -----------------------------
|
||||
echo.
|
||||
echo [4/7] Disabling Windows Firewall (closed network)...
|
||||
if "%ISXP%"=="1" (
|
||||
netsh firewall set opmode mode=disable >nul 2>&1
|
||||
) else (
|
||||
netsh advfirewall set allprofiles state off >nul
|
||||
)
|
||||
echo Windows Firewall disabled.
|
||||
|
||||
:: -- STEP 5: Power settings (max performance, no sleep) ----------------------------
|
||||
echo.
|
||||
echo [5/7] Configuring power settings...
|
||||
if "%ISXP%"=="1" (
|
||||
powercfg /setactive "Always On" >nul 2>&1
|
||||
powercfg /change "Always On" /monitor-timeout-ac 0 >nul 2>&1
|
||||
powercfg /change "Always On" /disk-timeout-ac 0 >nul 2>&1
|
||||
powercfg /change "Always On" /standby-timeout-ac 0 >nul 2>&1
|
||||
powercfg /hibernate off >nul 2>&1
|
||||
echo "Always On" power scheme activated; sleep/hibernate disabled.
|
||||
) else (
|
||||
powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 >nul 2>&1
|
||||
for /f "tokens=4" %%g in ('powercfg -list ^| findstr /i "Ultimate"') do (
|
||||
powercfg -setactive %%g
|
||||
)
|
||||
powercfg -change -standby-timeout-ac 0
|
||||
powercfg -change -hibernate-timeout-ac 0
|
||||
powercfg -change -monitor-timeout-ac 0
|
||||
powercfg -change -disk-timeout-ac 0
|
||||
powercfg -h off >nul 2>&1
|
||||
echo Ultimate Performance plan activated; sleep/hibernate disabled.
|
||||
)
|
||||
|
||||
:: -- STEP 6: Quiet the OS (notifications / UAC - modern Windows only) --------------
|
||||
echo.
|
||||
echo [6/7] Disabling notifications and UAC...
|
||||
if "%ISXP%"=="1" (
|
||||
echo XP has no Action Center or UAC - nothing to disable.
|
||||
) else (
|
||||
rem Machine-wide Group Policy keys (HKLM) so they apply to EVERY user,
|
||||
rem including the auto-login kiosk account.
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" ^
|
||||
/v DisableNotificationCenter /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoToastApplicationNotification /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoToastApplicationNotificationOnLockScreen /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||
/v NoCloudApplicationNotification /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableSoftLanding /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableWindowsSpotlightFeatures /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||
/v DisableWindowsConsumerFeatures /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows Defender Security Center\Notifications" ^
|
||||
/v DisableNotifications /t REG_DWORD /d 1 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v EnableLUA /t REG_DWORD /d 0 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||
/v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul
|
||||
echo Notifications disabled; UAC disabled ^(takes effect after reboot^).
|
||||
)
|
||||
|
||||
:: -- STEP 7: Auto-login --------------------------------------------------------------
|
||||
echo.
|
||||
echo [7/7] Configuring auto-login...
|
||||
:: Winlogon reads these on every boot (same keys since NT). AutoAdminLogon=1
|
||||
:: signs DefaultUserName in with DefaultPassword without a prompt.
|
||||
set WINLOGON=HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
||||
reg add "%WINLOGON%" /v AutoAdminLogon /t REG_SZ /d "1" /f >nul
|
||||
reg add "%WINLOGON%" /v DefaultUserName /t REG_SZ /d "%AUTOLOGIN_USER%" /f >nul
|
||||
reg add "%WINLOGON%" /v DefaultPassword /t REG_SZ /d "%AUTOLOGIN_PASS%" /f >nul
|
||||
reg add "%WINLOGON%" /v AutoAdminLogon /t REG_SZ /d "1" /f >nul
|
||||
reg add "%WINLOGON%" /v DefaultUserName /t REG_SZ /d "%AUTOLOGIN_USER%" /f >nul
|
||||
reg add "%WINLOGON%" /v DefaultPassword /t REG_SZ /d "%AUTOLOGIN_PASS%" /f >nul
|
||||
reg add "%WINLOGON%" /v DefaultDomainName /t REG_SZ /d "." /f >nul
|
||||
:: Re-arm auto-login after every sign-out (kiosk should never sit at a prompt).
|
||||
reg add "%WINLOGON%" /v ForceAutoLogon /t REG_SZ /d "1" /f >nul
|
||||
reg add "%WINLOGON%" /v ForceAutoLogon /t REG_SZ /d "1" /f >nul
|
||||
:: Clear any logon-count cap that would disable auto-login after N boots.
|
||||
reg delete "%WINLOGON%" /v AutoLogonCount /f >nul 2>&1
|
||||
:: Some builds require this flag off for a plain-text auto-login password to
|
||||
:: be honored (device-passwordless / Windows Hello preference).
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device" ^
|
||||
/v DevicePasswordLessBuildVersion /t REG_DWORD /d 0 /f >nul 2>&1
|
||||
if "%ISXP%"=="0" (
|
||||
rem Some Win10/11 builds require this flag off for a plain-text auto-login
|
||||
rem password to be honored (device-passwordless preference).
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device" ^
|
||||
/v DevicePasswordLessBuildVersion /t REG_DWORD /d 0 /f >nul 2>&1
|
||||
)
|
||||
echo Auto-login configured for user "%AUTOLOGIN_USER%".
|
||||
|
||||
:: ── Done ──────────────────────────────────────────────────────────────────────
|
||||
:: -- Done ----------------------------------------------------------------------------
|
||||
echo.
|
||||
echo ============================================================
|
||||
echo Installation Complete!
|
||||
@@ -332,9 +335,9 @@ echo Config dir : %DATA_DIR%
|
||||
echo Firewall : disabled (closed network)
|
||||
echo.
|
||||
echo Rebooting in 10 seconds...
|
||||
echo The Service and Agent will start automatically in the correct order.
|
||||
echo On first boot with DHCP, SecureConfig runs automatically —
|
||||
echo watch plasma or on main screen for the Request ID and Passphrase.
|
||||
echo The launcher starts automatically at login (single binary, no service).
|
||||
echo On first boot with DHCP, SecureConfig runs automatically -
|
||||
echo watch plasma or the main screen for the Request ID and Passphrase.
|
||||
echo.
|
||||
echo After reboot: use Console to install apps.
|
||||
echo.
|
||||
|
||||
+5
-11
@@ -11,9 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{27C769F3
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaConsole.DiffTests", "Console\tests\TeslaConsole.DiffTests\TeslaConsole.DiffTests.csproj", "{467AA87A-FBD4-45D7-B8F8-9336C95884D2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "Launcher\TeslaLauncherService.csproj", "{910D4404-B3A2-4217-B61C-D43E7F22CDAA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "Launcher\TeslaLauncherAgent.csproj", "{916DCDA5-3379-4383-96F0-AC7B26FAF64E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncher", "Launcher\TeslaLauncher.csproj", "{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\Tesla.Contract.csproj", "{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}"
|
||||
EndProject
|
||||
@@ -35,14 +33,10 @@ Global
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{916DCDA5-3379-4383-96F0-AC7B26FAF64E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
||||
Reference in New Issue
Block a user