mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(desktop): sign every Windows release artifact (#1548)
This commit is contained in:
1 parent
873c203b5d
commit
6e66c92dca
89 files changed
+2408
-2740
No files matched your search
@@ -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';
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user