Prepare Windows release and signing process (#16)

This commit is contained in:
2026-03-08 19:51:30 -07:00
committed by GitHub
parent 34d2dce8dc
commit c799a8de3c
113 changed files with 5042 additions and 386 deletions

View File

@@ -21,17 +21,31 @@ import { WindowGeometry } from '../types';
export type GeometryChangeCallback = (geometry: WindowGeometry) => void;
export type WindowFoundCallback = (geometry: WindowGeometry) => void;
export type WindowLostCallback = () => void;
export type WindowFocusChangeCallback = (focused: boolean) => void;
export abstract class BaseWindowTracker {
protected currentGeometry: WindowGeometry | null = null;
protected windowFound: boolean = false;
protected focusKnown: boolean = false;
protected windowFocused: boolean = false;
protected targetWindowFocused: boolean = false;
public onGeometryChange: GeometryChangeCallback | null = null;
public onWindowFound: WindowFoundCallback | null = null;
public onWindowLost: WindowLostCallback | null = null;
public onWindowFocusChange: WindowFocusChangeCallback | null = null;
private onWindowFocusChangeCallback: ((focused: boolean) => void) | null = null;
public get onWindowFocusChange(): ((focused: boolean) => void) | null {
return this.onWindowFocusChangeCallback;
}
public set onWindowFocusChange(callback: ((focused: boolean) => void) | null) {
this.onWindowFocusChangeCallback = callback;
}
public get onTargetWindowFocusChange(): ((focused: boolean) => void) | null {
return this.onWindowFocusChange;
}
public set onTargetWindowFocusChange(callback: ((focused: boolean) => void) | null) {
this.onWindowFocusChange = callback;
}
abstract start(): void;
abstract stop(): void;
@@ -44,23 +58,28 @@ export abstract class BaseWindowTracker {
return this.windowFound;
}
isFocused(): boolean {
return this.focusKnown ? this.windowFocused : this.windowFound;
isTargetWindowFocused(): boolean {
return this.targetWindowFocused;
}
protected updateTargetWindowFocused(focused: boolean): void {
if (this.targetWindowFocused === focused) {
return;
}
this.targetWindowFocused = focused;
this.onWindowFocusChangeCallback?.(focused);
}
protected updateFocus(focused: boolean): void {
const changed = !this.focusKnown || this.windowFocused !== focused;
this.focusKnown = true;
this.windowFocused = focused;
if (changed) {
this.onWindowFocusChange?.(focused);
}
this.updateTargetWindowFocused(focused);
}
protected updateGeometry(newGeometry: WindowGeometry | null): void {
if (newGeometry) {
if (!this.windowFound) {
this.windowFound = true;
this.updateTargetWindowFocused(true);
if (this.onWindowFound) this.onWindowFound(newGeometry);
}
@@ -75,14 +94,9 @@ export abstract class BaseWindowTracker {
if (this.onGeometryChange) this.onGeometryChange(newGeometry);
}
} else {
const focusChanged = this.focusKnown && this.windowFocused;
this.focusKnown = false;
this.windowFocused = false;
if (focusChanged) {
this.onWindowFocusChange?.(false);
}
if (this.windowFound) {
this.windowFound = false;
this.updateTargetWindowFocused(false);
this.currentGeometry = null;
if (this.onWindowLost) this.onWindowLost();
}

View File

@@ -21,14 +21,16 @@ import { HyprlandWindowTracker } from './hyprland-tracker';
import { SwayWindowTracker } from './sway-tracker';
import { X11WindowTracker } from './x11-tracker';
import { MacOSWindowTracker } from './macos-tracker';
import { WindowsWindowTracker } from './windows-tracker';
import { createLogger } from '../logger';
const log = createLogger('tracker');
export type Compositor = 'hyprland' | 'sway' | 'x11' | 'macos' | null;
export type Compositor = 'hyprland' | 'sway' | 'x11' | 'macos' | 'windows' | null;
export type Backend = 'auto' | Exclude<Compositor, null>;
export function detectCompositor(): Compositor {
if (process.platform === 'win32') return 'windows';
if (process.platform === 'darwin') return 'macos';
if (process.env.HYPRLAND_INSTANCE_SIGNATURE) return 'hyprland';
if (process.env.SWAYSOCK) return 'sway';
@@ -42,6 +44,7 @@ function normalizeCompositor(value: string): Compositor | null {
if (normalized === 'sway') return 'sway';
if (normalized === 'x11') return 'x11';
if (normalized === 'macos') return 'macos';
if (normalized === 'windows') return 'windows';
return null;
}
@@ -70,6 +73,8 @@ export function createWindowTracker(
return new X11WindowTracker(targetMpvSocketPath?.trim() || undefined);
case 'macos':
return new MacOSWindowTracker(targetMpvSocketPath?.trim() || undefined);
case 'windows':
return new WindowsWindowTracker(targetMpvSocketPath?.trim() || undefined);
default:
log.warn('No supported compositor detected. Window tracking disabled.');
return null;
@@ -82,4 +87,5 @@ export {
SwayWindowTracker,
X11WindowTracker,
MacOSWindowTracker,
WindowsWindowTracker,
};

View File

@@ -0,0 +1,111 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
parseWindowTrackerHelperFocusState,
parseWindowTrackerHelperOutput,
resolveWindowsTrackerHelper,
} from './windows-helper';
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('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',
],
helperPath: 'C:\\repo\\resources\\scripts\\get-mpv-window-windows.ps1',
});
});
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',
});
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',
});
});

