fix(desktop): harden capture and release packaging (#2049)

This commit is contained in:
Hampus
2026-08-27 23:54:38 +02:00
committed by GitHub
parent 5ee59c4675
commit 374db9ed2b
25 changed files with 1394 additions and 1317 deletions
+38 -35
View File
@@ -47,6 +47,8 @@ if (targetNativeArch === 'universal' && targetPlatform !== 'darwin') {
const targetArchs = electronArch && electronArch !== 'universal' ? [electronArch] : supportedTargetArchs;
const macTargetArchs = targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
const winGameCaptureTargetArchs =
targetPlatform === 'win32' && targetNativeArch ? [targetNativeArch] : supportedTargetArchs;
const winTargets = [
{
target: 'dir',
@@ -150,11 +152,9 @@ const nativeRuntimeFilePatterns = [
'node_modules/@fluxer/win-game-capture/package.json',
'node_modules/@fluxer/win-game-capture/index.js',
'node_modules/@fluxer/win-game-capture/loader-diagnostics.cjs',
'node_modules/@fluxer/win-game-capture/*.node',
'node_modules/@fluxer/win-game-capture/*.dll',
'node_modules/@fluxer/win-game-capture/*.exe',
'node_modules/@fluxer/win-game-capture/compatibility.json',
'node_modules/@fluxer/win-game-capture/fluxer-vulkan-layer.*.json',
...winGameCaptureTargetArchs.map(
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/@fluxer/win-clipboard/package.json',
'node_modules/@fluxer/win-clipboard/index.js',
'node_modules/@fluxer/win-clipboard/loader-diagnostics.cjs',
@@ -224,11 +224,10 @@ const nativeRuntimeFilePatterns = [
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/loader-diagnostics.cjs',
'node_modules/.pnpm/@fluxer+*/node_modules/@fluxer/*/pure.cjs',
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.node',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.dll',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.exe',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/compatibility.json',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/fluxer-vulkan-layer.*.json',
...winGameCaptureTargetArchs.map(
(arch) =>
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
@@ -352,6 +351,21 @@ function pnpmStoreDirName(packageName) {
return packageName.replace('/', '+');
}
function windowsGameCaptureArtifactExcludes(arch) {
const packageRoots = [
'node_modules/@fluxer/win-game-capture',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture',
];
const excludedNodeArchs = [...supportedTargetArchs, 'ia32'].filter((candidate) => candidate !== arch);
return packageRoots.flatMap((packageRoot) => [
`!${packageRoot}/compatibility.json`,
`!${packageRoot}/fluxer-game-hook.*`,
`!${packageRoot}/fluxer-inject-helper.*`,
`!${packageRoot}/fluxer-vulkan-layer.*`,
...excludedNodeArchs.map((excludedArch) => `!${packageRoot}/win-game-capture.win32-${excludedArch}-msvc.node`),
]);
}
function platformNativeExcludes(platform, arch) {
const keepFluxerPackages = new Set(fluxerNativePackagesByPlatform[platform] ?? []);
const fluxerPackageExcludes = fluxerNativePackages
@@ -371,6 +385,7 @@ function platformNativeExcludes(platform, arch) {
}
return [
...fluxerPackageExcludes,
...windowsGameCaptureArtifactExcludes(arch),
...velopackNativeFiles
.filter((fileName) => fileName !== keepVelopackNativeFile)
.map((fileName) => `!node_modules/velopack/lib/native/${fileName}`),
@@ -403,22 +418,11 @@ function platformTag(platform, arch) {
return null;
}
function addWindowsGameCaptureArtifacts(artifacts, tag, arch) {
const add = (relativePath) => {
artifacts.push({
packageName: '@fluxer/win-game-capture',
relativePath,
});
};
add(`win-game-capture.${tag}.node`);
add(`fluxer-game-hook.${tag}.dll`);
add(`fluxer-inject-helper.${tag}.exe`);
add(`fluxer-vulkan-layer.${tag}.dll`);
add(`fluxer-vulkan-layer.${tag}.json`);
if (arch === 'x64') {
add('fluxer-game-hook.win32-ia32-msvc.dll');
add('fluxer-inject-helper.win32-ia32-msvc.exe');
}
function addWindowsGameCaptureArtifacts(artifacts, tag) {
artifacts.push({
packageName: '@fluxer/win-game-capture',
relativePath: `win-game-capture.${tag}.node`,
});
}
function expectedNativeRuntimeArtifacts(platform, arch) {
@@ -477,7 +481,7 @@ function expectedNativeRuntimeArtifactsForArch(platform, arch) {
packageName: '@fluxer/win-process-loopback',
relativePath: `win-process-loopback.${tag}.node`,
});
addWindowsGameCaptureArtifacts(artifacts, tag, arch);
addWindowsGameCaptureArtifacts(artifacts, tag);
artifacts.push({
packageName: '@fluxer/win-clipboard',
relativePath: `win-clipboard.${tag}.node`,
@@ -1305,10 +1309,9 @@ module.exports = {
asarUnpack: [
'**/*.node',
'node_modules/@fluxer/win-process-loopback/*.node',
'node_modules/@fluxer/win-game-capture/*.node',
'node_modules/@fluxer/win-game-capture/*.dll',
'node_modules/@fluxer/win-game-capture/*.exe',
'node_modules/@fluxer/win-game-capture/*.json',
...winGameCaptureTargetArchs.map(
(arch) => `node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/@fluxer/win-clipboard/*.node',
'node_modules/@fluxer/win-shell/*.node',
'node_modules/@fluxer/win-toast/*.node',
@@ -1331,10 +1334,10 @@ module.exports = {
'node_modules/@fluxer/webauthn/*.node',
'node_modules/@fluxer/webauthn/*.so*',
'node_modules/.pnpm/@fluxer+win-process-loopback@*/node_modules/@fluxer/win-process-loopback/*.node',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.node',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.dll',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.exe',
'node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/*.json',
...winGameCaptureTargetArchs.map(
(arch) =>
`node_modules/.pnpm/@fluxer+win-game-capture@*/node_modules/@fluxer/win-game-capture/win-game-capture.win32-${arch}-msvc.node`,
),
'node_modules/.pnpm/@fluxer+win-clipboard@*/node_modules/@fluxer/win-clipboard/*.node',
'node_modules/.pnpm/@fluxer+win-shell@*/node_modules/@fluxer/win-shell/*.node',
'node_modules/.pnpm/@fluxer+win-toast@*/node_modules/@fluxer/win-toast/*.node',
@@ -8,6 +8,10 @@ publish = false
[workspace]
resolver = "2"
[features]
default = []
game-capture-hook = []
[lib]
crate-type = ["cdylib", "rlib"]
@@ -18,19 +18,7 @@
"index.d.ts",
"loader-diagnostics.cjs",
"win-game-capture.win32-x64-msvc.node",
"win-game-capture.win32-arm64-msvc.node",
"fluxer-game-hook.win32-x64-msvc.dll",
"fluxer-game-hook.win32-ia32-msvc.dll",
"fluxer-game-hook.win32-arm64-msvc.dll",
"fluxer-inject-helper.win32-x64-msvc.exe",
"fluxer-inject-helper.win32-ia32-msvc.exe",
"fluxer-inject-helper.win32-arm64-msvc.exe",
"fluxer-vulkan-layer.win32-x64-msvc.dll",
"fluxer-vulkan-layer.win32-x64-msvc.json",
"fluxer-vulkan-layer.win32-ia32-msvc.dll",
"fluxer-vulkan-layer.win32-ia32-msvc.json",
"fluxer-vulkan-layer.win32-arm64-msvc.dll",
"fluxer-vulkan-layer.win32-arm64-msvc.json"
"win-game-capture.win32-arm64-msvc.node"
],
"scripts": {
"build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon",
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::ptr::{null, null_mut};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, RECT},
Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO,
MonitorFromRect,
},
System::Threading::GetCurrentProcessId,
UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetForegroundWindow, GetWindow, GetWindowLongPtrW,
GetWindowRect, GetWindowThreadProcessId, IsWindow, IsWindowVisible, WS_BORDER, WS_MAXIMIZE,
},
};
const MONITOR_LIMIT: usize = 16;
fn parse_hwnd_source_id(source_id: &str) -> Option<HWND> {
let token = source_id.strip_prefix("window:")?.split(':').next()?;
let value = if let Some(hex) = token
.strip_prefix("0x")
.or_else(|| token.strip_prefix("0X"))
{
isize::from_str_radix(hex, 16).ok()?
} else {
token.parse::<isize>().ok()?
};
let hwnd = value as HWND;
(unsafe { IsWindow(hwnd) } != 0).then_some(hwnd)
}
fn parse_screen_ordinal(source_id: &str) -> Option<usize> {
let token = source_id.strip_prefix("screen:")?.split(':').next()?;
token.parse::<usize>().ok()
}
pub(crate) fn resolve_game_capture_target(
source_id: &str,
source_kind: &str,
) -> Result<HWND, String> {
if let Some(hwnd) = parse_hwnd_source_id(source_id) {
return Ok(hwnd);
}
if source_kind != "game" && source_kind != "screen" {
return Err(format!(
"invalid game capture source: {source_kind}:{source_id}"
));
}
let monitor = monitor_for_screen_source(source_id)?;
find_fullscreen_window_on_monitor(monitor)
.or_else(find_foreground_fullscreen_window)
.or_else(find_fullscreen_window_on_any_monitor)
.ok_or_else(|| "no fullscreen game window found on selected display".to_string())
}
fn monitor_for_screen_source(source_id: &str) -> Result<HMONITOR, String> {
let ordinal = parse_screen_ordinal(source_id).unwrap_or(0);
let monitors = enumerate_monitors();
monitors
.get(ordinal)
.copied()
.or_else(|| monitors.first().copied())
.ok_or_else(|| "no monitors available for game capture".to_string())
}
fn enumerate_monitors() -> Vec<HMONITOR> {
unsafe extern "system" fn enum_monitor(
monitor: HMONITOR,
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
_rect: *mut RECT,
param: LPARAM,
) -> i32 {
let monitors = &mut *(param as *mut Vec<HMONITOR>);
if monitors.len() >= MONITOR_LIMIT {
return 0;
}
monitors.push(monitor);
1
}
let mut monitors = Vec::with_capacity(MONITOR_LIMIT);
unsafe {
EnumDisplayMonitors(
null_mut(),
null(),
Some(enum_monitor),
&mut monitors as *mut _ as LPARAM,
);
}
assert!(monitors.len() <= MONITOR_LIMIT, "monitor targets bounded");
monitors
}
pub(crate) fn monitor_rect(monitor: HMONITOR) -> Option<RECT> {
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
rcMonitor: RECT::default(),
rcWork: RECT::default(),
dwFlags: 0,
};
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
return None;
}
Some(info.rcMonitor)
}
fn rect_matches_monitor(window_rect: &RECT, monitor_rect: &RECT) -> bool {
const TOLERANCE: i32 = 2;
(window_rect.left - monitor_rect.left).abs() <= TOLERANCE
&& (window_rect.top - monitor_rect.top).abs() <= TOLERANCE
&& (window_rect.right - monitor_rect.right).abs() <= TOLERANCE
&& (window_rect.bottom - monitor_rect.bottom).abs() <= TOLERANCE
}
fn is_regular_maximized_window(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
(style & WS_MAXIMIZE) != 0 && (style & WS_BORDER) != 0
}
fn is_fullscreen_window_on_monitor(hwnd: HWND, monitor: HMONITOR, monitor_rect: &RECT) -> bool {
if unsafe { IsWindowVisible(hwnd) } == 0 {
return false;
}
if !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
return false;
}
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
}
if pid == 0 || pid == unsafe { GetCurrentProcessId() } {
return false;
}
if is_regular_maximized_window(hwnd) {
return false;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return false;
}
let window_monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
window_monitor == monitor && rect_matches_monitor(&rect, monitor_rect)
}
fn find_foreground_fullscreen_window() -> Option<HWND> {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.is_null() {
return None;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return None;
}
let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
let monitor_rect = monitor_rect(monitor)?;
is_fullscreen_window_on_monitor(hwnd, monitor, &monitor_rect).then_some(hwnd)
}
fn find_fullscreen_window_on_any_monitor() -> Option<HWND> {
for monitor in enumerate_monitors() {
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
return Some(hwnd);
}
}
None
}
fn find_fullscreen_window_on_monitor(monitor: HMONITOR) -> Option<HWND> {
struct Search {
monitor: HMONITOR,
monitor_rect: RECT,
result: HWND,
own_pid: u32,
}
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
let search = &mut *(param as *mut Search);
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, &mut pid);
if pid == 0 || pid == search.own_pid {
return 1;
}
if !is_fullscreen_window_on_monitor(hwnd, search.monitor, &search.monitor_rect) {
return 1;
}
search.result = hwnd;
0
}
let monitor_rect = monitor_rect(monitor)?;
let mut search = Search {
monitor,
monitor_rect,
result: null_mut(),
own_pid: unsafe { GetCurrentProcessId() },
};
unsafe {
let _ = EnumWindows(Some(enum_window), &mut search as *mut _ as LPARAM);
}
(!search.result.is_null()).then_some(search.result)
}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use windows::Win32::{
Foundation::HMODULE,
Graphics::{
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device,
ID3D11DeviceContext,
},
Dxgi::{IDXGIAdapter, IDXGIDevice},
},
};
use windows::core::Interface;
pub(crate) fn create_shared_texture_device(
adapter: Option<&IDXGIAdapter>,
) -> Result<(ID3D11Device, ID3D11DeviceContext), String> {
let mut device = None;
let mut context = None;
let driver_type = if adapter.is_some() {
D3D_DRIVER_TYPE_UNKNOWN
} else {
D3D_DRIVER_TYPE_HARDWARE
};
unsafe {
D3D11CreateDevice(
adapter,
driver_type,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
.map_err(|error| format!("D3D11CreateDevice for shared capture texture: {error}"))?;
}
let device = device.ok_or("D3D11 shared texture device was None")?;
let context = context.ok_or("D3D11 shared texture context was None")?;
if let Ok(dxgi_device) = device.cast::<IDXGIDevice>() {
let _ = unsafe { dxgi_device.SetGPUThreadPriority(7) };
}
Ok((device, context))
}
@@ -394,6 +394,7 @@ pub(crate) fn resolve_output_size(
}
#[cfg(target_os = "windows")]
#[cfg(feature = "game-capture-hook")]
pub(crate) fn wall_clock_us() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
@@ -5,7 +5,9 @@ use crate::game_capture_abi::{ENV_FORCE_CPU, ENV_VERBOSE, env_flag_enabled};
use crate::nv12_gpu::Nv12GpuConverter;
use crate::{
CaptureInner,
capture_target::{monitor_rect, resolve_game_capture_target},
compatibility::{InjectionPolicy, injection_policy_for_window},
d3d11_device::create_shared_texture_device,
dxgi_capture::{resolve_output_size, wall_clock_us},
emit_lifecycle, emit_shared_texture_frame,
game_capture_abi::{
@@ -26,26 +28,18 @@ use std::{
sync::{Arc, Mutex, atomic::Ordering},
};
use windows::Win32::{
Foundation::{HANDLE as WinHandle, HMODULE},
Foundation::HANDLE as WinHandle,
Graphics::{
Direct3D::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_UNKNOWN},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
},
Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIDevice, IDXGIFactory1},
Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D},
Dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIFactory1},
},
};
use windows::core::Interface;
use windows_sys::Win32::{
Foundation::{
CloseHandle, FreeLibrary, HANDLE, HINSTANCE, HMODULE as WinSysHmodule, HWND,
INVALID_HANDLE_VALUE, LPARAM, RECT, WAIT_ABANDONED, WAIT_OBJECT_0, WAIT_TIMEOUT,
},
Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO,
MonitorFromRect,
},
Graphics::Gdi::{MONITOR_DEFAULTTONEAREST, MonitorFromRect},
System::{
Diagnostics::Debug::WriteProcessMemory,
LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryW},
@@ -67,10 +61,8 @@ use windows_sys::Win32::{
},
},
UI::WindowsAndMessaging::{
EnumWindows, GW_OWNER, GWL_STYLE, GetClientRect, GetForegroundWindow, GetWindow,
GetWindowLongPtrW, GetWindowRect, GetWindowThreadProcessId, HHOOK, IsWindow,
IsWindowVisible, PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_GETMESSAGE,
WM_NULL, WS_BORDER, WS_MAXIMIZE,
GetClientRect, GetWindowRect, GetWindowThreadProcessId, HHOOK, IsWindow, IsWindowVisible,
PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_GETMESSAGE, WM_NULL,
},
};
@@ -812,87 +804,6 @@ fn inject_via_set_windows_hook(
Ok(InstalledWindowsHook { hook, module })
}
fn parse_hwnd_source_id(source_id: &str) -> Option<HWND> {
let token = source_id.strip_prefix("window:")?.split(':').next()?;
let value = if let Some(hex) = token
.strip_prefix("0x")
.or_else(|| token.strip_prefix("0X"))
{
isize::from_str_radix(hex, 16).ok()?
} else {
token.parse::<isize>().ok()?
};
let hwnd = value as HWND;
if unsafe { IsWindow(hwnd) } != 0 {
Some(hwnd)
} else {
None
}
}
fn parse_screen_ordinal(source_id: &str) -> Option<usize> {
let token = source_id.strip_prefix("screen:")?.split(':').next()?;
token.parse::<usize>().ok()
}
pub(crate) fn resolve_game_capture_target(
source_id: &str,
source_kind: &str,
) -> Result<HWND, String> {
if let Some(hwnd) = parse_hwnd_source_id(source_id) {
return Ok(hwnd);
}
if source_kind == "game" || source_kind == "screen" {
let monitor = monitor_for_screen_source(source_id)?;
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
return Ok(hwnd);
}
if let Some(hwnd) = find_foreground_fullscreen_window() {
return Ok(hwnd);
}
if let Some(hwnd) = find_fullscreen_window_on_any_monitor() {
return Ok(hwnd);
}
return Err("no fullscreen game window found on selected display".into());
}
Err(format!(
"invalid game capture source: {source_kind}:{source_id}"
))
}
fn monitor_for_screen_source(source_id: &str) -> Result<HMONITOR, String> {
let ordinal = parse_screen_ordinal(source_id).unwrap_or(0);
let monitors = enumerate_monitors();
monitors
.get(ordinal)
.copied()
.or_else(|| monitors.first().copied())
.ok_or_else(|| "no monitors available for game capture".to_string())
}
fn enumerate_monitors() -> Vec<HMONITOR> {
unsafe extern "system" fn enum_monitor(
monitor: HMONITOR,
_hdc: windows_sys::Win32::Graphics::Gdi::HDC,
_rect: *mut RECT,
param: LPARAM,
) -> i32 {
let monitors = &mut *(param as *mut Vec<HMONITOR>);
monitors.push(monitor);
1
}
let mut monitors = Vec::new();
unsafe {
EnumDisplayMonitors(
null_mut(),
null(),
Some(enum_monitor),
&mut monitors as *mut _ as LPARAM,
);
}
monitors
}
fn target_process_id(hwnd: HWND) -> Result<u32, String> {
let mut pid = 0u32;
unsafe {
@@ -1039,155 +950,6 @@ fn choose_shared_buffer_dimensions(
Some((buffer_width, buffer_height))
}
fn monitor_rect(monitor: HMONITOR) -> Option<RECT> {
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
rcMonitor: RECT::default(),
rcWork: RECT::default(),
dwFlags: 0,
};
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
return None;
}
Some(info.rcMonitor)
}
fn rect_matches_monitor(window_rect: &RECT, monitor_rect: &RECT) -> bool {
let tolerance = 2;
(window_rect.left - monitor_rect.left).abs() <= tolerance
&& (window_rect.top - monitor_rect.top).abs() <= tolerance
&& (window_rect.right - monitor_rect.right).abs() <= tolerance
&& (window_rect.bottom - monitor_rect.bottom).abs() <= tolerance
}
fn is_regular_maximized_window(hwnd: HWND) -> bool {
let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32;
(style & WS_MAXIMIZE) != 0 && (style & WS_BORDER) != 0
}
fn is_fullscreen_window_on_monitor(hwnd: HWND, monitor: HMONITOR, monitor_rect: &RECT) -> bool {
if unsafe { IsWindowVisible(hwnd) } == 0 || !unsafe { GetWindow(hwnd, GW_OWNER) }.is_null() {
return false;
}
let mut pid = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, &mut pid);
}
if pid == 0 || pid == unsafe { GetCurrentProcessId() } {
return false;
}
if is_regular_maximized_window(hwnd) {
return false;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return false;
}
let window_monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
window_monitor == monitor && rect_matches_monitor(&rect, monitor_rect)
}
fn find_foreground_fullscreen_window() -> Option<HWND> {
let hwnd = unsafe { GetForegroundWindow() };
if hwnd.is_null() {
return None;
}
let mut rect = RECT::default();
if unsafe { GetWindowRect(hwnd, &mut rect) } == 0 {
return None;
}
let monitor = unsafe { MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST) };
let monitor_rect = monitor_rect(monitor)?;
if is_fullscreen_window_on_monitor(hwnd, monitor, &monitor_rect) {
Some(hwnd)
} else {
None
}
}
fn find_fullscreen_window_on_any_monitor() -> Option<HWND> {
for monitor in enumerate_monitors() {
if let Some(hwnd) = find_fullscreen_window_on_monitor(monitor) {
return Some(hwnd);
}
}
None
}
fn find_fullscreen_window_on_monitor(monitor: HMONITOR) -> Option<HWND> {
struct Search {
monitor: HMONITOR,
monitor_rect: RECT,
result: HWND,
own_pid: u32,
}
unsafe extern "system" fn enum_window(hwnd: HWND, param: LPARAM) -> i32 {
let search = &mut *(param as *mut Search);
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, &mut pid);
if pid != 0
&& pid != search.own_pid
&& is_fullscreen_window_on_monitor(hwnd, search.monitor, &search.monitor_rect)
{
search.result = hwnd;
return 0;
}
1
}
let monitor_rect = monitor_rect(monitor)?;
let mut search = Search {
monitor,
monitor_rect,
result: null_mut(),
own_pid: unsafe { GetCurrentProcessId() },
};
unsafe {
let _ = EnumWindows(Some(enum_window), &mut search as *mut _ as LPARAM);
}
if search.result.is_null() {
None
} else {
Some(search.result)
}
}
pub(crate) fn create_shared_texture_device(
adapter: Option<&IDXGIAdapter>,
) -> Result<(ID3D11Device, ID3D11DeviceContext), String> {
let mut device = None;
let mut context = None;
let driver_type = if adapter.is_some() {
D3D_DRIVER_TYPE_UNKNOWN
} else {
D3D_DRIVER_TYPE_HARDWARE
};
unsafe {
D3D11CreateDevice(
adapter,
driver_type,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
None,
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
.map_err(|e| format!("D3D11CreateDevice for shared game texture: {e}"))?;
}
let device = device.ok_or("D3D11 shared texture device was None")?;
let context = context.ok_or("D3D11 shared texture context was None")?;
set_gpu_thread_priority(&device);
Ok((device, context))
}
fn set_gpu_thread_priority(device: &ID3D11Device) {
if let Ok(dxgi_device) = device.cast::<IDXGIDevice>() {
let _ = unsafe { dxgi_device.SetGPUThreadPriority(7) };
}
}
const SHARED_TEXTURE_ADAPTER_LIMIT: u32 = 16;
fn shared_texture_adapter_candidates() -> Vec<Option<IDXGIAdapter>> {
@@ -3,13 +3,17 @@
#![deny(clippy::all)]
#![allow(unsafe_op_in_unsafe_fn)]
#[cfg(any(target_os = "windows", test))]
#[cfg(target_os = "windows")]
mod capture_target;
#[cfg(any(all(target_os = "windows", feature = "game-capture-hook"), test))]
mod compatibility;
#[cfg(target_os = "windows")]
mod d3d11_device;
#[cfg(any(target_os = "windows", test))]
mod dxgi_capture;
pub mod encoder_attach;
mod fallback;
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
mod game_capture;
mod game_capture_abi;
mod gpu_priority;
@@ -19,7 +23,7 @@ mod nv12_gpu;
mod sources;
#[cfg(any(target_os = "windows", test))]
mod stall;
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
mod vulkan_layer_registry;
#[cfg(target_os = "windows")]
mod wgc_capture;
@@ -39,7 +43,7 @@ use dxgi_capture::DxgiCaptureSession;
use fluxer_encoder_ring::EncoderFrameRate;
#[cfg(target_os = "windows")]
use fluxer_screen_frame_bus::EnqueueOutcome;
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
use game_capture::GameCaptureSession;
#[cfg(target_os = "windows")]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@@ -48,7 +52,6 @@ use wgc_capture::WgcCaptureSession;
const LIFECYCLE_QUEUE_LIMIT: usize = 8;
const START_OPTION_UNSUPPORTED_LIMIT: usize = 4;
const GAME_CAPTURE_HOOK_FORCE_DISABLED: bool = true;
type LifecycleTsfn = Arc<
ThreadsafeFunction<
@@ -226,7 +229,7 @@ pub struct CaptureInner {
pub session: Mutex<Option<DxgiCaptureSession>>,
#[cfg(target_os = "windows")]
pub(crate) wgc_session: Mutex<Option<WgcCaptureSession>>,
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
pub game_session: Mutex<Option<Arc<GameCaptureSession>>>,
pub running: std::sync::atomic::AtomicBool,
pub fallback: Mutex<Option<fallback::FallbackTracker>>,
@@ -421,6 +424,7 @@ pub(crate) fn note_media_frame_without_sink(inner: &CaptureInner, message: &'sta
}
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
pub(crate) fn note_cpu_fallback_frame_dropped(inner: &CaptureInner, message: &'static str) {
inner
.cpu_fallback_frames_dropped
@@ -470,7 +474,7 @@ impl ScreenCapture {
session: Mutex::new(None),
#[cfg(target_os = "windows")]
wgc_session: Mutex::new(None),
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
game_session: Mutex::new(None),
running: std::sync::atomic::AtomicBool::new(false),
fallback: Mutex::new(None),
@@ -586,43 +590,48 @@ impl ScreenCapture {
#[cfg(target_os = "windows")]
{
let frame_sink = frame_sink_counter_snapshot(&self.inner);
let guard = self.inner.game_session.lock();
if let Some(session) = guard.as_ref() {
let requested_injection_method = session.requested_injection_method().to_string();
let injection_method = session.used_injection_method().to_string();
if let Some(info) = session.read_shared_info() {
return Some(CaptureDiagnostics {
state: info.state,
api_type: info.api_type,
transport: info.transport,
fallback_reason: info.fallback_reason,
capture_flags: info.capture_flags,
width: info.width,
height: info.height,
dxgi_format: info.dxgi_format,
frame_counter: info.frame_counter as f64,
dropped_frame_counter: info.dropped_frame_counter as f64,
last_present_timestamp_us: info.last_present_timestamp_us as f64,
last_error: info.last_error,
#[cfg(feature = "game-capture-hook")]
{
let guard = self.inner.game_session.lock();
if let Some(session) = guard.as_ref() {
let requested_injection_method =
session.requested_injection_method().to_string();
let injection_method = session.used_injection_method().to_string();
if let Some(info) = session.read_shared_info() {
return Some(CaptureDiagnostics {
state: info.state,
api_type: info.api_type,
transport: info.transport,
fallback_reason: info.fallback_reason,
capture_flags: info.capture_flags,
width: info.width,
height: info.height,
dxgi_format: info.dxgi_format,
frame_counter: info.frame_counter as f64,
dropped_frame_counter: info.dropped_frame_counter as f64,
last_present_timestamp_us: info.last_present_timestamp_us as f64,
last_error: info.last_error,
requested_injection_method,
injection_method,
active_strategy: snapshot.active_strategy,
last_fallback_reason: snapshot.last_fallback_reason,
start_options: current_start_options(&self.inner),
frame_sink_accepted: frame_sink.accepted as f64,
frame_sink_coalesced: frame_sink.coalesced as f64,
frame_sink_rejected: frame_sink.rejected as f64,
media_frames_dropped_without_sink: frame_sink.dropped_without_sink
as f64,
cpu_fallback_frames_dropped: frame_sink.cpu_fallback_dropped as f64,
});
}
return Some(strategy_only_diagnostics(
&snapshot,
requested_injection_method,
injection_method,
active_strategy: snapshot.active_strategy,
last_fallback_reason: snapshot.last_fallback_reason,
start_options: current_start_options(&self.inner),
frame_sink_accepted: frame_sink.accepted as f64,
frame_sink_coalesced: frame_sink.coalesced as f64,
frame_sink_rejected: frame_sink.rejected as f64,
media_frames_dropped_without_sink: frame_sink.dropped_without_sink as f64,
cpu_fallback_frames_dropped: frame_sink.cpu_fallback_dropped as f64,
});
current_start_options(&self.inner),
frame_sink,
));
}
return Some(strategy_only_diagnostics(
&snapshot,
requested_injection_method,
injection_method,
current_start_options(&self.inner),
frame_sink,
));
}
Some(strategy_only_diagnostics(
&snapshot,
@@ -669,7 +678,7 @@ impl ScreenCapture {
#[napi(js_name = "getSharedTextureHandle")]
pub fn get_shared_texture_handle(&self) -> Option<SharedTextureHandleInfo> {
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let guard = self.inner.game_session.lock();
let session = guard.as_ref()?;
@@ -684,7 +693,7 @@ impl ScreenCapture {
}
None
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
None
}
@@ -706,6 +715,9 @@ impl ScreenCapture {
*guard = None;
let mut wgc_guard = self.inner.wgc_session.lock();
*wgc_guard = None;
}
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let mut game_guard = self.inner.game_session.lock();
*game_guard = None;
}
@@ -955,7 +967,8 @@ impl ScreenCapture {
return Err(napi::Error::from_reason("Capture already running"));
}
if source_kind == "game" && !GAME_CAPTURE_HOOK_FORCE_DISABLED {
#[cfg(feature = "game-capture-hook")]
if source_kind == "game" {
return self.start_windows_game(
source_id,
source_kind,
@@ -991,7 +1004,7 @@ impl ScreenCapture {
}
let hwnd = if source_kind == "game" {
let target = game_capture::resolve_game_capture_target(&source_id, &source_kind)
let target = capture_target::resolve_game_capture_target(&source_id, &source_kind)
.map_err(|e| {
napi::Error::from_reason(format!("Failed to resolve game capture target: {e}"))
})?;
@@ -1115,6 +1128,7 @@ impl ScreenCapture {
})
}
#[cfg(feature = "game-capture-hook")]
#[allow(clippy::too_many_arguments)]
fn start_windows_game(
&self,
@@ -1278,7 +1292,7 @@ pub fn is_supported() -> bool {
#[napi(js_name = "isGameCaptureHookAvailable")]
pub fn is_game_capture_hook_available() -> bool {
cfg!(target_os = "windows") && !GAME_CAPTURE_HOOK_FORCE_DISABLED
cfg!(all(target_os = "windows", feature = "game-capture-hook"))
}
#[napi(js_name = "getAvailability")]
@@ -1314,37 +1328,37 @@ pub fn restore_gpu_scheduling_priority(process_id: Option<u32>) -> Result<()> {
#[napi(js_name = "registerVulkanLayerManifest")]
pub fn register_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
vulkan_layer_registry::register_manifest(&manifest_path).map_err(napi::Error::from_reason)
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
let _ = manifest_path;
Err(napi::Error::from_reason(
"Vulkan game capture layer only supported on Windows",
"Vulkan game capture layer is not included in this build",
))
}
}
#[napi(js_name = "unregisterVulkanLayerManifest")]
pub fn unregister_vulkan_layer_manifest(manifest_path: String) -> Result<()> {
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
vulkan_layer_registry::unregister_manifest(&manifest_path).map_err(napi::Error::from_reason)
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
let _ = manifest_path;
Err(napi::Error::from_reason(
"Vulkan game capture layer only supported on Windows",
"Vulkan game capture layer is not included in this build",
))
}
}
#[napi(js_name = "getVulkanLayerRegistrationState")]
pub fn get_vulkan_layer_registration_state(manifest_path: String) -> VulkanLayerRegistrationState {
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "game-capture-hook"))]
{
let state = vulkan_layer_registry::registration_state(&manifest_path);
VulkanLayerRegistrationState {
@@ -1354,7 +1368,7 @@ pub fn get_vulkan_layer_registration_state(manifest_path: String) -> VulkanLayer
manifest_path: state.manifest_path,
}
}
#[cfg(not(target_os = "windows"))]
#[cfg(not(all(target_os = "windows", feature = "game-capture-hook")))]
{
VulkanLayerRegistrationState {
registered: false,
@@ -208,7 +208,7 @@ impl WgcCaptureSession {
requested_height: Option<u32>,
) -> Result<Self, String> {
ensure_winrt_initialized();
let (device, context) = crate::game_capture::create_shared_texture_device(None)?;
let (device, context) = crate::d3d11_device::create_shared_texture_device(None)?;
let dxgi_device: IDXGIDevice = device
.cast()
.map_err(|e| format!("IDXGIDevice cast: {e}"))?;
+46 -28
View File
@@ -157,43 +157,58 @@ function collectRuntimeArtifactPaths(packageDir) {
}
function isNativeRuntimeSidecar(fileName) {
return (
fileName.endsWith('.node') ||
/\.so(?:\.|$)/.test(fileName) ||
/\.(?:dll|exe)$/i.test(fileName) ||
isWindowsNativeRuntimeManifest(fileName)
);
return fileName.endsWith('.node') || /\.so(?:\.|$)/.test(fileName) || /\.(?:dll|exe)$/i.test(fileName);
}
function isWindowsNativeRuntimeManifest(fileName) {
return (
fileName === 'compatibility.json' || /^fluxer-vulkan-layer\.win32-(?:x64|ia32|arm64)-msvc\.json$/i.test(fileName)
);
function electronArch() {
return process.env.ELECTRON_ARCH || process.env.npm_config_arch || process.arch;
}
function addWinGameCaptureRuntimeArtifacts(artifacts, tag, arch) {
const add = (relativePath) => {
artifacts.push({
label: '@fluxer/win-game-capture',
relativePath,
runtimeFiles: [],
});
};
function primaryWinGameCaptureNodeFileName() {
const arch = electronArch();
const tag = platformTag(process.platform, arch);
if (!tag || process.platform !== 'win32') {
throw new Error(`Cannot resolve the primary win-game-capture node for ${process.platform}/${arch}`);
}
return `win-game-capture.${tag}.node`;
}
function removeStaleWinGameCaptureArtifacts(packageDir, primaryNodeFileName) {
if (!fs.existsSync(packageDir)) return;
const stale = fs
.readdirSync(packageDir, {withFileTypes: true})
.filter((entry) => {
if (!entry.isFile()) return false;
return (
entry.name.startsWith('fluxer-game-hook.') ||
entry.name.startsWith('fluxer-inject-helper.') ||
entry.name.startsWith('fluxer-vulkan-layer.') ||
(entry.name.startsWith('win-game-capture.') &&
entry.name.endsWith('.node') &&
entry.name !== primaryNodeFileName)
);
})
.map((entry) => entry.name)
.sort();
for (const fileName of stale) {
const artifactPath = path.join(packageDir, fileName);
fs.rmSync(artifactPath);
console.log(` Removed stale @fluxer/win-game-capture artifact ${path.relative(ROOT_DIR, artifactPath)}`);
}
}
function addWinGameCaptureRuntimeArtifacts(artifacts, tag) {
artifacts.push({
label: '@fluxer/win-game-capture',
relativePath: `win-game-capture.${tag}.node`,
});
add(`fluxer-game-hook.${tag}.dll`);
add(`fluxer-inject-helper.${tag}.exe`);
add(`fluxer-vulkan-layer.${tag}.dll`);
add(`fluxer-vulkan-layer.${tag}.json`);
if (arch === 'x64') {
add('fluxer-game-hook.win32-ia32-msvc.dll');
add('fluxer-inject-helper.win32-ia32-msvc.exe');
}
}
function copyRuntimeArtifactsToInstalledPackages({label, packageDir}) {
const primaryNodeFileName = label === '@fluxer/win-game-capture' ? primaryWinGameCaptureNodeFileName() : null;
if (primaryNodeFileName) {
removeStaleWinGameCaptureArtifacts(packageDir, primaryNodeFileName);
}
const artifacts = collectRuntimeArtifactPaths(packageDir);
if (artifacts.length === 0) {
return;
@@ -206,6 +221,9 @@ function copyRuntimeArtifactsToInstalledPackages({label, packageDir}) {
return;
}
for (const installedPackageDir of installedPackageDirs) {
if (primaryNodeFileName) {
removeStaleWinGameCaptureArtifacts(installedPackageDir, primaryNodeFileName);
}
for (const artifact of artifacts) {
const sourcePath = path.join(packageDir, artifact);
const targetPath = path.join(installedPackageDir, artifact);
@@ -225,7 +243,7 @@ function platformTag(platform, arch) {
return null;
}
function expectedNativeRuntimeArtifacts(platform = process.platform, arch = process.env.ELECTRON_ARCH || process.arch) {
function expectedNativeRuntimeArtifacts(platform = process.platform, arch = electronArch()) {
if (platform === 'darwin' && arch === 'universal') {
return [
...expectedNativeRuntimeArtifactsForArch(platform, 'arm64'),
@@ -284,7 +302,7 @@ function expectedNativeRuntimeArtifactsForArch(platform, arch) {
label: '@fluxer/win-process-loopback',
relativePath: `win-process-loopback.${tag}.node`,
});
addWinGameCaptureRuntimeArtifacts(artifacts, tag, arch);
addWinGameCaptureRuntimeArtifacts(artifacts, tag);
artifacts.push({
label: '@fluxer/win-clipboard',
relativePath: `win-clipboard.${tag}.node`,
-1
View File
@@ -688,7 +688,6 @@ export interface ElectronAPI {
pasteFromClipboard: () => Promise<void>;
onDeepLink: (callback: (url: string) => void) => () => void;
getInitialDeepLink: () => Promise<string | null>;
onRpcNavigate: (callback: (path: string) => void) => () => void;
autostartEnable: () => Promise<void>;
autostartDisable: () => Promise<void>;
autostartIsEnabled: () => Promise<boolean>;
-217
View File
@@ -1,217 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import http from 'node:http';
import {BUILD_CHANNEL} from '@electron/common/BuildChannel';
import {CANARY_APP_URL, STABLE_APP_URL} from '@electron/common/Constants';
import {getCustomAppUrl} from '@electron/common/DesktopConfig';
import {getMainWindow, showWindow} from '@electron/main/Window';
import {app} from 'electron';
import log from 'electron-log';
const RPC_PORT = BUILD_CHANNEL === 'canary' ? 21864 : 21863;
const ALLOWED_ORIGINS = [STABLE_APP_URL, CANARY_APP_URL];
const isAllowedOrigin = (origin?: string): boolean => {
if (!origin) return false;
if (ALLOWED_ORIGINS.includes(origin)) return true;
const customUrl = getCustomAppUrl();
return customUrl != null && origin === customUrl;
};
const refererMatchesAllowedOrigin = (referer?: string): boolean => {
if (!referer) return false;
if (ALLOWED_ORIGINS.some((allowed) => referer.startsWith(allowed))) return true;
const customUrl = getCustomAppUrl();
return customUrl != null && referer.startsWith(customUrl);
};
let server: http.Server | null = null;
interface RpcRequest {
method: string;
params?: Record<string, unknown>;
}
interface RpcResponse {
success: boolean;
data?: unknown;
error?: string;
}
const sendJson = (res: http.ServerResponse, status: number, data: RpcResponse) => {
res.writeHead(status, {'Content-Type': 'application/json'});
res.end(JSON.stringify(data));
};
const rejectIfDisallowedPage = (req: http.IncomingMessage, res: http.ServerResponse): boolean => {
const origin = req.headers.origin;
const referer = req.headers.referer;
if (!origin && !referer) {
res.writeHead(403);
res.end();
return true;
}
if (origin && !isAllowedOrigin(origin)) {
res.writeHead(403);
res.end();
return true;
}
if (referer && !refererMatchesAllowedOrigin(referer)) {
res.writeHead(403);
res.end();
return true;
}
if (origin && referer && !referer.startsWith(origin)) {
res.writeHead(403);
res.end();
return true;
}
return false;
};
const handleCors = (req: http.IncomingMessage, res: http.ServerResponse): boolean => {
const origin = req.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
res.writeHead(403);
res.end();
return true;
}
if (origin && isAllowedOrigin(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Max-Age', '86400');
}
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return true;
}
return false;
};
const parseBody = (req: http.IncomingMessage): Promise<RpcRequest | null> => {
return new Promise((resolve) => {
if (req.method !== 'POST') {
resolve(null);
return;
}
let body = '';
req.on('data', (chunk) => {
body += chunk;
if (body.length > 1024 * 1024) {
resolve(null);
}
});
req.on('end', () => {
try {
resolve(JSON.parse(body) as RpcRequest);
} catch {
resolve(null);
}
});
req.on('error', () => resolve(null));
});
};
const handleHealth = (_req: http.IncomingMessage, res: http.ServerResponse) => {
sendJson(res, 200, {
success: true,
data: {
status: 'ok',
channel: BUILD_CHANNEL,
version: app.getVersion(),
platform: process.platform,
},
});
};
const handleNavigate = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const body = await parseBody(req);
if (!body?.params?.path || typeof body.params['path'] !== 'string') {
sendJson(res, 400, {success: false, error: 'Missing or invalid path parameter'});
return;
}
const path = body.params['path'];
const mainWindow = getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
sendJson(res, 503, {success: false, error: 'Main window not available'});
return;
}
mainWindow.webContents.send('rpc-navigate', path);
showWindow();
sendJson(res, 200, {success: true, data: {navigated: true, path}});
};
const handleFocus = (_req: http.IncomingMessage, res: http.ServerResponse) => {
const mainWindow = getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
sendJson(res, 503, {success: false, error: 'Main window not available'});
return;
}
showWindow();
sendJson(res, 200, {success: true, data: {focused: true}});
};
const requestHandler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
const remoteAddress = req.socket.remoteAddress;
if (remoteAddress !== '127.0.0.1' && remoteAddress !== '::1' && remoteAddress !== '::ffff:127.0.0.1') {
res.writeHead(403);
res.end();
return;
}
if (rejectIfDisallowedPage(req, res)) {
return;
}
if (handleCors(req, res)) {
return;
}
const url = req.url ?? '/';
try {
switch (url) {
case '/health':
handleHealth(req, res);
break;
case '/navigate':
await handleNavigate(req, res);
break;
case '/focus':
handleFocus(req, res);
break;
default:
sendJson(res, 404, {success: false, error: 'Not found'});
}
} catch (error) {
log.error('[RPC] Request handler error:', error);
sendJson(res, 500, {success: false, error: 'Internal server error'});
}
};
export const startRpcServer = (): Promise<void> => {
return new Promise((resolve, reject) => {
if (server) {
resolve();
return;
}
server = http.createServer(requestHandler);
server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') {
log.warn(`[RPC] Port ${RPC_PORT} already in use, RPC server disabled`);
server = null;
resolve();
} else {
log.error('[RPC] Server error:', error);
reject(error);
}
});
server.listen(RPC_PORT, '127.0.0.1', () => {
log.info(`[RPC] Server listening on http://127.0.0.1:${RPC_PORT}`);
resolve();
});
});
};
export const stopRpcServer = (): Promise<void> => {
return new Promise((resolve) => {
if (!server) {
resolve();
return;
}
server.close((err) => {
if (err) {
log.error('[RPC] Error closing server:', err);
}
server = null;
resolve();
});
});
};
+1 -5
View File
@@ -68,7 +68,6 @@ import {
import {runNativeModulePreflight} from '@electron/main/NativeModulePreflight';
import {cleanupNativeScreenCapture, registerNativeScreenCaptureHandlers} from '@electron/main/NativeScreenCapture';
import {appendOpenH264Switches} from '@electron/main/OpenH264Manager';
import {startRpcServer, stopRpcServer} from '@electron/main/RpcServer';
import {cleanupLinuxChromiumSpellcheckDictionaries} from '@electron/main/Spellcheck';
import {registerUpdater} from '@electron/main/Updater';
import {
@@ -430,9 +429,6 @@ if (launchConfigurationError) {
showWindow();
}
});
void startRpcServer().catch((error: unknown) => {
log.error('[RPC] Failed to start RPC server:', error);
});
log.info('App initialized successfully');
})
.catch((error: unknown) => {
@@ -478,7 +474,7 @@ if (launchConfigurationError) {
cleanupNativeHardwareEncoderHandlers();
cleanupVirtmic();
destroyDesktopTray();
const asyncCleanups: Array<Promise<unknown>> = [stopRpcServer()];
const asyncCleanups: Array<Promise<unknown>> = [];
if (netLog.currentlyLogging) {
asyncCleanups.push(
netLog.stopLogging().catch((error) => {
-9
View File
@@ -425,15 +425,6 @@ const api: ElectronAPI = {
};
},
getInitialDeepLink: (): Promise<string | null> => ipcRenderer.invoke('get-initial-deep-link'),
onRpcNavigate: (callback: (path: string) => void): (() => void) => {
const handler = (_event: Electron.IpcRendererEvent, path: string): void => {
callback(path);
};
ipcRenderer.on('rpc-navigate', handler);
return () => {
ipcRenderer.removeListener('rpc-navigate', handler);
};
},
autostartEnable: (): Promise<void> => ipcRenderer.invoke('autostart-enable'),
autostartDisable: (): Promise<void> => ipcRenderer.invoke('autostart-disable'),
autostartIsEnabled: (): Promise<boolean> => ipcRenderer.invoke('autostart-is-enabled'),