XP11: single net40 binary runs on XP SP3 through Windows 11
Merge TeslaLauncherService (Session 0) + TeslaLauncherAgent (tray) into one userland TeslaLauncher.exe. The split existed only to work around Vista+ Session 0 isolation; running everything in the auto-logged-in admin session needs no service, no named pipe, and no flat<->wire conversion layer. The original Elsewhen software was likewise a single binary. net40 is the newest framework XP SP3 can install, and net40 assemblies load in-place on the 4.8 runtime in Win10/11 -- one exe covers both. - Contract: multi-target net48;net40. The net40 leg of PodRpcProtocol uses Newtonsoft.Json (STJ has no net40 target); JSON is shape-identical on the wire, and the request reader keeps date strings raw so Ping echoes byte-identically. Console keeps the untouched net48/STJ leg. - MiniZip.cs: central-directory ZIP extractor (stored/deflate/ZIP64) since net40 has no ZipFile; zip-slip guarded. - SecureConfig: RSACryptoServiceProvider instead of RSA.Create (net46+), no leaveOpen BinaryWriter/Reader, struct instead of ValueTuple, and no SetCompatibleTextRenderingDefault on the passcode thread (the merged app already has a form). netsh "interface ip" syntax was already XP-correct. - Volume: nircmd -> CoreAudio (Vista+) -> winmm waveOutSetVolume (XP). - Paths: CommonApplicationData resolved per-OS (XP has no C:\ProgramData); launcher log moved next to the key/config in the data dir. - install.bat: dual-OS (cacls/icacls, netsh firewall/advfirewall, dism and UAC/notification steps skipped on XP, .NET 4.0 redist check, XP DHCP reset via netsh); no service registration -- HKLM Run key + auto-login on both; RegisterApplicationRestart supplies crash-restart on Vista+. - Bench switches: /skipconfig, /port:NNNN. Verified: Console + diff suite (103 tests) still green on the net48 leg; E2E smoke test drove the net40 launcher over real TCP with the Console's own PodManagerConnection (STJ client vs Newtonsoft server) -- Ping echo, volume, app registry, launch/kill/watch, InstallProduct zip transfer + extract, and uninstall orphan cleanup: 17/17 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,10 +16,22 @@
|
||||
// one file, so the request/response shape cannot drift.
|
||||
// =============================================================================
|
||||
|
||||
// NET40 (XP11 single-binary launcher): System.Text.Json has no net40 target, so
|
||||
// this leg serializes with Newtonsoft.Json instead. Same framing, same JSON shape
|
||||
// (PascalCase member names, fields included, Guids as strings, ISO-8601 dates) —
|
||||
// an STJ client and a Newtonsoft server interoperate byte-compatibly on the wire.
|
||||
// Arguments surface as JToken here instead of JsonElement.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
#if NET40
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
#else
|
||||
using System.Text.Json;
|
||||
#endif
|
||||
|
||||
namespace Tesla.Net
|
||||
{
|
||||
@@ -27,13 +39,21 @@ namespace Tesla.Net
|
||||
public sealed class RpcRequest
|
||||
{
|
||||
public string Method { get; set; }
|
||||
#if NET40
|
||||
public List<JToken> Args { get; set; }
|
||||
#else
|
||||
public List<JsonElement> Args { get; set; }
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>One RPC result: the return value as JSON, or an error message.</summary>
|
||||
public sealed class RpcResponse
|
||||
{
|
||||
#if NET40
|
||||
public JToken Result { get; set; } // JSON null for void / null
|
||||
#else
|
||||
public JsonElement Result { get; set; } // JsonValueKind.Null for void / null
|
||||
#endif
|
||||
public string Error { get; set; } // null on success
|
||||
}
|
||||
|
||||
@@ -45,12 +65,18 @@ namespace Tesla.Net
|
||||
/// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
|
||||
public const int MaxFrameBytes = 16 * 1024 * 1024;
|
||||
|
||||
#if NET40
|
||||
// Newtonsoft serializes public fields of the wire types by default, and
|
||||
// writes ISO-8601 dates / string Guids like STJ — no special options needed.
|
||||
public static readonly JsonSerializer JsonOptions = JsonSerializer.CreateDefault();
|
||||
#else
|
||||
public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
// The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
|
||||
// FIELDS, which System.Text.Json ignores unless this is set.
|
||||
IncludeFields = true,
|
||||
};
|
||||
#endif
|
||||
|
||||
// ── Framing ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -88,6 +114,25 @@ namespace Tesla.Net
|
||||
|
||||
// ── Request ──────────────────────────────────────────────────────────
|
||||
|
||||
#if NET40
|
||||
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||
{
|
||||
var req = new RpcRequest { Method = method, Args = new List<JToken>() };
|
||||
if (args != null)
|
||||
foreach (var a in args)
|
||||
req.Args.Add(a == null ? JValue.CreateNull() : JToken.FromObject(a, JsonOptions));
|
||||
WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(req)));
|
||||
}
|
||||
|
||||
public static RpcRequest ReadRequest(Stream stream)
|
||||
=> JsonConvert.DeserializeObject<RpcRequest>(
|
||||
Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings);
|
||||
|
||||
// Keep date-looking strings as raw strings so an echoed argument (Ping)
|
||||
// goes back byte-identical instead of reformatted through DateTime.
|
||||
private static readonly JsonSerializerSettings ReadSettings =
|
||||
new JsonSerializerSettings { DateParseHandling = DateParseHandling.None };
|
||||
#else
|
||||
public static void WriteRequest(Stream stream, string method, object[] args)
|
||||
{
|
||||
var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
|
||||
@@ -99,9 +144,26 @@ namespace Tesla.Net
|
||||
|
||||
public static RpcRequest ReadRequest(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);
|
||||
#endif
|
||||
|
||||
// ── Response ─────────────────────────────────────────────────────────
|
||||
|
||||
#if NET40
|
||||
public static void WriteResponse(Stream stream, object result, string error)
|
||||
{
|
||||
object payload = error == null ? result : null;
|
||||
var resp = new RpcResponse
|
||||
{
|
||||
Result = payload == null ? JValue.CreateNull() : JToken.FromObject(payload, JsonOptions),
|
||||
Error = error,
|
||||
};
|
||||
WriteFrame(stream, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(resp)));
|
||||
}
|
||||
|
||||
public static RpcResponse ReadResponse(Stream stream)
|
||||
=> JsonConvert.DeserializeObject<RpcResponse>(
|
||||
Encoding.UTF8.GetString(ReadFrame(stream)), ReadSettings);
|
||||
#else
|
||||
public static void WriteResponse(Stream stream, object result, string error)
|
||||
{
|
||||
var resp = new RpcResponse
|
||||
@@ -116,5 +178,6 @@ namespace Tesla.Net
|
||||
|
||||
public static RpcResponse ReadResponse(Stream stream)
|
||||
=> JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user