Deploy: pod install scaffold -- supervisor entry-point, postinstall, packaging
Network-agnostic, air-gapped pod install for the two DOSBox titles (BT/RP 4.10), fitting the existing TeslaConsole/TeslaLauncher pod-bay architecture. emulator/DEPLOYMENT-PLAN.md Full design: bridge on the one NIC (pods form a source-proven P2P TCP mesh, so NAT/slirp is out); launcher keeps the bay IP, the DOSBox guest bridges at bayIP+100 (egg/mesh/game endpoint); two-edit +100 convention (postinstall + console DOSBox flag); static, air-gapped, <=32 pods. emulator/pod-launch/ (C# net8 supervising entry-point) Creates a Job Object (KILL_ON_JOB_CLOSE), launches DOSBox-X + the render bridge into it and blocks -- kill this process and both die (kernel-enforced, even on a hard TerminateProcess of a hung session; verified). Mode dispatch: bt/rp wired (also serve camera + live mission-review via egg hostType); review + diagnostics recognized but fail clean until their DOS launch args are known. emulator/deploy/ (install side) postinstall.bat (thin elevated wrapper) + configure.ps1 (NIC detect, realnic bind as a contiguous letter-leading GUID fragment, game IP = bayIP+100, stable MAC, render net_*.conf templates, stamp WATTCP.CFG my_ip) + tokenized conf templates + package.ps1 (assemble the zip). Render/bind/stamp + packaging verified against a scratch tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
// pod-launch -- supervising launch entry-point for the two DOSBox titles.
|
||||
//
|
||||
// Contract (see ../DEPLOYMENT-PLAN.md "Launch dispatch"):
|
||||
// * TeslaConsole -> TeslaLauncher runs this exe in the game dir with args
|
||||
// selecting game/mode.
|
||||
// * It creates a Windows Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
// launches DOSBox-X AND the render bridge INTO the job, then BLOCKS on
|
||||
// DOSBox. It is the single authoritative handle for the session.
|
||||
// * Kill this process -> the kernel kills DOSBox + the renderer when the job's
|
||||
// last handle closes -- holds even on a hard TerminateProcess of a hung
|
||||
// DOSBox, where no cooperative shutdown can be relied on. No orphans.
|
||||
// * DOSBox exits on its own (mission end) -> we tear the job down (renderer
|
||||
// too) and return DOSBox's exit code.
|
||||
//
|
||||
// It MUST NOT be a batch/cmd/start/Start-Process hand-off: those decouple from
|
||||
// the child, which then survives the parent. This process HOLDS the job handle
|
||||
// and WAITS.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace VwePod
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
Options opt;
|
||||
try { opt = Options.Parse(args); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("pod-launch: " + ex.Message);
|
||||
Options.Usage();
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!opt.Mode.Supported)
|
||||
{
|
||||
// Recognized ExitCodeID mode with no emulator launch wired yet.
|
||||
// Fail clean with the reason instead of guessing a launch.
|
||||
Console.Error.WriteLine($"pod-launch: mode '{opt.Mode.Name}' is not supported on the emulator yet.");
|
||||
Console.Error.WriteLine(" " + opt.Mode.Note);
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (opt.DryRun)
|
||||
{
|
||||
Console.WriteLine("pod-launch DRY RUN");
|
||||
Console.WriteLine($" mode : {opt.Mode.Name}");
|
||||
Console.WriteLine($" layout : {opt.Layout}");
|
||||
Console.WriteLine($" root : {opt.Root}");
|
||||
Console.WriteLine($" dosbox : {opt.DosBox}");
|
||||
Console.WriteLine($" conf : {opt.Conf}");
|
||||
Console.WriteLine($" renderer : {(opt.RendererExe ?? "(pyw) " + opt.BridgeScript)}");
|
||||
Console.WriteLine($" awe-rom : {(opt.AweRom ?? "(none -> sound off)")}");
|
||||
Console.WriteLine($" work : {opt.Work}");
|
||||
Console.WriteLine($" bridge : pos={opt.BridgePos} size={opt.BridgeW}x{opt.BridgeH} dosbox@={opt.DosBoxX},{opt.DosBoxY}");
|
||||
Console.WriteLine($" flags : nobridge={opt.NoBridge} nosound={opt.NoSound} mipmap={opt.Mipmap} nofocus={opt.NoFocus}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(opt.Work);
|
||||
|
||||
// pcap backend needs wpcap.dll; without it the NE2000 comes up dead
|
||||
// ("NO PACKET DRIVER FOUND" at netnub). Don't trust the caller's PATH.
|
||||
PrependNpcapToPath();
|
||||
|
||||
// Fresh wire artifacts each run (the RIO tap appends across runs).
|
||||
TryDelete(Path.Combine(opt.Work, "live.fifodump"));
|
||||
TryDelete(Path.Combine(opt.Work, "riotap.txt"));
|
||||
|
||||
ApplySharedEnv(opt); // env DOSBox inherits (VPX_*/VWE_*/layout)
|
||||
|
||||
JobObject job = null;
|
||||
Process dosbox = null;
|
||||
Process bridge = null;
|
||||
try
|
||||
{
|
||||
job = new JobObject();
|
||||
|
||||
// ---- DOSBox-X: the pod. Launched into the job first. ----
|
||||
string dbArgs = "-conf \"" + opt.Conf + "\"";
|
||||
dosbox = ChildProcess.StartInJob(
|
||||
job, opt.DosBox, dbArgs, null,
|
||||
Path.Combine(opt.Work, "pod_out.txt"),
|
||||
Path.Combine(opt.Work, "pod_err.txt"),
|
||||
opt.DosBoxDir);
|
||||
Console.WriteLine($"pod PID {dosbox.Id} conf={Path.GetFileName(opt.Conf)} work={opt.Work}");
|
||||
if (!opt.NoSound)
|
||||
Console.WriteLine("sound ON: boot adds ~4 min for the SoundFont upload");
|
||||
|
||||
// ---- Render bridge (Dave's GL): also into the job. ----
|
||||
if (!opt.NoBridge)
|
||||
{
|
||||
var benv = new Dictionary<string, string>
|
||||
{
|
||||
// SDL honors the window position at creation. Set it ONLY
|
||||
// for the bridge -- setting it globally would also move
|
||||
// DOSBox-X's own SDL window.
|
||||
["SDL_VIDEO_WINDOW_POS"] = opt.BridgePos,
|
||||
["BRIDGE_W"] = opt.BridgeW.ToString(),
|
||||
["BRIDGE_H"] = opt.BridgeH.ToString(),
|
||||
};
|
||||
if (opt.Mipmap) benv["VRVIEW_MIPMAP"] = "1";
|
||||
|
||||
string brExe, brArgs;
|
||||
if (opt.RendererExe != null)
|
||||
{
|
||||
// Deploy: a frozen renderer exe (renderer packaging is an
|
||||
// OPEN item -- freeze vs embedded Python).
|
||||
brExe = opt.RendererExe;
|
||||
brArgs = "tcp:8621 \"" + Path.Combine(opt.Work, "live.fifodump") + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dev: windowless Python launcher (pyw), NOT py (py opens a
|
||||
// console that parks over the displays).
|
||||
brExe = "pyw";
|
||||
brArgs = "-3.13 \"" + opt.BridgeScript + "\" tcp:8621 \"" +
|
||||
Path.Combine(opt.Work, "live.fifodump") + "\"";
|
||||
}
|
||||
bridge = ChildProcess.StartInJob(
|
||||
job, brExe, brArgs, benv,
|
||||
Path.Combine(opt.Work, "bridge_out.txt"),
|
||||
Path.Combine(opt.Work, "bridge_err.txt"),
|
||||
null);
|
||||
Console.WriteLine($"bridge PID {bridge.Id} [GL]");
|
||||
}
|
||||
|
||||
// Ctrl-C / console close: tear the job down (kills children) so an
|
||||
// interactive stop is as clean as a TeslaLauncher kill. (A hard
|
||||
// TerminateProcess skips this, but the job still closes with us.)
|
||||
Console.CancelKeyPress += (s, e) => { e.Cancel = false; job.Dispose(); };
|
||||
|
||||
// Window layout LAST so it wins over the child windows: renderer
|
||||
// -> always-on-top; DOSBox -> parked + foreground (keeps the emu
|
||||
// thread at foreground priority so the RIO ACK deadline isn't
|
||||
// missed). Give the windows a moment to exist first.
|
||||
if (!opt.NoFocus)
|
||||
{
|
||||
Thread.Sleep(3000);
|
||||
Console.WriteLine("focus: " + Focus.Apply(opt.DosBoxX, opt.DosBoxY));
|
||||
}
|
||||
|
||||
// Authoritative for the whole session: block until DOSBox exits.
|
||||
dosbox.WaitForExit();
|
||||
int code = dosbox.ExitCode;
|
||||
Console.WriteLine($"pod exited ({code}); tearing down session");
|
||||
return code;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Closing the job handle fires KILL_ON_JOB_CLOSE -> the renderer
|
||||
// (and anything DOSBox spawned) dies. This runs on normal exit;
|
||||
// on a hard kill of THIS process the OS does the same for us.
|
||||
job?.Dispose();
|
||||
dosbox?.Dispose();
|
||||
bridge?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrependNpcapToPath()
|
||||
{
|
||||
const string npcap = @"C:\Windows\System32\Npcap";
|
||||
string path = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
if (path.IndexOf("\\Npcap", StringComparison.OrdinalIgnoreCase) < 0 && Directory.Exists(npcap))
|
||||
Environment.SetEnvironmentVariable("PATH", npcap + ";" + path);
|
||||
}
|
||||
|
||||
// Env every child inherits (DOSBox reads these). Bridge-only vars are set
|
||||
// per-process in Main so they don't leak onto DOSBox's SDL window.
|
||||
private static void ApplySharedEnv(Options opt)
|
||||
{
|
||||
void Set(string k, string v) => Environment.SetEnvironmentVariable(k, v);
|
||||
|
||||
Set("VPXLOG", Path.Combine(opt.Work, "vpxresp.txt"));
|
||||
Set("VPX_RESPOND", "1");
|
||||
Set("VPX_RENDER", "1");
|
||||
Set("VPX_DUMPDIR", opt.Work);
|
||||
Set("VPX_FIFODUMP", Path.Combine(opt.Work, "live.fifodump"));
|
||||
Set("VPX_FIFOSOCK", "8621");
|
||||
Set("RIO_TAP", Path.Combine(opt.Work, "riotap.txt"));
|
||||
Set("VPX_NOMAIN", "1"); // the GL bridge is the main out-the-window view
|
||||
Set("VPX_CLEAR", "0,0,0");
|
||||
|
||||
// Display layout.
|
||||
if (opt.Layout == LayoutMode.Cockpit)
|
||||
{
|
||||
Set("VPX_COCKPIT", "1");
|
||||
Environment.SetEnvironmentVariable("VPX_EXPLODE", null);
|
||||
// Head windows (edit for the rig; deployment variant of the
|
||||
// pod_deploy.ps1 RECTS block -- OPEN item).
|
||||
Set("VPX_WIN0", opt.RadarRect); // radar (HEAD C)
|
||||
Set("VPX_WIN4", opt.Mfd3Rect); // 3-color MFD (HEAD B)
|
||||
Set("VPX_WIN3", opt.Mfd2Rect); // 2-color MFD (HEAD A)
|
||||
}
|
||||
else
|
||||
{
|
||||
Set("VPX_EXPLODE", "1"); // 7-window debug spread
|
||||
Environment.SetEnvironmentVariable("VPX_COCKPIT", null);
|
||||
}
|
||||
|
||||
if (opt.NoSound)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("VWE_AWE32", null);
|
||||
Environment.SetEnvironmentVariable("VWE_AWE_ROM", null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Set("VWE_AWE32", "1");
|
||||
if (opt.AweRom != null) Set("VWE_AWE_ROM", opt.AweRom);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryDelete(string p)
|
||||
{
|
||||
try { if (File.Exists(p)) File.Delete(p); } catch { /* in use / gone */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user