mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-11 04:19:26 -07:00
fix: address CodeRabbit review and ci
This commit is contained in:
@@ -1,14 +1,13 @@
|
|||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import { BaseWindowTracker } from '../../window-trackers';
|
import { BaseWindowTracker } from '../../window-trackers';
|
||||||
import { WindowGeometry } from '../../types';
|
import { WindowGeometry } from '../../types';
|
||||||
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
|
|
||||||
const WINDOWS_OVERLAY_REVEAL_DELAY_MS = 48;
|
const WINDOWS_OVERLAY_REVEAL_DELAY_MS = 48;
|
||||||
const pendingWindowsOverlayRevealTimeoutByWindow = new WeakMap<
|
const pendingWindowsOverlayRevealTimeoutByWindow = new WeakMap<
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
ReturnType<typeof setTimeout>
|
ReturnType<typeof setTimeout>
|
||||||
>();
|
>();
|
||||||
const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
|
||||||
|
|
||||||
function setOverlayWindowOpacity(window: BrowserWindow, opacity: number): void {
|
function setOverlayWindowOpacity(window: BrowserWindow, opacity: number): void {
|
||||||
const opacityCapableWindow = window as BrowserWindow & {
|
const opacityCapableWindow = window as BrowserWindow & {
|
||||||
setOpacity?: (opacity: number) => void;
|
setOpacity?: (opacity: number) => void;
|
||||||
|
|||||||
1
src/core/services/overlay-window-flags.ts
Normal file
1
src/core/services/overlay-window-flags.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
||||||
@@ -10,11 +10,12 @@ import {
|
|||||||
} from './overlay-window-input';
|
} from './overlay-window-input';
|
||||||
import { buildOverlayWindowOptions } from './overlay-window-options';
|
import { buildOverlayWindowOptions } from './overlay-window-options';
|
||||||
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
import { normalizeOverlayWindowBoundsForPlatform } from './overlay-window-bounds';
|
||||||
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
|
export { OVERLAY_WINDOW_CONTENT_READY_FLAG } from './overlay-window-flags';
|
||||||
|
|
||||||
const logger = createLogger('main:overlay-window');
|
const logger = createLogger('main:overlay-window');
|
||||||
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||||
const overlayWindowContentReady = new WeakSet<BrowserWindow>();
|
const overlayWindowContentReady = new WeakSet<BrowserWindow>();
|
||||||
const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
|
||||||
|
|
||||||
export function isOverlayWindowContentReady(window: BrowserWindow): boolean {
|
export function isOverlayWindowContentReady(window: BrowserWindow): boolean {
|
||||||
if (window.isDestroyed()) {
|
if (window.isDestroyed()) {
|
||||||
|
|||||||
53
src/main.ts
53
src/main.ts
@@ -137,9 +137,9 @@ import {
|
|||||||
clearWindowsOverlayOwnerNative,
|
clearWindowsOverlayOwnerNative,
|
||||||
ensureWindowsOverlayTransparencyNative,
|
ensureWindowsOverlayTransparencyNative,
|
||||||
getWindowsForegroundProcessNameNative,
|
getWindowsForegroundProcessNameNative,
|
||||||
queryWindowsForegroundProcessName,
|
|
||||||
queryWindowsTargetWindowHandle,
|
queryWindowsTargetWindowHandle,
|
||||||
setWindowsOverlayOwnerNative,
|
setWindowsOverlayOwnerNative,
|
||||||
|
shouldUseWindowsTrackerPowershellFallback,
|
||||||
} from './window-trackers/windows-helper';
|
} from './window-trackers/windows-helper';
|
||||||
import {
|
import {
|
||||||
commandNeedsOverlayStartupPrereqs,
|
commandNeedsOverlayStartupPrereqs,
|
||||||
@@ -453,6 +453,7 @@ import { handleCliCommandRuntimeServiceWithContext } from './main/cli-runtime';
|
|||||||
import { createOverlayModalRuntimeService } from './main/overlay-runtime';
|
import { createOverlayModalRuntimeService } from './main/overlay-runtime';
|
||||||
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
|
import { createOverlayModalInputState } from './main/runtime/overlay-modal-input-state';
|
||||||
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
|
import { openYoutubeTrackPicker } from './main/runtime/youtube-picker-open';
|
||||||
|
import { openRuntimeOptionsModal as openRuntimeOptionsModalRuntime } from './main/runtime/runtime-options-open';
|
||||||
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
|
||||||
import { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
import { writeSessionBindingsArtifact } from './main/runtime/session-bindings-artifact';
|
||||||
import { openOverlayHostedModal } from './main/runtime/overlay-hosted-modal-open';
|
import { openOverlayHostedModal } from './main/runtime/overlay-hosted-modal-open';
|
||||||
@@ -1903,7 +1904,6 @@ let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout
|
|||||||
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
let windowsVisibleOverlayZOrderSyncInFlight = false;
|
||||||
let windowsVisibleOverlayZOrderSyncQueued = false;
|
let windowsVisibleOverlayZOrderSyncQueued = false;
|
||||||
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
let windowsVisibleOverlayForegroundPollInFlight = false;
|
|
||||||
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
|
||||||
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
let lastWindowsVisibleOverlayBlurredAtMs = 0;
|
||||||
|
|
||||||
@@ -1940,9 +1940,11 @@ function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath?: string | nu
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const helperTargetHwnd = queryWindowsTargetWindowHandle({ targetMpvSocketPath });
|
if (shouldUseWindowsTrackerPowershellFallback()) {
|
||||||
if (helperTargetHwnd !== null) {
|
const helperTargetHwnd = queryWindowsTargetWindowHandle({ targetMpvSocketPath });
|
||||||
return helperTargetHwnd;
|
if (helperTargetHwnd !== null) {
|
||||||
|
return helperTargetHwnd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -2103,6 +2105,15 @@ function ensureWindowsVisibleOverlayForegroundPollLoop(): void {
|
|||||||
}, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS);
|
}, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearWindowsVisibleOverlayForegroundPollLoop(): void {
|
||||||
|
if (windowsVisibleOverlayForegroundPollInterval === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(windowsVisibleOverlayForegroundPollInterval);
|
||||||
|
windowsVisibleOverlayForegroundPollInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleVisibleOverlayBlurRefresh(): void {
|
function scheduleVisibleOverlayBlurRefresh(): void {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
return;
|
return;
|
||||||
@@ -2211,23 +2222,19 @@ function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
const opened = openOverlayHostedModal(
|
void openRuntimeOptionsModalRuntime({
|
||||||
{
|
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
||||||
ensureOverlayWindowsReadyForVisibilityActions: () =>
|
ensureOverlayWindowsReadyForVisibilityActions(),
|
||||||
ensureOverlayWindowsReadyForVisibilityActions(),
|
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
||||||
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs),
|
||||||
},
|
logWarn: (message) => logger.warn(message),
|
||||||
{
|
}).then((opened) => {
|
||||||
channel: IPC_CHANNELS.event.runtimeOptionsOpen,
|
if (!opened) {
|
||||||
modal: 'runtime-options',
|
showMpvOsd('Runtime options overlay unavailable.');
|
||||||
preferModalWindow: true,
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
if (!opened) {
|
|
||||||
showMpvOsd('Runtime options overlay unavailable.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openJimakuOverlay(): void {
|
function openJimakuOverlay(): void {
|
||||||
@@ -3034,6 +3041,8 @@ const {
|
|||||||
annotationSubtitleWsService.stop();
|
annotationSubtitleWsService.stop();
|
||||||
},
|
},
|
||||||
stopTexthookerService: () => texthookerService.stop(),
|
stopTexthookerService: () => texthookerService.stop(),
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop(),
|
||||||
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
||||||
clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
|
clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
|
||||||
getModalOverlayWindow: () => overlayManager.getModalWindow(),
|
getModalOverlayWindow: () => overlayManager.getModalWindow(),
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ test('notifyOverlayModalOpened enables input on visible main overlay window when
|
|||||||
assert.equal(mainWindow.webContentsFocused, true);
|
assert.equal(mainWindow.webContentsFocused, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleOverlayModalClosed resets modal state even when modal window does not exist', () => {
|
test('handleOverlayModalClosed is a no-op when no modal window can be targeted', () => {
|
||||||
const state: boolean[] = [];
|
const state: boolean[] = [];
|
||||||
const runtime = createOverlayModalRuntimeService(
|
const runtime = createOverlayModalRuntimeService(
|
||||||
{
|
{
|
||||||
@@ -509,13 +509,14 @@ test('handleOverlayModalClosed resets modal state even when modal window does no
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
restoreOnModalClose: 'runtime-options',
|
restoreOnModalClose: 'runtime-options',
|
||||||
});
|
});
|
||||||
|
assert.equal(sent, false);
|
||||||
runtime.notifyOverlayModalOpened('runtime-options');
|
runtime.notifyOverlayModalOpened('runtime-options');
|
||||||
runtime.handleOverlayModalClosed('runtime-options');
|
runtime.handleOverlayModalClosed('runtime-options');
|
||||||
|
|
||||||
assert.deepEqual(state, [true, false]);
|
assert.deepEqual(state, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
||||||
@@ -637,10 +638,11 @@ test('warm modal window reopen becomes interactive immediately on the second ope
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
|
||||||
|
const modalWindow = createMockWindow();
|
||||||
const runtime = createOverlayModalRuntimeService({
|
const runtime = createOverlayModalRuntimeService({
|
||||||
getMainWindow: () => null,
|
getMainWindow: () => null,
|
||||||
getModalWindow: () => null,
|
getModalWindow: () => modalWindow as never,
|
||||||
createModalWindow: () => null,
|
createModalWindow: () => modalWindow as never,
|
||||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||||
setModalWindowBounds: () => {},
|
setModalWindowBounds: () => {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { BrowserWindow } from 'electron';
|
import type { BrowserWindow } from 'electron';
|
||||||
import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
import type { OverlayHostedModal } from '../shared/ipc/contracts';
|
||||||
import type { WindowGeometry } from '../types';
|
import type { WindowGeometry } from '../types';
|
||||||
|
import { OVERLAY_WINDOW_CONTENT_READY_FLAG } from '../core/services/overlay-window-flags';
|
||||||
|
|
||||||
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
||||||
const OVERLAY_WINDOW_CONTENT_READY_FLAG = '__subminerOverlayContentReady';
|
|
||||||
|
|
||||||
export interface OverlayWindowResolver {
|
export interface OverlayWindowResolver {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
|
|||||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
|
calls.push('clear-windows-visible-overlay-foreground-poll-loop'),
|
||||||
getMainOverlayWindow: () => ({
|
getMainOverlayWindow: () => ({
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
destroy: () => calls.push('destroy-main-overlay-window'),
|
destroy: () => calls.push('destroy-main-overlay-window'),
|
||||||
@@ -99,6 +101,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
|
|||||||
unregisterAllGlobalShortcuts: () => {},
|
unregisterAllGlobalShortcuts: () => {},
|
||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||||
getMainOverlayWindow: () => ({
|
getMainOverlayWindow: () => ({
|
||||||
isDestroyed: () => true,
|
isDestroyed: () => true,
|
||||||
destroy: () => calls.push('destroy-main-overlay-window'),
|
destroy: () => calls.push('destroy-main-overlay-window'),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
unregisterAllGlobalShortcuts: () => void;
|
unregisterAllGlobalShortcuts: () => void;
|
||||||
stopSubtitleWebsocket: () => void;
|
stopSubtitleWebsocket: () => void;
|
||||||
stopTexthookerService: () => void;
|
stopTexthookerService: () => void;
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () => void;
|
||||||
getMainOverlayWindow: () => DestroyableWindow | null;
|
getMainOverlayWindow: () => DestroyableWindow | null;
|
||||||
clearMainOverlayWindow: () => void;
|
clearMainOverlayWindow: () => void;
|
||||||
getModalOverlayWindow: () => DestroyableWindow | null;
|
getModalOverlayWindow: () => DestroyableWindow | null;
|
||||||
@@ -64,6 +65,8 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
||||||
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
||||||
stopTexthookerService: () => deps.stopTexthookerService(),
|
stopTexthookerService: () => deps.stopTexthookerService(),
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () =>
|
||||||
|
deps.clearWindowsVisibleOverlayForegroundPollLoop(),
|
||||||
destroyMainOverlayWindow: () => {
|
destroyMainOverlayWindow: () => {
|
||||||
const window = deps.getMainOverlayWindow();
|
const window = deps.getMainOverlayWindow();
|
||||||
if (!window) return;
|
if (!window) return;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
|||||||
unregisterAllGlobalShortcuts: () => {},
|
unregisterAllGlobalShortcuts: () => {},
|
||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
|
clearWindowsVisibleOverlayForegroundPollLoop: () => {},
|
||||||
getMainOverlayWindow: () => null,
|
getMainOverlayWindow: () => null,
|
||||||
clearMainOverlayWindow: () => {},
|
clearMainOverlayWindow: () => {},
|
||||||
getModalOverlayWindow: () => null,
|
getModalOverlayWindow: () => null,
|
||||||
|
|||||||
88
src/main/runtime/runtime-options-open.test.ts
Normal file
88
src/main/runtime/runtime-options-open.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { openRuntimeOptionsModal } from './runtime-options-open';
|
||||||
|
|
||||||
|
test('runtime options open prefers dedicated modal window on first attempt', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const opened = await openRuntimeOptionsModal({
|
||||||
|
ensureOverlayStartupPrereqs: () => {
|
||||||
|
calls.push('ensure-startup');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {
|
||||||
|
calls.push('ensure-windows');
|
||||||
|
},
|
||||||
|
sendToActiveOverlayWindow: (channel, payload, options) => {
|
||||||
|
calls.push(`send:${channel}`);
|
||||||
|
assert.equal(payload, undefined);
|
||||||
|
assert.deepEqual(options, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
preferModalWindow: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
waitForModalOpen: async () => true,
|
||||||
|
logWarn: () => {
|
||||||
|
throw new Error('should not warn on first-attempt success');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(opened, true);
|
||||||
|
assert.deepEqual(calls, ['ensure-startup', 'ensure-windows', 'send:runtime-options:open']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime options open retries after an open timeout', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
let waitCalls = 0;
|
||||||
|
|
||||||
|
const opened = await openRuntimeOptionsModal({
|
||||||
|
ensureOverlayStartupPrereqs: () => {
|
||||||
|
calls.push('ensure-startup');
|
||||||
|
},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {
|
||||||
|
calls.push('ensure-windows');
|
||||||
|
},
|
||||||
|
sendToActiveOverlayWindow: (channel, payload, options) => {
|
||||||
|
calls.push(`send:${channel}`);
|
||||||
|
assert.equal(payload, undefined);
|
||||||
|
assert.deepEqual(options, {
|
||||||
|
restoreOnModalClose: 'runtime-options',
|
||||||
|
preferModalWindow: true,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
waitForModalOpen: async () => {
|
||||||
|
waitCalls += 1;
|
||||||
|
return waitCalls === 2;
|
||||||
|
},
|
||||||
|
logWarn: (message) => {
|
||||||
|
warnings.push(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(opened, true);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'ensure-startup',
|
||||||
|
'ensure-windows',
|
||||||
|
'send:runtime-options:open',
|
||||||
|
'ensure-startup',
|
||||||
|
'ensure-windows',
|
||||||
|
'send:runtime-options:open',
|
||||||
|
]);
|
||||||
|
assert.deepEqual(warnings, [
|
||||||
|
'Runtime options modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime options open fails when no overlay window can be targeted', async () => {
|
||||||
|
const opened = await openRuntimeOptionsModal({
|
||||||
|
ensureOverlayStartupPrereqs: () => {},
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => {},
|
||||||
|
sendToActiveOverlayWindow: () => false,
|
||||||
|
waitForModalOpen: async () => true,
|
||||||
|
logWarn: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(opened, false);
|
||||||
|
});
|
||||||
45
src/main/runtime/runtime-options-open.ts
Normal file
45
src/main/runtime/runtime-options-open.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
|
||||||
|
|
||||||
|
const RUNTIME_OPTIONS_MODAL: OverlayHostedModal = 'runtime-options';
|
||||||
|
const RUNTIME_OPTIONS_OPEN_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
|
export async function openRuntimeOptionsModal(deps: {
|
||||||
|
ensureOverlayStartupPrereqs: () => void;
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: {
|
||||||
|
restoreOnModalClose?: OverlayHostedModal;
|
||||||
|
preferModalWindow?: boolean;
|
||||||
|
},
|
||||||
|
) => boolean;
|
||||||
|
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const sendOpen = (): boolean => {
|
||||||
|
deps.ensureOverlayStartupPrereqs();
|
||||||
|
deps.ensureOverlayWindowsReadyForVisibilityActions();
|
||||||
|
return deps.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||||
|
restoreOnModalClose: RUNTIME_OPTIONS_MODAL,
|
||||||
|
preferModalWindow: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!sendOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await deps.waitForModalOpen(RUNTIME_OPTIONS_MODAL, RUNTIME_OPTIONS_OPEN_TIMEOUT_MS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.logWarn(
|
||||||
|
'Runtime options modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||||
|
);
|
||||||
|
if (!sendOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await deps.waitForModalOpen(RUNTIME_OPTIONS_MODAL, RUNTIME_OPTIONS_OPEN_TIMEOUT_MS);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import * as windowsHelper from './windows-helper';
|
||||||
import {
|
import {
|
||||||
lowerWindowsOverlayInZOrder,
|
lowerWindowsOverlayInZOrder,
|
||||||
parseWindowTrackerHelperForegroundProcess,
|
parseWindowTrackerHelperForegroundProcess,
|
||||||
@@ -204,6 +205,41 @@ test('queryWindowsTargetWindowHandle resolves the selected hwnd from the powersh
|
|||||||
assert.deepEqual(capturedArgs, ['\\\\.\\pipe\\subminer-socket']);
|
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', () => {
|
test('resolveWindowsTrackerHelper auto mode prefers native helper when present', () => {
|
||||||
const helper = resolveWindowsTrackerHelper({
|
const helper = resolveWindowsTrackerHelper({
|
||||||
dirname: 'C:\\repo\\dist\\window-trackers',
|
dirname: 'C:\\repo\\dist\\window-trackers',
|
||||||
|
|||||||
@@ -64,6 +64,21 @@ function normalizeHelperMode(value: string | undefined): WindowsTrackerHelperMod
|
|||||||
return 'auto';
|
return 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
function inferHelperKindFromPath(helperPath: string): WindowsTrackerHelperKind | null {
|
function inferHelperKindFromPath(helperPath: string): WindowsTrackerHelperKind | null {
|
||||||
const normalized = helperPath.trim().toLowerCase();
|
const normalized = helperPath.trim().toLowerCase();
|
||||||
if (normalized.endsWith('.exe')) return 'native';
|
if (normalized.endsWith('.exe')) return 'native';
|
||||||
|
|||||||
@@ -19,7 +19,10 @@
|
|||||||
import { BaseWindowTracker } from './base-tracker';
|
import { BaseWindowTracker } from './base-tracker';
|
||||||
import type { WindowGeometry } from '../types';
|
import type { WindowGeometry } from '../types';
|
||||||
import type { MpvPollResult } from './win32';
|
import type { MpvPollResult } from './win32';
|
||||||
import { queryWindowsTrackerMpvWindows } from './windows-helper';
|
import {
|
||||||
|
queryWindowsTrackerMpvWindows,
|
||||||
|
shouldUseWindowsTrackerPowershellFallback,
|
||||||
|
} from './windows-helper';
|
||||||
import { createLogger } from '../logger';
|
import { createLogger } from '../logger';
|
||||||
|
|
||||||
const log = createLogger('tracker').child('windows');
|
const log = createLogger('tracker').child('windows');
|
||||||
@@ -32,18 +35,8 @@ type WindowsTrackerDeps = {
|
|||||||
now?: () => number;
|
now?: () => number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function shouldUsePowershellTrackerFallback(): boolean {
|
|
||||||
const helperMode = process.env.SUBMINER_WINDOWS_TRACKER_HELPER?.trim().toLowerCase();
|
|
||||||
if (helperMode === 'powershell') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const helperPath = process.env.SUBMINER_WINDOWS_TRACKER_HELPER_PATH?.trim().toLowerCase();
|
|
||||||
return helperPath?.endsWith('.ps1') ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultPollMpvWindows(targetMpvSocketPath?: string | null): MpvPollResult {
|
function defaultPollMpvWindows(targetMpvSocketPath?: string | null): MpvPollResult {
|
||||||
if (targetMpvSocketPath && shouldUsePowershellTrackerFallback()) {
|
if (targetMpvSocketPath && shouldUseWindowsTrackerPowershellFallback()) {
|
||||||
const helperResult = queryWindowsTrackerMpvWindows({
|
const helperResult = queryWindowsTrackerMpvWindows({
|
||||||
targetMpvSocketPath,
|
targetMpvSocketPath,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user