The Launcher's XP11 port (8730b9b) now extends to everything: one net40
flavor across Console, vPOD, Contract, and SecureConfig (Newtonsoft.Json
everywhere; the net48/System.Text.Json legs and their #if splits are gone
since nothing consumed them).
Console (net40, single TFM like the Launcher):
- The ~31 BinaryFormatter bitmap blobs in the .resx files became raw
embedded files under assets/icons/ (extracted byte-faithfully via a
serialization surrogate — the animated square_throbber.gif survives),
loaded by Properties.Resources.EmbeddedBitmap/EmbeddedIcon. Reason:
System.Resources.Extensions' DeserializingResourceReader is net461+
and cannot load on net40. Strings stay in the .resx.
- IReadOnlyList -> IList in AppRegistry (net45+ interface).
vPOD (net40, single TFM):
- Zip extraction now shares the Launcher's MiniZip.cs (linked source), so
the diff-test install round-trip exercises it against ZipArchive zips.
- RPC args as JTokens; LaunchApps.json persistence via Newtonsoft;
Thread.VolatileRead instead of Volatile.Read.
Contract/SecureConfig: net40-only; Client/** (PodManagerConnection) now
ships in the one build. The Launcher package gains
TeslaSecureConfiguration.dll as a dependency of the client half.
Tests: the net48 xunit host loads the net40 assemblies (both CLR4), so
the suite exercises exactly what ships — 106/106 green. Also verified
live: net40 console provisioned, managed, and ran a full RP mission
against net40 vPOD (beacon/passphrase/RSA, 53290 RPC, egg load,
Run/Stop Mission).
Version: 4.11.4.3 across Launcher, Console, and vPOD (vPOD joins the
suite version line; was 1.0.0). Ship the dotNetFx40 redistributable in
Launcher/assets for XP-era pods.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
235 lines
7.0 KiB
C#
235 lines
7.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Windows.Forms;
|
|
using System.Xml;
|
|
using Tesla.Net;
|
|
|
|
namespace TeslaConsole;
|
|
|
|
/// <summary>Role a launch entry fills; only meaningful for products that prompt with the start-params dialog.</summary>
|
|
public enum AppHostType
|
|
{
|
|
None,
|
|
GameClient,
|
|
LiveCamera,
|
|
MissionReview
|
|
}
|
|
|
|
/// <summary>One launchable application a product registers on a pod.</summary>
|
|
public class LaunchEntryDefinition
|
|
{
|
|
public Guid LaunchKey;
|
|
|
|
public string DisplayName = "";
|
|
|
|
public string Exe = "";
|
|
|
|
/// <summary>Arguments; the token "{res}" expands to " -res W H" when a custom resolution is chosen, else "".</summary>
|
|
public string Args = "";
|
|
|
|
/// <summary>Optional; when null/empty the working directory is derived from <see cref="Exe"/>.</summary>
|
|
public string WorkingDirectory;
|
|
|
|
public bool AutoRestart = true;
|
|
|
|
public AppHostType HostType;
|
|
|
|
/// <summary>Builds the wire <see cref="LaunchData"/> for this entry, expanding {res} from the optional resolution.</summary>
|
|
public LaunchData ToLaunchData(Size? resolution)
|
|
{
|
|
string resArg = resolution.HasValue
|
|
? $" -res {resolution.Value.Width} {resolution.Value.Height}"
|
|
: "";
|
|
LaunchData data = default(LaunchData);
|
|
data.LaunchPair.DisplayName = DisplayName;
|
|
data.LaunchPair.LaunchKey = LaunchKey;
|
|
data.ExeFile = Exe;
|
|
data.Arguments = (Args ?? "").Replace("{res}", resArg);
|
|
data.WorkingDirectory = string.IsNullOrEmpty(WorkingDirectory)
|
|
? Path.GetDirectoryName(Exe)
|
|
: WorkingDirectory;
|
|
data.AutoRestart = AutoRestart;
|
|
return data;
|
|
}
|
|
}
|
|
|
|
/// <summary>An installable product: an install/package key plus the launch entries it registers.</summary>
|
|
public class ProductDefinition
|
|
{
|
|
public Guid Id;
|
|
|
|
public string Name = "";
|
|
|
|
public string MenuText = "";
|
|
|
|
/// <summary>When true, installing prompts for resolution + roles and only selected-role entries are registered.</summary>
|
|
public bool HostTypeDialog;
|
|
|
|
public List<LaunchEntryDefinition> Entries = new List<LaunchEntryDefinition>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Data-driven catalog of installable products, loaded from RedPlanet\Apps.xml (next to the exe).
|
|
/// Replaces the products that were hardcoded in <see cref="SiteManagement"/>.
|
|
/// </summary>
|
|
public static class AppRegistry
|
|
{
|
|
private static readonly List<ProductDefinition> mProducts = new List<ProductDefinition>();
|
|
|
|
private static readonly Dictionary<Guid, ProductDefinition> mById = new Dictionary<Guid, ProductDefinition>();
|
|
|
|
// IList, not IReadOnlyList: the read-only interfaces are net45+ and the
|
|
// XP11 console targets net40. Callers only enumerate/index it.
|
|
public static IList<ProductDefinition> Products => mProducts;
|
|
|
|
public static string CatalogPath =>
|
|
Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "RedPlanet\\Apps.xml");
|
|
|
|
static AppRegistry()
|
|
{
|
|
Reload();
|
|
}
|
|
|
|
public static ProductDefinition GetProduct(Guid id)
|
|
{
|
|
return mById.TryGetValue(id, out var product) ? product : null;
|
|
}
|
|
|
|
/// <summary>(Re)loads the catalog from <see cref="CatalogPath"/>. Safe if the file is missing — yields an empty catalog.</summary>
|
|
public static void Reload()
|
|
{
|
|
LoadFromFile(CatalogPath);
|
|
}
|
|
|
|
/// <summary>Loads the catalog from an explicit path. Safe if the file is missing — yields an empty catalog.</summary>
|
|
public static void LoadFromFile(string path)
|
|
{
|
|
mProducts.Clear();
|
|
mById.Clear();
|
|
if (!File.Exists(path))
|
|
{
|
|
return;
|
|
}
|
|
XmlDocument doc = new XmlDocument();
|
|
doc.Load(path);
|
|
foreach (XmlNode node in doc.DocumentElement.ChildNodes)
|
|
{
|
|
if (node.NodeType == XmlNodeType.Element && node.Name == "Product")
|
|
{
|
|
ProductDefinition product = ParseProduct(node);
|
|
mProducts.Add(product);
|
|
mById[product.Id] = product;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Adds a product to the catalog and persists the whole catalog to disk.</summary>
|
|
public static void AddProduct(ProductDefinition product)
|
|
{
|
|
mProducts.Add(product);
|
|
mById[product.Id] = product;
|
|
Save();
|
|
}
|
|
|
|
/// <summary>Writes the catalog back to <see cref="CatalogPath"/>.</summary>
|
|
public static void Save()
|
|
{
|
|
XmlDocument doc = new XmlDocument();
|
|
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
|
|
doc.AppendChild(doc.CreateComment(
|
|
" Tesla Console product catalog. Managed by Site Management (Add Product...). "));
|
|
XmlElement root = doc.CreateElement("AppCatalog");
|
|
doc.AppendChild(root);
|
|
foreach (ProductDefinition product in mProducts)
|
|
{
|
|
XmlElement pe = doc.CreateElement("Product");
|
|
pe.SetAttribute("id", product.Id.ToString("D"));
|
|
pe.SetAttribute("name", product.Name);
|
|
pe.SetAttribute("menuText", product.MenuText);
|
|
pe.SetAttribute("hostTypeDialog", product.HostTypeDialog ? "true" : "false");
|
|
foreach (LaunchEntryDefinition entry in product.Entries)
|
|
{
|
|
XmlElement le = doc.CreateElement("Launch");
|
|
le.SetAttribute("key", entry.LaunchKey.ToString("D"));
|
|
le.SetAttribute("displayName", entry.DisplayName);
|
|
le.SetAttribute("exe", entry.Exe);
|
|
le.SetAttribute("args", entry.Args ?? "");
|
|
if (!string.IsNullOrEmpty(entry.WorkingDirectory))
|
|
{
|
|
le.SetAttribute("workingDirectory", entry.WorkingDirectory);
|
|
}
|
|
le.SetAttribute("autoRestart", entry.AutoRestart ? "true" : "false");
|
|
le.SetAttribute("hostType", entry.HostType.ToString());
|
|
pe.AppendChild(le);
|
|
}
|
|
root.AppendChild(pe);
|
|
}
|
|
Directory.CreateDirectory(Path.GetDirectoryName(CatalogPath));
|
|
doc.Save(CatalogPath);
|
|
}
|
|
|
|
private static ProductDefinition ParseProduct(XmlNode node)
|
|
{
|
|
ProductDefinition product = new ProductDefinition
|
|
{
|
|
Id = ParseGuid(Attr(node, "id")),
|
|
Name = Attr(node, "name"),
|
|
MenuText = Attr(node, "menuText"),
|
|
HostTypeDialog = ParseBool(Attr(node, "hostTypeDialog"))
|
|
};
|
|
if (string.IsNullOrEmpty(product.MenuText))
|
|
{
|
|
product.MenuText = product.Name + "...";
|
|
}
|
|
foreach (XmlNode child in node.ChildNodes)
|
|
{
|
|
if (child.NodeType == XmlNodeType.Element && child.Name == "Launch")
|
|
{
|
|
product.Entries.Add(ParseLaunch(child));
|
|
}
|
|
}
|
|
return product;
|
|
}
|
|
|
|
private static LaunchEntryDefinition ParseLaunch(XmlNode node)
|
|
{
|
|
return new LaunchEntryDefinition
|
|
{
|
|
LaunchKey = ParseGuid(Attr(node, "key")),
|
|
DisplayName = Attr(node, "displayName"),
|
|
Exe = Attr(node, "exe"),
|
|
Args = Attr(node, "args"),
|
|
WorkingDirectory = HasAttr(node, "workingDirectory") ? Attr(node, "workingDirectory") : null,
|
|
AutoRestart = !HasAttr(node, "autoRestart") || ParseBool(Attr(node, "autoRestart")),
|
|
HostType = ParseHostType(Attr(node, "hostType"))
|
|
};
|
|
}
|
|
|
|
private static bool HasAttr(XmlNode node, string name)
|
|
{
|
|
return node.Attributes?[name] != null;
|
|
}
|
|
|
|
private static string Attr(XmlNode node, string name)
|
|
{
|
|
return node.Attributes?[name]?.InnerText ?? "";
|
|
}
|
|
|
|
private static Guid ParseGuid(string value)
|
|
{
|
|
return Guid.TryParse(value, out var guid) ? guid : Guid.Empty;
|
|
}
|
|
|
|
private static bool ParseBool(string value)
|
|
{
|
|
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || value == "1";
|
|
}
|
|
|
|
private static AppHostType ParseHostType(string value)
|
|
{
|
|
return Enum.TryParse<AppHostType>(value, ignoreCase: true, out var result) ? result : AppHostType.None;
|
|
}
|
|
}
|