您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fixes infinite 'Waiting' splash screen on older Linksys models
// ==UserScript== // @name Linksys Router Infinite Waiting Fix // @description Fixes infinite 'Waiting' splash screen on older Linksys models // @version 1.0 // @match http://192.168.*/* // @match http://192.168.1.1/* // @run-at document-start // @grant none // @license MIT // @namespace linksys-sux.bonetrail.net // ==/UserScript== (function () { 'use strict'; const state = { faked: false, bootStarted: false, good: { devices: null, conns: null }, }; const TAG = '[RAINIER-PATCH]'; const log = (...a) => console.log(TAG, ...a); const warn = (...a) => console.warn(TAG, ...a); const DM_FLAG = Symbol('dmPatched'); const TX_FLAG = Symbol('txPatched'); const FN_FLAG = Symbol('fnPatched'); // --- Caches of good data we can reuse when the new endpoints fail --- const cache = { devices: null, // from ANY successful devicelist call connections: null // from ANY successful networkconnections call }; window.__RAINIER_CACHE = cache; // expose to second IIFE // Utility ------------------------------------------------------------- function normalizeGetDevicesArgs(args) { if (typeof args[0] === 'object' && args[0] !== null) return args[0]; return { cb: args[0], exclusions: args[1], threshold: args[2], cbError: args[3], doPollForChange: args[4], currentRevision: args[5], }; } function wrapMethod(obj, name, wrapper) { const orig = obj[name]; if (typeof orig !== 'function' || orig[FN_FLAG]) return; const patched = wrapper(orig); patched[FN_FLAG] = true; obj[name] = patched; } function safe(fn) { try { return fn(); } catch (_) {} } // Predicate helpers for action URLs ---------------------------------- function isDevicesAction(a) { return /\/jnap\/devicelist\/GetDevices/i.test(a); } function isDevices3Action(a) { return /\/jnap\/devicelist\/GetDevices3/i.test(a); } function isNetConnsAction(a) { return /\/jnap\/networkconnections\/GetNetworkConnections/i.test(a); } function isNetConns2Action(a) { return /\/jnap\/networkconnections\/GetNetworkConnections2/i.test(a); } // Builders for fake responses ---------------------------------------- function buildDevicesFallback() { if (cache.devices) { // Clone minimally return { result: 'OK', output: { revision: cache.devices.output?.revision ?? Date.now(), devices: cache.devices.output?.devices ?? [], deletedDeviceIDs: cache.devices.output?.deletedDeviceIDs ?? [] } }; } return { result: 'OK', output: { revision: Date.now(), devices: [], deletedDeviceIDs: [] } }; } function buildNetConnsFallback() { if (cache.connections) { return { result: 'OK', output: { connections: cache.connections.output?.connections ?? [] } }; } return { result: 'OK', output: { connections: [] } }; } // DeviceManager patch ------------------------------------------------- function patchDeviceManager(dm) { if (!dm || dm[DM_FLAG]) return; wrapMethod(dm, 'getDevices', (orig) => function patchedGetDevices() { const opts = normalizeGetDevicesArgs(arguments); const userCb = typeof opts.cb === 'function' ? opts.cb : () => {}; const userCbError = typeof opts.cbError === 'function' ? opts.cbError : null; const timeoutMs = (opts.threshold || 30000) + 5000; let finished = false; function done(list) { if (finished) return; finished = true; try { userCb(list || []); } catch (e) { warn('user cb blew up', e); } } function fail(err) { warn('getDevices failed/hung, faking empty list', err); if (userCbError) safe(() => userCbError(err)); safe(() => RAINIER?.event?.fire?.('devices.revisionUpdated')); done([]); } const timer = setTimeout(() => fail(new Error('timeout')), timeoutMs); const patchedOpts = { ...opts }; patchedOpts.cb = (list) => { clearTimeout(timer); done(list); }; patchedOpts.cbError = (err) => { clearTimeout(timer); fail(err || new Error('unknown error')); }; try { return orig.call(this, patchedOpts); } catch (e) { clearTimeout(timer); fail(e); } }); dm[DM_FLAG] = true; log('deviceManager patched'); } // JNAP Transaction patch --------------------------------------------- function patchJnap(jnap) { if (!jnap || jnap[TX_FLAG]) return; const OrigTx = jnap.Transaction; if (typeof OrigTx !== 'function') return; jnap.Transaction = function patchedTransaction(opts) { const tx = OrigTx.call(this, opts); if (!tx || tx[TX_FLAG]) return tx; tx[TX_FLAG] = true; // Wrap per-request cb wrapMethod(tx, 'add', (origAdd) => function (req) { try { if (req && typeof req.cb === 'function') { const action = req.action; const origCb = req.cb; req.cb = function (resp) { // Save good data to cache if (resp && resp.result === 'OK') { if (isDevicesAction(action)) cache.devices = resp; if (isNetConnsAction(action)) cache.connections = resp; } // Fix bad responses if (!resp || resp.result !== 'OK') { if (isDevices3Action(action)) { warn('JNAP tx cb intercepted, faking OK for', action, resp); resp = buildDevicesFallback(); } else if (isNetConns2Action(action)) { warn('JNAP tx cb intercepted, faking OK for', action, resp); resp = buildNetConnsFallback(); } } try { origCb(resp); } catch (e) { warn('req.cb threw', e); } }; } } catch (e) { warn('tx.add wrap error', e); } return origAdd.call(this, req); }); // Ensure onComplete always has OK if (opts && typeof opts.onComplete === 'function') { const origOnComplete = opts.onComplete; opts.onComplete = function (T) { if (!T || T.result !== 'OK') { warn('onComplete intercepted, forcing OK', T); T = { result: 'OK' }; } try { origOnComplete(T); } catch (e) { warn('onComplete threw', e); } }; } return tx; }; jnap[TX_FLAG] = true; log('RAINIER.jnap.Transaction patched'); } // Watchdog loop ------------------------------------------------------- const seenDMs = new WeakSet(); function tick() { const R = window.RAINIER; if (!R) return; patchJnap(R.jnap); const dm = R.deviceManager; if (dm && !seenDMs.has(dm)) { seenDMs.add(dm); patchDeviceManager(dm); } } let n = 0; const fast = setInterval(() => { n++; tick(); if (n > 400) { clearInterval(fast); setInterval(tick, 1000); } }, 25); document.addEventListener('DOMContentLoaded', tick, { once: true }); })(); /* --------------------------------------------------------------------- * Low-level XHR patch: cache short-circuit + in-flight de-dupe * -------------------------------------------------------------------*/ (function () { console.log('[RAINIER-PATCH] swapped out XMLHTTPReqeuest'); // Use the same cache as the first IIFE const cache = window.__RAINIER_CACHE || { devices: null, connections: null }; const good = { devices: null, conns: null }; // keep for backwards compat in this block const lastRev = { devices: -1, conns: -1 }; const inflight = new Map(); // bodyString -> [xhr1, xhr2, ...] const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; window.__RAINIER_FIX = { hasRanUIFix: false, fixUI: () => { console.log('[RAINIER-PATCH] Inited UI manually'); // pulls in items for menu window.RAINIER.connect.AppletManager.initialize(() => _) // hides loading spinner, only after request is done window.RAINIER.ui.init() // builds menu items window.RAINIER.ui.MainMenu.initialize() // loads in dashboard widgets window.RAINIER.connect.widgetManager.setupWidgets() // gets top menu kinda working window.RAINIER.ui.TopMenu.initialize() } }; const CACHEABLE = /GetDevices(?:\d+)?$|GetNetworkConnections(?:\d+)?$/i; function cacheKeyForAction(action) { if (/GetDevices/i.test(action)) return 'devices'; if (/GetNetworkConnections/i.test(action)) return 'connections'; return null; } function getSinceRev(req) { return (req && req.request && typeof req.request.sinceRevision === 'number') ? req.request.sinceRevision : 0; } function respondFromCache(xhr, payload) { setTimeout(() => { try { overwriteResponse(xhr, payload); if (xhr.readyState !== 4) { try { Object.defineProperty(xhr, 'readyState', { value: 4 }); } catch {} xhr.dispatchEvent(new Event('readystatechange')); } xhr.dispatchEvent(new Event('load')); xhr.dispatchEvent(new Event('loadend')); console.log('[RAINIER-PATCH] Served JNAP batch from cache'); } catch (e) { console.warn('[RAINIER-PATCH] respondFromCache failed', e); } }, 0); } XMLHttpRequest.prototype.open = function (m, url) { this.__isJnap = /\/jnap\//i.test(url); this.__url = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { if (!this.__isJnap) return origSend.apply(this, arguments); // Try to parse outgoing batch let batch; try { batch = JSON.parse(body); } catch {} const isBatch = Array.isArray(batch) && batch.every(o => o && o.action); if (isBatch) { // 1) Try to satisfy entirely from cache const allCacheable = batch.every(req => CACHEABLE.test(req.action)); if (allCacheable) { const canServeAll = batch.every(req => { const key = cacheKeyForAction(req.action); if (!key) return false; const cached = cache[key]; if (!cached) return false; const sr = getSinceRev(req); const cachedRev = cached.output?.revision ?? lastRev[key] ?? -1; return typeof cachedRev === 'number' && cachedRev >= sr; }); if (canServeAll) { const responses = batch.map(req => { const key = cacheKeyForAction(req.action); return cache[key]; }); return respondFromCache(this, { result: 'OK', responses }); } } // 2) Rewrite sinceRevision=0 to our last known revision to avoid heavy dumps let modified = false; for (const req of batch) { const key = cacheKeyForAction(req.action); if (!key) continue; const cached = cache[key]; const cachedRev = cached?.output?.revision ?? lastRev[key]; if (!req.request) req.request = {}; if (typeof req.request.sinceRevision !== 'number') req.request.sinceRevision = 0; if (req.request.sinceRevision === 0 && typeof cachedRev === 'number' && cachedRev > 0) { req.request.sinceRevision = cachedRev; modified = true; } } if (modified) { body = JSON.stringify(batch); } // 3) In-flight de-dupe const bodyKey = body; if (inflight.has(bodyKey)) { inflight.get(bodyKey).push(this); this.__piggyback__ = true; } else { inflight.set(bodyKey, [this]); } } this.addEventListener('load', function () { // Only the "leader" will run this block (piggybackers will be replayed) try { const raw = this.responseText; let data; try { data = JSON.parse(raw); } catch (e) { return; } // JNAP batch => {result, responses:[...]} if (data && data.result === 'OK' && Array.isArray(data.responses) && isBatch) { data.responses.forEach((r, i) => { const action = batch[i]?.action || ''; if (r.result === 'OK') { if (/GetDevices$/i.test(action)) { good.devices = r; cache.devices = r; if (typeof r.output?.revision === 'number') lastRev.devices = r.output.revision; } if (/GetNetworkConnections$/i.test(action)) { good.conns = r; cache.connections = r; // no explicit revision in connections response usually } } else { if (/GetDevices3$/i.test(action)) data.responses[i] = good.devices || fakeDevices(); if (/GetNetworkConnections2$/i.test(action)) data.responses[i] = good.conns || fakeConns(); } }); // If we mutated the payload, overwrite if (JSON.stringify(data) !== raw) { console.log('[RAINIER-PATCH] Overwrote broken request. Old:', raw, 'New:', data); overwriteResponse(this, data); if (!window.__RAINIER_FIX.hasRanUIFix) { window.__RAINIER_FIX.hasRanUIFix = true; // give a sec for their garbage to figure itself out setTimeout(window.__RAINIER_FIX.fixUI, 1000); } } } } catch (e) { console.warn('[XHR-PATCH] parse/patch failed', e); } finally { // Release piggybackers if (isBatch) { const bodyKey = typeof body === 'string' ? body : JSON.stringify(batch); const waiters = inflight.get(bodyKey) || []; inflight.delete(bodyKey); if (waiters.length > 1) { for (const x of waiters) { if (x === this) continue; // leader already handled try { overwriteResponse(x, JSON.parse(this.responseText)); if (x.readyState !== 4) { Object.defineProperty(x, 'readyState', { value: 4 }); x.dispatchEvent(new Event('readystatechange')); } x.dispatchEvent(new Event('load')); x.dispatchEvent(new Event('loadend')); } catch (e) { console.warn('piggyback replay failed', e); } } } } } }); return origSend.apply(this, arguments); }; function overwriteResponse(xhr, obj) { const text = JSON.stringify(obj); Object.defineProperty(xhr, 'responseText', { value: text }); Object.defineProperty(xhr, 'response', { value: text }); } function fakeDevices() { return { result: 'OK', output: { revision: Date.now(), devices: [], deletedDeviceIDs: [] } }; } function fakeConns() { return { result: 'OK', output: { connections: [] } }; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址