View File

@@ -0,0 +1,284 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
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 type { WindowGeometry } from '../types';
import { createLogger } from '../logger';
const log = createLogger('tracker').child('windows-helper');
export type WindowsTrackerHelperKind = 'powershell' | 'native';
export type WindowsTrackerHelperMode = 'auto' | 'powershell' | 'native';
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 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);
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 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;
}

View File

@@ -0,0 +1,119 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { WindowsWindowTracker } from './windows-tracker';
test('WindowsWindowTracker skips overlapping polls while helper is in flight', async () => {
let helperCalls = 0;
let release: (() => void) | undefined;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const tracker = new WindowsWindowTracker(undefined, {
resolveHelper: () => ({
kind: 'powershell',
command: 'powershell.exe',
args: ['-File', 'helper.ps1'],
helperPath: 'helper.ps1',
}),
runHelper: async () => {
helperCalls += 1;
await gate;
return {
stdout: '0,0,640,360',
stderr: 'focus=focused',
};
},
});
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
assert.equal(helperCalls, 1);
assert.ok(release);
release();
await new Promise((resolve) => setTimeout(resolve, 0));
});
test('WindowsWindowTracker updates geometry from helper output', async () => {
const tracker = new WindowsWindowTracker(undefined, {
resolveHelper: () => ({
kind: 'powershell',
command: 'powershell.exe',
args: ['-File', 'helper.ps1'],
helperPath: 'helper.ps1',
}),
runHelper: async () => ({
stdout: '10,20,1280,720',
stderr: 'focus=focused',
}),
});
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(tracker.getGeometry(), {
x: 10,
y: 20,
width: 1280,
height: 720,
});
assert.equal(tracker.isTargetWindowFocused(), true);
});
test('WindowsWindowTracker clears geometry for helper misses', async () => {
const tracker = new WindowsWindowTracker(undefined, {
resolveHelper: () => ({
kind: 'powershell',
command: 'powershell.exe',
args: ['-File', 'helper.ps1'],
helperPath: 'helper.ps1',
}),
runHelper: async () => ({
stdout: 'not-found',
stderr: 'focus=not-focused',
}),
});
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(tracker.getGeometry(), null);
assert.equal(tracker.isTargetWindowFocused(), false);
});
test('WindowsWindowTracker retries without socket filter when filtered helper lookup misses', async () => {
const helperCalls: Array<string | null> = [];
const tracker = new WindowsWindowTracker('\\\\.\\pipe\\subminer-socket', {
resolveHelper: () => ({
kind: 'powershell',
command: 'powershell.exe',
args: ['-File', 'helper.ps1'],
helperPath: 'helper.ps1',
}),
runHelper: async (_spec, _mode, targetMpvSocketPath) => {
helperCalls.push(targetMpvSocketPath);
if (targetMpvSocketPath) {
return {
stdout: 'not-found',
stderr: 'focus=not-focused',
};
}
return {
stdout: '25,30,1440,810',
stderr: 'focus=focused',
};
},
});
(tracker as unknown as { pollGeometry: () => void }).pollGeometry();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.deepEqual(helperCalls, ['\\\\.\\pipe\\subminer-socket', null]);
assert.deepEqual(tracker.getGeometry(), {
x: 25,
y: 30,
width: 1440,
height: 810,
});
assert.equal(tracker.isTargetWindowFocused(), true);
});

View File

