mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:33:56 +08:00
⛅ Sync 2026-09-29 21:45:00
This commit is contained in:
1 parent
a4d6e3e4b9
commit
da77daf88e
191 files changed
+8077
-6247
No files matched your search
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Pure subscription decoder. The fetcher returns raw bytes
|
||||
* and the orchestrator used to inline the JSON / base64 / SIP008
|
||||
* handling. Pulling it out gives the orchestrator a single
|
||||
* `decode(content, log, url)` call and lets us unit-test each input
|
||||
* shape without involving the network or UCI.
|
||||
*
|
||||
* Three input shapes are recognised, in order:
|
||||
* 1. JSON with a top-level 'servers' array (clash proxy-provider)
|
||||
* 2. JSON array directly (the same data, just unwrapped)
|
||||
* 3. Shadowsocks SIP008 JSON: top-level array of objects with
|
||||
* server+method. Each entry is tagged nodetype='sip008' so the
|
||||
* orchestrator can dispatch them differently downstream.
|
||||
*
|
||||
* On JSON failure, decode the content as base64 and split on '\n'
|
||||
* to get one share-link URI per line (sing-box subscription format).
|
||||
*
|
||||
* On total failure (no JSON, no base64), return an empty list and
|
||||
* let the caller log the empty result.
|
||||
*
|
||||
* `url` is optional and only used to enrich the parse-failed log
|
||||
* line; the decoder itself never makes a network call.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty, decodeBase64Str, redactUrl } from '../homeproxy-pro.uc';
|
||||
|
||||
export function decode(content, log, url) {
|
||||
if (isEmpty(content))
|
||||
return [];
|
||||
|
||||
let nodes;
|
||||
try {
|
||||
const parsed = json(content);
|
||||
nodes = parsed.servers || parsed;
|
||||
|
||||
/* Shadowsocks SIP008: each entry is a JSON object with
|
||||
* server + method, not a URI string.
|
||||
*
|
||||
* The probe has to check the type first. It used to be a bare
|
||||
* `nodes[0].server`, which THROWS in ucode when nodes[0] is a
|
||||
* string ("left-hand side expression is not an array or
|
||||
* object") - and the throw was swallowed by the JSON
|
||||
* try/catch below, so a perfectly valid `{"servers":["vless://…"]}`
|
||||
* or `["vless://…"]` subscription fell through to the base64
|
||||
* fallback, failed there too and decoded to an empty list. Two
|
||||
* real subscription shapes were silently discarded, and the
|
||||
* decoder test had recorded that as the contract. The same
|
||||
* guard applies inside the loop so a mixed list (SIP008 objects
|
||||
* plus share links) keeps both halves instead of losing
|
||||
* everything on the first string. */
|
||||
if (type(nodes[0]) === 'object' && nodes[0].server && nodes[0].method) {
|
||||
for (let i = 0; i < length(nodes); i++)
|
||||
if (type(nodes[i]) === 'object')
|
||||
nodes[i].nodetype = 'sip008';
|
||||
}
|
||||
} catch (e) {
|
||||
/* The URL carries the subscription token and this line lands in
|
||||
* homeproxy-pro.log, which is world-readable. fetcher.uc and the
|
||||
* orchestrator both redact; this path was missed. */
|
||||
const tag = url ? sprintf('for %s, ', redactUrl(url)) : '';
|
||||
log(sprintf('JSON parse failed %strying base64: %s', tag, e.message));
|
||||
const decoded = decodeBase64Str(content);
|
||||
nodes = decoded ? split(trim(decoded), '\n') : [];
|
||||
}
|
||||
|
||||
return nodes;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Thin wrapper around homeproxy-pro's wGETVerbose that logs
|
||||
* the failure and returns a flat { content, error } shape. The
|
||||
* orchestrator used to inline the empty-content check + log
|
||||
* message right next to the call, which made the loop body hard
|
||||
* to read and conflated the fetch's success/failure decision with
|
||||
* the downstream decode step.
|
||||
*
|
||||
* On success: returns { content: <string>, error: null }.
|
||||
* On failure: returns { content: null, error: <string> }, having
|
||||
* already logged the failure with the URL. The caller can keep
|
||||
* the loop flat: 'if (result.content === null) continue;'.
|
||||
*
|
||||
* No caching, no retry, no backoff - the orchestrator decides
|
||||
* those if it ever needs them. The Fetcher is a single HTTP GET
|
||||
* + error log, nothing more.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty, redactUrl, wGETVerbose } from '../homeproxy-pro.uc';
|
||||
|
||||
export function fetch(url, user_agent, log) {
|
||||
const result = wGETVerbose(url, user_agent);
|
||||
if (isEmpty(result.content)) {
|
||||
/* Redact the URL before logging: the query string is the
|
||||
* subscription token and homeproxy-pro.log is world-readable on
|
||||
* a default install. The original URL goes back to the caller
|
||||
* via result.error so error propagation still works. */
|
||||
log(sprintf('Failed to fetch resources from %s: %s',
|
||||
redactUrl(url), result.error || 'empty response'));
|
||||
return { content: null, error: result.error || 'empty response' };
|
||||
}
|
||||
return { content: result.content, error: null };
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Pure subscription filter. The orchestrator
|
||||
* (update_subscriptions.uc) used to inline both the keyword
|
||||
* whitelist/blacklist check and the post-parse policy application
|
||||
* (tls.insecure override, protocol_options.packet_encoding for
|
||||
* vless/vmess). They live here so the orchestrator stays focused on
|
||||
* the fetch/cache/repository flow and the pure logic is unit-testable
|
||||
* without UCI access.
|
||||
*
|
||||
* Neither function touches module state: mode / keywords / opts
|
||||
* come in as arguments, and a logger is passed in for the
|
||||
* invalid-regex warning. That makes the module callable from a
|
||||
* plain test stub and means the orchestrator can keep its own
|
||||
* file-writer log() around the call.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { isEmpty } from '../homeproxy-pro.uc';
|
||||
|
||||
/* Decide whether a node named `name` should be dropped. `mode` is
|
||||
* one of:
|
||||
* 'disabled' - filter is off, return false (keep)
|
||||
* 'blacklist' - drop if any keyword regex matches
|
||||
* 'whitelist' - drop unless any keyword regex matches
|
||||
*
|
||||
* `keywords` is a list of strings; each is compiled as a regexp.
|
||||
* One bad regex is logged and skipped, not treated as fatal - this
|
||||
* matches the pre-B1 behaviour, where a stray `[` would not block
|
||||
* the whole subscription.
|
||||
*
|
||||
* Returns true when the node should be skipped. */
|
||||
export function check(name, mode, keywords, log) {
|
||||
if (isEmpty(name) || mode === 'disabled' || isEmpty(keywords))
|
||||
return false;
|
||||
|
||||
let matched = false;
|
||||
for (let i in keywords) {
|
||||
let patten;
|
||||
try {
|
||||
patten = regexp(i);
|
||||
} catch (e) {
|
||||
log(sprintf('Skipping invalid filter keyword regex: %s.', i));
|
||||
continue;
|
||||
}
|
||||
if (patten && match(name, patten))
|
||||
matched = true;
|
||||
}
|
||||
if (mode === 'whitelist')
|
||||
matched = !matched;
|
||||
|
||||
return matched;
|
||||
};
|
||||
|
||||
/* Apply the two policy tweaks the orchestrator used to do inline. The subject
|
||||
* is a canonical Node - the same shape parser/normalize.uc produces and
|
||||
* parser/flatten.uc turns back into UCI keys - not the parser's flat
|
||||
* UCI-key dict:
|
||||
*
|
||||
* - tls.insecure is set when the node has TLS enabled and the user opted in
|
||||
* via subscription.allow_insecure='1'
|
||||
* - protocol_options.packet_encoding is set on vless/vmess nodes to the
|
||||
* subscription's default
|
||||
*
|
||||
* This used to run on the flat dict, which is why the orchestrator applied
|
||||
* policy on one representation and handed a different one to the Repository.
|
||||
* The canonical Node is now the only business object between the parser and
|
||||
* the Repository, and this was the last step still forcing the flat shape.
|
||||
* The tweaks are a property of the subscription, not of the parsed link, so
|
||||
* they deliberately do not take part in the duplicate fingerprint - which is
|
||||
* computed from the parser output before this runs.
|
||||
*
|
||||
* Mutates and returns the node; the return value is the same object as the
|
||||
* input, so callers can chain. */
|
||||
export function apply_policy(node, opts) {
|
||||
if (!node)
|
||||
return node;
|
||||
|
||||
if (node.tls && node.tls.enabled === '1' && opts.allow_insecure === '1')
|
||||
node.tls.insecure = '1';
|
||||
|
||||
if (node.type in ['vless', 'vmess']) {
|
||||
/* normalize() always builds protocol_options; a hand-made node
|
||||
* (a test fixture, a future caller) may not have it, and the flat
|
||||
* version simply created the key. */
|
||||
if (!node.protocol_options)
|
||||
node.protocol_options = {};
|
||||
|
||||
node.protocol_options.packet_encoding = opts.packet_encoding;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Every UCI write the
|
||||
* subscription pipeline performs now lives in this module. Three
|
||||
* entry points cover what used to be six uci.set+commit sites in
|
||||
* update_subscriptions.uc plus the existing node add/update/remove
|
||||
* walk:
|
||||
*
|
||||
* apply_nodes - the add / update / remove walk
|
||||
* for the nodes the subscription
|
||||
* fetch returned this run.
|
||||
* apply_main_node_refs - main_node / main_udp_node cleanup:
|
||||
* remove stale urltest members, switch
|
||||
* main_node if its target is gone,
|
||||
* reset both to 'nil' when the
|
||||
* subscription has no nodes.
|
||||
* scrub_stale_urltest_refs - one-shot pass over every UCI
|
||||
* routing_node that purges dead
|
||||
* urltest_nodes entries left behind by
|
||||
* nodes that have since been removed.
|
||||
*
|
||||
* Writes are driven by canonical Node objects (post- parser/normalize).
|
||||
* UCI is flat, so apply_nodes expands each node with parser/flatten.uc -
|
||||
* and that call is the module's only canonical -> UCI boundary: callers
|
||||
* hand this module canonical Nodes, never flat dicts, and every UCI key
|
||||
* name comes from parser/mapping.uc. The orchestrator
|
||||
* (update_subscriptions.uc) never writes UCI directly; it orchestrates
|
||||
* fetch + parse + normalize + filter + policy + reload and owns the one
|
||||
* commit.
|
||||
*
|
||||
* A2 (single commit): this module mutates the caller's cursor and never
|
||||
* commits. It used to commit at nine sites - once at the end of
|
||||
* apply_nodes, seven inside apply_main_node_refs, and once per scrubbed
|
||||
* routing_node - while update_subscriptions.uc's own comments called
|
||||
* that "the repository's single commit". A crash between two of them
|
||||
* left the nodes written but main_node still pointing at a deleted
|
||||
* section, and there was no single point at which the run became
|
||||
* visible. The transaction boundary is now the caller's one commit,
|
||||
* after every mutation is staged.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { md5 } from 'digest';
|
||||
|
||||
import { isEmpty } from '../homeproxy-pro.uc';
|
||||
|
||||
/* Relative, like config/loader.uc's '../parser/mapping.uc': ucode's
|
||||
* resolver only searches top-level module names in the -L tree, so a
|
||||
* bare 'parser/flatten.uc' does not resolve. */
|
||||
import { flatten } from '../parser/flatten.uc';
|
||||
|
||||
/* --- apply_nodes -------------------------------------------------------- */
|
||||
|
||||
/* Add / update / remove the subscription-owned nodes against UCI.
|
||||
*
|
||||
* node_cache - { [groupHash]: { [nameHash]: canonical_node } }
|
||||
* A lookup table keyed by subscription URL group
|
||||
* and then by the per-node name hash. The fetch
|
||||
* loop populates this for de-duplication.
|
||||
* node_result - [[canonical_node, ...]] (one inner list per URL)
|
||||
* The de-duplicated, policy-applied nodes in the
|
||||
* order they should appear in UCI.
|
||||
*
|
||||
* Behaviour parity with the previous code:
|
||||
* - user-created nodes (no grouphash) are never touched
|
||||
* - nodes whose subscription has no cache entry (network
|
||||
* failure for that URL) are left in place; we don't delete
|
||||
* a node just because today's fetch didn't return it
|
||||
* - a node in UCI but not in the cache is deleted
|
||||
* - a node in both has every field of the new config written
|
||||
* (including options the stored section did not have yet),
|
||||
* and the options the new config no longer carries are removed
|
||||
* - new nodes are added under name = md5(groupHash + label), the
|
||||
* same hash the cache uses, so a subsequent re-fetch recognises
|
||||
* them as existing
|
||||
*
|
||||
* Returns the { added, removed } counts. No commit here: the caller's
|
||||
* single commit is the boundary of "this run wrote something". */
|
||||
function apply_nodes(uci, uciconfig, ucinode, node_cache, node_result, log) {
|
||||
let added = 0, removed = 0;
|
||||
/* Mark the canonical Node objects the foreach loop above has
|
||||
* already updated in place, so the second loop skips them. We
|
||||
* use object identity (the same Node object is held in node_cache
|
||||
* and in node_result, so `seen[node] === node` works as a set),
|
||||
* not the section name or nameHash: the orchestrator hands the
|
||||
* Repository the same Node instances, and tying the marker to the
|
||||
* object means a stale string key cannot match a renamed node. */
|
||||
const seen = {};
|
||||
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* User-created nodes do not have a grouphash. The
|
||||
* repository must never touch them, even if the
|
||||
* subscription fetch returns nothing for today. */
|
||||
if (!cfg.grouphash)
|
||||
return null;
|
||||
|
||||
/* No cache entry for this subscription group means
|
||||
* the fetch failed for that URL. Leave the existing
|
||||
* nodes in place so the user does not lose them on
|
||||
* a transient network blip. */
|
||||
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
|
||||
return null;
|
||||
|
||||
const incoming = node_cache[cfg.grouphash][cfg['.name']];
|
||||
|
||||
if (!incoming) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
removed++;
|
||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Canonical Node -> flat UCI keys. flatten() drops
|
||||
* nulls so a freshly-saved canonical Node does not
|
||||
* litter /etc/config with `tls_sni ''` entries for
|
||||
* fields the parser never set; the Loader treats null
|
||||
* and absent the same, so this is observably
|
||||
* equivalent. */
|
||||
const flat = flatten(incoming);
|
||||
|
||||
/* Write every option the new config carries, including
|
||||
* ones the stored section did not have yet. Walking only
|
||||
* the OLD section's keys used to miss new options
|
||||
* (plugin, tls_sni, packet_encoding) the subscription
|
||||
* started sending after the node was first imported. */
|
||||
for (let v in keys(flat))
|
||||
uci.set(uciconfig, cfg['.name'], v, flat[v]);
|
||||
|
||||
/* Then drop the options the new config no longer
|
||||
* carries. `.name` / `.type` / `.index` are section
|
||||
* metadata, not options; deleting them is meaningless
|
||||
* at best. */
|
||||
for (let v in keys(cfg)) {
|
||||
if (substr(v, 0, 1) === '.')
|
||||
continue;
|
||||
if (!(v in flat))
|
||||
uci.delete(uciconfig, cfg['.name'], v);
|
||||
}
|
||||
|
||||
seen[incoming] = true;
|
||||
});
|
||||
|
||||
for (let nodes in node_result)
|
||||
map(nodes, (node) => {
|
||||
if (seen[node])
|
||||
return null;
|
||||
|
||||
/* normalize() exposes the source label as `name`; `label`
|
||||
* is only set when the orchestrator carries it over. Relying
|
||||
* on `label` alone hashed every node of a subscription to the
|
||||
* same section name, because undefined stringifies
|
||||
* identically - four nodes collapsed into one section mixing
|
||||
* several protocols' options. Accept either. */
|
||||
const label = node.label || node.name;
|
||||
const nameHash = md5(node.grouphash + label);
|
||||
|
||||
/* The one boundary where a canonical Node becomes UCI keys.
|
||||
*
|
||||
* UCI is a flat key/value store, so this expansion has to
|
||||
* happen somewhere; keeping it inside the Repository's write
|
||||
* path is what lets every other layer - the orchestrator, the
|
||||
* filter, the policy step - speak canonical only. If a field
|
||||
* must not reach UCI, or its UCI name must change, this call
|
||||
* and parser/mapping.uc are the places to look; no caller
|
||||
* hands this function a flat dict. */
|
||||
const flat = flatten(node);
|
||||
|
||||
uci.set(uciconfig, nameHash, 'node');
|
||||
for (let v in keys(flat))
|
||||
uci.set(uciconfig, nameHash, v, flat[v]);
|
||||
|
||||
added++;
|
||||
log(sprintf('Adding node: %s.', label));
|
||||
});
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
/* --- apply_main_node_refs ---------------------------------------------- */
|
||||
|
||||
/* Prune the entries of an UCI list that no longer point at a live
|
||||
* node. Used twice below for `main_urltest_nodes` and
|
||||
* `main_udp_urltest_nodes`; the helper writes the cleaned list back
|
||||
* only when it actually changed, and returns it for the caller to
|
||||
* inspect (an empty list is the trigger to switch to first_server). */
|
||||
function maybeScrubUrltestNodes(uci, uciconfig, ucimain, listOption, log) {
|
||||
const old_list = uci.get(uciconfig, ucimain, listOption) || [];
|
||||
const cleaned = filter(old_list, (v) => {
|
||||
if (!uci.get(uciconfig, v)) {
|
||||
log(sprintf('Node %s is gone, removing from urltest list.', v));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (length(cleaned) !== length(old_list))
|
||||
uci.set(uciconfig, ucimain, listOption, cleaned);
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/* Switch a main_* field off the dangling reference and onto the first
|
||||
* available node. The two callers below differ only in the UCI option
|
||||
* name (`main_node` vs `main_udp_node`) and the user-facing log line,
|
||||
* so this is what made the pre-refactor code mirror itself.
|
||||
*
|
||||
* `current_value` is what the field currently points at (the user-chosen
|
||||
* section name, the literal 'urltest', or 'same' / 'nil'). We only
|
||||
* overwrite the field when the section named by `current_value` no
|
||||
* longer exists in UCI; the previous version asked `uci.get(uciconfig,
|
||||
* field)` which is "does a section literally named `field` exist" - the
|
||||
* config has cfg0xxxx names, never one named `main_node`, so the check
|
||||
* was always false and the user's pick got silently replaced with
|
||||
* first_server on every subscription update.
|
||||
*
|
||||
* The new value is also pushed onto `result.log` so the orchestrator
|
||||
* can replay it (it uses `result.log` to log the same lines verbatim;
|
||||
* the homeproxy-pro.log capture goes through the separate `log` arg). */
|
||||
function switchMainNodeOffDanglingRef(uci, uciconfig, ucimain, field, current_value, first_server, result, log) {
|
||||
if (uci.get(uciconfig, current_value))
|
||||
return current_value;
|
||||
uci.set(uciconfig, ucimain, field, first_server);
|
||||
const msg = sprintf('Main%s node is gone, switching to the first node.',
|
||||
field === 'main_udp_node' ? ' UDP' : '');
|
||||
log(msg);
|
||||
push(result.log, msg);
|
||||
return first_server;
|
||||
}
|
||||
|
||||
/* Replace the 4 inline uci.set/commit sites (plus the 2-line
|
||||
* 'reset to nil' block) in update_subscriptions.uc. Logic parity:
|
||||
*
|
||||
* - if main_node is set and at least one subscription node exists:
|
||||
* * when main_node === 'urltest', scrub main_urltest_nodes down
|
||||
* to nodes that still exist, then switch main_node to the first
|
||||
* surviving one if the urltest set is empty
|
||||
* * otherwise, switch main_node to first_server if its target is
|
||||
* gone
|
||||
* * same shape for main_udp_node, only when main_udp_node is set
|
||||
* AND not 'same'
|
||||
* - if main_node is set but no subscription nodes exist, reset
|
||||
* both main_node and main_udp_node to 'nil'
|
||||
*
|
||||
* `ctx` carries the inputs the orchestrator used to read from UCI:
|
||||
* { main_node, main_udp_node, has_nodes }
|
||||
*
|
||||
* Mutates only, no commit. The orchestrator commits once after
|
||||
* apply_nodes, this method and scrub_stale_urltest_refs have all
|
||||
* staged their changes, so a run is either fully visible or not at all.
|
||||
*
|
||||
* Returns { main_node: <new>, main_udp_node: <new>, log: [..] } so
|
||||
* the orchestrator can log "Main node is gone, switching to ..." /
|
||||
* "No available node, disable tproxy." exactly as before. */
|
||||
function apply_main_node_refs(uci, uciconfig, ucimain, ucinode, ctx, log) {
|
||||
/* Object destructuring is not part of the dialect the ucode on the
|
||||
* target accepts (ImmortalWrt ucode 2026.01.16 rejects
|
||||
* `const { a } = x` with "Expecting variable name"), so pull the
|
||||
* fields out by name. tests/ucode/test_ucode_grammar.sh guards this. */
|
||||
const main_node = ctx.main_node,
|
||||
main_udp_node = ctx.main_udp_node;
|
||||
const result = { main_node, main_udp_node, log: [] };
|
||||
|
||||
if (isEmpty(main_node))
|
||||
return result;
|
||||
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
if (!first_server) {
|
||||
/* No nodes left at all: reset both to 'nil'. */
|
||||
uci.set(uciconfig, ucimain, 'main_node', 'nil');
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
|
||||
result.main_node = 'nil';
|
||||
result.main_udp_node = 'nil';
|
||||
push(result.log, 'No available node, disable tproxy.');
|
||||
return result;
|
||||
}
|
||||
|
||||
if (main_node === 'urltest') {
|
||||
const cleaned = maybeScrubUrltestNodes(uci, uciconfig, ucimain, 'main_urltest_nodes', log);
|
||||
|
||||
if (!length(cleaned))
|
||||
result.main_node = switchMainNodeOffDanglingRef(uci, uciconfig, ucimain, 'main_node', main_node, first_server, result, log);
|
||||
} else {
|
||||
result.main_node = switchMainNodeOffDanglingRef(uci, uciconfig, ucimain, 'main_node', main_node, first_server, result, log);
|
||||
}
|
||||
|
||||
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
|
||||
if (main_udp_node === 'urltest') {
|
||||
const cleaned = maybeScrubUrltestNodes(uci, uciconfig, ucimain, 'main_udp_urltest_nodes', log);
|
||||
|
||||
if (!length(cleaned))
|
||||
result.main_udp_node = switchMainNodeOffDanglingRef(uci, uciconfig, ucimain, 'main_udp_node', main_udp_node, first_server, result, log);
|
||||
} else {
|
||||
result.main_udp_node = switchMainNodeOffDanglingRef(uci, uciconfig, ucimain, 'main_udp_node', main_udp_node, first_server, result, log);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- scrub_stale_urltest_refs ----------------------------------------- */
|
||||
|
||||
/* Walk every routing_node and prune the urltest_nodes list to drop
|
||||
* entries that no longer point at a live node. Mutates only: the caller
|
||||
* commits once after this returns. Returns { changed } - the number of
|
||||
* routing_nodes it rewrote - which is what the old `commits` counter
|
||||
* measured (it counted its own commits; the previous code committed
|
||||
* inside the foreach loop, once per scrubbed routing_node). */
|
||||
function scrub_stale_urltest_refs(uci, uciconfig, log) {
|
||||
let changed = 0;
|
||||
|
||||
uci.foreach(uciconfig, 'routing_node', (cfg) => {
|
||||
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
|
||||
return null;
|
||||
|
||||
const cleaned_nodes = filter(cfg.urltest_nodes, (v) => uci.get(uciconfig, v));
|
||||
if (length(cleaned_nodes) === length(cfg.urltest_nodes))
|
||||
return null;
|
||||
|
||||
uci.set(uciconfig, cfg['.name'], 'urltest_nodes', cleaned_nodes);
|
||||
changed++;
|
||||
log(sprintf('Routing node %s: removed gone nodes from urltest list.', cfg['.name']));
|
||||
});
|
||||
|
||||
return { changed };
|
||||
}
|
||||
|
||||
export const Repository = {
|
||||
apply_nodes,
|
||||
apply_main_node_refs,
|
||||
scrub_stale_urltest_refs
|
||||
};
|
||||
Reference in new issue
Block a user