fix(extension): bound frame injection and retry failures

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