Cleanup final

This commit is contained in:
2026-05-31 14:50:32 +02:00
parent 865c3589bd
commit 33665ed896
13 changed files with 168 additions and 71 deletions
+2 -8
View File
@@ -2,6 +2,7 @@ import { AppRuntimeConfig } from "../config/runtime-config";
import { NodecgProcessManager } from "../nodecg/process-manager";
import { PreparedNodecgRuntime } from "../nodecg/runtime-provisioner";
import { getRemainingDelayMs } from "../utils/timing";
import { ApplicationPaths } from "./paths";
import { createShutdownService, ShutdownService } from "./shutdown-service";
export type ApplicationState = "idle" | "preparing" | "starting" | "ready" | "stopping" | "stopped" | "failed";
@@ -21,14 +22,7 @@ export type ApplicationControllerConfig = {
appVersion: string;
isPackaged: boolean;
isWindows: boolean;
paths: {
rootPath: string;
sourceNodecgRuntimePath: string;
userDataPath: string;
nodecgBaseUrl: string;
mainDashboardUrl: string;
loadingDashboardUrl: string;
};
paths: ApplicationPaths;
deps: {
createLoadingWindow: () => ApplicationWindow;
createMainWindow: () => ApplicationWindow;
-10
View File
@@ -53,16 +53,6 @@ export function getEnv(name: string, fallback: string): string {
return getOptionalEnv(name) ?? fallback;
}
export function parseEnvInt(name: string, fallback: number): number {
const rawValue = process.env[name];
if (!rawValue) {
return fallback;
}
const parsedValue = Number.parseInt(rawValue, 10);
return Number.isFinite(parsedValue) ? parsedValue : fallback;
}
export function parseEnvIntInRange(name: string, fallback: number, min: number, max: number): number {
// We throw here instead of silently coercing to avoid hidden misconfiguration in production.
const rawValue = process.env[name];
+6 -2
View File
@@ -1,12 +1,16 @@
import { ChildProcess, SpawnOptions } from "node:child_process";
import { SpawnOptions } from "node:child_process";
export type PlatformProcessKillerDeps = {
platform: NodeJS.Platform;
spawnProcess: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
spawnProcess: (command: string, args: string[], options: SpawnOptions) => SpawnedKillerProcess;
killProcess: (pid: number, signal: NodeJS.Signals) => void;
log: (...args: unknown[]) => void;
};
type SpawnedKillerProcess = {
on: (event: "error", listener: (error: Error) => void) => unknown;
};
export function killProcessTree(pid: number, signal: NodeJS.Signals, deps: PlatformProcessKillerDeps): boolean {
if (!Number.isSafeInteger(pid) || pid <= 0) {
deps.log(`Invalid pid for process tree termination: ${pid}`);
+19 -3
View File
@@ -1,4 +1,4 @@
import { ChildProcess, spawn, SpawnOptions } from "node:child_process";
import { spawn, SpawnOptions } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
@@ -17,7 +17,7 @@ type NodecgProcessManagerConfig = {
};
type NodecgProcessManagerDeps = {
spawnProcess: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
spawnProcess: (command: string, args: string[], options: SpawnOptions) => NodecgChildProcess;
pathExists: (candidatePath: string) => boolean;
fetchUrl: typeof fetch;
platform: NodeJS.Platform;
@@ -31,6 +31,22 @@ type NodecgProcessManagerDeps = {
hasReadWriteAccess: (candidatePath: string) => boolean;
};
type NodecgChildProcess = {
pid?: number;
killed: boolean;
exitCode: number | null;
signalCode: NodeJS.Signals | null;
stdout?: ProcessOutputStream | null;
stderr?: ProcessOutputStream | null;
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
on(event: "error", listener: (error: Error) => void): unknown;
once(event: "exit", listener: () => void): unknown;
};
type ProcessOutputStream = {
on(event: "data", listener: (chunk: unknown) => void): unknown;
};
export type NodecgProcessManager = {
startNodecgProcess: () => Promise<void>;
waitForNodecgReady: (startTime: number) => Promise<void>;
@@ -50,7 +66,7 @@ export function createNodecgProcessManager({
}: NodecgProcessManagerConfig): NodecgProcessManager {
const resolvedDeps = resolveDeps(deps);
let nodecgProcess: ChildProcess | null = null;
let nodecgProcess: NodecgChildProcess | null = null;
let nodecgState: NodecgProcessState = "idle";
let startNodecgPromise: Promise<void> | null = null;
let stopNodecgPromise: Promise<void> | null = null;
+14 -12
View File
@@ -2,6 +2,7 @@ import fs from "node:fs";
import { getDefaultUpdateConfigPath } from "../app/paths";
import { AppRuntimeConfig } from "../config/runtime-config";
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
import { UpdateFileConfig, validateHttpUrl } from "./update-schema";
const DEFAULT_UPDATE_ASSET_PATTERN = "Scoreko-setup-.*\\.exe$";
@@ -17,8 +18,17 @@ type UpdateConfigOptions = {
allowInsecureHttp: boolean;
};
type UpdateRuntimeConfig = Pick<
AppRuntimeConfig,
| "updateApiUrl"
| "updateAssetPattern"
| "updateConfigPathOverride"
| "updateReleasePageUrl"
| "updatesEnabled"
>;
export function loadUpdateSettings(
appConfig: AppRuntimeConfig,
appConfig: UpdateRuntimeConfig,
rootPath: string,
log: (...args: unknown[]) => void,
options: UpdateConfigOptions = { allowInsecureHttp: true },
@@ -32,12 +42,12 @@ export function loadUpdateSettings(
...(apiUrl ? { apiUrl } : {}),
...(releasePageUrl ? { releasePageUrl } : {}),
assetPattern:
appConfig.updateAssetPattern || readOptionalString(fileConfig.assetPattern) || DEFAULT_UPDATE_ASSET_PATTERN,
appConfig.updateAssetPattern || readNonEmptyString(fileConfig.assetPattern) || DEFAULT_UPDATE_ASSET_PATTERN,
};
}
export function readUpdateFileConfig(
appConfig: AppRuntimeConfig,
appConfig: Pick<AppRuntimeConfig, "updateConfigPathOverride">,
rootPath: string,
log: (...args: unknown[]) => void,
): UpdateFileConfig {
@@ -70,18 +80,10 @@ function normalizeUpdateFileConfig(value: unknown): UpdateFileConfig {
}
function readOptionalHttpUrl(value: unknown, options: UpdateConfigOptions): string | undefined {
const rawValue = readOptionalString(value);
const rawValue = readNonEmptyString(value);
if (!rawValue) {
return undefined;
}
return validateHttpUrl(rawValue, options) ?? undefined;
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+5 -2
View File
@@ -1,5 +1,5 @@
import { BrowserWindow, dialog } from "electron";
import type { MessageBoxOptions } from "electron";
import type { MessageBoxOptions, MessageBoxReturnValue } from "electron";
import { ReleaseUpdate } from "./update-schema";
@@ -41,6 +41,9 @@ export async function askToInstallUpdate(update: ReleaseUpdate, parentWindow: Br
return result.response === 0;
}
function showMessageBox(parentWindow: BrowserWindow | null, options: MessageBoxOptions) {
function showMessageBox(
parentWindow: BrowserWindow | null,
options: MessageBoxOptions,
): Promise<MessageBoxReturnValue> {
return parentWindow ? dialog.showMessageBox(parentWindow, options) : dialog.showMessageBox(options);
}
+9 -12
View File
@@ -1,3 +1,5 @@
import { isRecord, readNonEmptyString } from "../utils/unknown-values";
export type GiteaReleaseAsset = {
name: string;
browserDownloadUrl: string;
@@ -47,11 +49,14 @@ export function parseGiteaRelease(value: unknown): GiteaRelease | null {
return null;
}
const title = readNonEmptyString(value.name);
const pageUrl = readOptionalUrlString(value.html_url);
return {
tagName,
assets,
...(readOptionalString(value.name) ? { title: readOptionalString(value.name) } : {}),
...(readOptionalUrlString(value.html_url) ? { pageUrl: readOptionalUrlString(value.html_url) } : {}),
...(title ? { title } : {}),
...(pageUrl ? { pageUrl } : {}),
};
}
@@ -177,16 +182,12 @@ function normalizeVersion(version: string): number[] {
}
function readRequiredString(value: unknown): string | null {
const text = readOptionalString(value);
const text = readNonEmptyString(value);
return text && text.length > 0 ? text : null;
}
function readOptionalString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
function readOptionalUrlString(value: unknown): string | undefined {
const rawValue = readOptionalString(value);
const rawValue = readNonEmptyString(value);
if (!rawValue) {
return undefined;
}
@@ -194,10 +195,6 @@ function readOptionalUrlString(value: unknown): string | undefined {
return validateHttpUrl(rawValue, { allowInsecureHttp: true }) ?? undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isPresent<T>(value: T | null): value is T {
return value !== null;
}
+7
View File
@@ -0,0 +1,7 @@
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function readNonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}