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.
|
// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
#if NET40
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
#else
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace Tesla.Net
|
namespace Tesla.Net
|
||||||
{
|
{
|
||||||
@@ -27,13 +39,21 @@ namespace Tesla.Net
|
|||||||
public sealed class RpcRequest
|
public sealed class RpcRequest
|
||||||
{
|
{
|
||||||
public string Method { get; set; }
|
public string Method { get; set; }
|
||||||
|
#if NET40
|
||||||
|
public List<JToken> Args { get; set; }
|
||||||
|
#else
|
||||||
public List<JsonElement> Args { get; set; }
|
public List<JsonElement> Args { get; set; }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
||||||
public sealed class RpcResponse
|
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
|
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
|
||||||
|
#endif
|
||||||
public string Error { get; set; } // null on success
|
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>
|
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
|
||||||
public const int MaxFrameBytes = 16 * 1024 * 1024;
|
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
|
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
||||||
// FIELDS, which System.Text.Json ignores unless this is set.
|
// FIELDS, which System.Text.Json ignores unless this is set.
|
||||||
IncludeFields = true,
|
IncludeFields = true,
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
// ── Framing ──────────────────────────────────────────────────────────
|
// ── Framing ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -88,6 +114,25 @@ namespace Tesla.Net
|
|||||||
|
|
||||||
// ── Request ──────────────────────────────────────────────────────────
|
// ── 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)
|
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||||
{
|
{
|
||||||
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
||||||
@@ -99,9 +144,26 @@ namespace Tesla.Net
|
|||||||
|
|
||||||
public static RpcRequest ReadRequest(Stream stream)
|
public static RpcRequest ReadRequest(Stream stream)
|
||||||
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
||||||
|
#endif
|
||||||
|
|
||||||
// ── Response ─────────────────────────────────────────────────────────
|
// ── 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)
|
public static void WriteResponse(Stream stream, object result, string error)
|
||||||
{
|
{
|
||||||
var resp = new RpcResponse
|
var resp = new RpcResponse
|
||||||
@@ -116,5 +178,6 @@ namespace Tesla.Net
|
|||||||
|
|
||||||
public static RpcResponse ReadResponse(Stream stream)
|
public static RpcResponse ReadResponse(Stream stream)
|
||||||
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- net48 only: both consumers (Console and Launcher) target .NET Framework 4.8.
|
<!-- net48: the Console (and the client/crypto stack under Client/**).
|
||||||
(Was multi-targeted net48;net8.0-windows while the Launcher was on net8.) -->
|
net40: the XP11 single-binary Launcher — the oldest framework installable
|
||||||
<TargetFramework>net48</TargetFramework>
|
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
|
<!-- CRITICAL: the output assembly MUST be named TeslaConsoleLaunchLib at
|
||||||
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
|
version 1.0.0.0. BinaryFormatter embeds the assembly name in the wire
|
||||||
@@ -24,17 +26,27 @@
|
|||||||
<NoWarn>$(NoWarn);SYSLIB0011</NoWarn>
|
<NoWarn>$(NoWarn);SYSLIB0011</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- net48 reference assemblies so the project builds without a full targeting pack,
|
<!-- .NET Framework reference assemblies so the project builds without a full
|
||||||
plus System.Text.Json (built into the net8 shared framework, a package on net48). -->
|
targeting pack (resolves per-TFM, covers net40 and net48). -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
<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" />
|
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||||
</ItemGroup>
|
</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
|
<!-- The TCP/OFB client (Client/**) is net48-only: it depends on the crypto-stream
|
||||||
handshake in TeslaSecureConfiguration.dll. The Launcher (net6) is the SERVER
|
handshake in TeslaSecureConfiguration.dll. The Launcher is the SERVER end of
|
||||||
end of this protocol and never references these classes, so they are excluded
|
this protocol and never references these classes, so the net40 leg carries
|
||||||
from the net6.0-windows build (which carries only the wire data types). -->
|
only the wire data types + PodRpc framing. -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
|
||||||
<Compile Remove="Client\**\*.cs" />
|
<Compile Remove="Client\**\*.cs" />
|
||||||
</ItemGroup>
|
</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
|
## Architecture
|
||||||
|
|
||||||
TeslaLauncher has three components that work together:
|
One userland tray application — no Windows Service, no IPC.
|
||||||
|
|
||||||
### TeslaLauncherService (Windows Service, Session 0)
|
The original Elsewhen software was a single service (on Win2k/XP a service could still
|
||||||
- Runs at boot before any user logs in
|
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
|
- 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) and shows the
|
||||||
- Handles first-boot network configuration (SecureConfig)
|
Request ID / Passphrase on screen + COM2 plasma
|
||||||
- Handles game file transfers from the Console (InstallProduct)
|
- 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)
|
Requires the kiosk account (`Firestorm`) to be in **Administrators** — SecureConfig's
|
||||||
- Runs in the logged-in user's desktop session
|
`netsh`/hostname writes and product `postinstall.bat` driver installs need the admin
|
||||||
- Executes commands that require desktop access: launching/killing apps, volume control
|
token (UAC is disabled by the installer on modern Windows, so no prompts).
|
||||||
- Manages `LaunchApps.xml` (installed games registry)
|
|
||||||
- On first boot, displays SecureConfig Request ID and Passphrase
|
|
||||||
|
|
||||||
### SecureConfig (first-boot protocol)
|
### SecureConfig (first-boot protocol)
|
||||||
- Assigns a temporary IP and broadcasts a UDP beacon so the Console can discover the pod
|
- 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
|
- TCP handshake establishes an OFB-encrypted session with RSA key exchange
|
||||||
- Session key is saved for all subsequent Console connections
|
- 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
|
## Communication Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
TeslaConsole ──TCP 53290 (OFB + framed JSON)──> TeslaLauncherService
|
TeslaConsole ──TCP 53290 (OFB + framed JSON)──> TeslaLauncher.exe (user session)
|
||||||
│
|
|
||||||
Named Pipe (JSON)
|
|
||||||
│
|
|
||||||
v
|
|
||||||
TeslaLauncherAgent
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `TeslaLauncherService.cs` | Windows Service implementation |
|
| `TeslaLauncher.cs` | The whole launcher: tray, TCP listener, RPC dispatch, install, processes, volume |
|
||||||
| `TeslaLauncherService.csproj` | Service project (net48, generic host + Windows service) |
|
| `TeslaLauncher.csproj` | net40 WinForms exe project |
|
||||||
| `TeslaLauncherAgent.cs` | Userspace Agent implementation |
|
| `MiniZip.cs` | Central-directory ZIP extractor (net40 has no `ZipFile`; stored + deflate + ZIP64) |
|
||||||
| `TeslaLauncherAgent.csproj` | Agent project (WinForms, net48) |
|
| `SecureConfig.cs` | First-boot secure configuration protocol + OFB duplex stream |
|
||||||
| `LaunchModels_Shared.cs` | Service↔Agent IPC types (Tesla.Launcher.Shared). Wire types (Tesla.Net) now come from `../Contract/Tesla.Contract.csproj` |
|
| `build.bat` | Builds + assembles the package |
|
||||||
| `SecureConfig.cs` | First-boot secure configuration protocol |
|
| `install.bat` | Dual-OS installer (XP SP3 and Win10/11 code paths; run as Administrator) |
|
||||||
| `build.bat` | Builds both components |
|
|
||||||
| `install.bat` | Installs on a cockpit PC (run as Administrator) |
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -57,34 +63,47 @@ Requirements:
|
|||||||
- Internet access for NuGet restore (first build only)
|
- Internet access for NuGet restore (first build only)
|
||||||
|
|
||||||
```
|
```
|
||||||
build.bat :: build both components + assemble the package
|
build.bat :: build + assemble the package
|
||||||
build.bat /service :: build Service only
|
|
||||||
build.bat /agent :: build Agent only
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Output goes to `dist\TeslaLauncher\` (with `Service\` and `Agent\` subdirectories plus
|
Output goes to `dist\TeslaLauncher\` (with `App\` plus `install.bat` and redist
|
||||||
`install.bat`) and `dist\TeslaLauncher-podpkg.zip`, mirroring `Console\dist\`. The projects
|
folders) and `dist\TeslaLauncher-podpkg.zip`. The project is published in place
|
||||||
are published in place (framework-dependent net48) — they reference `../Contract`, so they
|
(framework-dependent net40) — it references `../Contract`, so it cannot be staged
|
||||||
cannot be staged into a temp folder. Each folder holds the exe plus its dependency DLLs;
|
into a temp folder. `App\` holds the exe plus `Newtonsoft.Json.dll` and
|
||||||
the target pod needs only .NET Framework 4.8 (built into Windows 10/11), no bundled runtime.
|
`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
|
## 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
|
2. Run `TeslaLauncher\install.bat` as Administrator
|
||||||
|
|
||||||
The installer:
|
The installer detects the OS and branches where the tooling differs:
|
||||||
- Registers the Service (delayed auto-start)
|
|
||||||
- Configures the Agent for auto-login startup
|
| Step | XP SP3 | Windows 10/11 |
|
||||||
- Installs OpenAL and DirectX runtimes
|
|------|--------|---------------|
|
||||||
- Enables SMB1 file sharing
|
| .NET | installs 4.0 redist from `dotnet40\` if missing | 4.8 built in — nothing |
|
||||||
- Creates `C:\Games` with appropriate permissions
|
| ACLs | `cacls` | `icacls` |
|
||||||
- Resets network adapters to DHCP for SecureConfig
|
| 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
|
## First Boot
|
||||||
|
|
||||||
1. Cockpit boots with DHCP (unconfigured state)
|
1. Cockpit boots with DHCP (unconfigured state), auto-logs into the kiosk account
|
||||||
2. Service runs SecureConfig: broadcasts beacon, displays codes on screen
|
2. Launcher runs SecureConfig: broadcasts beacon, displays codes on screen + plasma
|
||||||
3. Console operator sees the pod's Request ID and enters the Passphrase
|
3. Console operator sees the pod's Request ID and enters the Passphrase
|
||||||
4. Console sends encrypted network configuration
|
4. Console sends encrypted network configuration
|
||||||
5. Pod applies the configuration and is ready for normal operation
|
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
|
## 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 |
|
| Path | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `C:\ProgramData\TeslaLauncher\TeslaKeyStore.key` | Session key (32 bytes) |
|
| `<CommonAppData>\TeslaLauncher\TeslaKeyStore.key` | Session key (32 bytes) |
|
||||||
| `C:\ProgramData\TeslaLauncher\LaunchApps.xml` | Installed games registry |
|
| `<CommonAppData>\TeslaLauncher\LaunchApps.xml` | Installed games registry (same XML shape as the two-process Agent wrote) |
|
||||||
| `C:\ProgramData\TeslaLauncher\configuring.json` | Transient: SecureConfig codes for Agent display |
|
| `<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 |
|
| `C:\Games\` | Game installation directory |
|
||||||
|
|
||||||
## Wire Protocol
|
## Wire Protocol
|
||||||
|
|
||||||
The Console talks to the Service with **length-prefixed System.Text.Json frames**
|
The Console talks to the launcher with **length-prefixed JSON frames** over the
|
||||||
over the OFB-encrypted TCP stream (dispatch by method name) — see
|
OFB-encrypted TCP stream (dispatch by method name) — see
|
||||||
`../Contract/PodRpcProtocol.cs`, shared by both ends. This replaced the original
|
`../Contract/PodRpcProtocol.cs`, shared by both ends. The Contract multi-targets
|
||||||
`BinaryFormatter` + serialized-`MethodBase` scheme. The `Tesla.Net` wire types now
|
`net48;net40`: the Console's net48 leg serializes with System.Text.Json, the
|
||||||
live in `../Contract/Tesla.Contract.csproj`, the single source of truth shared with
|
launcher's net40 leg with Newtonsoft.Json (STJ has no net40 target). The JSON is
|
||||||
the Console.
|
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
|
Volume on XP falls back from CoreAudio (Vista+) to `nircmd.exe` / winmm
|
||||||
that avoid the nested struct layout of the wire format.
|
`waveOutSetVolume`.
|
||||||
|
|||||||
+32
-20
@@ -416,7 +416,10 @@ namespace TeslaSecureConfig
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
(_adapterIndex, _adapterId, _adapterName) = FindFirstEthernetAdapter();
|
var adapter = FindFirstEthernetAdapter();
|
||||||
|
_adapterIndex = adapter.Index;
|
||||||
|
_adapterId = adapter.Id;
|
||||||
|
_adapterName = adapter.Name;
|
||||||
}
|
}
|
||||||
_log = logger ?? (s => Debug.WriteLine(s));
|
_log = logger ?? (s => Debug.WriteLine(s));
|
||||||
}
|
}
|
||||||
@@ -845,23 +848,23 @@ namespace TeslaSecureConfig
|
|||||||
Log("Secure connection to console negotiated.");
|
Log("Secure connection to console negotiated.");
|
||||||
|
|
||||||
// ── 4. RSA key exchange ─────────────────────────────────
|
// ── 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.");
|
Log("Sending console final key.");
|
||||||
using (var bw = new BinaryWriter(ofb, Encoding.UTF8, leaveOpen: true))
|
// 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.Write(rsa.ToXmlString(false)); // public key only
|
||||||
bw.Flush();
|
bw.Flush();
|
||||||
}
|
|
||||||
|
|
||||||
Log("Receiving console key.");
|
Log("Receiving console key.");
|
||||||
byte[] sessionKey;
|
var br = new BinaryReader(ofb, Encoding.UTF8);
|
||||||
using (var br = new BinaryReader(ofb, Encoding.UTF8, leaveOpen: true))
|
|
||||||
{
|
|
||||||
int encLen = br.ReadInt32();
|
int encLen = br.ReadInt32();
|
||||||
byte[] enc = br.ReadBytes(encLen);
|
byte[] enc = br.ReadBytes(encLen);
|
||||||
sessionKey = rsa.Decrypt(enc, RSAEncryptionPadding.Pkcs1);
|
byte[] sessionKey = rsa.Decrypt(enc, false); // PKCS#1 v1.5
|
||||||
}
|
|
||||||
Log($"Console key received ({sessionKey.Length} bytes).");
|
Log($"Console key received ({sessionKey.Length} bytes).");
|
||||||
|
|
||||||
// Store session key — used for OFB on the management port (53290)
|
// Store session key — used for OFB on the management port (53290)
|
||||||
@@ -945,11 +948,12 @@ namespace TeslaSecureConfig
|
|||||||
_plasma.WriteLine("Passphrase: {0}", _passphrase);
|
_plasma.WriteLine("Passphrase: {0}", _passphrase);
|
||||||
|
|
||||||
#if WINFORMS
|
#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(() =>
|
var t = new Thread(() =>
|
||||||
{
|
{
|
||||||
Application.EnableVisualStyles();
|
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
|
||||||
_displayForm = new PasscodeDisplayForm(_requestId, _passphrase);
|
_displayForm = new PasscodeDisplayForm(_requestId, _passphrase);
|
||||||
_displayForm.ShowDialog();
|
_displayForm.ShowDialog();
|
||||||
})
|
})
|
||||||
@@ -1129,10 +1133,18 @@ namespace TeslaSecureConfig
|
|||||||
Log($"Warning: target IP {targetIp} not on any local subnet — keeping [{_adapterIndex}] {_adapterName}");
|
Log($"Warning: target IP {targetIp} not on any local subnet — keeping [{_adapterIndex}] {_adapterName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns (IPv4 interface index, adapter GUID).
|
// IPv4 interface index + adapter GUID + display name (a struct instead of a
|
||||||
// Index is used with `netsh interface ipv4 ... interface=N` (avoids name-quoting issues).
|
// ValueTuple — net40 has no System.ValueTuple).
|
||||||
// GUID is used for direct registry writes to persist static IP across reboots.
|
// Index avoids netsh name-quoting issues; GUID is used for direct registry
|
||||||
private static (int index, string id, string name) FindFirstEthernetAdapter()
|
// 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())
|
foreach (var nic in NetworkInterface.GetAllNetworkInterfaces())
|
||||||
{
|
{
|
||||||
@@ -1150,7 +1162,7 @@ namespace TeslaSecureConfig
|
|||||||
{
|
{
|
||||||
int idx = nic.GetIPProperties().GetIPv4Properties().Index;
|
int idx = nic.GetIPProperties().GetIPv4Properties().Index;
|
||||||
Log2($"Selected physical adapter: [{idx}] {nic.Name} / {nic.Description}");
|
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) { }
|
catch (NetworkInformationException) { }
|
||||||
}
|
}
|
||||||
@@ -1165,7 +1177,7 @@ namespace TeslaSecureConfig
|
|||||||
string.Equals(nic.Description, displayName, StringComparison.OrdinalIgnoreCase))
|
string.Equals(nic.Description, displayName, StringComparison.OrdinalIgnoreCase))
|
||||||
return nic.Id;
|
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)
|
// 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
|
// 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 ---
|
// --- 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
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.5.2.0
|
VisualStudioVersion = 17.5.2.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "TeslaLauncherAgent.csproj", "{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncher", "TeslaLauncher.csproj", "{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "TeslaLauncherService.csproj", "{37D7E6C3-8DFD-CCB8-AFDF-18F2C0A48862}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{96766D0D-C3A1-A4C4-E93D-B963FBAC4C56}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
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>
|
|
||||||
+30
-56
@@ -1,55 +1,52 @@
|
|||||||
@echo off
|
@echo off
|
||||||
:: =============================================================================
|
:: =============================================================================
|
||||||
:: TeslaLauncher — Unified Build / Package Script
|
:: TeslaLauncher - Unified Build / Package Script (XP11 single binary)
|
||||||
:: =============================================================================
|
:: =============================================================================
|
||||||
:: Publishes TeslaLauncherService.exe and TeslaLauncherAgent.exe as
|
:: Publishes TeslaLauncher.exe as a framework-dependent net40 build and
|
||||||
:: framework-dependent net48 builds and assembles the installable TeslaLauncher\
|
:: assembles the installable TeslaLauncher\ package next to install.bat.
|
||||||
:: 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.
|
:: 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:
|
:: Requirements:
|
||||||
:: .NET SDK (6.0+) to drive the build https://dotnet.microsoft.com/download
|
:: .NET SDK (6.0+) to drive the build https://dotnet.microsoft.com/download
|
||||||
:: Internet access for NuGet restore (first build only)
|
:: Internet access for NuGet restore (first build only)
|
||||||
::
|
::
|
||||||
:: Usage:
|
:: Usage:
|
||||||
:: build.bat - build both components + package
|
:: build.bat - build + package
|
||||||
:: build.bat /service - build Service only
|
:: build.bat /q - no pause on exit
|
||||||
:: build.bat /agent - build Agent only
|
|
||||||
::
|
::
|
||||||
:: Output (under dist\, parity with Console\dist\):
|
:: Output (under dist\, parity with Console\dist\):
|
||||||
:: dist\TeslaLauncher\
|
:: dist\TeslaLauncher\
|
||||||
:: install.bat
|
:: install.bat
|
||||||
:: Service\TeslaLauncherService.exe
|
:: App\TeslaLauncher.exe (+ Newtonsoft.Json.dll, TeslaConsoleLaunchLib.dll)
|
||||||
:: Agent\TeslaLauncherAgent.exe
|
:: dx9201006\ openal\ UltraVNC\ dotnet40\ (redist, mirrored from assets\)
|
||||||
:: dx9201006\ openal\ UltraVNC\ (redist, mirrored from assets\)
|
|
||||||
:: dist\TeslaLauncher-podpkg.zip (the deployable package)
|
:: dist\TeslaLauncher-podpkg.zip (the deployable package)
|
||||||
::
|
::
|
||||||
:: NOTE: the projects reference ..\Contract\Tesla.Contract.csproj, so they are
|
:: NOTE: the project references ..\Contract\Tesla.Contract.csproj, so it is
|
||||||
:: published IN PLACE (not staged into a temp folder) — the project reference
|
:: published IN PLACE (not staged into a temp folder) - the project reference
|
||||||
:: must be able to resolve relative to this directory.
|
:: must be able to resolve relative to this directory.
|
||||||
:: =============================================================================
|
:: =============================================================================
|
||||||
|
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
set ROOT=%~dp0
|
set ROOT=%~dp0
|
||||||
:: Package under dist\ (parity with Console\dist\).
|
|
||||||
set BUILD_DIR=%ROOT%dist\TeslaLauncher
|
set BUILD_DIR=%ROOT%dist\TeslaLauncher
|
||||||
set ZIP=%ROOT%dist\TeslaLauncher-podpkg.zip
|
set ZIP=%ROOT%dist\TeslaLauncher-podpkg.zip
|
||||||
|
|
||||||
:: -- Parse arguments ----------------------------------------------------------
|
|
||||||
set BUILD_SERVICE=1
|
|
||||||
set BUILD_AGENT=1
|
|
||||||
set QUIET=0
|
set QUIET=0
|
||||||
|
|
||||||
for %%a in (%*) do (
|
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 ============================================================
|
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 Output : %BUILD_DIR%
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo.
|
echo.
|
||||||
@@ -68,60 +65,38 @@ echo SDK : %SDK_VER%
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: -- Clean prior package output ----------------------------------------------
|
:: -- 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%\Service" rmdir /s /q "%BUILD_DIR%\Service"
|
||||||
if exist "%BUILD_DIR%\Agent" rmdir /s /q "%BUILD_DIR%\Agent"
|
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%\dx9201006" rmdir /s /q "%BUILD_DIR%\dx9201006"
|
||||||
if exist "%BUILD_DIR%\openal" rmdir /s /q "%BUILD_DIR%\openal"
|
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%\UltraVNC" rmdir /s /q "%BUILD_DIR%\UltraVNC"
|
||||||
|
if exist "%BUILD_DIR%\dotnet40" rmdir /s /q "%BUILD_DIR%\dotnet40"
|
||||||
|
|
||||||
:: -- Build Service ------------------------------------------------------------
|
:: -- Build --------------------------------------------------------------------
|
||||||
if %BUILD_SERVICE%==0 goto :skip_service
|
echo [1/1] Publishing TeslaLauncher (net40 single binary)...
|
||||||
|
|
||||||
echo [1/2] Publishing TeslaLauncherService (Session 0 Windows Service)...
|
|
||||||
echo.
|
echo.
|
||||||
dotnet publish "%ROOT%TeslaLauncherService.csproj" ^
|
dotnet publish "%ROOT%TeslaLauncher.csproj" ^
|
||||||
-c Release ^
|
-c Release ^
|
||||||
-o "%BUILD_DIR%\Service"
|
-o "%BUILD_DIR%\App"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: Service build failed.
|
echo ERROR: build failed.
|
||||||
if "%QUIET%"=="0" pause
|
if "%QUIET%"=="0" pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
echo.
|
echo.
|
||||||
echo Service : %BUILD_DIR%\Service\TeslaLauncherService.exe
|
echo Launcher : %BUILD_DIR%\App\TeslaLauncher.exe
|
||||||
echo.
|
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 --------------------------------------
|
:: -- Copy shared assets into the package --------------------------------------
|
||||||
:: Mirror each redist folder under assets\ into the package root so install.bat's
|
:: 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%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\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\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\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 ----------------------------------------------------------
|
:: -- Zip the package ----------------------------------------------------------
|
||||||
echo Zipping package...
|
echo Zipping package...
|
||||||
@@ -133,14 +108,13 @@ echo ============================================================
|
|||||||
echo Build complete
|
echo Build complete
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo.
|
echo.
|
||||||
if %BUILD_SERVICE%==1 echo Service : %BUILD_DIR%\Service\TeslaLauncherService.exe
|
echo Launcher : %BUILD_DIR%\App\TeslaLauncher.exe
|
||||||
if %BUILD_AGENT%==1 echo Agent : %BUILD_DIR%\Agent\TeslaLauncherAgent.exe
|
|
||||||
echo.
|
echo.
|
||||||
echo Package : %BUILD_DIR%
|
echo Package : %BUILD_DIR%
|
||||||
echo Zip : %ZIP%
|
echo Zip : %ZIP%
|
||||||
echo.
|
echo.
|
||||||
echo Next steps:
|
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 2. Run TeslaLauncher\install.bat as Administrator
|
||||||
echo.
|
echo.
|
||||||
if "%QUIET%"=="0" pause
|
if "%QUIET%"=="0" pause
|
||||||
|
|||||||
+178
-175
@@ -1,40 +1,53 @@
|
|||||||
@echo off
|
@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:
|
:: Must be run from the TeslaLauncher\ folder produced by build.bat:
|
||||||
::
|
::
|
||||||
:: TeslaLauncher\
|
:: TeslaLauncher\
|
||||||
:: install.bat <- this file
|
:: install.bat <- this file
|
||||||
:: Service\TeslaLauncherService.exe
|
:: App\TeslaLauncher.exe (net40 single binary)
|
||||||
:: Agent\TeslaLauncherAgent.exe
|
|
||||||
:: dx9201006\DXSETUP.exe (DirectX June 2010 redist)
|
:: dx9201006\DXSETUP.exe (DirectX June 2010 redist)
|
||||||
:: openal\oalinst.exe (OpenAL 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
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo Tesla Launcher v4.11.4.1 - Installation
|
echo Tesla Launcher v4.11.4.1 - Installation (single binary)
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
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 ROOT=%~dp0
|
||||||
set SERVICE_SRC=%ROOT%Service
|
set APP_SRC=%ROOT%App
|
||||||
set AGENT_SRC=%ROOT%Agent
|
set INSTALL_DIR=%ProgramFiles%\TeslaLauncher
|
||||||
set INSTALL_DIR=C:\Program Files\TeslaLauncher
|
set LAUNCHER_EXE=%INSTALL_DIR%\TeslaLauncher.exe
|
||||||
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 CONSOLE_PORT=53290
|
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
|
:: 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_USER=Firestorm
|
||||||
set AUTOLOGIN_PASS=thor6
|
set AUTOLOGIN_PASS=thor6
|
||||||
|
|
||||||
@@ -42,139 +55,161 @@ set AUTOLOGIN_PASS=thor6
|
|||||||
:: the mw4files / c shares browse cleanly. Change once per site if needed.
|
:: the mw4files / c shares browse cleanly. Change once per site if needed.
|
||||||
set WORKGROUP=Tesla
|
set WORKGROUP=Tesla
|
||||||
|
|
||||||
:: ── Admin check ───────────────────────────────────────────────────────────────
|
:: -- Admin check -----------------------------------------------------------------
|
||||||
net session >nul 2>&1
|
net session >nul 2>&1
|
||||||
if %errorlevel% neq 0 (
|
if %errorlevel% neq 0 (
|
||||||
echo ERROR: This script must be run as Administrator.
|
echo ERROR: This script must be run as Administrator.
|
||||||
pause & exit /b 1
|
pause & exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: ── Verify source files ───────────────────────────────────────────────────────
|
:: -- Verify source files ---------------------------------------------------------
|
||||||
if not exist "%SERVICE_SRC%\TeslaLauncherService.exe" (
|
if not exist "%APP_SRC%\TeslaLauncher.exe" (
|
||||||
echo ERROR: TeslaLauncherService.exe not found in %SERVICE_SRC%
|
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
|
|
||||||
)
|
|
||||||
if not exist "%AGENT_SRC%\TeslaLauncherAgent.exe" (
|
|
||||||
echo ERROR: TeslaLauncherAgent.exe not found in %AGENT_SRC%
|
|
||||||
echo Run build.bat first, then run install.bat from the TeslaLauncher\ folder.
|
echo Run build.bat first, then run install.bat from the TeslaLauncher\ folder.
|
||||||
pause & exit /b 1
|
pause & exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
:: ── Detect and clean up existing installation ─────────────────────────────────
|
:: -- .NET Framework 4.0 check (XP ships no .NET; Win10/11 has 4.8 built in) ------
|
||||||
set EXISTING=0
|
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" /v Install >nul 2>&1
|
||||||
if exist "%SERVICE_EXE%" set EXISTING=1
|
if not errorlevel 1 goto :dotnet_ok
|
||||||
if exist "%AGENT_EXE%" set EXISTING=1
|
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 (
|
||||||
|
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
|
||||||
|
|
||||||
if "%EXISTING%"=="1" (
|
:: -- Detect and clean up existing installation -----------------------------------
|
||||||
echo Existing installation detected. Cleaning up...
|
echo Cleaning up any existing installation...
|
||||||
echo.
|
|
||||||
|
|
||||||
:: Stop and kill the Agent process
|
:: Stop launcher / legacy agent processes
|
||||||
|
taskkill /F /IM TeslaLauncher.exe >nul 2>&1
|
||||||
taskkill /F /IM TeslaLauncherAgent.exe >nul 2>&1
|
taskkill /F /IM TeslaLauncherAgent.exe >nul 2>&1
|
||||||
echo Agent process stopped.
|
|
||||||
|
|
||||||
:: Stop and remove the service, wait for it to fully stop
|
:: Remove the legacy two-process service registration (pre-XP11 installs)
|
||||||
sc stop "%SERVICE_NAME%" >nul 2>&1
|
|
||||||
sc stop "Tesla Application Launcher" >nul 2>&1
|
sc stop "Tesla Application Launcher" >nul 2>&1
|
||||||
timeout /t 3 /nobreak >nul
|
ping -n 4 127.0.0.1 >nul
|
||||||
sc delete "%SERVICE_NAME%" >nul 2>&1
|
|
||||||
sc delete "Tesla Application Launcher" >nul 2>&1
|
sc delete "Tesla Application Launcher" >nul 2>&1
|
||||||
timeout /t 2 /nobreak >nul
|
reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v TeslaLauncherAgent /f >nul 2>&1
|
||||||
echo Service stopped and removed.
|
|
||||||
|
|
||||||
:: Delete old executables, logs, and session key so SecureConfig re-runs
|
:: Delete old executables, logs, and session key so SecureConfig re-runs
|
||||||
del /F /Q "%SERVICE_EXE%" >nul 2>&1
|
if exist "%LAUNCHER_EXE%" (
|
||||||
del /F /Q "%AGENT_EXE%" >nul 2>&1
|
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%\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%\TeslaKeyStore.key" >nul 2>&1
|
||||||
del /F /Q "%DATA_DIR%\LaunchApps.xml" >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%\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.
|
echo Old executables, logs, session key, and app config deleted.
|
||||||
|
|
||||||
:: Reset all physical Ethernet adapters to DHCP so the SecureConfig
|
:: Reset all Ethernet adapters to DHCP so the SecureConfig protocol triggers
|
||||||
:: protocol triggers correctly on the next boot.
|
:: on the next boot. The static address entry must be REMOVED, not just the
|
||||||
:: We must REMOVE the static IP address entry first, then enable DHCP.
|
:: DHCP flag flipped, or IsMachineConfigured() still sees a static adapter.
|
||||||
:: 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...
|
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
|
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 Network adapters reset to DHCP.
|
||||||
echo.
|
echo.
|
||||||
) 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
|
|
||||||
)
|
|
||||||
|
|
||||||
:: ── STEP 1: Create directories ────────────────────────────────────────────────
|
:: -- STEP 1: Create directories ---------------------------------------------------
|
||||||
echo [1/8] Creating directories...
|
echo [1/7] Creating directories...
|
||||||
if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%"
|
if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%"
|
||||||
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
|
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
|
||||||
if not exist "C:\Games" mkdir "C:\Games"
|
if not exist "C:\Games" mkdir "C:\Games"
|
||||||
|
|
||||||
:: Grant all users modify access to the data and games directories so
|
:: Grant Users modify access to the data and games directories so the launcher
|
||||||
:: the Service and Agent can write files (LaunchApps.xml, game installs).
|
:: 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 "%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
|
icacls "C:\Games" /grant *S-1-5-32-545:(OI)(CI)M /T >nul 2>&1
|
||||||
|
)
|
||||||
echo %INSTALL_DIR%
|
echo %INSTALL_DIR%
|
||||||
echo %DATA_DIR% (Users: modify access)
|
echo %DATA_DIR% (Users: modify access)
|
||||||
echo C:\Games (Users: modify access)
|
echo C:\Games (Users: modify access)
|
||||||
|
|
||||||
:: ── STEP 2: Copy files ────────────────────────────────────────────────────────
|
:: -- STEP 2: Copy files ------------------------------------------------------------
|
||||||
echo [2/8] Copying files...
|
echo [2/7] Copying files...
|
||||||
:: net48 framework-dependent build: each folder holds the exe + its dependency
|
:: net40 framework-dependent build: the folder holds the exe + its dependency
|
||||||
:: DLLs + .config. Copy both folders into the install dir (shared DLLs overwrite
|
:: DLLs (Newtonsoft.Json, TeslaConsoleLaunchLib) + .config.
|
||||||
:: identically; the two .exe.config files coexist).
|
copy /Y "%APP_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
||||||
copy /Y "%SERVICE_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
|
||||||
copy /Y "%AGENT_SRC%\*.*" "%INSTALL_DIR%\" >nul
|
|
||||||
|
|
||||||
:: Install DirectX June 2010 runtime (required by games)
|
:: Install DirectX June 2010 runtime (required by games; supports XP and Win10/11)
|
||||||
if exist "%ROOT%\dx9201006\DXSETUP.exe" (
|
if exist "%ROOT%dx9201006\DXSETUP.exe" (
|
||||||
echo Installing DirectX runtime...
|
echo Installing DirectX runtime...
|
||||||
"%ROOT%\dx9201006\DXSETUP.exe" /silent
|
"%ROOT%dx9201006\DXSETUP.exe" /silent
|
||||||
echo DirectX installed.
|
echo DirectX installed.
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Install OpenAL runtime (required by game audio)
|
:: Install OpenAL runtime (required by game audio)
|
||||||
if exist "%ROOT%\openal\oalinst.exe" (
|
if exist "%ROOT%openal\oalinst.exe" (
|
||||||
echo Installing OpenAL runtime...
|
echo Installing OpenAL runtime...
|
||||||
"%ROOT%\openal\oalinst.exe" /s
|
"%ROOT%openal\oalinst.exe" /s
|
||||||
echo OpenAL installed.
|
echo OpenAL installed.
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Install UltraVNC server (remote diagnostics). The Inno installer lays down
|
:: Install UltraVNC server (remote diagnostics). x64 build for modern Windows;
|
||||||
:: the files and (via /loadinf) sets the install dir + VNC password; we then
|
:: XP uses the x86 build when the package carries one.
|
||||||
:: register and start the service EXPLICITLY with winvnc.exe. Doing the service
|
set UVNC_SETUP=
|
||||||
:: step ourselves means a missing/incomplete "install service" task in the
|
if "%ISXP%"=="1" (
|
||||||
:: answer file can't leave the pod with the files present but nothing listening.
|
if exist "%ROOT%UltraVNC\UltraVNC_x86_Setup.exe" set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x86_Setup.exe
|
||||||
set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x64_Setup.exe
|
) else (
|
||||||
set UVNC_DIR=C:\Program Files\uvnc bvba\UltraVNC
|
if exist "%ROOT%UltraVNC\UltraVNC_x64_Setup.exe" set UVNC_SETUP=%ROOT%UltraVNC\UltraVNC_x64_Setup.exe
|
||||||
if exist "%UVNC_SETUP%" (
|
)
|
||||||
|
if defined UVNC_SETUP (
|
||||||
echo Installing UltraVNC server...
|
echo Installing UltraVNC server...
|
||||||
start "" /wait "%UVNC_SETUP%" /verysilent /norestart /loadinf="%ROOT%UltraVNC\UltraVNC.inf"
|
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
|
:: SMB1 + DirectPlay: native on XP; must be re-enabled on Win10/11.
|
||||||
:: reaches SMB1 shares (client) and hosts the mw4files / c shares below (server),
|
if "%ISXP%"=="0" (
|
||||||
:: and Win10/11 ship both SMB1 sub-features off by default — so enable all three
|
echo Enabling SMB1 file sharing ^(client + server^)...
|
||||||
:: (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 /All /NoRestart >nul 2>&1
|
||||||
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Client /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
|
dism /Online /Enable-Feature /FeatureName:SMB1Protocol-Server /All /NoRestart >nul 2>&1
|
||||||
echo SMB1 client + server enabled (reboot required to take effect).
|
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...
|
echo Creating network shares...
|
||||||
if not exist "C:\mw4files" mkdir "C:\mw4files"
|
if not exist "C:\mw4files" mkdir "C:\mw4files"
|
||||||
:: Grant Everyone modify on the NTFS side so the share grant is effective.
|
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
|
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).
|
)
|
||||||
net share mw4files /delete >nul 2>&1
|
net share mw4files /delete >nul 2>&1
|
||||||
net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1
|
net share mw4files=C:\mw4files /grant:Everyone,FULL >nul 2>&1
|
||||||
echo "Share \\%COMPUTERNAME%\mw4files -> C:\mw4files (Everyone: Full)"
|
echo "Share \\%COMPUTERNAME%\mw4files -> C:\mw4files (Everyone: Full)"
|
||||||
@@ -182,130 +217,96 @@ net share c /delete >nul 2>&1
|
|||||||
net share c=C:\ /grant:Everyone,FULL >nul 2>&1
|
net share c=C:\ /grant:Everyone,FULL >nul 2>&1
|
||||||
echo "Share \\%COMPUTERNAME%\c -> C:\ (Everyone: Full)"
|
echo "Share \\%COMPUTERNAME%\c -> C:\ (Everyone: Full)"
|
||||||
|
|
||||||
:: Join the site workgroup so every pod shares one browse list. Add-Computer
|
:: Join the site workgroup so every pod shares one browse list.
|
||||||
:: 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).
|
|
||||||
echo Joining workgroup "%WORKGROUP%"...
|
echo Joining workgroup "%WORKGROUP%"...
|
||||||
|
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
|
powershell -NoProfile -Command "try { Add-Computer -WorkgroupName '%WORKGROUP%' -Force -ErrorAction Stop } catch { }" >nul 2>&1
|
||||||
|
)
|
||||||
echo Workgroup set to "%WORKGROUP%" (effective after reboot).
|
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.
|
echo Done.
|
||||||
|
|
||||||
:: ── STEP 3: Install Windows Service ──────────────────────────────────────────
|
:: -- STEP 3: Launcher auto-start (no Windows Service on XP11) ----------------------
|
||||||
echo [3/8] Installing Windows Service...
|
echo [3/7] Configuring launcher auto-start...
|
||||||
sc create "%SERVICE_NAME%" ^
|
:: The single binary runs in the auto-logged-in user session. Restart-after-crash
|
||||||
binPath= "\"%SERVICE_EXE%\"" ^
|
:: comes from RegisterApplicationRestart (Vista+) inside the launcher itself.
|
||||||
DisplayName= "%SERVICE_DISPLAY%" ^
|
set AUTORUN_KEY=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
||||||
start= delayed-auto ^
|
reg add "%AUTORUN_KEY%" /v "TeslaLauncher" /t REG_SZ ^
|
||||||
obj= LocalSystem ^
|
/d "\"%LAUNCHER_EXE%\"" /f >nul
|
||||||
type= own
|
echo Added TeslaLauncher to HKLM\...\Run (no service - single userland binary).
|
||||||
|
|
||||||
if %errorlevel% neq 0 (
|
:: -- STEP 4: Disable Windows Firewall (closed network) -----------------------------
|
||||||
echo ERROR: Failed to register Windows Service.
|
echo.
|
||||||
pause & exit /b 1
|
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.
|
||||||
|
|
||||||
sc description "%SERVICE_NAME%" ^
|
:: -- STEP 5: Power settings (max performance, no sleep) ----------------------------
|
||||||
"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.
|
||||||
echo [4/8] Disabling Windows Firewall (closed network)...
|
echo [5/7] Configuring power settings...
|
||||||
:: Disable firewall on all profiles — pods run on a closed network
|
if "%ISXP%"=="1" (
|
||||||
netsh advfirewall set allprofiles state off
|
powercfg /setactive "Always On" >nul 2>&1
|
||||||
echo Windows Firewall disabled on all profiles.
|
powercfg /change "Always On" /monitor-timeout-ac 0 >nul 2>&1
|
||||||
|
powercfg /change "Always On" /disk-timeout-ac 0 >nul 2>&1
|
||||||
:: ── STEP 5: Power settings (max performance, no sleep) ──────────────────────
|
powercfg /change "Always On" /standby-timeout-ac 0 >nul 2>&1
|
||||||
echo.
|
powercfg /hibernate off >nul 2>&1
|
||||||
echo [5/8] Configuring power settings...
|
echo "Always On" power scheme activated; sleep/hibernate disabled.
|
||||||
|
) else (
|
||||||
:: Activate Ultimate Performance plan (hidden by default, GUID is well-known)
|
|
||||||
powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 >nul 2>&1
|
powercfg -duplicatescheme e9a42b02-d5df-448d-aa00-03f14749eb61 >nul 2>&1
|
||||||
for /f "tokens=4" %%g in ('powercfg -list ^| findstr /i "Ultimate"') do (
|
for /f "tokens=4" %%g in ('powercfg -list ^| findstr /i "Ultimate"') do (
|
||||||
powercfg -setactive %%g
|
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 -standby-timeout-ac 0
|
||||||
powercfg -change -hibernate-timeout-ac 0
|
powercfg -change -hibernate-timeout-ac 0
|
||||||
powercfg -change -monitor-timeout-ac 0
|
powercfg -change -monitor-timeout-ac 0
|
||||||
powercfg -change -disk-timeout-ac 0
|
powercfg -change -disk-timeout-ac 0
|
||||||
powercfg -h off >nul 2>&1
|
powercfg -h off >nul 2>&1
|
||||||
echo Sleep, hibernate, monitor timeout, and disk timeout disabled.
|
echo Ultimate Performance plan activated; sleep/hibernate disabled.
|
||||||
|
)
|
||||||
|
|
||||||
:: ── STEP 6: Disable notifications ───────────────────────────────────────────
|
:: -- STEP 6: Quiet the OS (notifications / UAC - modern Windows only) --------------
|
||||||
echo.
|
echo.
|
||||||
echo [6/8] Disabling notifications...
|
echo [6/7] Disabling notifications and UAC...
|
||||||
:: These are machine-wide Group Policy keys (HKLM) so they apply to EVERY user,
|
if "%ISXP%"=="1" (
|
||||||
:: including the auto-login kiosk account. Per-user HKCU tweaks would only touch
|
echo XP has no Action Center or UAC - nothing to disable.
|
||||||
:: the admin running this installer, not the account the pod actually runs as.
|
) else (
|
||||||
|
rem Machine-wide Group Policy keys (HKLM) so they apply to EVERY user,
|
||||||
:: Remove Action Center / Notification Center entirely
|
rem including the auto-login kiosk account.
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Explorer" ^
|
||||||
/v DisableNotificationCenter /t REG_DWORD /d 1 /f >nul
|
/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" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||||
/v NoToastApplicationNotification /t REG_DWORD /d 1 /f >nul
|
/v NoToastApplicationNotification /t REG_DWORD /d 1 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||||
/v NoToastApplicationNotificationOnLockScreen /t REG_DWORD /d 1 /f >nul
|
/v NoToastApplicationNotificationOnLockScreen /t REG_DWORD /d 1 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\PushNotifications" ^
|
||||||
/v NoCloudApplicationNotification /t REG_DWORD /d 1 /f >nul
|
/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" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||||
/v DisableSoftLanding /t REG_DWORD /d 1 /f >nul
|
/v DisableSoftLanding /t REG_DWORD /d 1 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||||
/v DisableWindowsSpotlightFeatures /t REG_DWORD /d 1 /f >nul
|
/v DisableWindowsSpotlightFeatures /t REG_DWORD /d 1 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ^
|
||||||
/v DisableWindowsConsumerFeatures /t REG_DWORD /d 1 /f >nul
|
/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" ^
|
reg add "HKLM\SOFTWARE\Microsoft\Windows Defender Security Center\Notifications" ^
|
||||||
/v DisableNotifications /t REG_DWORD /d 1 /f >nul
|
/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" ^
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||||
/v EnableLUA /t REG_DWORD /d 0 /f >nul
|
/v EnableLUA /t REG_DWORD /d 0 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||||
/v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul
|
/v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul
|
||||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
|
||||||
/v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul
|
/v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul
|
||||||
echo UAC disabled (takes effect after reboot).
|
echo Notifications disabled; UAC disabled ^(takes effect after reboot^).
|
||||||
|
)
|
||||||
|
|
||||||
:: ── STEP 8: Configure Agent auto-start and auto-login ────────────────────────
|
:: -- STEP 7: Auto-login --------------------------------------------------------------
|
||||||
echo.
|
echo.
|
||||||
echo [8/8] Configuring Agent auto-start and auto-login...
|
echo [7/7] Configuring auto-login...
|
||||||
|
:: Winlogon reads these on every boot (same keys since NT). AutoAdminLogon=1
|
||||||
set AUTORUN_KEY=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
:: signs DefaultUserName in with DefaultPassword without a prompt.
|
||||||
reg add "%AUTORUN_KEY%" /v "TeslaLauncherAgent" /t REG_SZ ^
|
|
||||||
/d "\"%AGENT_EXE%\"" /f >nul
|
|
||||||
echo Added TeslaLauncherAgent to HKLM\...\Run.
|
|
||||||
|
|
||||||
:: 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.
|
|
||||||
set WINLOGON=HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
|
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 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 DefaultUserName /t REG_SZ /d "%AUTOLOGIN_USER%" /f >nul
|
||||||
@@ -315,13 +316,15 @@ reg add "%WINLOGON%" /v DefaultDomainName /t REG_SZ /d "." /f >nu
|
|||||||
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.
|
:: Clear any logon-count cap that would disable auto-login after N boots.
|
||||||
reg delete "%WINLOGON%" /v AutoLogonCount /f >nul 2>&1
|
reg delete "%WINLOGON%" /v AutoLogonCount /f >nul 2>&1
|
||||||
:: Some builds require this flag off for a plain-text auto-login password to
|
if "%ISXP%"=="0" (
|
||||||
:: be honored (device-passwordless / Windows Hello preference).
|
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" ^
|
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device" ^
|
||||||
/v DevicePasswordLessBuildVersion /t REG_DWORD /d 0 /f >nul 2>&1
|
/v DevicePasswordLessBuildVersion /t REG_DWORD /d 0 /f >nul 2>&1
|
||||||
|
)
|
||||||
echo Auto-login configured for user "%AUTOLOGIN_USER%".
|
echo Auto-login configured for user "%AUTOLOGIN_USER%".
|
||||||
|
|
||||||
:: ── Done ──────────────────────────────────────────────────────────────────────
|
:: -- Done ----------------------------------------------------------------------------
|
||||||
echo.
|
echo.
|
||||||
echo ============================================================
|
echo ============================================================
|
||||||
echo Installation Complete!
|
echo Installation Complete!
|
||||||
@@ -332,9 +335,9 @@ echo Config dir : %DATA_DIR%
|
|||||||
echo Firewall : disabled (closed network)
|
echo Firewall : disabled (closed network)
|
||||||
echo.
|
echo.
|
||||||
echo Rebooting in 10 seconds...
|
echo Rebooting in 10 seconds...
|
||||||
echo The Service and Agent will start automatically in the correct order.
|
echo The launcher starts automatically at login (single binary, no service).
|
||||||
echo On first boot with DHCP, SecureConfig runs automatically —
|
echo On first boot with DHCP, SecureConfig runs automatically -
|
||||||
echo watch plasma or on main screen for the Request ID and Passphrase.
|
echo watch plasma or the main screen for the Request ID and Passphrase.
|
||||||
echo.
|
echo.
|
||||||
echo After reboot: use Console to install apps.
|
echo After reboot: use Console to install apps.
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
+5
-11
@@ -11,9 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{27C769F3
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaConsole.DiffTests", "Console\tests\TeslaConsole.DiffTests\TeslaConsole.DiffTests.csproj", "{467AA87A-FBD4-45D7-B8F8-9336C95884D2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaConsole.DiffTests", "Console\tests\TeslaConsole.DiffTests\TeslaConsole.DiffTests.csproj", "{467AA87A-FBD4-45D7-B8F8-9336C95884D2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherService", "Launcher\TeslaLauncherService.csproj", "{910D4404-B3A2-4217-B61C-D43E7F22CDAA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncher", "Launcher\TeslaLauncher.csproj", "{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeslaLauncherAgent", "Launcher\TeslaLauncherAgent.csproj", "{916DCDA5-3379-4383-96F0-AC7B26FAF64E}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\Tesla.Contract.csproj", "{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesla.Contract", "Contract\Tesla.Contract.csproj", "{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -35,14 +33,10 @@ Global
|
|||||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{467AA87A-FBD4-45D7-B8F8-9336C95884D2}.Release|Any CPU.Build.0 = 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
|
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{910D4404-B3A2-4217-B61C-D43E7F22CDAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E1B7C4A9-5D2C-4F0B-9A3E-7C61D4B8F0A2}.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
|
|
||||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Debug|Any CPU.ActiveCfg = Debug|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}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0B2E3F1F-56C4-4B1A-AC26-4BF34AA10F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|||||||
Reference in New Issue
Block a user