Delegate Electron host lifecycle to EveElectron
This commit is contained in:
parent
3678a9e728
commit
8894de671e
2 changed files with 34 additions and 91 deletions
|
|
@ -7,9 +7,7 @@ import { fileURLToPath } from "node:url";
|
||||||
import {
|
import {
|
||||||
AetheriaCultMeshClient,
|
AetheriaCultMeshClient,
|
||||||
} from "./aetheria-cultmesh.js";
|
} from "./aetheria-cultmesh.js";
|
||||||
import { AetheriaRtsIpcChannels } from "./aetheria-rts-generated-bindings.js";
|
import { startEveElectronProviderHost } from "@gamecult/eve-electron/live-provider-host";
|
||||||
import { createEveElectronWindow, registerEveWindowControls } from "@gamecult/eve-electron";
|
|
||||||
import { EveCultMeshProviderClient } from "@gamecult/eve-electron/provider-client";
|
|
||||||
import { CultMesh } from "cultmesh-ts";
|
import { CultMesh } from "cultmesh-ts";
|
||||||
import { encode } from "@msgpack/msgpack";
|
import { encode } from "@msgpack/msgpack";
|
||||||
|
|
||||||
|
|
@ -36,20 +34,15 @@ const assetProtocol = "aetheria-cdn";
|
||||||
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
|
let daemonProcess: ChildProcessWithoutNullStreams | null = null;
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let aetheriaClient: AetheriaCultMeshClient | null = null;
|
let aetheriaClient: AetheriaCultMeshClient | null = null;
|
||||||
let eveProviderClient: EveCultMeshProviderClient | null = null;
|
let eveHost: { close(): Promise<void>; window: BrowserWindow } | null = null;
|
||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
writeElectronSmokeResult({ ok: false, stage: "electron-ready" });
|
writeElectronSmokeResult({ ok: false, stage: "electron-ready" });
|
||||||
mainWindow = createWindow();
|
|
||||||
registerIpc();
|
|
||||||
registerAssetProtocol();
|
|
||||||
showStartup("Preparing Aetheria Starbridge", "Building daemon if needed.");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (launchLocalDaemon) {
|
if (launchLocalDaemon) {
|
||||||
await ensureDotnetBuild();
|
await ensureDotnetBuild();
|
||||||
showStartup("Launching Aetheria Starbridge", "Starting the Aetheria daemon.");
|
|
||||||
mkdirSync(runtimeRoot, { recursive: true });
|
mkdirSync(runtimeRoot, { recursive: true });
|
||||||
daemonProcess = startDotnet("aetheria-daemon", daemonDll, [
|
daemonProcess = startDotnet("aetheria-daemon", daemonDll, [
|
||||||
"--state",
|
"--state",
|
||||||
|
|
@ -71,7 +64,6 @@ app.whenReady().then(async () => {
|
||||||
"--no-odin-announcements",
|
"--no-odin-announcements",
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
showStartup("Connecting Aetheria Starbridge", `Using daemon ${daemonCultMeshUri}.`);
|
|
||||||
mkdirSync(runtimeRoot, { recursive: true });
|
mkdirSync(runtimeRoot, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,22 +79,28 @@ app.whenReady().then(async () => {
|
||||||
"aetheria-electron-client",
|
"aetheria-electron-client",
|
||||||
{ publicationMode: launchLocalDaemon ? "local" : "remote" });
|
{ publicationMode: launchLocalDaemon ? "local" : "remote" });
|
||||||
|
|
||||||
eveProviderClient = new EveCultMeshProviderClient({
|
registerAssetProtocol();
|
||||||
providerId: "aetheria.daemon",
|
eveHost = await startEveElectronProviderHost({
|
||||||
advertisementRecordRef: "eve:provider:aetheria.daemon",
|
electron: { app, BrowserWindow, ipcMain, shell },
|
||||||
peerId: daemonId,
|
dependencies: { CultMesh, encode },
|
||||||
verseId,
|
providerTarget: {
|
||||||
role: "aetheria-daemon",
|
providerId: "aetheria.daemon",
|
||||||
endpoints: localDaemonRudpEndpoint ? [localDaemonRudpEndpoint] : [],
|
advertisementRecordRef: "eve:provider:aetheria.daemon",
|
||||||
}, { CultMesh, encode }, { runtimeId: "eve-electron-aetheria" });
|
peerId: daemonId,
|
||||||
writeElectronSmokeResult({ ok: false, stage: "provider-clients-ready" });
|
verseId,
|
||||||
|
role: "aetheria-daemon",
|
||||||
showStartup("Launching Aetheria Starbridge", "Waiting for the daemon CultMesh frame.");
|
endpoints: localDaemonRudpEndpoint ? [localDaemonRudpEndpoint] : [],
|
||||||
await aetheriaClient.waitForFrame(30000);
|
},
|
||||||
writeElectronSmokeResult({ ok: false, stage: "daemon-frame-ready" });
|
renderer: rendererIndex,
|
||||||
await mainWindow.loadFile(rendererIndex, {
|
runtimeId: "eve-electron-aetheria",
|
||||||
query: { surface: process.env.EVE_SURFACE_ID || "aetheria.game" },
|
surfaceId: process.env.EVE_SURFACE_ID || "aetheria.game",
|
||||||
|
window: {
|
||||||
|
show: !electronSmoke,
|
||||||
|
backgroundColor: "#0b1016",
|
||||||
|
title: "Aetheria Starbridge",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
mainWindow = eveHost.window;
|
||||||
writeElectronSmokeResult({ ok: false, stage: "renderer-loaded" });
|
writeElectronSmokeResult({ ok: false, stage: "renderer-loaded" });
|
||||||
if (electronSmoke) {
|
if (electronSmoke) {
|
||||||
await withTimeout(mainWindow.webContents.executeJavaScript("window.eveProvider.providerAdvertisement()"), 10000, "provider advertisement smoke");
|
await withTimeout(mainWindow.webContents.executeJavaScript("window.eveProvider.providerAdvertisement()"), 10000, "provider advertisement smoke");
|
||||||
|
|
@ -120,7 +118,6 @@ app.whenReady().then(async () => {
|
||||||
exitElectronSmoke(0);
|
exitElectronSmoke(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await showFailure(error);
|
|
||||||
if (electronSmoke) {
|
if (electronSmoke) {
|
||||||
writeElectronSmokeResult({
|
writeElectronSmokeResult({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -128,30 +125,18 @@ app.whenReady().then(async () => {
|
||||||
});
|
});
|
||||||
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
||||||
exitElectronSmoke(1);
|
exitElectronSmoke(1);
|
||||||
|
} else {
|
||||||
|
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
isQuitting = true;
|
isQuitting = true;
|
||||||
void aetheriaClient?.close();
|
void aetheriaClient?.close();
|
||||||
void eveProviderClient?.close();
|
|
||||||
stopChild(daemonProcess);
|
stopChild(daemonProcess);
|
||||||
});
|
});
|
||||||
|
|
||||||
function createWindow(): BrowserWindow {
|
|
||||||
return createEveElectronWindow({
|
|
||||||
show: !electronSmoke,
|
|
||||||
backgroundColor: "#0b1016",
|
|
||||||
title: "Aetheria Starbridge",
|
|
||||||
preload: resolve(projectRoot, "node_modules", "@gamecult", "eve-electron", "src", "eve-provider-preload-entry.cjs"),
|
|
||||||
}, { BrowserWindow, shell });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, unknown>> {
|
async function runElectronSmoke(window: BrowserWindow): Promise<Record<string, unknown>> {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
let lastResult: Record<string, unknown> | null = null;
|
let lastResult: Record<string, unknown> | null = null;
|
||||||
|
|
@ -233,7 +218,6 @@ function isElectronSmokeReady(result: Record<string, unknown>): boolean {
|
||||||
const renderSplatsResolved = objectValue(result.renderSplatsResolved);
|
const renderSplatsResolved = objectValue(result.renderSplatsResolved);
|
||||||
const gravityResolved = objectValue(result.gravityResolved);
|
const gravityResolved = objectValue(result.gravityResolved);
|
||||||
const objectsResolved = objectValue(result.objectsResolved);
|
const objectsResolved = objectValue(result.objectsResolved);
|
||||||
const renderSplatsViewport = objectValue(result.renderSplatsViewport);
|
|
||||||
const eveFieldSurface = objectValue(result.eveFieldSurface);
|
const eveFieldSurface = objectValue(result.eveFieldSurface);
|
||||||
const eveReceipt = objectValue(result.eveReceipt);
|
const eveReceipt = objectValue(result.eveReceipt);
|
||||||
return result.hasApi === true &&
|
return result.hasApi === true &&
|
||||||
|
|
@ -268,10 +252,6 @@ function stringValue(value: unknown): string {
|
||||||
return typeof value === "string" ? value : "";
|
return typeof value === "string" ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function numberValue(value: unknown): number {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function objectValue(value: unknown): Record<string, unknown> | null {
|
function objectValue(value: unknown): Record<string, unknown> | null {
|
||||||
return value != null && typeof value === "object" ? value as Record<string, unknown> : null;
|
return value != null && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||||
}
|
}
|
||||||
|
|
@ -291,8 +271,8 @@ function exitElectronSmoke(exitCode: number): void {
|
||||||
isQuitting = true;
|
isQuitting = true;
|
||||||
void aetheriaClient?.close();
|
void aetheriaClient?.close();
|
||||||
aetheriaClient = null;
|
aetheriaClient = null;
|
||||||
void eveProviderClient?.close();
|
void eveHost?.close();
|
||||||
eveProviderClient = null;
|
eveHost = null;
|
||||||
stopChild(daemonProcess);
|
stopChild(daemonProcess);
|
||||||
daemonProcess = null;
|
daemonProcess = null;
|
||||||
mainWindow?.destroy();
|
mainWindow?.destroy();
|
||||||
|
|
@ -300,45 +280,6 @@ function exitElectronSmoke(exitCode: number): void {
|
||||||
app.exit(exitCode);
|
app.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerIpc(): void {
|
|
||||||
ipcMain.handle(AetheriaRtsIpcChannels.eveProviderAdvertisement, () => requireEveProviderClient().providerAdvertisement());
|
|
||||||
ipcMain.handle(AetheriaRtsIpcChannels.eveSurface, (_event, request: { surfaceId?: string }) =>
|
|
||||||
requireEveProviderClient().surface(request?.surfaceId));
|
|
||||||
ipcMain.handle(AetheriaRtsIpcChannels.submitEveCommand, async (_event, request: Record<string, unknown>) => {
|
|
||||||
const submission = await requireEveProviderClient().submitCommand(request);
|
|
||||||
const receipt = await waitForEveReceipt(submission, 5000);
|
|
||||||
return { ...submission, ...receipt, commandId: submission.commandId, accepted: receiptStateAccepted(receipt) };
|
|
||||||
});
|
|
||||||
ipcMain.handle(AetheriaRtsIpcChannels.eveDocument, (_event, request) =>
|
|
||||||
requireEveProviderClient().resolveDocument(request));
|
|
||||||
registerEveWindowControls(ipcMain, BrowserWindow, "eve-electron:window-control");
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireEveProviderClient(): EveCultMeshProviderClient {
|
|
||||||
if (!eveProviderClient)
|
|
||||||
throw new Error("Eve CultMesh provider client is not initialized.");
|
|
||||||
return eveProviderClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForEveReceipt(submission: Record<string, unknown>, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
||||||
const started = Date.now();
|
|
||||||
let lastError: unknown;
|
|
||||||
while (Date.now() - started < timeoutMs) {
|
|
||||||
try {
|
|
||||||
return objectValue(await requireEveProviderClient().receipt(submission)) ?? {};
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error;
|
|
||||||
await delay(50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastError ?? new Error(`Timed out waiting for Eve receipt ${String(submission.commandId ?? "")}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiptStateAccepted(receipt: Record<string, unknown>): boolean {
|
|
||||||
const state = stringValue(receipt.state ?? receipt[4]).toLowerCase();
|
|
||||||
return state === "accepted" || state === "reconciled";
|
|
||||||
}
|
|
||||||
|
|
||||||
function withTimeout<T>(work: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
function withTimeout<T>(work: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
work,
|
work,
|
||||||
|
|
@ -472,10 +413,6 @@ function delay(milliseconds: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStartup(title: string, detail: string): void {
|
|
||||||
void mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml(title, detail))}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showFailure(error: unknown): Promise<void> {
|
async function showFailure(error: unknown): Promise<void> {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
await mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml("Aetheria Starbridge failed to start", message))}`);
|
await mainWindow?.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startupHtml("Aetheria Starbridge failed to start", message))}`);
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,15 @@ if ($appText.Contains('aetheria.rts.electron') -or
|
||||||
}
|
}
|
||||||
|
|
||||||
$mainText = Get-Content Electron\main.ts -Raw
|
$mainText = Get-Content Electron\main.ts -Raw
|
||||||
if (-not $mainText.Contains('eve-provider-preload-entry.cjs') -or (Test-Path Electron\preload.cjs)) {
|
if (-not $mainText.Contains('startEveElectronProviderHost') -or (Test-Path Electron\preload.cjs)) {
|
||||||
Write-Error "Stage 7C Electron RTS verifier failed: Electron does not consume the EveElectron-owned preload entry."
|
Write-Error "Stage 7C Electron RTS verifier failed: Electron does not consume the EveElectron-owned preload entry."
|
||||||
}
|
}
|
||||||
|
$hostRuntimeText = Get-Content node_modules\@gamecult\eve-electron\src\live-provider-host.mjs -Raw
|
||||||
|
if (-not $hostRuntimeText.Contains('eve-provider-preload-entry.cjs') -or
|
||||||
|
-not $hostRuntimeText.Contains('registerEveProviderIpc') -or
|
||||||
|
-not $hostRuntimeText.Contains('createEveElectronWindow')) {
|
||||||
|
Write-Error "Stage 7C Electron RTS verifier failed: EveElectron does not own the live provider host lifecycle."
|
||||||
|
}
|
||||||
if (-not $mainText.Contains('await eveProvider.providerAdvertisement()')) {
|
if (-not $mainText.Contains('await eveProvider.providerAdvertisement()')) {
|
||||||
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify provider advertisement discovery through preload."
|
Write-Error "Stage 7C Electron RTS verifier failed: Electron smoke does not verify provider advertisement discovery through preload."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue