feat(desktop): sign every Windows release artifact (#1548)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-10 23:22:19 +02:00
1 parent 873c203b5d
commit 6e66c92dca
89 files changed
+2408 -2740

No files matched your search

-10
View File
@@ -1,10 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {DesktopBuildVariant} from './Types';
const EMBEDDED_DESKTOP_BUILD_VARIANT = process.env.FLUXER_DESKTOP_BUILD_VARIANT;
export const DESKTOP_BUILD_VARIANT: DesktopBuildVariant =
EMBEDDED_DESKTOP_BUILD_VARIANT === 'windows-game-capture' ? 'windows-game-capture' : 'default';
export const IS_WINDOWS_GAME_CAPTURE_BUILD = DESKTOP_BUILD_VARIANT === 'windows-game-capture';
+1 -5
View File
@@ -14,12 +14,9 @@ export interface LinuxAppearanceSnapshot {
accent: {r: number; g: number; b: number} | null;
}
export type DesktopBuildVariant = 'default' | 'windows-game-capture';
export interface DesktopInfo {
version: string;
channel: 'stable' | 'canary';
buildVariant: DesktopBuildVariant;
arch: string;
hardwareArch: string;
runningUnderRosetta: boolean;
@@ -532,7 +529,7 @@ export interface NativeScreenCaptureLifecycleMessage {
source?: NativeScreenCaptureLifecycleSource;
}
export type NativeScreenCaptureStrategy = 'game-hook' | 'dxgi-duplication' | 'window-gdi' | string;
export type NativeScreenCaptureStrategy = 'game-hook' | 'wgc' | 'dxgi-duplication' | 'window-gdi' | string;
export interface NativeScreenCaptureDiagnostics {
state?: number;
@@ -650,7 +647,6 @@ export type TrayActionPayload =
export interface ElectronAPI {
platform: NodeJS.Platform;
buildChannel: 'stable' | 'canary';
buildVariant: DesktopBuildVariant;
getDesktopInfo: () => Promise<DesktopInfo>;
getGpuInfo: () => Promise<GpuInfo>;
getOpenH264Status: () => Promise<OpenH264Status>;
+1 -9
View File
@@ -249,20 +249,12 @@ function formatReleaseChannelLabel(value: string): string {
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
function formatBuildVariantLabel(value: DesktopInfo['buildVariant']): string {
if (value === 'windows-game-capture') {
return 'Windows Game Capture';
}
return '';
}
function getBuildString(desktopInfo: DesktopInfo): string {
const buildVersion = process.env.PUBLIC_BUILD_VERSION || process.env.BUILD_VERSION || desktopInfo.version || 'dev';
const releaseChannel = formatReleaseChannelLabel(
process.env.PUBLIC_RELEASE_CHANNEL || process.env.RELEASE_CHANNEL || desktopInfo.channel,
);
const buildVariant = formatBuildVariantLabel(desktopInfo.buildVariant);
return `${releaseChannel} Desktop${buildVariant ? ` ${buildVariant}` : ''} ${buildVersion}`;
return `${releaseChannel} Desktop ${buildVersion}`;
}
function safeGetLocale(): string {
@@ -91,13 +91,11 @@ function loadNativeScreenCapture({
addon,
tccStatus = 'not-determined',
frameSinkHandle = null,
windowsGameCaptureModuleEnabled = false,
} = {}) {
const handlers = new Map();
const calls = {
logs: {debug: [], warn: []},
nativeModuleImports: [],
windowsGameCapturePolicyEnableCalls: 0,
};
let uuidCounter = 0;
@@ -163,16 +161,6 @@ function loadNativeScreenCapture({
normalizeScreenCaptureDimension: (value) => value,
};
}
if (specifier === './WindowsGameCapturePolicy') {
return {
WINDOWS_GAME_CAPTURE_DISABLED_DETAIL: 'windows-game-capture-disabled-until-code-signed',
WINDOWS_GAME_CAPTURE_DISABLED_REASON: 'disabled-by-launch',
WINDOWS_GAME_CAPTURE_MODULE_ENABLED: windowsGameCaptureModuleEnabled,
enableWindowsGameCaptureModuleForCurrentProcess: () => {
calls.windowsGameCapturePolicyEnableCalls += 1;
},
};
}
throw new Error(`Unexpected import: ${specifier}`);
}
@@ -180,6 +168,7 @@ function loadNativeScreenCapture({
const context = vm.createContext({
Buffer,
ArrayBuffer,
Error,
console,
clearTimeout,
exports: module.exports,
@@ -457,7 +446,7 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
});
});
test('reports Windows native game capture disabled before loading the addon', async () => {
test('reports a Windows native game capture load failure when requiring the addon throws', async () => {
const harness = loadNativeScreenCapture({platform: 'win32'});
harness.module.registerNativeScreenCaptureHandlers();
@@ -465,24 +454,50 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
assert.deepEqual(plain(availability), {
available: false,
backend: 'windows-game-capture',
reason: 'disabled-by-launch',
detail: 'windows-game-capture-disabled-until-code-signed',
capabilities: {hidesCursor: true, screens: false, windows: false},
reason: 'load-failed',
detail: 'No fake addon configured for @fluxer/win-game-capture',
windowsHagsState: 'enabled',
windowsHagsDetail: 'HwSchMode=2',
});
assert.deepEqual(harness.calls.nativeModuleImports, []);
assert.equal(harness.calls.windowsGameCapturePolicyEnableCalls, 0);
assert.deepEqual(harness.calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.equal(harness.calls.logs.warn[0][0], 'Failed to load Windows native screen capture addon');
});
test('loads Windows native game capture only for the game capture build variant', async () => {
test('reports a Windows native game capture load failure surfaced by the addon loadError', async () => {
const {addon} = makeNativeAddon();
const loadError = new Error(
'@fluxer/win-game-capture native module failed to load.\nmodule=@fluxer/win-game-capture\nreason=native binary not found',
);
loadError.name = 'NativeModuleLoadError';
loadError.nativeDiagnostics = {
schemaVersion: 1,
moduleName: '@fluxer/win-game-capture',
reason: 'native binary not found',
};
addon.loadError = loadError;
const harness = loadNativeScreenCapture({platform: 'win32', addon});
harness.module.registerNativeScreenCaptureHandlers();
const availability = await harness.handlers.get('native-screen-capture:get-availability')();
assert.deepEqual(plain(availability), {
available: false,
backend: 'windows-game-capture',
reason: 'load-failed',
detail:
'@fluxer/win-game-capture native module failed to load.\nmodule=@fluxer/win-game-capture\nreason=native binary not found',
windowsHagsState: 'enabled',
windowsHagsDetail: 'HwSchMode=2',
});
assert.deepEqual(harness.calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.equal(harness.calls.logs.warn[0][0], 'Windows native screen capture addon reported load error');
assert.equal(harness.calls.logs.warn[0][1], loadError);
});
test('loads Windows native game capture and reports its backend capabilities', async () => {
const {addon} = makeNativeAddon();
addon.getAvailability = () => ({available: true, backend: 'windows-game-capture'});
const harness = loadNativeScreenCapture({
platform: 'win32',
addon,
windowsGameCaptureModuleEnabled: true,
});
const harness = loadNativeScreenCapture({platform: 'win32', addon});
harness.module.registerNativeScreenCaptureHandlers();
const availability = await harness.handlers.get('native-screen-capture:get-availability')();
@@ -495,7 +510,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
capabilities: {hidesCursor: false, screens: true, windows: true},
});
assert.deepEqual(harness.calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.equal(harness.calls.windowsGameCapturePolicyEnableCalls, 1);
});
test('routes Windows display sources through the native screen path without remapping to game', async () => {
@@ -504,7 +518,6 @@ describe('NativeScreenCapture source identity and capability reporting', () => {
const harness = loadNativeScreenCapture({
platform: 'win32',
addon,
windowsGameCaptureModuleEnabled: true,
frameSinkHandle: (captureId) => ({native: true, captureId}),
});
harness.module.registerNativeScreenCaptureHandlers();
+16 -41
View File
@@ -20,12 +20,6 @@ import {ipcMain} from 'electron';
import {getTccStatus} from './MacTcc';
import {isValidStartOptions, normalizeScreenCaptureDimension} from './NativeScreenCaptureValidation';
import {createNativeVoiceEngineScreenFrameSinkHandle} from './NativeVoiceEngine';
import {
enableWindowsGameCaptureModuleForCurrentProcess,
WINDOWS_GAME_CAPTURE_DISABLED_DETAIL,
WINDOWS_GAME_CAPTURE_DISABLED_REASON,
WINDOWS_GAME_CAPTURE_MODULE_ENABLED,
} from './WindowsGameCapturePolicy';
const logger = createChildLogger('NativeScreenCapture');
const requireModule = createRequire(import.meta.url);
@@ -150,11 +144,9 @@ interface ActiveNativeScreenSession {
const MAX_NATIVE_SCREEN_SESSIONS_PER_SENDER = 2;
const GAME_CAPTURE_DETAIL = {
hookDisabled: 'game-capture-hook-disabled',
addonError: 'game-capture-addon-error',
} as const;
const FLUXER_GAME_CAPTURE_DISABLE_HOOK = 'FLUXER_GAME_CAPTURE_DISABLE_HOOK';
const WINDOWS_HAGS_REGISTRY_PATH = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\GraphicsDrivers';
const WINDOWS_HAGS_REGISTRY_VALUE = 'HwSchMode';
const WINDOWS_HAGS_REGISTRY_READ_TIMEOUT_MS = 1500;
@@ -164,11 +156,6 @@ interface WindowsHagsDiagnostic {
windowsHagsDetail?: string;
}
function isGameCaptureHookDisabledByEnv(): boolean {
const value = process.env[FLUXER_GAME_CAPTURE_DISABLE_HOOK];
return typeof value === 'string' && value.length > 0;
}
let cachedLoadResult: NativeAddonLoadResult | undefined;
let cachedWindowsHagsDiagnostic: {expiresAtMs: number; value: WindowsHagsDiagnostic} | undefined;
let handlersRegistered = false;
@@ -362,22 +349,7 @@ function loadLinuxNativeScreenCaptureAddon(): NativeAddonLoadResult {
}
function loadWindowsNativeScreenCaptureAddon(): NativeAddonLoadResult {
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) {
const result: NativeAddonLoadResult = {
platform: process.platform,
availability: {
available: false,
backend: 'windows-game-capture',
reason: WINDOWS_GAME_CAPTURE_DISABLED_REASON,
detail: WINDOWS_GAME_CAPTURE_DISABLED_DETAIL,
capabilities: {hidesCursor: true, screens: false, windows: false},
},
};
cachedLoadResult = result;
return result;
}
try {
enableWindowsGameCaptureModuleForCurrentProcess();
const addon = requireModule('@fluxer/win-game-capture') as WindowsNativeScreenCaptureModule;
if (addon.loadError) {
const detail = describeAddonLoadError(addon.loadError);
@@ -846,9 +818,6 @@ async function startNativeScreenCapture(
}
const requestedWidth = normalizeScreenCaptureDimension(options.width);
const requestedHeight = normalizeScreenCaptureDimension(options.height);
if (loadResult.platform === 'win32' && options.sourceKind === 'game' && isGameCaptureHookDisabledByEnv()) {
throw new Error(`Game capture hook disabled by environment (${GAME_CAPTURE_DETAIL.hookDisabled})`);
}
const captureId = options.captureId?.trim() || randomUUID();
if (activeSessions.has(captureId)) {
throw new Error('Native screen capture id is already active');
@@ -900,16 +869,22 @@ async function startNativeScreenCapture(
);
},
};
if (options.sourceKind === 'game') {
session.onStalled = (stallMessage) => {
if (session.finalized) return;
logger.info('Game capture reported a stall (non-fatal)', {captureId, detail: stallMessage});
};
session.onDiagnostic = (diagnosticMessage) => {
if (session.finalized) return;
logger.debug('Game capture diagnostic', {captureId, detail: diagnosticMessage});
};
}
session.onStalled = (stallMessage) => {
if (session.finalized) return;
logger.info('Native screen capture reported a stall (non-fatal)', {
captureId,
sourceKind: options.sourceKind,
detail: stallMessage,
});
};
session.onDiagnostic = (diagnosticMessage) => {
if (session.finalized) return;
logger.debug('Native screen capture diagnostic', {
captureId,
sourceKind: options.sourceKind,
detail: diagnosticMessage,
});
};
capture.on('error', session.onError);
capture.on('closed', session.onClosed);
if (session.onStalled) capture.on('stalled', session.onStalled);
-2
View File
@@ -3,7 +3,6 @@
import {createRequire} from 'node:module';
import os from 'node:os';
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {DESKTOP_BUILD_VARIANT} from '@electron/common/BuildVariant';
import type {
AppMetricsSnapshot,
CpuInfo,
@@ -139,7 +138,6 @@ export async function getDesktopInfo(options: DesktopInfoOptions = {}): Promise<
return {
version: app.getVersion(),
channel: BUILD_CHANNEL,
buildVariant: DESKTOP_BUILD_VARIANT,
arch: process.arch,
hardwareArch,
runningUnderRosetta,
-2
View File
@@ -2,7 +2,6 @@
import http from 'node:http';
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {DESKTOP_BUILD_VARIANT} from '@electron/common/BuildVariant';
import {CANARY_APP_URL, STABLE_APP_URL} from '@electron/common/Constants';
import {getCustomAppUrl} from '@electron/common/DesktopConfig';
import {getMainWindow, showWindow} from '@electron/main/Window';
@@ -115,7 +114,6 @@ const handleHealth = (_req: http.IncomingMessage, res: http.ServerResponse) => {
data: {
status: 'ok',
channel: BUILD_CHANNEL,
build_variant: DESKTOP_BUILD_VARIANT,
version: app.getVersion(),
platform: process.platform,
},
@@ -45,7 +45,6 @@ function loadStreamingPriority({
currentPriority = 0,
elevateResult = true,
restoreResult = true,
windowsGameCaptureModuleEnabled = false,
} = {}) {
const calls = {
elevate: [],
@@ -58,7 +57,6 @@ function loadStreamingPriority({
guardStop: [],
setPriority: [],
nativeModuleImports: [],
windowsGameCapturePolicyEnableCalls: 0,
logs: {debug: [], info: [], warn: []},
};
@@ -119,15 +117,6 @@ function loadStreamingPriority({
if (specifier === 'electron') return {app, powerSaveBlocker};
if (specifier === 'electron-log') return log;
if (specifier === './WindowsScreenCaptureGuard') return guard;
if (specifier === './WindowsGameCapturePolicy') {
return {
WINDOWS_GAME_CAPTURE_DISABLED_DETAIL: 'windows-game-capture-disabled-until-code-signed',
WINDOWS_GAME_CAPTURE_MODULE_ENABLED: windowsGameCaptureModuleEnabled,
enableWindowsGameCaptureModuleForCurrentProcess: () => {
calls.windowsGameCapturePolicyEnableCalls += 1;
},
};
}
throw new Error(`Unexpected import: ${specifier}`);
}
@@ -156,7 +145,7 @@ function loadStreamingPriority({
}
describe('StreamingPriority GPU scheduling priority', () => {
test('keeps streaming priority active without loading unsigned Windows game capture code', () => {
test('elevates GPU scheduling priority on Windows', () => {
const webContents = makeWebContents(2001);
const {calls, module} = loadStreamingPriority({
metrics: [
@@ -170,8 +159,14 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.acquireStreamingPriority(webContents);
assert.deepEqual(calls.nativeModuleImports, []);
assert.deepEqual(calls.elevate, []);
assert.deepEqual(calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.deepEqual(calls.elevate, [
{processId: 1000, priorityClass: 'high'},
{processId: 2001, priorityClass: 'high'},
{processId: 3001, priorityClass: 'high'},
{processId: 3002, priorityClass: 'high'},
{processId: 3003, priorityClass: 'high'},
]);
assert.deepEqual(calls.setPriority, [-7]);
assert.deepEqual(webContents.backgroundThrottlingAllowed, [false]);
assert.equal(calls.intervals.length, 1);
@@ -190,14 +185,20 @@ describe('StreamingPriority GPU scheduling priority', () => {
supported: true,
priorityClass: 'high',
env: GPU_SCHEDULING_PRIORITY_ENV,
nativeModuleStatus: 'unavailable',
nativeModuleLoadErrorDetail: 'windows-game-capture-disabled-until-code-signed',
nativeModuleStatus: 'loaded',
nativeModuleLoadErrorDetail: null,
refreshActive: true,
refreshIntervalMs: 20000,
trackedWebContents: 1,
elevatedProcesses: [],
elevatedProcesses: [
{processId: 1000, priorityClass: 'high'},
{processId: 2001, priorityClass: 'high'},
{processId: 3001, priorityClass: 'high'},
{processId: 3002, priorityClass: 'high'},
{processId: 3003, priorityClass: 'high'},
],
lastAcquire: {
status: 'native-module-unavailable',
status: 'succeeded',
priorityClass: 'high',
targets: [
{processId: 1000, reasons: ['native-main-encoder-capture']},
@@ -206,26 +207,19 @@ describe('StreamingPriority GPU scheduling priority', () => {
{processId: 3002, reasons: ['chromium-video-encode']},
{processId: 3003, reasons: ['chromium-video-capture']},
],
elevatedProcessIds: [],
elevatedProcessIds: [1000, 2001, 3001, 3002, 3003],
skippedProcessIds: [],
failedProcessIds: [
{processId: 1000, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 2001, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 3001, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 3002, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 3003, reason: 'windows-game-capture-disabled-until-code-signed'},
],
detail: 'windows-game-capture-disabled-until-code-signed',
failedProcessIds: [],
},
lastRestore: null,
},
});
module.acquireStreamingPriority(webContents);
assert.equal(calls.elevate.length, 0);
assert.equal(calls.elevate.length, 5);
assert.deepEqual(
normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.lastAcquire.skippedProcessIds),
[],
[1000, 2001, 3001, 3002, 3003],
);
module.releaseStreamingPriority();
@@ -233,13 +227,19 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.releaseStreamingPriority();
assert.deepEqual(webContents.backgroundThrottlingAllowed, [false, false, true]);
assert.deepEqual(calls.restore, []);
assert.deepEqual(calls.restore, [
{processId: 1000},
{processId: 2001},
{processId: 3001},
{processId: 3002},
{processId: 3003},
]);
assert.deepEqual(calls.setPriority, [-7, 0]);
assert.deepEqual(normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.elevatedProcesses), []);
assert.deepEqual(normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.lastRestore), {
status: 'no-active-priority',
processIds: [],
restoredProcessIds: [],
status: 'succeeded',
processIds: [1000, 2001, 3001, 3002, 3003],
restoredProcessIds: [1000, 2001, 3001, 3002, 3003],
failedProcessIds: [],
});
assert.deepEqual(calls.guardStop, ['streaming-priority-release']);
@@ -251,22 +251,20 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.acquireStreamingPriority();
assert.deepEqual(calls.elevate, []);
assert.deepEqual(calls.elevate, [{processId: 1000, priorityClass: 'realtime'}]);
assert.equal(module.getStreamingPriorityDiagnostics().gpuScheduling.priorityClass, 'realtime');
}
});
test('loads Windows game capture GPU priority support only for the game capture build variant', () => {
test('loads the Windows game capture native module for GPU priority elevation', () => {
const webContents = makeWebContents(2001);
const {calls, module} = loadStreamingPriority({
windowsGameCaptureModuleEnabled: true,
metrics: [{pid: 3001, type: 'GPU'}],
});
module.acquireStreamingPriority(webContents);
assert.deepEqual(calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.equal(calls.windowsGameCapturePolicyEnableCalls, 1);
assert.deepEqual(
calls.elevate.map((entry) => entry.processId),
[1000, 2001, 3001],
@@ -317,7 +315,7 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.acquireStreamingPriority();
assert.deepEqual(calls.elevate, []);
assert.deepEqual(calls.elevate, [{processId: 1000, priorityClass: 'high'}]);
assert.equal(calls.logs.warn.length, 1);
assert.equal(calls.logs.warn[0][0], '[StreamingPriority] Ignoring invalid GPU scheduling priority override');
});
@@ -329,12 +327,12 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.releaseStreamingPriority();
assert.deepEqual(calls.setPriority, []);
assert.deepEqual(calls.restore, []);
assert.deepEqual(calls.restore, [{processId: 1000}]);
assert.equal(module.getStreamingPriorityDiagnostics().processPriority.elevated, false);
assert.equal(module.getStreamingPriorityDiagnostics().processPriority.savedPriority, null);
});
test('records native module diagnostics when unsigned game capture is disabled', () => {
test('records the real native module load failure detail', () => {
const webContents = makeWebContents(2001);
const {calls, module} = loadStreamingPriority({
addonLoadError: new Error('native binary missing'),
@@ -344,11 +342,11 @@ describe('StreamingPriority GPU scheduling priority', () => {
module.acquireStreamingPriority(webContents);
assert.deepEqual(calls.elevate, []);
assert.deepEqual(calls.nativeModuleImports, []);
assert.deepEqual(calls.nativeModuleImports, ['@fluxer/win-game-capture']);
assert.equal(module.getStreamingPriorityDiagnostics().gpuScheduling.nativeModuleStatus, 'unavailable');
assert.equal(
module.getStreamingPriorityDiagnostics().gpuScheduling.nativeModuleLoadErrorDetail,
'windows-game-capture-disabled-until-code-signed',
'native binary missing',
);
assert.deepEqual(normalize(module.getStreamingPriorityDiagnostics().gpuScheduling.lastAcquire), {
status: 'native-module-unavailable',
@@ -361,15 +359,41 @@ describe('StreamingPriority GPU scheduling priority', () => {
elevatedProcessIds: [],
skippedProcessIds: [],
failedProcessIds: [
{processId: 1000, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 2001, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 3001, reason: 'windows-game-capture-disabled-until-code-signed'},
{processId: 1000, reason: 'native binary missing'},
{processId: 2001, reason: 'native binary missing'},
{processId: 3001, reason: 'native binary missing'},
],
detail: 'windows-game-capture-disabled-until-code-signed',
detail: 'native binary missing',
});
assert.equal(calls.logs.debug[0][0], '[StreamingPriority] Windows GPU priority module reported load error');
assert.equal(
calls.logs.debug[0][0],
calls.logs.debug[1][0],
'[StreamingPriority] Cannot elevate GPU scheduling priority; native module unavailable',
);
});
test('reduces a multi-line native load error to its first line for per-process reasons', () => {
const {calls, module} = loadStreamingPriority({
addonLoadError: new Error(
'@fluxer/win-game-capture native module failed to load.\nreason=native binary not found\nnativeRoot=C:/fluxer',
),
});
module.acquireStreamingPriority();
module.releaseStreamingPriority();
assert.deepEqual(calls.elevate, []);
const diagnostics = module.getStreamingPriorityDiagnostics();
assert.equal(
diagnostics.gpuScheduling.nativeModuleLoadErrorDetail,
'@fluxer/win-game-capture native module failed to load.\nreason=native binary not found\nnativeRoot=C:/fluxer',
);
assert.deepEqual(normalize(diagnostics.gpuScheduling.lastAcquire.failedProcessIds), [
{processId: 1000, reason: '@fluxer/win-game-capture native module failed to load.'},
]);
assert.equal(
diagnostics.gpuScheduling.lastAcquire.detail,
'@fluxer/win-game-capture native module failed to load.\nreason=native binary not found\nnativeRoot=C:/fluxer',
);
});
});
+13 -15
View File
@@ -4,11 +4,6 @@ import {createRequire} from 'node:module';
import os from 'node:os';
import {app, powerSaveBlocker} from 'electron';
import log from 'electron-log';
import {
enableWindowsGameCaptureModuleForCurrentProcess,
WINDOWS_GAME_CAPTURE_DISABLED_DETAIL,
WINDOWS_GAME_CAPTURE_MODULE_ENABLED,
} from './WindowsGameCapturePolicy';
import {retainWindowsScreenCaptureGuard, stopWindowsScreenCaptureGuard} from './WindowsScreenCaptureGuard';
const STREAMING_PRIORITY = os.constants?.priority?.PRIORITY_ABOVE_NORMAL ?? -7;
@@ -127,6 +122,15 @@ function formatErrorDetail(error: unknown): string {
return String(error);
}
const GPU_PRIORITY_MODULE_UNAVAILABLE_DETAIL = 'Windows GPU priority native API unavailable';
function gpuPriorityModuleUnavailableReason(): string {
if (gpuPriorityModuleLoadErrorDetail === null) return GPU_PRIORITY_MODULE_UNAVAILABLE_DETAIL;
const lineBreakIndex = gpuPriorityModuleLoadErrorDetail.indexOf('\n');
if (lineBreakIndex === -1) return gpuPriorityModuleLoadErrorDetail;
return gpuPriorityModuleLoadErrorDetail.slice(0, lineBreakIndex).trimEnd();
}
function resolveGpuSchedulingPriority(): WindowsGpuSchedulingPriority | null {
const raw = process.env[GPU_SCHEDULING_PRIORITY_ENV];
if (raw == null || raw.trim() === '') return 'high';
@@ -189,13 +193,7 @@ function restoreProcessPriority(): void {
function loadWindowsGpuPriorityModule(): WindowsGpuPriorityModule | null {
if (process.platform !== 'win32') return null;
if (gpuPriorityModule !== undefined) return gpuPriorityModule;
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) {
gpuPriorityModuleLoadErrorDetail = WINDOWS_GAME_CAPTURE_DISABLED_DETAIL;
gpuPriorityModule = null;
return null;
}
try {
enableWindowsGameCaptureModuleForCurrentProcess();
const addon = requireModule('@fluxer/win-game-capture') as WindowsGpuPriorityModule;
if (addon.loadError) {
gpuPriorityModuleLoadErrorDetail = formatErrorDetail(addon.loadError);
@@ -405,9 +403,9 @@ function elevateGpuSchedulingPriority(webContents?: Electron.WebContents): void
skippedProcessIds: [],
failedProcessIds: targets.slice(0, MAX_GPU_PRIORITY_DIAGNOSTIC_TARGETS).map((target) => ({
processId: target.processId,
reason: gpuPriorityModuleLoadErrorDetail ?? 'Windows GPU priority native API unavailable',
reason: gpuPriorityModuleUnavailableReason(),
})),
detail: gpuPriorityModuleLoadErrorDetail ?? 'Windows GPU priority native API unavailable',
detail: gpuPriorityModuleLoadErrorDetail ?? GPU_PRIORITY_MODULE_UNAVAILABLE_DETAIL,
};
log.debug('[StreamingPriority] Cannot elevate GPU scheduling priority; native module unavailable', {
priorityClass: GPU_SCHEDULING_PRIORITY,
@@ -504,9 +502,9 @@ function restoreGpuSchedulingPriority(): void {
restoredProcessIds: [],
failedProcessIds: processIds.map((processId) => ({
processId,
reason: gpuPriorityModuleLoadErrorDetail ?? 'Windows GPU priority native API unavailable',
reason: gpuPriorityModuleUnavailableReason(),
})),
detail: gpuPriorityModuleLoadErrorDetail ?? 'Windows GPU priority native API unavailable',
detail: gpuPriorityModuleLoadErrorDetail ?? GPU_PRIORITY_MODULE_UNAVAILABLE_DETAIL,
};
log.debug('[StreamingPriority] Cannot restore GPU scheduling priority; native module unavailable', {
processIds,
+1 -4
View File
@@ -2,7 +2,6 @@
import {createRequire} from 'node:module';
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {DESKTOP_BUILD_VARIANT} from '@electron/common/BuildVariant';
import {isPortableMode} from '@electron/common/UserDataPath';
import {destroyDesktopTray} from '@electron/main/DesktopTray';
import {isFlatpakRuntime} from '@electron/main/LinuxSandbox';
@@ -72,9 +71,7 @@ function getDesktopDownloadArch(arch: NodeJS.Architecture): DesktopDownloadArch
const DESKTOP_DOWNLOAD_ARCH = getDesktopDownloadArch(process.arch);
const UPDATE_API_ENDPOINT = BUILD_CHANNEL === 'canary' ? 'https://api.canary.fluxer.app' : 'https://api.fluxer.app';
const UPDATE_VARIANT_SEGMENT =
process.platform === 'win32' && DESKTOP_BUILD_VARIANT !== 'default' ? `/${DESKTOP_BUILD_VARIANT}` : '';
const UPDATE_BASE_URL = `${UPDATE_API_ENDPOINT}/dl/desktop/${BUILD_CHANNEL}/${process.platform}/${DESKTOP_DOWNLOAD_ARCH}${UPDATE_VARIANT_SEGMENT}`;
const UPDATE_BASE_URL = `${UPDATE_API_ENDPOINT}/dl/desktop/${BUILD_CHANNEL}/${process.platform}/${DESKTOP_DOWNLOAD_ARCH}`;
const DOWNLOAD_PAGE_URL =
BUILD_CHANNEL === 'canary' ? 'https://canary.fluxer.app/download' : 'https://fluxer.app/download';
@@ -1,15 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {IS_WINDOWS_GAME_CAPTURE_BUILD} from '@electron/common/BuildVariant';
const WINDOWS_GAME_CAPTURE_MODULE_ENV = 'FLUXER_WINDOWS_GAME_CAPTURE_MODULE_ENABLED';
export const WINDOWS_GAME_CAPTURE_DISABLED_DETAIL = 'windows-game-capture-disabled-until-code-signed';
export const WINDOWS_GAME_CAPTURE_DISABLED_REASON = 'disabled-by-launch';
export const WINDOWS_GAME_CAPTURE_MODULE_ENABLED =
IS_WINDOWS_GAME_CAPTURE_BUILD || process.env.FLUXER_WINDOWS_GAME_CAPTURE_MODULE_ENABLED === 'true';
export function enableWindowsGameCaptureModuleForCurrentProcess(): void {
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) return;
process.env[WINDOWS_GAME_CAPTURE_MODULE_ENV] = 'true';
}
@@ -3,10 +3,6 @@
import {execFileSync} from 'node:child_process';
import {createRequire} from 'node:module';
import log from 'electron-log';
import {
enableWindowsGameCaptureModuleForCurrentProcess,
WINDOWS_GAME_CAPTURE_MODULE_ENABLED,
} from './WindowsGameCapturePolicy';
const requireModule = createRequire(import.meta.url);
const VULKAN_IMPLICIT_LAYERS_REGISTRY_KEY = 'Software\\Khronos\\Vulkan\\ImplicitLayers';
@@ -21,6 +17,7 @@ interface VulkanLayerRegistrationState {
type WindowsGameCaptureModule = {
loadError?: Error | null;
isGameCaptureHookAvailable?: () => boolean;
registerVulkanLayerManifest?: () => boolean;
unregisterVulkanLayerManifest?: () => boolean;
resolveVulkanLayerManifestPath?: () => string | null;
@@ -39,8 +36,12 @@ function parseRegistryValueNames(stdout: string): Array<string> {
return valueNames;
}
function normalizeVulkanLayerValueName(valueName: string): string {
return valueName.replace(/\//g, '\\').toLowerCase();
}
function isFluxerGameCaptureVulkanLayerValue(valueName: string): boolean {
const normalized = valueName.replace(/\//g, '\\').toLowerCase();
const normalized = normalizeVulkanLayerValueName(valueName);
if (!normalized.includes('\\@fluxer\\win-game-capture\\')) return false;
return /\\fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/.test(normalized);
}
@@ -66,13 +67,33 @@ function deleteVulkanLayerRegistryValue(root: string, valueName: string): void {
});
}
function removeStaleFluxerGameCaptureVulkanLayers(): void {
function isSameVulkanLayerManifestPath(left: string, right: string): boolean {
return normalizeVulkanLayerValueName(left) === normalizeVulkanLayerValueName(right);
}
function removeStaleFluxerGameCaptureVulkanLayers(keepManifestPath: string | null): void {
if (process.platform !== 'win32') return;
for (const root of VULKAN_REGISTRY_ROOTS) {
const valueNames = queryVulkanLayerRegistryValues(root);
let valueNames: Array<string>;
try {
valueNames = queryVulkanLayerRegistryValues(root);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to enumerate Vulkan implicit layer registry values', {root, error});
continue;
}
for (const valueName of valueNames) {
if (!isFluxerGameCaptureVulkanLayerValue(valueName)) continue;
deleteVulkanLayerRegistryValue(root, valueName);
if (keepManifestPath !== null && isSameVulkanLayerManifestPath(valueName, keepManifestPath)) continue;
try {
deleteVulkanLayerRegistryValue(root, valueName);
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to remove stale Fluxer Vulkan layer registry value', {
root,
valueName,
error,
});
continue;
}
log.info('[VulkanGameCaptureLayer] Removed stale Fluxer Vulkan layer registry value', {root, valueName});
}
}
@@ -80,31 +101,35 @@ function removeStaleFluxerGameCaptureVulkanLayers(): void {
function loadWindowsGameCaptureModule(): WindowsGameCaptureModule | null {
if (process.platform !== 'win32') return null;
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) return null;
enableWindowsGameCaptureModuleForCurrentProcess();
const addon = requireModule('@fluxer/win-game-capture') as WindowsGameCaptureModule;
if (addon.loadError) {
log.warn('[VulkanGameCaptureLayer] Native game capture addon unavailable', addon.loadError);
try {
const addon = requireModule('@fluxer/win-game-capture') as WindowsGameCaptureModule;
if (addon.loadError) {
log.warn('[VulkanGameCaptureLayer] Native game capture addon unavailable', addon.loadError);
return null;
}
return addon;
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to load the native game capture addon', error);
return null;
}
return addon;
}
export function initializeWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
if (!addon || addon.isGameCaptureHookAvailable?.() !== true) {
removeStaleFluxerGameCaptureVulkanLayers(null);
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer left unregistered; hook-based game capture is disabled');
return;
}
try {
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) {
removeStaleFluxerGameCaptureVulkanLayers();
log.info('[VulkanGameCaptureLayer] Native game capture disabled until Windows binaries are code signed');
return;
}
const addon = loadWindowsGameCaptureModule();
if (!addon) return;
const manifestPath = addon.resolveVulkanLayerManifestPath?.() ?? null;
removeStaleFluxerGameCaptureVulkanLayers(manifestPath);
const registered = addon.registerVulkanLayerManifest?.() ?? false;
const state = addon.getVulkanLayerRegistrationState?.() ?? null;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer registration checked', {
registered,
manifestPath: addon.resolveVulkanLayerManifestPath?.() ?? null,
manifestPath,
state,
});
} catch (error) {
@@ -114,19 +139,15 @@ export function initializeWindowsVulkanGameCaptureLayer(): void {
export function unregisterWindowsVulkanGameCaptureLayer(): void {
if (process.platform !== 'win32') return;
const addon = loadWindowsGameCaptureModule();
try {
if (!WINDOWS_GAME_CAPTURE_MODULE_ENABLED) {
removeStaleFluxerGameCaptureVulkanLayers();
return;
}
const addon = loadWindowsGameCaptureModule();
if (!addon) return;
const unregistered = addon.unregisterVulkanLayerManifest?.() ?? false;
const unregistered = addon?.unregisterVulkanLayerManifest?.() ?? false;
log.info('[VulkanGameCaptureLayer] Vulkan implicit layer unregistration attempted', {
unregistered,
manifestPath: addon.resolveVulkanLayerManifestPath?.() ?? null,
manifestPath: addon?.resolveVulkanLayerManifestPath?.() ?? null,
});
} catch (error) {
log.warn('[VulkanGameCaptureLayer] Failed to unregister Vulkan implicit layer', error);
}
removeStaleFluxerGameCaptureVulkanLayers(null);
}
-2
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {DESKTOP_BUILD_VARIANT} from '@electron/common/BuildVariant';
import type {
AppMetricsSnapshot,
ClipboardWriteFileOptions,
@@ -356,7 +355,6 @@ applyStartupAccessibilitySettings();
const api: ElectronAPI = {
platform: process.platform,
buildChannel: BUILD_CHANNEL,
buildVariant: DESKTOP_BUILD_VARIANT,
getDesktopInfo: (): Promise<DesktopInfo> => ipcRenderer.invoke('get-desktop-info'),
getGpuInfo: (): Promise<GpuInfo> => ipcRenderer.invoke('get-gpu-info'),
getAppMetrics: (): Promise<AppMetricsSnapshot> => ipcRenderer.invoke('get-app-metrics'),