mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: improve NodeCG runtime installation and relaunch behavior
This commit is contained in:
+19
-5
@@ -50,7 +50,7 @@ function focusExistingWindow(): void {
|
||||
}
|
||||
|
||||
async function launchApplication(): Promise<void> {
|
||||
const nodecgRootPath = prepareUserNodecgRuntime({
|
||||
const preparedRuntime = prepareUserNodecgRuntime({
|
||||
sourceRuntimePath: sourceNodecgRuntimePath,
|
||||
userDataPath: app.getPath("userData"),
|
||||
appVersion: app.getVersion(),
|
||||
@@ -58,9 +58,16 @@ async function launchApplication(): Promise<void> {
|
||||
log,
|
||||
});
|
||||
|
||||
if (preparedRuntime.installed && app.isPackaged) {
|
||||
log("Runtime was installed or refreshed; relaunching Scoreko before starting NodeCG.");
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
nodecgManager = createNodecgProcessManager({
|
||||
isDev,
|
||||
nodecgRootPath,
|
||||
nodecgRootPath: preparedRuntime.runtimePath,
|
||||
nodecgBaseUrl,
|
||||
appConfig,
|
||||
log,
|
||||
@@ -70,9 +77,7 @@ async function launchApplication(): Promise<void> {
|
||||
mainWindow = createMainWindow({ appConfig, rootPath, mainDashboardUrl });
|
||||
loadingWindow = createLoadingWindow({ appConfig, rootPath });
|
||||
|
||||
await nodecgManager.startNodecgProcess();
|
||||
|
||||
await nodecgManager.waitForNodecgReady(Date.now());
|
||||
await startNodecg();
|
||||
|
||||
if (!loadingWindow || loadingWindow.isDestroyed()) {
|
||||
return;
|
||||
@@ -99,6 +104,15 @@ async function launchApplication(): Promise<void> {
|
||||
closeLoadingWindow();
|
||||
}
|
||||
|
||||
async function startNodecg(): Promise<void> {
|
||||
if (!nodecgManager) {
|
||||
throw new Error("NodeCG process manager is not initialized.");
|
||||
}
|
||||
|
||||
await nodecgManager.startNodecgProcess();
|
||||
await nodecgManager.waitForNodecgReady(Date.now());
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
|
||||
@@ -80,7 +80,8 @@ export function createNodecgProcessManager({
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: resolvedDeps.platform !== "win32",
|
||||
shell: resolvedDeps.platform === "win32",
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
@@ -163,7 +164,15 @@ export function createNodecgProcessManager({
|
||||
killNodecgProcessTree(pid, "SIGTERM", log, resolvedDeps);
|
||||
|
||||
stopNodecgPromise = new Promise((resolve) => {
|
||||
let completed = false;
|
||||
|
||||
const complete = () => {
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
completed = true;
|
||||
|
||||
if (nodecgProcess === processToStop) {
|
||||
nodecgProcess = null;
|
||||
}
|
||||
@@ -181,6 +190,7 @@ export function createNodecgProcessManager({
|
||||
if (processToStop.exitCode === null && processToStop.signalCode === null) {
|
||||
log(`NodeCG did not exit after SIGTERM, forcing SIGKILL pid=${pid}`);
|
||||
killNodecgProcessTree(pid, "SIGKILL", log, resolvedDeps);
|
||||
complete();
|
||||
}
|
||||
},
|
||||
Math.max(0, appConfig.nodecgKillTimeoutMs),
|
||||
|
||||
@@ -28,6 +28,11 @@ type RuntimeProvisionerDeps = {
|
||||
writeFileSync: (filePath: string, content: string) => unknown;
|
||||
};
|
||||
|
||||
export type PreparedNodecgRuntime = {
|
||||
runtimePath: string;
|
||||
installed: boolean;
|
||||
};
|
||||
|
||||
type RuntimeManifest = {
|
||||
appVersion?: unknown;
|
||||
bundleName?: unknown;
|
||||
@@ -47,14 +52,16 @@ export function prepareUserNodecgRuntime({
|
||||
bundleName,
|
||||
log,
|
||||
deps,
|
||||
}: RuntimeProvisionerConfig): string {
|
||||
}: RuntimeProvisionerConfig): PreparedNodecgRuntime {
|
||||
const resolvedDeps = resolveDeps(deps);
|
||||
const targetRuntimePath = path.join(userDataPath, "nodecg");
|
||||
|
||||
validateSourceRuntime(sourceRuntimePath, bundleName, resolvedDeps.existsSync);
|
||||
resolvedDeps.mkdirSync(targetRuntimePath, { recursive: true });
|
||||
|
||||
if (shouldInstallRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps)) {
|
||||
const installed = shouldInstallRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps);
|
||||
|
||||
if (installed) {
|
||||
log(`Installing managed NodeCG runtime into ${targetRuntimePath}`);
|
||||
installManagedRuntime(sourceRuntimePath, targetRuntimePath, appVersion, bundleName, resolvedDeps);
|
||||
}
|
||||
@@ -63,7 +70,7 @@ export function prepareUserNodecgRuntime({
|
||||
resolvedDeps.mkdirSync(path.join(targetRuntimePath, writableDir), { recursive: true });
|
||||
}
|
||||
|
||||
return targetRuntimePath;
|
||||
return { runtimePath: targetRuntimePath, installed };
|
||||
}
|
||||
|
||||
function resolveDeps(deps?: Partial<RuntimeProvisionerDeps>): RuntimeProvisionerDeps {
|
||||
@@ -166,7 +173,10 @@ function installManagedRuntime(
|
||||
);
|
||||
}
|
||||
|
||||
function readJson(filePath: string, deps: Pick<RuntimeProvisionerDeps, "existsSync" | "readFileSync">): RuntimeManifest | null {
|
||||
function readJson(
|
||||
filePath: string,
|
||||
deps: Pick<RuntimeProvisionerDeps, "existsSync" | "readFileSync">,
|
||||
): RuntimeManifest | null {
|
||||
if (!deps.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { SpawnOptions } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
import { AppRuntimeConfig } from "../main/config/runtime-config";
|
||||
@@ -238,6 +239,44 @@ test("startNodeCG fails if the port is already in use", async () => {
|
||||
}, /is already in use/);
|
||||
});
|
||||
|
||||
test("startNodeCG spawns Electron directly on Windows", async () => {
|
||||
const child = new MockChildProcess(3210);
|
||||
let capturedCommand: string | null = null;
|
||||
let capturedArgs: string[] | null = null;
|
||||
const capturedOptions: SpawnOptions[] = [];
|
||||
|
||||
const manager = createNodecgProcessManager({
|
||||
isDev: false,
|
||||
nodecgRootPath: "C:\\Users\\tester\\AppData\\Roaming\\scoreko\\nodecg",
|
||||
nodecgBaseUrl: "http://127.0.0.1:9090",
|
||||
appConfig: getBaseConfig(),
|
||||
log: () => undefined,
|
||||
deps: {
|
||||
platform: "win32",
|
||||
execPath: "C:\\Program Files\\Scoreko\\scoreko.exe",
|
||||
pathExists: () => true,
|
||||
hasReadWriteAccess: () => true,
|
||||
probePortAvailable: async () => true,
|
||||
spawnProcess: (command, args, options) => {
|
||||
capturedCommand = command;
|
||||
capturedArgs = args;
|
||||
capturedOptions.push(options);
|
||||
return child as unknown as import("node:child_process").ChildProcess;
|
||||
},
|
||||
stdoutWrite: () => undefined,
|
||||
stderrWrite: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
await manager.startNodecgProcess();
|
||||
|
||||
assert.equal(capturedCommand, "C:\\Program Files\\Scoreko\\scoreko.exe");
|
||||
assert.deepEqual(capturedArgs, ["C:\\Users\\tester\\AppData\\Roaming\\scoreko\\nodecg\\index.js"]);
|
||||
assert.equal(capturedOptions[0]?.shell, false);
|
||||
assert.equal(capturedOptions[0]?.windowsHide, true);
|
||||
assert.equal(capturedOptions[0]?.env?.ELECTRON_RUN_AS_NODE, "1");
|
||||
});
|
||||
|
||||
test("waitForNodeCGReady exposes diagnostics when NodeCG exits before readiness", async () => {
|
||||
const child = new MockChildProcess(4242);
|
||||
const manager = createNodecgProcessManager({
|
||||
|
||||
@@ -70,7 +70,7 @@ test("prepareUserNodecgRuntime copies the packaged runtime into userData", () =>
|
||||
[path.join(source, ".scoreko-runtime.json")]: JSON.stringify({ bundleVersion: "0.1.0", nodecgVersion: "2.6.4" }),
|
||||
});
|
||||
|
||||
const runtimePath = prepareUserNodecgRuntime({
|
||||
const preparedRuntime = prepareUserNodecgRuntime({
|
||||
sourceRuntimePath: source,
|
||||
userDataPath: userData,
|
||||
appVersion: "0.1.0",
|
||||
@@ -79,7 +79,8 @@ test("prepareUserNodecgRuntime copies the packaged runtime into userData", () =>
|
||||
deps,
|
||||
});
|
||||
|
||||
assert.equal(runtimePath, path.join(userData, "nodecg"));
|
||||
assert.equal(preparedRuntime.runtimePath, path.join(userData, "nodecg"));
|
||||
assert.equal(preparedRuntime.installed, true);
|
||||
assert.equal(state.copied.length, 1);
|
||||
assert.ok(state.paths.has(path.join(userData, "nodecg", "cfg")));
|
||||
assert.ok(state.paths.has(path.join(userData, "nodecg", "db")));
|
||||
@@ -106,7 +107,7 @@ test("prepareUserNodecgRuntime keeps an up-to-date runtime in place", () => {
|
||||
},
|
||||
);
|
||||
|
||||
prepareUserNodecgRuntime({
|
||||
const preparedRuntime = prepareUserNodecgRuntime({
|
||||
sourceRuntimePath: source,
|
||||
userDataPath: userData,
|
||||
appVersion: "0.1.0",
|
||||
@@ -115,6 +116,7 @@ test("prepareUserNodecgRuntime keeps an up-to-date runtime in place", () => {
|
||||
deps,
|
||||
});
|
||||
|
||||
assert.equal(preparedRuntime.installed, false);
|
||||
assert.equal(state.copied.length, 0);
|
||||
assert.equal(state.removed.length, 0);
|
||||
});
|
||||
@@ -138,7 +140,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan
|
||||
},
|
||||
);
|
||||
|
||||
prepareUserNodecgRuntime({
|
||||
const preparedRuntime = prepareUserNodecgRuntime({
|
||||
sourceRuntimePath: source,
|
||||
userDataPath: userData,
|
||||
appVersion: "0.1.0",
|
||||
@@ -147,6 +149,7 @@ test("prepareUserNodecgRuntime refreshes managed files when the app version chan
|
||||
deps,
|
||||
});
|
||||
|
||||
assert.equal(preparedRuntime.installed, true);
|
||||
assert.equal(state.copied.length, 1);
|
||||
assert.ok(state.removed.includes(path.join(target, "node_modules")));
|
||||
assert.ok(state.removed.includes(path.join(target, "bundles")));
|
||||
|
||||
Reference in New Issue
Block a user