@@ -0,0 +1,176 @@
/*
SubMiner - All-in-one sentence mining overlay
Copyright (C) 2024 sudacode
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { execFile, type ExecFileException } from 'child_process';
import { BaseWindowTracker } from './base-tracker';
import {
parseWindowTrackerHelperFocusState,
parseWindowTrackerHelperOutput,
resolveWindowsTrackerHelper,
type WindowsTrackerHelperLaunchSpec,
} from './windows-helper';
import { createLogger } from '../logger';
const log = createLogger('tracker').child('windows');
type WindowsTrackerRunnerResult = {
stdout: string;
stderr: string;
};
type WindowsTrackerDeps = {
resolveHelper?: () => WindowsTrackerHelperLaunchSpec | null;
runHelper?: (
spec: WindowsTrackerHelperLaunchSpec,
mode: 'geometry',
targetMpvSocketPath: string | null,
) => Promise<WindowsTrackerRunnerResult>;
};
function runHelperWithExecFile(
spec: WindowsTrackerHelperLaunchSpec,
mode: 'geometry',
targetMpvSocketPath: string | null,
): Promise<WindowsTrackerRunnerResult> {
return new Promise((resolve, reject) => {
const modeArgs =
spec.kind === 'native' ? ['--mode', mode] : ['-Mode', mode];
const args = targetMpvSocketPath
? [...spec.args, ...modeArgs, targetMpvSocketPath]
: [...spec.args, ...modeArgs];
execFile(
spec.command,
args,
{
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 class WindowsWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
private pollInFlight = false;
private helperSpec: WindowsTrackerHelperLaunchSpec | null;
private readonly targetMpvSocketPath: string | null;
private readonly runHelper: (
spec: WindowsTrackerHelperLaunchSpec,
mode: 'geometry',
targetMpvSocketPath: string | null,
) => Promise<WindowsTrackerRunnerResult>;
private lastExecErrorFingerprint: string | null = null;
private lastExecErrorLoggedAtMs = 0;
constructor(targetMpvSocketPath?: string, deps: WindowsTrackerDeps = {}) {
super();
this.targetMpvSocketPath = targetMpvSocketPath?.trim() || null;
this.helperSpec = deps.resolveHelper ? deps.resolveHelper() : resolveWindowsTrackerHelper();
this.runHelper = deps.runHelper ?? runHelperWithExecFile;
}
start(): void {
this.pollInterval = setInterval(() => this.pollGeometry(), 250);
this.pollGeometry();
}
stop(): void {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}
}
private maybeLogExecError(error: Error, stderr: string): void {
const now = Date.now();
const fingerprint = `${error.message}|${stderr.trim()}`;
const shouldLog =
this.lastExecErrorFingerprint !== fingerprint || now - this.lastExecErrorLoggedAtMs >= 5000;
if (!shouldLog) {
return;
}
this.lastExecErrorFingerprint = fingerprint;
this.lastExecErrorLoggedAtMs = now;
log.warn('Windows helper execution failed', {
helperPath: this.helperSpec?.helperPath ?? null,
helperKind: this.helperSpec?.kind ?? null,
error: error.message,
stderr: stderr.trim(),
});
}
private async runHelperWithSocketFallback(): Promise<WindowsTrackerRunnerResult> {
if (!this.helperSpec) {
return { stdout: 'not-found', stderr: '' };
}
try {
const primary = await this.runHelper(this.helperSpec, 'geometry', this.targetMpvSocketPath);
const primaryGeometry = parseWindowTrackerHelperOutput(primary.stdout);
if (primaryGeometry || !this.targetMpvSocketPath) {
return primary;
}
} catch (error) {
if (!this.targetMpvSocketPath) {
throw error;
}
}
return await this.runHelper(this.helperSpec, 'geometry', null);
}
private pollGeometry(): void {
if (this.pollInFlight || !this.helperSpec) {
return;
}
this.pollInFlight = true;
void this.runHelperWithSocketFallback()
.then(({ stdout, stderr }) => {
const geometry = parseWindowTrackerHelperOutput(stdout);
const focusState = parseWindowTrackerHelperFocusState(stderr);
this.updateTargetWindowFocused(focusState ?? Boolean(geometry));
this.updateGeometry(geometry);
})
.catch((error: unknown) => {
const err = error instanceof Error ? error : new Error(String(error));
const stderr =
typeof error === 'object' &&
error !== null &&
'stderr' in error &&
typeof (error as { stderr?: unknown }).stderr === 'string'
? (error as { stderr: string }).stderr
: '';
this.maybeLogExecError(err, stderr);
this.updateGeometry(null);
})
.finally(() => {
this.pollInFlight = false;
});
}
}