mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-11 04:19:26 -07:00
refactor: simplify Windows tracker helper API
This commit is contained in:
@@ -1,401 +0,0 @@
|
||||
param(
|
||||
[ValidateSet('geometry', 'foreground-process', 'bind-overlay', 'lower-overlay', 'set-owner', 'clear-owner', 'target-hwnd')]
|
||||
[string]$Mode = 'geometry',
|
||||
[string]$SocketPath,
|
||||
[string]$OverlayWindowHandle
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
try {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class SubMinerWindowsHelper {
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool SetWindowPos(
|
||||
IntPtr hWnd,
|
||||
IntPtr hWndInsertAfter,
|
||||
int X,
|
||||
int Y,
|
||||
int cx,
|
||||
int cy,
|
||||
uint uFlags
|
||||
);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern void SetLastError(uint dwErrCode);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
|
||||
}
|
||||
"@
|
||||
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
||||
$SWP_NOSIZE = 0x0001
|
||||
$SWP_NOMOVE = 0x0002
|
||||
$SWP_NOACTIVATE = 0x0010
|
||||
$SWP_NOOWNERZORDER = 0x0200
|
||||
$SWP_FLAGS = $SWP_NOSIZE -bor $SWP_NOMOVE -bor $SWP_NOACTIVATE -bor $SWP_NOOWNERZORDER
|
||||
$GWL_EXSTYLE = -20
|
||||
$WS_EX_TOPMOST = 0x00000008
|
||||
$GWLP_HWNDPARENT = -8
|
||||
$HWND_TOP = [IntPtr]::Zero
|
||||
$HWND_BOTTOM = [IntPtr]::One
|
||||
$HWND_TOPMOST = [IntPtr](-1)
|
||||
$HWND_NOTOPMOST = [IntPtr](-2)
|
||||
|
||||
function Assert-SetWindowLongPtrSucceeded {
|
||||
param(
|
||||
[IntPtr]$Result,
|
||||
[string]$Operation
|
||||
)
|
||||
|
||||
if ($Result -ne [IntPtr]::Zero) {
|
||||
return
|
||||
}
|
||||
|
||||
if ([Runtime.InteropServices.Marshal]::GetLastWin32Error() -eq 0) {
|
||||
return
|
||||
}
|
||||
|
||||
$lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
throw "$Operation failed ($lastError)"
|
||||
}
|
||||
|
||||
function Assert-SetWindowPosSucceeded {
|
||||
param(
|
||||
[bool]$Result,
|
||||
[string]$Operation
|
||||
)
|
||||
|
||||
if ($Result) {
|
||||
return
|
||||
}
|
||||
|
||||
$lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
throw "$Operation failed ($lastError)"
|
||||
}
|
||||
|
||||
if ($Mode -eq 'foreground-process') {
|
||||
$foregroundWindow = [SubMinerWindowsHelper]::GetForegroundWindow()
|
||||
if ($foregroundWindow -eq [IntPtr]::Zero) {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
[uint32]$foregroundProcessId = 0
|
||||
[void][SubMinerWindowsHelper]::GetWindowThreadProcessId($foregroundWindow, [ref]$foregroundProcessId)
|
||||
if ($foregroundProcessId -eq 0) {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
$foregroundProcess = Get-Process -Id $foregroundProcessId -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Output "process=$($foregroundProcess.ProcessName)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Mode -eq 'clear-owner') {
|
||||
if ([string]::IsNullOrWhiteSpace($OverlayWindowHandle)) {
|
||||
[Console]::Error.WriteLine('overlay-window-handle-required')
|
||||
exit 1
|
||||
}
|
||||
|
||||
[IntPtr]$overlayWindow = [IntPtr]([int64]$OverlayWindowHandle)
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowLongPtr($overlayWindow, $GWLP_HWNDPARENT, [IntPtr]::Zero)
|
||||
Assert-SetWindowLongPtrSucceeded -Result $result -Operation 'clear-owner'
|
||||
Write-Output 'ok'
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Get-WindowBounds {
|
||||
param([IntPtr]$hWnd)
|
||||
|
||||
$rect = New-Object SubMinerWindowsHelper+RECT
|
||||
$size = [System.Runtime.InteropServices.Marshal]::SizeOf($rect)
|
||||
$dwmResult = [SubMinerWindowsHelper]::DwmGetWindowAttribute(
|
||||
$hWnd,
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS,
|
||||
[ref]$rect,
|
||||
$size
|
||||
)
|
||||
|
||||
if ($dwmResult -ne 0) {
|
||||
if (-not [SubMinerWindowsHelper]::GetWindowRect($hWnd, [ref]$rect)) {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$width = $rect.Right - $rect.Left
|
||||
$height = $rect.Bottom - $rect.Top
|
||||
if ($width -le 0 -or $height -le 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
X = $rect.Left
|
||||
Y = $rect.Top
|
||||
Width = $width
|
||||
Height = $height
|
||||
Area = $width * $height
|
||||
}
|
||||
}
|
||||
|
||||
$commandLineByPid = @{}
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
foreach ($process in Get-CimInstance Win32_Process) {
|
||||
$commandLineByPid[[uint32]$process.ProcessId] = $process.CommandLine
|
||||
}
|
||||
}
|
||||
|
||||
$mpvMatches = New-Object System.Collections.Generic.List[object]
|
||||
$targetWindowState = 'not-found'
|
||||
$foregroundWindow = [SubMinerWindowsHelper]::GetForegroundWindow()
|
||||
$callback = [SubMinerWindowsHelper+EnumWindowsProc]{
|
||||
param([IntPtr]$hWnd, [IntPtr]$lParam)
|
||||
|
||||
if (-not [SubMinerWindowsHelper]::IsWindowVisible($hWnd)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
[uint32]$windowProcessId = 0
|
||||
[void][SubMinerWindowsHelper]::GetWindowThreadProcessId($hWnd, [ref]$windowProcessId)
|
||||
if ($windowProcessId -eq 0) {
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$process = Get-Process -Id $windowProcessId -ErrorAction Stop
|
||||
} catch {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($process.ProcessName -ine 'mpv') {
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
$commandLine = $commandLineByPid[[uint32]$windowProcessId]
|
||||
if ([string]::IsNullOrWhiteSpace($commandLine)) {
|
||||
return $true
|
||||
}
|
||||
if (
|
||||
($commandLine -notlike "*--input-ipc-server=$SocketPath*") -and
|
||||
($commandLine -notlike "*--input-ipc-server $SocketPath*")
|
||||
) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
if ([SubMinerWindowsHelper]::IsIconic($hWnd)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath) -and $targetWindowState -ne 'visible') {
|
||||
$targetWindowState = 'minimized'
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
$bounds = Get-WindowBounds -hWnd $hWnd
|
||||
if ($null -eq $bounds) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
$targetWindowState = 'visible'
|
||||
}
|
||||
|
||||
$mpvMatches.Add([PSCustomObject]@{
|
||||
HWnd = $hWnd
|
||||
X = $bounds.X
|
||||
Y = $bounds.Y
|
||||
Width = $bounds.Width
|
||||
Height = $bounds.Height
|
||||
Area = $bounds.Area
|
||||
IsForeground = ($foregroundWindow -ne [IntPtr]::Zero -and $hWnd -eq $foregroundWindow)
|
||||
})
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
[void][SubMinerWindowsHelper]::EnumWindows($callback, [IntPtr]::Zero)
|
||||
|
||||
if ($Mode -eq 'lower-overlay') {
|
||||
if ([string]::IsNullOrWhiteSpace($OverlayWindowHandle)) {
|
||||
[Console]::Error.WriteLine('overlay-window-handle-required')
|
||||
exit 1
|
||||
}
|
||||
|
||||
[IntPtr]$overlayWindow = [IntPtr]([int64]$OverlayWindowHandle)
|
||||
|
||||
[void][SubMinerWindowsHelper]::SetWindowPos(
|
||||
$overlayWindow,
|
||||
$HWND_NOTOPMOST,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$SWP_FLAGS
|
||||
)
|
||||
[void][SubMinerWindowsHelper]::SetWindowPos(
|
||||
$overlayWindow,
|
||||
$HWND_BOTTOM,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$SWP_FLAGS
|
||||
)
|
||||
Write-Output 'ok'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$focusedMatch = $mpvMatches | Where-Object { $_.IsForeground } | Select-Object -First 1
|
||||
if ($null -ne $focusedMatch) {
|
||||
[Console]::Error.WriteLine('focus=focused')
|
||||
} else {
|
||||
[Console]::Error.WriteLine('focus=not-focused')
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
[Console]::Error.WriteLine("state=$targetWindowState")
|
||||
}
|
||||
|
||||
if ($mpvMatches.Count -eq 0) {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$bestMatch = if ($null -ne $focusedMatch) {
|
||||
$focusedMatch
|
||||
} else {
|
||||
$mpvMatches | Sort-Object -Property Area, Width, Height -Descending | Select-Object -First 1
|
||||
}
|
||||
|
||||
if ($Mode -eq 'target-hwnd') {
|
||||
Write-Output "$($bestMatch.HWnd)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Mode -eq 'set-owner') {
|
||||
if ([string]::IsNullOrWhiteSpace($OverlayWindowHandle)) {
|
||||
[Console]::Error.WriteLine('overlay-window-handle-required')
|
||||
exit 1
|
||||
}
|
||||
|
||||
[IntPtr]$overlayWindow = [IntPtr]([int64]$OverlayWindowHandle)
|
||||
$targetWindow = [IntPtr]$bestMatch.HWnd
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowLongPtr($overlayWindow, $GWLP_HWNDPARENT, $targetWindow)
|
||||
Assert-SetWindowLongPtrSucceeded -Result $result -Operation 'set-owner'
|
||||
Write-Output 'ok'
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Mode -eq 'bind-overlay') {
|
||||
if ([string]::IsNullOrWhiteSpace($OverlayWindowHandle)) {
|
||||
[Console]::Error.WriteLine('overlay-window-handle-required')
|
||||
exit 1
|
||||
}
|
||||
|
||||
[IntPtr]$overlayWindow = [IntPtr]([int64]$OverlayWindowHandle)
|
||||
$targetWindow = [IntPtr]$bestMatch.HWnd
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowLongPtr($overlayWindow, $GWLP_HWNDPARENT, $targetWindow)
|
||||
Assert-SetWindowLongPtrSucceeded -Result $result -Operation 'bind-overlay owner assignment'
|
||||
$targetWindowExStyle = [SubMinerWindowsHelper]::GetWindowLong($targetWindow, $GWL_EXSTYLE)
|
||||
$targetWindowIsTopmost = ($targetWindowExStyle -band $WS_EX_TOPMOST) -ne 0
|
||||
|
||||
$overlayExStyle = [SubMinerWindowsHelper]::GetWindowLong($overlayWindow, $GWL_EXSTYLE)
|
||||
$overlayIsTopmost = ($overlayExStyle -band $WS_EX_TOPMOST) -ne 0
|
||||
if ($targetWindowIsTopmost -and -not $overlayIsTopmost) {
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowPos(
|
||||
$overlayWindow, $HWND_TOPMOST, 0, 0, 0, 0, $SWP_FLAGS
|
||||
)
|
||||
Assert-SetWindowPosSucceeded -Result $result -Operation 'bind-overlay topmost adjustment'
|
||||
} elseif (-not $targetWindowIsTopmost -and $overlayIsTopmost) {
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowPos(
|
||||
$overlayWindow, $HWND_NOTOPMOST, 0, 0, 0, 0, $SWP_FLAGS
|
||||
)
|
||||
Assert-SetWindowPosSucceeded -Result $result -Operation 'bind-overlay notopmost adjustment'
|
||||
}
|
||||
|
||||
$GW_HWNDPREV = 3
|
||||
$windowAboveMpv = [SubMinerWindowsHelper]::GetWindow($targetWindow, $GW_HWNDPREV)
|
||||
|
||||
if ($windowAboveMpv -ne [IntPtr]::Zero -and $windowAboveMpv -eq $overlayWindow) {
|
||||
Write-Output 'ok'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$insertAfter = $HWND_TOP
|
||||
if ($windowAboveMpv -ne [IntPtr]::Zero) {
|
||||
$aboveExStyle = [SubMinerWindowsHelper]::GetWindowLong($windowAboveMpv, $GWL_EXSTYLE)
|
||||
$aboveIsTopmost = ($aboveExStyle -band $WS_EX_TOPMOST) -ne 0
|
||||
if ($aboveIsTopmost -eq $targetWindowIsTopmost) {
|
||||
$insertAfter = $windowAboveMpv
|
||||
}
|
||||
}
|
||||
|
||||
[SubMinerWindowsHelper]::SetLastError(0)
|
||||
$result = [SubMinerWindowsHelper]::SetWindowPos(
|
||||
$overlayWindow, $insertAfter, 0, 0, 0, 0, $SWP_FLAGS
|
||||
)
|
||||
Assert-SetWindowPosSucceeded -Result $result -Operation 'bind-overlay z-order adjustment'
|
||||
Write-Output 'ok'
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Output "$($bestMatch.X),$($bestMatch.Y),$($bestMatch.Width),$($bestMatch.Height)"
|
||||
} catch {
|
||||
[Console]::Error.WriteLine($_.Exception.Message)
|
||||
exit 1
|
||||
}
|
||||
@@ -8,8 +8,6 @@ const repoRoot = path.resolve(scriptDir, '..');
|
||||
const rendererSourceDir = path.join(repoRoot, 'src', 'renderer');
|
||||
const rendererOutputDir = path.join(repoRoot, 'dist', 'renderer');
|
||||
const scriptsOutputDir = path.join(repoRoot, 'dist', 'scripts');
|
||||
const windowsHelperSourcePath = path.join(scriptDir, 'get-mpv-window-windows.ps1');
|
||||
const windowsHelperOutputPath = path.join(scriptsOutputDir, 'get-mpv-window-windows.ps1');
|
||||
const macosHelperSourcePath = path.join(scriptDir, 'get-mpv-window-macos.swift');
|
||||
const macosHelperBinaryPath = path.join(scriptsOutputDir, 'get-mpv-window-macos');
|
||||
const macosHelperSourceCopyPath = path.join(scriptsOutputDir, 'get-mpv-window-macos.swift');
|
||||
@@ -33,11 +31,6 @@ function copyRendererAssets() {
|
||||
process.stdout.write(`Staged renderer assets in ${rendererOutputDir}\n`);
|
||||
}
|
||||
|
||||
function stageWindowsHelper() {
|
||||
copyFile(windowsHelperSourcePath, windowsHelperOutputPath);
|
||||
process.stdout.write(`Staged Windows helper: ${windowsHelperOutputPath}\n`);
|
||||
}
|
||||
|
||||
function fallbackToMacosSource() {
|
||||
copyFile(macosHelperSourcePath, macosHelperSourceCopyPath);
|
||||
process.stdout.write(`Staged macOS helper source fallback: ${macosHelperSourceCopyPath}\n`);
|
||||
@@ -77,7 +70,6 @@ function buildMacosHelper() {
|
||||
|
||||
function main() {
|
||||
copyRendererAssets();
|
||||
stageWindowsHelper();
|
||||
buildMacosHelper();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,323 +1,60 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as windowsHelper from './windows-helper';
|
||||
import {
|
||||
lowerWindowsOverlayInZOrder,
|
||||
parseWindowTrackerHelperForegroundProcess,
|
||||
parseWindowTrackerHelperFocusState,
|
||||
parseWindowTrackerHelperOutput,
|
||||
parseWindowTrackerHelperState,
|
||||
queryWindowsForegroundProcessName,
|
||||
queryWindowsTargetWindowHandle,
|
||||
queryWindowsTrackerMpvWindows,
|
||||
resolveWindowsTrackerHelper,
|
||||
syncWindowsOverlayToMpvZOrder,
|
||||
} from './windows-helper';
|
||||
import { findWindowsMpvTargetWindowHandle } from './windows-helper';
|
||||
import type { MpvPollResult } from './win32';
|
||||
|
||||
test('parseWindowTrackerHelperOutput parses helper geometry output', () => {
|
||||
assert.deepEqual(parseWindowTrackerHelperOutput('120,240,1280,720'), {
|
||||
x: 120,
|
||||
y: 240,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
});
|
||||
});
|
||||
|
||||
test('parseWindowTrackerHelperOutput returns null for misses and invalid payloads', () => {
|
||||
assert.equal(parseWindowTrackerHelperOutput('not-found'), null);
|
||||
assert.equal(parseWindowTrackerHelperOutput('1,2,3'), null);
|
||||
assert.equal(parseWindowTrackerHelperOutput('1,2,0,4'), null);
|
||||
});
|
||||
|
||||
test('parseWindowTrackerHelperFocusState parses helper stderr metadata', () => {
|
||||
assert.equal(parseWindowTrackerHelperFocusState('focus=focused'), true);
|
||||
assert.equal(parseWindowTrackerHelperFocusState('focus=not-focused'), false);
|
||||
assert.equal(parseWindowTrackerHelperFocusState('warning\nfocus=focused\nnote'), true);
|
||||
assert.equal(parseWindowTrackerHelperFocusState(''), null);
|
||||
});
|
||||
|
||||
test('parseWindowTrackerHelperState parses helper stderr metadata', () => {
|
||||
assert.equal(parseWindowTrackerHelperState('state=visible'), 'visible');
|
||||
assert.equal(parseWindowTrackerHelperState('focus=not-focused\nstate=minimized'), 'minimized');
|
||||
assert.equal(parseWindowTrackerHelperState('state=unknown'), null);
|
||||
assert.equal(parseWindowTrackerHelperState(''), null);
|
||||
});
|
||||
|
||||
test('parseWindowTrackerHelperForegroundProcess parses helper stdout metadata', () => {
|
||||
assert.equal(parseWindowTrackerHelperForegroundProcess('process=mpv'), 'mpv');
|
||||
assert.equal(parseWindowTrackerHelperForegroundProcess('process=chrome'), 'chrome');
|
||||
assert.equal(parseWindowTrackerHelperForegroundProcess('not-found'), null);
|
||||
assert.equal(parseWindowTrackerHelperForegroundProcess(''), null);
|
||||
});
|
||||
|
||||
test('queryWindowsForegroundProcessName reads foreground process from powershell helper', async () => {
|
||||
const processName = await queryWindowsForegroundProcessName({
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async () => ({
|
||||
stdout: 'process=mpv',
|
||||
stderr: '',
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(processName, 'mpv');
|
||||
});
|
||||
|
||||
test('queryWindowsForegroundProcessName returns null when no powershell helper is available', async () => {
|
||||
const processName = await queryWindowsForegroundProcessName({
|
||||
resolveHelper: () => ({
|
||||
kind: 'native',
|
||||
command: 'helper.exe',
|
||||
args: [],
|
||||
helperPath: 'helper.exe',
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(processName, null);
|
||||
});
|
||||
|
||||
test('syncWindowsOverlayToMpvZOrder forwards socket path and overlay handle to powershell helper', async () => {
|
||||
let capturedMode: string | null = null;
|
||||
let capturedArgs: string[] | null = null;
|
||||
|
||||
const synced = await syncWindowsOverlayToMpvZOrder({
|
||||
overlayWindowHandle: '12345',
|
||||
targetMpvSocketPath: '\\\\.\\pipe\\subminer-socket',
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async (_spec, mode, extraArgs = []) => {
|
||||
capturedMode = mode;
|
||||
capturedArgs = extraArgs;
|
||||
return {
|
||||
stdout: 'ok',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(synced, true);
|
||||
assert.equal(capturedMode, 'bind-overlay');
|
||||
assert.deepEqual(capturedArgs, ['\\\\.\\pipe\\subminer-socket', '12345']);
|
||||
});
|
||||
|
||||
test('lowerWindowsOverlayInZOrder forwards overlay handle to powershell helper', async () => {
|
||||
let capturedMode: string | null = null;
|
||||
let capturedArgs: string[] | null = null;
|
||||
|
||||
const lowered = await lowerWindowsOverlayInZOrder({
|
||||
overlayWindowHandle: '67890',
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelper: async (_spec, mode, extraArgs = []) => {
|
||||
capturedMode = mode;
|
||||
capturedArgs = extraArgs;
|
||||
return {
|
||||
stdout: 'ok',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(lowered, true);
|
||||
assert.equal(capturedMode, 'lower-overlay');
|
||||
assert.deepEqual(capturedArgs, ['', '67890']);
|
||||
});
|
||||
|
||||
test('queryWindowsTrackerMpvWindows resolves geometry from the powershell helper', () => {
|
||||
let capturedMode: string | null = null;
|
||||
let capturedArgs: string[] | null = null;
|
||||
|
||||
const result = queryWindowsTrackerMpvWindows({
|
||||
targetMpvSocketPath: '\\\\.\\pipe\\subminer-socket',
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelperSync: (_spec, mode, extraArgs = []) => {
|
||||
capturedMode = mode;
|
||||
capturedArgs = extraArgs;
|
||||
return {
|
||||
stdout: '120,240,1280,720',
|
||||
stderr: 'focus=focused\nstate=visible',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
test('findWindowsMpvTargetWindowHandle prefers the focused mpv window', () => {
|
||||
const result: MpvPollResult = {
|
||||
matches: [
|
||||
{
|
||||
hwnd: 0,
|
||||
bounds: {
|
||||
x: 120,
|
||||
y: 240,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
hwnd: 111,
|
||||
bounds: { x: 0, y: 0, width: 1280, height: 720 },
|
||||
area: 1280 * 720,
|
||||
isForeground: false,
|
||||
},
|
||||
{
|
||||
hwnd: 222,
|
||||
bounds: { x: 10, y: 10, width: 800, height: 600 },
|
||||
area: 800 * 600,
|
||||
isForeground: true,
|
||||
},
|
||||
],
|
||||
focusState: true,
|
||||
windowState: 'visible',
|
||||
});
|
||||
assert.equal(capturedMode, 'geometry');
|
||||
assert.deepEqual(capturedArgs, ['\\\\.\\pipe\\subminer-socket']);
|
||||
};
|
||||
|
||||
assert.equal(findWindowsMpvTargetWindowHandle(result), 222);
|
||||
});
|
||||
|
||||
test('queryWindowsTargetWindowHandle resolves the selected hwnd from the powershell helper', () => {
|
||||
let capturedMode: string | null = null;
|
||||
let capturedArgs: string[] | null = null;
|
||||
|
||||
const hwnd = queryWindowsTargetWindowHandle({
|
||||
targetMpvSocketPath: '\\\\.\\pipe\\subminer-socket',
|
||||
resolveHelper: () => ({
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-File', 'helper.ps1'],
|
||||
helperPath: 'helper.ps1',
|
||||
}),
|
||||
runHelperSync: (_spec, mode, extraArgs = []) => {
|
||||
capturedMode = mode;
|
||||
capturedArgs = extraArgs;
|
||||
return {
|
||||
stdout: '12345',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(hwnd, 12345);
|
||||
assert.equal(capturedMode, 'target-hwnd');
|
||||
assert.deepEqual(capturedArgs, ['\\\\.\\pipe\\subminer-socket']);
|
||||
});
|
||||
|
||||
test('shouldUseWindowsTrackerPowershellFallback returns true for explicit powershell mode', () => {
|
||||
assert.equal(
|
||||
windowsHelper.shouldUseWindowsTrackerPowershellFallback({
|
||||
helperModeEnv: 'powershell',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldUseWindowsTrackerPowershellFallback returns true for ps1 helper path override', () => {
|
||||
assert.equal(
|
||||
windowsHelper.shouldUseWindowsTrackerPowershellFallback({
|
||||
helperPathEnv: 'C:\\custom\\tracker.ps1',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('shouldUseWindowsTrackerPowershellFallback returns false for default and native modes', () => {
|
||||
assert.equal(
|
||||
windowsHelper.shouldUseWindowsTrackerPowershellFallback({
|
||||
helperModeEnv: 'auto',
|
||||
helperPathEnv: undefined,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
windowsHelper.shouldUseWindowsTrackerPowershellFallback({
|
||||
helperModeEnv: 'native',
|
||||
helperPathEnv: 'C:\\custom\\tracker.exe',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveWindowsTrackerHelper auto mode prefers native helper when present', () => {
|
||||
const helper = resolveWindowsTrackerHelper({
|
||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||
resourcesPath: 'C:\\repo\\resources',
|
||||
existsSync: (candidate) =>
|
||||
candidate === 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.exe',
|
||||
helperModeEnv: 'auto',
|
||||
});
|
||||
|
||||
assert.deepEqual(helper, {
|
||||
kind: 'native',
|
||||
command: 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.exe',
|
||||
args: [],
|
||||
helperPath: 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.exe',
|
||||
});
|
||||
});
|
||||
|
||||
test('resolveWindowsTrackerHelper auto mode falls back to powershell helper', () => {
|
||||
const helper = resolveWindowsTrackerHelper({
|
||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||
resourcesPath: 'C:\\repo\\resources',
|
||||
existsSync: (candidate) =>
|
||||
candidate === 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1',
|
||||
helperModeEnv: 'auto',
|
||||
});
|
||||
|
||||
assert.deepEqual(helper, {
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-File',
|
||||
'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1',
|
||||
test('findWindowsMpvTargetWindowHandle falls back to the largest visible mpv window', () => {
|
||||
const result: MpvPollResult = {
|
||||
matches: [
|
||||
{
|
||||
hwnd: 111,
|
||||
bounds: { x: 0, y: 0, width: 640, height: 360 },
|
||||
area: 640 * 360,
|
||||
isForeground: false,
|
||||
},
|
||||
{
|
||||
hwnd: 222,
|
||||
bounds: { x: 10, y: 10, width: 1920, height: 1080 },
|
||||
area: 1920 * 1080,
|
||||
isForeground: false,
|
||||
},
|
||||
],
|
||||
helperPath: 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1',
|
||||
});
|
||||
focusState: false,
|
||||
windowState: 'visible',
|
||||
};
|
||||
|
||||
assert.equal(findWindowsMpvTargetWindowHandle(result), 222);
|
||||
});
|
||||
|
||||
test('resolveWindowsTrackerHelper explicit powershell mode ignores native helper', () => {
|
||||
const helper = resolveWindowsTrackerHelper({
|
||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||
resourcesPath: 'C:\\repo\\resources',
|
||||
existsSync: (candidate) =>
|
||||
candidate === 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.exe' ||
|
||||
candidate === 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1',
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
test('findWindowsMpvTargetWindowHandle returns null when no mpv windows are visible', () => {
|
||||
const result: MpvPollResult = {
|
||||
matches: [],
|
||||
focusState: false,
|
||||
windowState: 'not-found',
|
||||
};
|
||||
|
||||
assert.equal(helper?.kind, 'powershell');
|
||||
assert.equal(helper?.helperPath, 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1');
|
||||
});
|
||||
|
||||
test('resolveWindowsTrackerHelper explicit native mode fails cleanly when helper is missing', () => {
|
||||
const helper = resolveWindowsTrackerHelper({
|
||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||
resourcesPath: 'C:\\repo\\resources',
|
||||
existsSync: () => false,
|
||||
helperModeEnv: 'native',
|
||||
});
|
||||
|
||||
assert.equal(helper, null);
|
||||
});
|
||||
|
||||
test('resolveWindowsTrackerHelper explicit helper path overrides default search', () => {
|
||||
const helper = resolveWindowsTrackerHelper({
|
||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||
resourcesPath: 'C:\\repo\\resources',
|
||||
existsSync: (candidate) => candidate === 'D:\\custom\\tracker.ps1',
|
||||
helperModeEnv: 'auto',
|
||||
helperPathEnv: 'D:\\custom\\tracker.ps1',
|
||||
});
|
||||
|
||||
assert.deepEqual(helper, {
|
||||
kind: 'powershell',
|
||||
command: 'powershell.exe',
|
||||
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', 'D:\\custom\\tracker.ps1'],
|
||||
helperPath: 'D:\\custom\\tracker.ps1',
|
||||
});
|
||||
assert.equal(findWindowsMpvTargetWindowHandle(result), null);
|
||||
});
|
||||
|
||||
@@ -16,503 +16,41 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { execFile, spawnSync, type ExecFileException } from 'child_process';
|
||||
import type { WindowGeometry } from '../types';
|
||||
import type { MpvPollResult } from './win32';
|
||||
import { createLogger } from '../logger';
|
||||
|
||||
const log = createLogger('tracker').child('windows-helper');
|
||||
|
||||
export type WindowsTrackerHelperKind = 'powershell' | 'native';
|
||||
export type WindowsTrackerHelperMode = 'auto' | 'powershell' | 'native';
|
||||
export type WindowsTrackerHelperRunMode =
|
||||
| 'geometry'
|
||||
| 'foreground-process'
|
||||
| 'bind-overlay'
|
||||
| 'lower-overlay'
|
||||
| 'set-owner'
|
||||
| 'clear-owner'
|
||||
| 'target-hwnd';
|
||||
|
||||
export type WindowsTrackerHelperLaunchSpec = {
|
||||
kind: WindowsTrackerHelperKind;
|
||||
command: string;
|
||||
args: string[];
|
||||
helperPath: string;
|
||||
};
|
||||
|
||||
type ResolveWindowsTrackerHelperOptions = {
|
||||
dirname?: string;
|
||||
resourcesPath?: string;
|
||||
helperModeEnv?: string | undefined;
|
||||
helperPathEnv?: string | undefined;
|
||||
existsSync?: (candidate: string) => boolean;
|
||||
mkdirSync?: (candidate: string, options: { recursive: true }) => void;
|
||||
copyFileSync?: (source: string, destination: string) => void;
|
||||
};
|
||||
|
||||
const windowsPath = path.win32;
|
||||
|
||||
function normalizeHelperMode(value: string | undefined): WindowsTrackerHelperMode {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
if (normalized === 'powershell' || normalized === 'native') {
|
||||
return normalized;
|
||||
}
|
||||
return 'auto';
|
||||
function loadWin32(): typeof import('./win32') {
|
||||
return require('./win32') as typeof import('./win32');
|
||||
}
|
||||
|
||||
export function shouldUseWindowsTrackerPowershellFallback(options: {
|
||||
helperModeEnv?: string | undefined;
|
||||
helperPathEnv?: string | undefined;
|
||||
} = {}): boolean {
|
||||
const mode = normalizeHelperMode(
|
||||
options.helperModeEnv ?? process.env.SUBMINER_WINDOWS_TRACKER_HELPER,
|
||||
);
|
||||
if (mode === 'powershell') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const helperPath = options.helperPathEnv ?? process.env.SUBMINER_WINDOWS_TRACKER_HELPER_PATH;
|
||||
return helperPath?.trim().toLowerCase().endsWith('.ps1') ?? false;
|
||||
export function findWindowsMpvTargetWindowHandle(result?: MpvPollResult): number | null {
|
||||
const poll = result ?? loadWin32().findMpvWindows();
|
||||
const focused = poll.matches.find((match) => match.isForeground);
|
||||
const best =
|
||||
focused ?? [...poll.matches].sort((a, b) => b.area - a.area || b.bounds.width - a.bounds.width)[0];
|
||||
return best?.hwnd ?? null;
|
||||
}
|
||||
|
||||
function inferHelperKindFromPath(helperPath: string): WindowsTrackerHelperKind | null {
|
||||
const normalized = helperPath.trim().toLowerCase();
|
||||
if (normalized.endsWith('.exe')) return 'native';
|
||||
if (normalized.endsWith('.ps1')) return 'powershell';
|
||||
return null;
|
||||
}
|
||||
|
||||
function materializeAsarHelper(
|
||||
sourcePath: string,
|
||||
kind: WindowsTrackerHelperKind,
|
||||
deps: Required<Pick<ResolveWindowsTrackerHelperOptions, 'mkdirSync' | 'copyFileSync'>>,
|
||||
): string | null {
|
||||
if (!sourcePath.includes('.asar')) {
|
||||
return sourcePath;
|
||||
}
|
||||
|
||||
const fileName = kind === 'native' ? 'get-mpv-window-windows.exe' : 'get-mpv-window-windows.ps1';
|
||||
const targetDir = windowsPath.join(os.tmpdir(), 'subminer', 'helpers');
|
||||
const targetPath = windowsPath.join(targetDir, fileName);
|
||||
|
||||
export function setWindowsOverlayOwner(overlayHwnd: number, mpvHwnd: number): boolean {
|
||||
try {
|
||||
deps.mkdirSync(targetDir, { recursive: true });
|
||||
deps.copyFileSync(sourcePath, targetPath);
|
||||
log.info(`Materialized Windows helper from asar: ${targetPath}`);
|
||||
return targetPath;
|
||||
} catch (error) {
|
||||
log.warn(`Failed to materialize Windows helper from asar: ${sourcePath}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createLaunchSpec(
|
||||
helperPath: string,
|
||||
kind: WindowsTrackerHelperKind,
|
||||
): WindowsTrackerHelperLaunchSpec {
|
||||
if (kind === 'native') {
|
||||
return {
|
||||
kind,
|
||||
command: helperPath,
|
||||
args: [],
|
||||
helperPath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind,
|
||||
command: 'powershell.exe',
|
||||
args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', helperPath],
|
||||
helperPath,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHelperPathOverride(
|
||||
helperPathEnv: string | undefined,
|
||||
mode: WindowsTrackerHelperMode,
|
||||
): { path: string; kind: WindowsTrackerHelperKind } | null {
|
||||
const helperPath = helperPathEnv?.trim();
|
||||
if (!helperPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inferredKind = inferHelperKindFromPath(helperPath);
|
||||
const kind = mode === 'auto' ? inferredKind : mode;
|
||||
if (!kind) {
|
||||
log.warn(
|
||||
`Ignoring SUBMINER_WINDOWS_TRACKER_HELPER_PATH with unsupported extension: ${helperPath}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return { path: helperPath, kind };
|
||||
}
|
||||
|
||||
function getHelperCandidates(
|
||||
dirname: string,
|
||||
resourcesPath: string | undefined,
|
||||
): Array<{
|
||||
path: string;
|
||||
kind: WindowsTrackerHelperKind;
|
||||
}> {
|
||||
const scriptFileBase = 'get-mpv-window-windows';
|
||||
const candidates: Array<{ path: string; kind: WindowsTrackerHelperKind }> = [];
|
||||
|
||||
if (resourcesPath) {
|
||||
candidates.push({
|
||||
path: windowsPath.join(resourcesPath, 'scripts', `${scriptFileBase}.exe`),
|
||||
kind: 'native',
|
||||
});
|
||||
candidates.push({
|
||||
path: windowsPath.join(resourcesPath, 'scripts', `${scriptFileBase}.ps1`),
|
||||
kind: 'powershell',
|
||||
});
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
path: windowsPath.join(dirname, '..', 'scripts', `${scriptFileBase}.exe`),
|
||||
kind: 'native',
|
||||
});
|
||||
candidates.push({
|
||||
path: windowsPath.join(dirname, '..', 'scripts', `${scriptFileBase}.ps1`),
|
||||
kind: 'powershell',
|
||||
});
|
||||
candidates.push({
|
||||
path: windowsPath.join(dirname, '..', '..', 'scripts', `${scriptFileBase}.exe`),
|
||||
kind: 'native',
|
||||
});
|
||||
candidates.push({
|
||||
path: windowsPath.join(dirname, '..', '..', 'scripts', `${scriptFileBase}.ps1`),
|
||||
kind: 'powershell',
|
||||
});
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function parseWindowTrackerHelperOutput(output: string): WindowGeometry | null {
|
||||
const result = output.trim();
|
||||
if (!result || result === 'not-found') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = result.split(',');
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [xText, yText, widthText, heightText] = parts;
|
||||
const x = Number.parseInt(xText!, 10);
|
||||
const y = Number.parseInt(yText!, 10);
|
||||
const width = Number.parseInt(widthText!, 10);
|
||||
const height = Number.parseInt(heightText!, 10);
|
||||
if (
|
||||
!Number.isFinite(x) ||
|
||||
!Number.isFinite(y) ||
|
||||
!Number.isFinite(width) ||
|
||||
!Number.isFinite(height) ||
|
||||
width <= 0 ||
|
||||
height <= 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
export function parseWindowTrackerHelperFocusState(output: string): boolean | null {
|
||||
const focusLine = output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('focus='));
|
||||
|
||||
if (!focusLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = focusLine.slice('focus='.length).trim().toLowerCase();
|
||||
if (value === 'focused') {
|
||||
return true;
|
||||
}
|
||||
if (value === 'not-focused') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseWindowTrackerHelperState(output: string): 'visible' | 'minimized' | null {
|
||||
const stateLine = output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('state='));
|
||||
|
||||
if (!stateLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = stateLine.slice('state='.length).trim().toLowerCase();
|
||||
if (value === 'visible') {
|
||||
return 'visible';
|
||||
}
|
||||
if (value === 'minimized') {
|
||||
return 'minimized';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseWindowTrackerHelperForegroundProcess(output: string): string | null {
|
||||
const processLine = output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.startsWith('process='));
|
||||
|
||||
if (!processLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = processLine.slice('process='.length).trim();
|
||||
return value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
type WindowsTrackerHelperRunnerResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
function runWindowsTrackerHelperWithSpawnSync(
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs: string[] = [],
|
||||
): WindowsTrackerHelperRunnerResult | null {
|
||||
const modeArgs = spec.kind === 'native' ? ['--mode', mode] : ['-Mode', mode];
|
||||
const result = spawnSync(spec.command, [...spec.args, ...modeArgs, ...extraArgs], {
|
||||
encoding: 'utf-8',
|
||||
timeout: 1000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
stdout: result.stdout ?? '',
|
||||
stderr: result.stderr ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function runWindowsTrackerHelperWithExecFile(
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs: string[] = [],
|
||||
): Promise<WindowsTrackerHelperRunnerResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const modeArgs = spec.kind === 'native' ? ['--mode', mode] : ['-Mode', mode];
|
||||
execFile(
|
||||
spec.command,
|
||||
[...spec.args, ...modeArgs, ...extraArgs],
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
timeout: 1000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
windowsHide: true,
|
||||
},
|
||||
(error: ExecFileException | null, stdout: string, stderr: string) => {
|
||||
if (error) {
|
||||
reject(Object.assign(error, { stderr }));
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryWindowsForegroundProcessName(deps: {
|
||||
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
|
||||
runHelper?: (
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs?: string[],
|
||||
) => Promise<WindowsTrackerHelperRunnerResult>;
|
||||
} = {}): Promise<string | null> {
|
||||
const spec =
|
||||
deps.resolveHelper?.() ??
|
||||
resolveWindowsTrackerHelper({
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
if (!spec || spec.kind !== 'powershell') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runHelper = deps.runHelper ?? runWindowsTrackerHelperWithExecFile;
|
||||
const { stdout } = await runHelper(spec, 'foreground-process');
|
||||
return parseWindowTrackerHelperForegroundProcess(stdout);
|
||||
}
|
||||
|
||||
export function queryWindowsTrackerMpvWindows(deps: {
|
||||
targetMpvSocketPath?: string | null;
|
||||
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
|
||||
runHelperSync?: (
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs?: string[],
|
||||
) => WindowsTrackerHelperRunnerResult | null;
|
||||
} = {}): MpvPollResult | null {
|
||||
const targetMpvSocketPath = deps.targetMpvSocketPath?.trim();
|
||||
if (!targetMpvSocketPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spec =
|
||||
deps.resolveHelper?.() ??
|
||||
resolveWindowsTrackerHelper({
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
if (!spec || spec.kind !== 'powershell') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runHelper = deps.runHelperSync ?? runWindowsTrackerHelperWithSpawnSync;
|
||||
const result = runHelper(spec, 'geometry', [targetMpvSocketPath]);
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const geometry = parseWindowTrackerHelperOutput(result.stdout);
|
||||
if (!geometry) {
|
||||
return {
|
||||
matches: [],
|
||||
focusState: parseWindowTrackerHelperFocusState(result.stderr) ?? false,
|
||||
windowState: parseWindowTrackerHelperState(result.stderr) ?? 'not-found',
|
||||
};
|
||||
}
|
||||
|
||||
const focusState = parseWindowTrackerHelperFocusState(result.stderr) ?? false;
|
||||
return {
|
||||
matches: [
|
||||
{
|
||||
hwnd: 0,
|
||||
bounds: geometry,
|
||||
area: geometry.width * geometry.height,
|
||||
isForeground: focusState,
|
||||
},
|
||||
],
|
||||
focusState,
|
||||
windowState: parseWindowTrackerHelperState(result.stderr) ?? 'visible',
|
||||
};
|
||||
}
|
||||
|
||||
export function queryWindowsTargetWindowHandle(deps: {
|
||||
targetMpvSocketPath?: string | null;
|
||||
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
|
||||
runHelperSync?: (
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs?: string[],
|
||||
) => WindowsTrackerHelperRunnerResult | null;
|
||||
} = {}): number | null {
|
||||
const targetMpvSocketPath = deps.targetMpvSocketPath?.trim();
|
||||
if (!targetMpvSocketPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spec =
|
||||
deps.resolveHelper?.() ??
|
||||
resolveWindowsTrackerHelper({
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
if (!spec || spec.kind !== 'powershell') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runHelper = deps.runHelperSync ?? runWindowsTrackerHelperWithSpawnSync;
|
||||
const result = runHelper(spec, 'target-hwnd', [targetMpvSocketPath]);
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handle = Number.parseInt(result.stdout.trim(), 10);
|
||||
return Number.isFinite(handle) && handle > 0 ? handle : null;
|
||||
}
|
||||
|
||||
export async function syncWindowsOverlayToMpvZOrder(deps: {
|
||||
overlayWindowHandle: string;
|
||||
targetMpvSocketPath?: string | null;
|
||||
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
|
||||
runHelper?: (
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs?: string[],
|
||||
) => Promise<WindowsTrackerHelperRunnerResult>;
|
||||
}): Promise<boolean> {
|
||||
const spec =
|
||||
deps.resolveHelper?.() ??
|
||||
resolveWindowsTrackerHelper({
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
if (!spec || spec.kind !== 'powershell') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const runHelper = deps.runHelper ?? runWindowsTrackerHelperWithExecFile;
|
||||
const extraArgs = [deps.targetMpvSocketPath ?? '', deps.overlayWindowHandle];
|
||||
const { stdout } = await runHelper(spec, 'bind-overlay', extraArgs);
|
||||
return stdout.trim() === 'ok';
|
||||
}
|
||||
|
||||
export async function lowerWindowsOverlayInZOrder(deps: {
|
||||
overlayWindowHandle: string;
|
||||
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
|
||||
runHelper?: (
|
||||
spec: WindowsTrackerHelperLaunchSpec,
|
||||
mode: WindowsTrackerHelperRunMode,
|
||||
extraArgs?: string[],
|
||||
) => Promise<WindowsTrackerHelperRunnerResult>;
|
||||
}): Promise<boolean> {
|
||||
const spec =
|
||||
deps.resolveHelper?.() ??
|
||||
resolveWindowsTrackerHelper({
|
||||
helperModeEnv: 'powershell',
|
||||
});
|
||||
if (!spec || spec.kind !== 'powershell') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const runHelper = deps.runHelper ?? runWindowsTrackerHelperWithExecFile;
|
||||
const { stdout } = await runHelper(spec, 'lower-overlay', ['', deps.overlayWindowHandle]);
|
||||
return stdout.trim() === 'ok';
|
||||
}
|
||||
|
||||
export function setWindowsOverlayOwnerNative(overlayHwnd: number, mpvHwnd: number): boolean {
|
||||
try {
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
win32.setOverlayOwner(overlayHwnd, mpvHwnd);
|
||||
loadWin32().setOverlayOwner(overlayHwnd, mpvHwnd);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureWindowsOverlayTransparencyNative(overlayHwnd: number): boolean {
|
||||
export function ensureWindowsOverlayTransparency(overlayHwnd: number): boolean {
|
||||
try {
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
win32.ensureOverlayTransparency(overlayHwnd);
|
||||
loadWin32().ensureOverlayTransparency(overlayHwnd);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function bindWindowsOverlayAboveMpvNative(overlayHwnd: number, mpvHwnd: number): boolean {
|
||||
export function bindWindowsOverlayAboveMpv(overlayHwnd: number, mpvHwnd: number): boolean {
|
||||
try {
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
const win32 = loadWin32();
|
||||
win32.bindOverlayAboveMpv(overlayHwnd, mpvHwnd);
|
||||
win32.ensureOverlayTransparency(overlayHwnd);
|
||||
return true;
|
||||
@@ -521,85 +59,19 @@ export function bindWindowsOverlayAboveMpvNative(overlayHwnd: number, mpvHwnd: n
|
||||
}
|
||||
}
|
||||
|
||||
export function clearWindowsOverlayOwnerNative(overlayHwnd: number): boolean {
|
||||
export function clearWindowsOverlayOwner(overlayHwnd: number): boolean {
|
||||
try {
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
win32.clearOverlayOwner(overlayHwnd);
|
||||
loadWin32().clearOverlayOwner(overlayHwnd);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getWindowsForegroundProcessNameNative(): string | null {
|
||||
export function getWindowsForegroundProcessName(): string | null {
|
||||
try {
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
return win32.getForegroundProcessName();
|
||||
return loadWin32().getForegroundProcessName();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWindowsTrackerHelper(
|
||||
options: ResolveWindowsTrackerHelperOptions = {},
|
||||
): WindowsTrackerHelperLaunchSpec | null {
|
||||
const existsSync = options.existsSync ?? fs.existsSync;
|
||||
const mkdirSync = options.mkdirSync ?? fs.mkdirSync;
|
||||
const copyFileSync = options.copyFileSync ?? fs.copyFileSync;
|
||||
const dirname = options.dirname ?? __dirname;
|
||||
const resourcesPath = options.resourcesPath ?? process.resourcesPath;
|
||||
const mode = normalizeHelperMode(
|
||||
options.helperModeEnv ?? process.env.SUBMINER_WINDOWS_TRACKER_HELPER,
|
||||
);
|
||||
const override = normalizeHelperPathOverride(
|
||||
options.helperPathEnv ?? process.env.SUBMINER_WINDOWS_TRACKER_HELPER_PATH,
|
||||
mode,
|
||||
);
|
||||
|
||||
if (override) {
|
||||
if (!existsSync(override.path)) {
|
||||
log.warn(`Configured Windows tracker helper path does not exist: ${override.path}`);
|
||||
return null;
|
||||
}
|
||||
const helperPath = materializeAsarHelper(override.path, override.kind, {
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
});
|
||||
return helperPath ? createLaunchSpec(helperPath, override.kind) : null;
|
||||
}
|
||||
|
||||
const candidates = getHelperCandidates(dirname, resourcesPath);
|
||||
const orderedCandidates =
|
||||
mode === 'powershell'
|
||||
? candidates.filter((candidate) => candidate.kind === 'powershell')
|
||||
: mode === 'native'
|
||||
? candidates.filter((candidate) => candidate.kind === 'native')
|
||||
: candidates;
|
||||
|
||||
for (const candidate of orderedCandidates) {
|
||||
if (!existsSync(candidate.path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const helperPath = materializeAsarHelper(candidate.path, candidate.kind, {
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
});
|
||||
if (!helperPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info(`Using Windows helper (${candidate.kind}): ${helperPath}`);
|
||||
return createLaunchSpec(helperPath, candidate.kind);
|
||||
}
|
||||
|
||||
if (mode === 'native') {
|
||||
log.warn('Windows native tracker helper requested but no helper was found.');
|
||||
} else if (mode === 'powershell') {
|
||||
log.warn('Windows PowerShell tracker helper requested but no helper was found.');
|
||||
} else {
|
||||
log.warn('Windows tracker helper not found.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
import { BaseWindowTracker } from './base-tracker';
|
||||
import type { WindowGeometry } from '../types';
|
||||
import type { MpvPollResult } from './win32';
|
||||
import {
|
||||
queryWindowsTrackerMpvWindows,
|
||||
shouldUseWindowsTrackerPowershellFallback,
|
||||
} from './windows-helper';
|
||||
import { createLogger } from '../logger';
|
||||
|
||||
const log = createLogger('tracker').child('windows');
|
||||
@@ -35,16 +31,8 @@ type WindowsTrackerDeps = {
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
function defaultPollMpvWindows(targetMpvSocketPath?: string | null): MpvPollResult {
|
||||
if (targetMpvSocketPath && shouldUseWindowsTrackerPowershellFallback()) {
|
||||
const helperResult = queryWindowsTrackerMpvWindows({
|
||||
targetMpvSocketPath,
|
||||
});
|
||||
if (helperResult) {
|
||||
return helperResult;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultPollMpvWindows(_targetMpvSocketPath?: string | null): MpvPollResult {
|
||||
void _targetMpvSocketPath;
|
||||
const win32 = require('./win32') as typeof import('./win32');
|
||||
return win32.findMpvWindows();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user