mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-18 07:13:12 +00:00
fix(extension): bound frame injection and retry failures
This commit is contained in:
+70
-11
@@ -80,6 +80,8 @@ let currentTargetHasVideo = false;
|
||||
// popup look as if the user never selected a tab.
|
||||
let requestedTargetTabId = null;
|
||||
let requestedTargetTitle = null;
|
||||
let requestedTargetRetryBlockedTabId = null;
|
||||
let requestedTargetRetryBlockedMessage = null;
|
||||
let pendingRequestedActivationCount = 0;
|
||||
let targetActivationGeneration = 0;
|
||||
let activeTargetActivation = null;
|
||||
@@ -224,6 +226,7 @@ function ensureState() {
|
||||
'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
|
||||
'currentTargetFrameId', 'currentTargetDocumentId', 'currentTargetHasVideo',
|
||||
'requestedTargetTabId', 'requestedTargetTitle',
|
||||
'requestedTargetRetryBlockedTabId', 'requestedTargetRetryBlockedMessage',
|
||||
'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt',
|
||||
'hcmDesynced', 'chatActivityTimeline'
|
||||
], (data) => {
|
||||
@@ -246,6 +249,11 @@ function ensureState() {
|
||||
&& typeof data.requestedTargetTitle === 'string'
|
||||
? data.requestedTargetTitle
|
||||
: null;
|
||||
requestedTargetRetryBlockedTabId = normalizeTabId(data.requestedTargetRetryBlockedTabId);
|
||||
requestedTargetRetryBlockedMessage = requestedTargetRetryBlockedTabId !== null
|
||||
&& typeof data.requestedTargetRetryBlockedMessage === 'string'
|
||||
? data.requestedTargetRetryBlockedMessage
|
||||
: null;
|
||||
if (data.currentTabTitle !== undefined) {
|
||||
currentTabTitle = currentTabId !== null && typeof data.currentTabTitle === 'string'
|
||||
? data.currentTabTitle
|
||||
@@ -2340,6 +2348,31 @@ function createTargetActivationSupersededError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
const SCRIPT_INJECTION_TIMEOUT_MS = 5000;
|
||||
|
||||
function executeScriptWithTimeout(options, timeoutMs = SCRIPT_INJECTION_TIMEOUT_MS) {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
||||
return chrome.scripting.executeScript(options);
|
||||
}
|
||||
let timeoutId = null;
|
||||
const label = Array.isArray(options?.files) && options.files.length > 0
|
||||
? options.files.join(', ')
|
||||
: 'function injection';
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
const error = new Error(`Script injection timed out after ${timeoutMs}ms (${label})`);
|
||||
error.code = 'script_injection_timeout';
|
||||
reject(error);
|
||||
}, timeoutMs);
|
||||
});
|
||||
return Promise.race([
|
||||
chrome.scripting.executeScript(options),
|
||||
timeout
|
||||
]).finally(() => {
|
||||
if (timeoutId !== null) clearTimeout(timeoutId);
|
||||
});
|
||||
}
|
||||
|
||||
async function injectMediaFrameMonitors(tabId, contentTarget) {
|
||||
// The all-frames target is only a best-effort sweep: one inaccessible
|
||||
// frame can make Chromium reject the entire sweep. Always include the
|
||||
@@ -2353,12 +2386,15 @@ async function injectMediaFrameMonitors(tabId, contentTarget) {
|
||||
let injectedCount = 0;
|
||||
for (const target of targets) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
await executeScriptWithTimeout({
|
||||
target,
|
||||
files: ['media-frame-monitor.js']
|
||||
});
|
||||
}, 2000);
|
||||
injectedCount++;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error?.code === 'script_injection_timeout') {
|
||||
addLog(`Media-frame monitor injection timed out for ${JSON.stringify(target)}`, 'warn');
|
||||
}
|
||||
// One denied widget frame must not block the selected player.
|
||||
}
|
||||
}
|
||||
@@ -2424,12 +2460,12 @@ async function injectContentScript(tabId, {
|
||||
}
|
||||
if (needsPageApiSeek) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({
|
||||
await executeScriptWithTimeout({
|
||||
target: scriptTarget,
|
||||
world: 'MAIN',
|
||||
files: ['page-api-seek-overrides.js']
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
await executeScriptWithTimeout({
|
||||
target: scriptTarget,
|
||||
world: 'MAIN',
|
||||
func: installPageApiSeekBridge
|
||||
@@ -2440,16 +2476,16 @@ async function injectContentScript(tabId, {
|
||||
}
|
||||
}
|
||||
|
||||
await chrome.scripting.executeScript({
|
||||
await executeScriptWithTimeout({
|
||||
target: scriptTarget,
|
||||
files: ['page-api-seek-overrides.js']
|
||||
});
|
||||
await chrome.scripting.executeScript({
|
||||
await executeScriptWithTimeout({
|
||||
target: scriptTarget,
|
||||
func: setPageApiSeekEnabled,
|
||||
args: [pageApiSeekReady]
|
||||
});
|
||||
const injectionResults = await chrome.scripting.executeScript({
|
||||
const injectionResults = await executeScriptWithTimeout({
|
||||
target: scriptTarget,
|
||||
files: ['chat-format.js', 'chat-overlay.js', 'content.js']
|
||||
});
|
||||
@@ -2652,9 +2688,13 @@ async function rememberRequestedTarget(tabId, tabTitle) {
|
||||
if (normalizedTabId === null) return false;
|
||||
requestedTargetTabId = normalizedTabId;
|
||||
requestedTargetTitle = typeof tabTitle === 'string' ? tabTitle : null;
|
||||
requestedTargetRetryBlockedTabId = null;
|
||||
requestedTargetRetryBlockedMessage = null;
|
||||
await chrome.storage.session.set({
|
||||
requestedTargetTabId,
|
||||
requestedTargetTitle
|
||||
requestedTargetTitle,
|
||||
requestedTargetRetryBlockedTabId: null,
|
||||
requestedTargetRetryBlockedMessage: null
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -2666,9 +2706,13 @@ async function clearRequestedTarget(expectedTabId = null) {
|
||||
}
|
||||
requestedTargetTabId = null;
|
||||
requestedTargetTitle = null;
|
||||
requestedTargetRetryBlockedTabId = null;
|
||||
requestedTargetRetryBlockedMessage = null;
|
||||
await chrome.storage.session.set({
|
||||
requestedTargetTabId: null,
|
||||
requestedTargetTitle: null
|
||||
requestedTargetTitle: null,
|
||||
requestedTargetRetryBlockedTabId: null,
|
||||
requestedTargetRetryBlockedMessage: null
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -2678,7 +2722,8 @@ async function retryRequestedTarget() {
|
||||
if (selectedTabId === null
|
||||
|| normalizeTabId(currentTabId) === selectedTabId
|
||||
|| pendingRequestedActivationCount > 0
|
||||
|| activeTargetActivation) {
|
||||
|| activeTargetActivation
|
||||
|| normalizeTabId(requestedTargetRetryBlockedTabId) === selectedTabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2702,6 +2747,14 @@ async function retryRequestedTarget() {
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error?.code !== HOST_ACCESS_REQUIRED_STATUS) {
|
||||
requestedTargetRetryBlockedTabId = selectedTabId;
|
||||
requestedTargetRetryBlockedMessage = error?.message || 'Script injection failed';
|
||||
await chrome.storage.session.set({
|
||||
requestedTargetRetryBlockedTabId,
|
||||
requestedTargetRetryBlockedMessage
|
||||
});
|
||||
}
|
||||
addLog(`Requested target retry failed: ${error.message}`, 'warn');
|
||||
return injectionFailureResponse(error);
|
||||
}
|
||||
@@ -3391,7 +3444,12 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
? 'ready'
|
||||
: pendingTarget?.tabId === targetTabId
|
||||
? 'access_required'
|
||||
: normalizeTabId(requestedTargetRetryBlockedTabId) === targetTabId
|
||||
? 'error'
|
||||
: 'activating';
|
||||
const targetActivationError = normalizeTabId(requestedTargetRetryBlockedTabId) === targetTabId
|
||||
? requestedTargetRetryBlockedMessage
|
||||
: null;
|
||||
sendResponse({
|
||||
status,
|
||||
peerId,
|
||||
@@ -3403,6 +3461,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
targetHasVideo: currentTargetHasVideo,
|
||||
targetReady,
|
||||
targetActivationState,
|
||||
targetActivationError,
|
||||
pendingTargetTabId: pendingTarget?.tabId ?? null,
|
||||
pendingTargetHost: pendingTarget?.host ?? null,
|
||||
pendingTargetOriginPattern: pendingTarget?.originPattern ?? null,
|
||||
|
||||
@@ -2,6 +2,7 @@ export const MEDIA_FRAME_ACCESS_REQUIRED = 'media_frame_access_required';
|
||||
const MIN_PLAYER_FRAME_AREA = 320 * 180;
|
||||
const MIN_PLAYER_ASPECT_RATIO = 1.15;
|
||||
const MAX_PLAYER_ASPECT_RATIO = 2.6;
|
||||
const DEFAULT_PROBE_TIMEOUT_MS = 1500;
|
||||
|
||||
function normalizeFrameId(value) {
|
||||
return Number.isInteger(value) && value >= 0 ? value : 0;
|
||||
@@ -463,10 +464,29 @@ function mergeFrameResults(...groups) {
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
|
||||
async function executeInAccessibleFrames(chromeApi, targets, func, args) {
|
||||
function executeWithTimeout(task, timeoutMs, label) {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return task();
|
||||
let timeoutId = null;
|
||||
const timeout = new Promise((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
const error = new Error(`${label} timed out after ${timeoutMs}ms`);
|
||||
error.code = 'media_frame_probe_timeout';
|
||||
reject(error);
|
||||
}, timeoutMs);
|
||||
});
|
||||
return Promise.race([task(), timeout]).finally(() => {
|
||||
if (timeoutId !== null) clearTimeout(timeoutId);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeInAccessibleFrames(chromeApi, targets, func, args, timeoutMs = DEFAULT_PROBE_TIMEOUT_MS) {
|
||||
const settled = await Promise.all(targets.map(async target => {
|
||||
try {
|
||||
const result = await chromeApi.scripting.executeScript({ target, func, args });
|
||||
const result = await executeWithTimeout(
|
||||
() => chromeApi.scripting.executeScript({ target, func, args }),
|
||||
timeoutMs,
|
||||
`Frame probe for ${JSON.stringify(target)}`
|
||||
);
|
||||
return Array.isArray(result) ? result : [];
|
||||
} catch {
|
||||
return [];
|
||||
@@ -478,7 +498,8 @@ async function executeInAccessibleFrames(chromeApi, targets, func, args) {
|
||||
export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
attempts = 8,
|
||||
retryDelayMs = 200,
|
||||
probeDelayMs = 60
|
||||
probeDelayMs = 60,
|
||||
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS
|
||||
} = {}) {
|
||||
let fallback = null;
|
||||
let missingAccess = null;
|
||||
@@ -489,13 +510,15 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
chromeApi,
|
||||
[{ tabId, frameIds: [0] }],
|
||||
inspectMediaFrame,
|
||||
[null]
|
||||
[null],
|
||||
probeTimeoutMs
|
||||
);
|
||||
const allFrameResults = await executeInAccessibleFrames(
|
||||
chromeApi,
|
||||
listMediaFrameScriptTargets(tabId),
|
||||
inspectMediaFrame,
|
||||
[null]
|
||||
[null],
|
||||
probeTimeoutMs
|
||||
);
|
||||
let results = mergeFrameResults(topResults, allFrameResults);
|
||||
const embeddedFrameCount = topResults.reduce(
|
||||
@@ -507,7 +530,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
chromeApi,
|
||||
listFrameProbeTargets(tabId, embeddedFrameCount),
|
||||
inspectMediaFrame,
|
||||
[null]
|
||||
[null],
|
||||
probeTimeoutMs
|
||||
);
|
||||
results = mergeFrameResults(results, individuallyProbed);
|
||||
}
|
||||
@@ -521,7 +545,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
chromeApi,
|
||||
frameTargets,
|
||||
installParentFrameVisibilityProbe,
|
||||
[token]
|
||||
[token],
|
||||
probeTimeoutMs
|
||||
);
|
||||
// Four passes match the maximum same-origin recursion depth.
|
||||
for (let pass = 0; pass < 4; pass++) {
|
||||
@@ -529,7 +554,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
chromeApi,
|
||||
frameTargets,
|
||||
dispatchParentFrameVisibilityProbe,
|
||||
[token]
|
||||
[token],
|
||||
probeTimeoutMs
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, probeDelayMs));
|
||||
}
|
||||
@@ -537,7 +563,8 @@ export async function resolveMediaContentTarget(chromeApi, tabId, {
|
||||
chromeApi,
|
||||
frameTargets,
|
||||
inspectMediaFrame,
|
||||
[token]
|
||||
[token],
|
||||
probeTimeoutMs
|
||||
);
|
||||
if (inspected.length > 0) results = mergeFrameResults(results, inspected);
|
||||
} catch {
|
||||
|
||||
+11
-1
@@ -2808,13 +2808,23 @@ function refreshDebugInfo() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (res?.targetActivationState === 'error') {
|
||||
if (elements.videoDebug) {
|
||||
elements.videoDebug.textContent = res.targetActivationError
|
||||
? `Injection fehlgeschlagen: ${res.targetActivationError}`
|
||||
: 'Video-Injection fehlgeschlagen.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (elements.videoDebug) elements.videoDebug.textContent = getMessage('DEBUG_NO_TAB');
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.targetReady !== true) {
|
||||
if (elements.videoDebug) {
|
||||
elements.videoDebug.textContent = getMessage('DEBUG_TARGET_ACTIVATING');
|
||||
elements.videoDebug.textContent = res.targetActivationState === 'error'
|
||||
? `Injection fehlgeschlagen: ${res.targetActivationError || 'unbekannter Fehler'}`
|
||||
: getMessage('DEBUG_TARGET_ACTIVATING');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user