@@ -2,7 +2,7 @@ import browser from 'webextension-polyfill';
22
33import type { Profile , RequestHeader } from '#entities/request-profile/types' ;
44
5- import { BrowserStorageKey } from './shared/constants' ;
5+ import { BrowserStorageKey , RuntimeMessageType } from './shared/constants' ;
66import { browserAction } from './shared/utils/browserAPI' ;
77import { logger , LogLevel } from './shared/utils/logger' ;
88import { setBrowserHeaders } from './shared/utils/setBrowserHeaders' ;
@@ -77,9 +77,25 @@ if (process.env.NODE_ENV === 'development') {
7777const BADGE_COLOR = '#ffffff' ;
7878const PAGE_CONSOLE_LOG_MESSAGE_TYPE = 'cloudhood:page-console-log' ;
7979const LOG_MIRROR_SOURCE = 'background' ;
80+ const WATCHDOG_ALARM_NAME = 'cloudhood-headers-watchdog' ;
81+ const WATCHDOG_PERIOD_MINUTES = 3 ;
82+ const WATCHDOG_INITIAL_DELAY_MINUTES = 1 ;
83+ const MAX_APPLY_STALENESS_MS = 30 * 60 * 1000 ;
84+ const AUTO_HEAL_FAILURE_THRESHOLD = 3 ;
85+ const MAX_DEBUG_LOGS = 3000 ;
8086
8187let mirrorLogsToPageConsole = false ;
8288let mirroredLogSeq = 0 ;
89+ const workerBootId = `${ Date . now ( ) . toString ( 36 ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
90+ const workerBootAt = Date . now ( ) ;
91+ let debugLogSeq = 0 ;
92+ const debugLogsBuffer : Array < {
93+ seq : number ;
94+ timestamp : number ;
95+ level : LogLevel ;
96+ message : string ;
97+ args : string [ ] ;
98+ } > = [ ] ;
8399
84100function safeStringify ( value : unknown ) : string {
85101 const seen = new WeakSet < object > ( ) ;
@@ -120,6 +136,17 @@ function getConsoleMethodForLevel(level: LogLevel): 'log' | 'info' | 'warn' | 'e
120136 }
121137}
122138
139+ function appendDebugLog ( entry : { timestamp : number ; level : LogLevel ; message : string ; args : string [ ] } ) {
140+ debugLogSeq += 1 ;
141+ debugLogsBuffer . push ( {
142+ seq : debugLogSeq ,
143+ ...entry ,
144+ } ) ;
145+ if ( debugLogsBuffer . length > MAX_DEBUG_LOGS ) {
146+ debugLogsBuffer . splice ( 0 , debugLogsBuffer . length - MAX_DEBUG_LOGS ) ;
147+ }
148+ }
149+
123150async function updateMirrorLogsModeFromStorage ( ) : Promise < void > {
124151 try {
125152 const result = await browser . storage . local . get ( [ BrowserStorageKey . MirrorLogsToPageConsole ] ) ;
@@ -130,6 +157,14 @@ async function updateMirrorLogsModeFromStorage(): Promise<void> {
130157}
131158
132159logger . setExternalSink ( async ( { level, message, args, timestamp } ) => {
160+ const serializedArgs = args . map ( item => safeStringify ( item ) ) ;
161+ appendDebugLog ( {
162+ timestamp,
163+ level,
164+ message,
165+ args : serializedArgs ,
166+ } ) ;
167+
133168 if ( ! mirrorLogsToPageConsole ) return ;
134169
135170 const activeTabs = await browser . tabs . query ( {
@@ -147,7 +182,7 @@ logger.setExternalSink(async ({ level, message, args, timestamp }) => {
147182 level,
148183 consoleMethod : getConsoleMethodForLevel ( level ) ,
149184 message,
150- args : args . map ( item => safeStringify ( item ) ) ,
185+ args : serializedArgs ,
151186 timestamp,
152187 } ,
153188 } ) ;
@@ -188,6 +223,90 @@ let applyCounter = 0;
188223let lastRequestedReason = 'unknown' ;
189224let lastAppliedStorageFingerprint : string | null = null ;
190225let lastAppliedMeta : { seq : number ; updatedAt : number } = { seq : 0 , updatedAt : 0 } ;
226+ let lastApplyAttemptAt : number | null = null ;
227+ let lastSuccessfulApplyAt : number | null = null ;
228+ let lastApplyFailureAt : number | null = null ;
229+ let consecutiveApplyFailures = 0 ;
230+
231+ function getApplyHealthSnapshot ( ) {
232+ const now = Date . now ( ) ;
233+ return {
234+ workerBootId,
235+ workerUptimeMs : now - workerBootAt ,
236+ applyInProgress,
237+ applyPending,
238+ applyCounter,
239+ lastRequestedReason,
240+ lastApplyAttemptAt,
241+ lastSuccessfulApplyAt,
242+ lastApplyFailureAt,
243+ consecutiveApplyFailures,
244+ millisSinceLastSuccess : lastSuccessfulApplyAt ? now - lastSuccessfulApplyAt : null ,
245+ lastAppliedStorageFingerprint,
246+ lastAppliedMeta,
247+ } ;
248+ }
249+
250+ async function buildDebugLogsExportPayload ( ) {
251+ const now = Date . now ( ) ;
252+ const storage = await browser . storage . local . get ( [
253+ BrowserStorageKey . Profiles ,
254+ BrowserStorageKey . SelectedProfile ,
255+ BrowserStorageKey . IsPaused ,
256+ BrowserStorageKey . HeadersConfigMeta ,
257+ BrowserStorageKey . MirrorLogsToPageConsole ,
258+ ] ) ;
259+ let dynamicRules : browser . DeclarativeNetRequest . Rule [ ] = [ ] ;
260+ let sessionRules : browser . DeclarativeNetRequest . Rule [ ] = [ ] ;
261+
262+ try {
263+ dynamicRules = await browser . declarativeNetRequest . getDynamicRules ( ) ;
264+ } catch {
265+ dynamicRules = [ ] ;
266+ }
267+ try {
268+ sessionRules = await browser . declarativeNetRequest . getSessionRules ( ) ;
269+ } catch {
270+ sessionRules = [ ] ;
271+ }
272+
273+ return {
274+ exportedAt : new Date ( now ) . toISOString ( ) ,
275+ worker : {
276+ bootId : workerBootId ,
277+ bootedAt : new Date ( workerBootAt ) . toISOString ( ) ,
278+ uptimeMs : now - workerBootAt ,
279+ } ,
280+ health : getApplyHealthSnapshot ( ) ,
281+ storage,
282+ dnr : {
283+ dynamicRulesCount : dynamicRules . length ,
284+ sessionRulesCount : sessionRules . length ,
285+ dynamicRules,
286+ sessionRules,
287+ } ,
288+ logs : debugLogsBuffer ,
289+ } ;
290+ }
291+
292+ async function ensureWatchdogAlarm ( ) {
293+ try {
294+ await browser . alarms . create ( WATCHDOG_ALARM_NAME , {
295+ delayInMinutes : WATCHDOG_INITIAL_DELAY_MINUTES ,
296+ periodInMinutes : WATCHDOG_PERIOD_MINUTES ,
297+ } ) ;
298+ logger . info (
299+ `🩺 Watchdog alarm configured: ${ safeStringify ( {
300+ name : WATCHDOG_ALARM_NAME ,
301+ delayInMinutes : WATCHDOG_INITIAL_DELAY_MINUTES ,
302+ periodInMinutes : WATCHDOG_PERIOD_MINUTES ,
303+ workerBootId,
304+ } ) } `,
305+ ) ;
306+ } catch ( error ) {
307+ logger . error ( `❌ Failed to configure watchdog alarm: ${ safeStringify ( { error } ) } ` ) ;
308+ }
309+ }
191310
192311function normalizeHeadersConfigMeta ( value : unknown ) : { seq : number ; updatedAt : number } {
193312 if ( value && typeof value === 'object' ) {
@@ -236,6 +355,7 @@ async function applyHeadersFromStorageQueue(reason: string) {
236355 while ( applyPending ) {
237356 applyPending = false ;
238357 const applyId = ++ applyCounter ;
358+ lastApplyAttemptAt = Date . now ( ) ;
239359
240360 const startedAt = Date . now ( ) ;
241361 const result = await browser . storage . local . get ( [
@@ -334,9 +454,14 @@ async function applyHeadersFromStorageQueue(reason: string) {
334454 metaChange : isNewer
335455 ? `seq:${ prevMeta . seq } →${ meta . seq } , updatedAt:${ prevMeta . updatedAt } →${ meta . updatedAt } `
336456 : `unchanged (stale meta preserved: seq=${ lastAppliedMeta . seq } , updatedAt=${ lastAppliedMeta . updatedAt } )` ,
457+ healthBeforeReset : getApplyHealthSnapshot ( ) ,
337458 } ) } `,
338459 ) ;
460+ consecutiveApplyFailures = 0 ;
461+ lastSuccessfulApplyAt = Date . now ( ) ;
339462 } catch ( error ) {
463+ consecutiveApplyFailures += 1 ;
464+ lastApplyFailureAt = Date . now ( ) ;
340465 logger . error (
341466 `❌ Apply failed (state NOT updated, will retry on next change): ${ safeStringify ( {
342467 applyId,
@@ -347,6 +472,7 @@ async function applyHeadersFromStorageQueue(reason: string) {
347472 } ,
348473 attemptedFingerprint : fp ,
349474 attemptedMeta : meta ,
475+ healthAfterFailure : getApplyHealthSnapshot ( ) ,
350476 } ) } `,
351477 ) ;
352478 } finally {
@@ -364,8 +490,49 @@ async function applyHeadersFromStorageQueue(reason: string) {
364490 }
365491}
366492
493+ async function runWatchdog ( reason : string ) {
494+ const now = Date . now ( ) ;
495+ const millisSinceLastSuccess = lastSuccessfulApplyAt ? now - lastSuccessfulApplyAt : null ;
496+ const shouldForceByFailures = consecutiveApplyFailures >= AUTO_HEAL_FAILURE_THRESHOLD ;
497+ const shouldForceByStaleness =
498+ millisSinceLastSuccess !== null &&
499+ ! applyInProgress &&
500+ ! applyPending &&
501+ millisSinceLastSuccess > MAX_APPLY_STALENESS_MS ;
502+
503+ logger . debug (
504+ `🩺 Watchdog tick: ${ safeStringify ( {
505+ reason,
506+ millisSinceLastSuccess,
507+ shouldForceByFailures,
508+ shouldForceByStaleness,
509+ health : getApplyHealthSnapshot ( ) ,
510+ } ) } `,
511+ ) ;
512+
513+ if ( ! shouldForceByFailures && ! shouldForceByStaleness ) return ;
514+
515+ const trigger = shouldForceByFailures ? 'consecutive-failures' : 'staleness' ;
516+ logger . warn (
517+ `🩹 Watchdog auto-heal triggered: ${ safeStringify ( {
518+ reason,
519+ trigger,
520+ threshold : {
521+ autoHealFailureThreshold : AUTO_HEAL_FAILURE_THRESHOLD ,
522+ maxApplyStalenessMs : MAX_APPLY_STALENESS_MS ,
523+ } ,
524+ health : getApplyHealthSnapshot ( ) ,
525+ } ) } `,
526+ ) ;
527+
528+ await applyHeadersFromStorageQueue ( `watchdog:${ trigger } :${ reason } ` ) ;
529+ }
530+
531+ ensureWatchdogAlarm ( ) . catch ( ( ) => undefined ) ;
532+
367533browser . runtime . onStartup . addListener ( async function ( ) {
368534 logger . info ( 'Extension startup triggered' ) ;
535+ ensureWatchdogAlarm ( ) . catch ( ( ) => undefined ) ;
369536
370537 const result = await browser . storage . local . get ( [
371538 BrowserStorageKey . Profiles ,
@@ -459,8 +626,30 @@ browser.storage.onChanged.addListener(async (changes, areaName) => {
459626 }
460627} ) ;
461628
629+ browser . alarms . onAlarm . addListener ( alarm => {
630+ if ( alarm . name !== WATCHDOG_ALARM_NAME ) return ;
631+ runWatchdog ( 'alarms.onAlarm' ) . catch ( error => {
632+ logger . error ( `❌ Watchdog execution failed: ${ safeStringify ( { error } ) } ` ) ;
633+ } ) ;
634+ } ) ;
635+
636+ browser . runtime . onMessage . addListener ( ( message : unknown ) => {
637+ if ( ! message || typeof message !== 'object' ) return undefined ;
638+
639+ const payload = message as Record < string , unknown > ;
640+ if ( payload . type !== RuntimeMessageType . ExportDebugLogs ) return undefined ;
641+
642+ return buildDebugLogsExportPayload ( )
643+ . then ( result => ( { ok : true , result } ) )
644+ . catch ( error => ( {
645+ ok : false ,
646+ error : safeStringify ( error ) ,
647+ } ) ) ;
648+ } ) ;
649+
462650browser . runtime . onInstalled . addListener ( async details => {
463651 logger . info ( 'Extension installed/updated:' , details . reason ) ;
652+ ensureWatchdogAlarm ( ) . catch ( ( ) => undefined ) ;
464653
465654 const result = await browser . storage . local . get ( [
466655 BrowserStorageKey . Profiles ,
0 commit comments