🎁 Sync 2026-10-05 11:45:22

This commit is contained in:
github-actions[bot] committed 2026-10-05 11:45:22 +08:00
1 parent 41b75a01bd
commit 6f0e562fa5
7 files changed
+250 -24

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ LUCI_DEPENDS:= \
PKG_NAME:=luci-app-homeproxy-pro
PKG_VERSION:=28.10.1.14
PKG_RELEASE:=20
PKG_RELEASE:=21
LUCI_BASENAME:=homeproxy-pro
@@ -316,14 +316,31 @@ return view.extend({
s = m.section(form.NamedSection, 'config', 'homeproxy-pro', _('Diagnostic report'));
s.anonymous = true;
o = s.option(form.DummyValue, '_debug_report');
o.rawhtml = true;
o.render = function() {
return E('div', { 'class': 'cbi-value' }, [
E('p', {}, _('Collects system, dependency, routing, firewall and configuration state into one file. Credentials are masked by option name; public addresses and LAN topology are not. Read it before posting it anywhere.')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, () => {
/* The button belongs on the title line, not parked under the
* description where it reads as part of the paragraph.
*
* It cannot simply be passed in as part of the title: form.js builds
* the heading with `E('h3', {}, this.title)` behind a
* `typeof(this.title) == 'string'` guard, so a node here is dropped
* and the heading disappears entirely. The section's own render() is
* the seam instead - it runs on every re-render and resolves with the
* finished section element, so the button goes into the h3 that
* render() has just built. Nothing of LuCI's own rendering is
* reproduced, which is what keeps this from silently drifting when
* form.js changes. */
const reportButton = () => E('button', {
/* The same class triple as the "Clean log" button above, on
* purpose: `btn` is what carries the button's font-size, so
* matching it keeps the two the same size without this view
* hard-coding a size of its own that the theme can change
* under it. */
'class': 'btn cbi-button cbi-button-action',
'style': 'margin-left: 8px;',
/* createHandlerFn(ctx, fn) only uses ctx to resolve a string
* method name and as the `this` for the call; fn is an arrow
* function, so ctx is inert here. The section is passed anyway
* so the binding reads like the one it replaced. */
'click': ui.createHandlerFn(s, () => {
return hp.rpcCall('debug_report', [], { expect: { '': {} } })
.then((res) => {
if (!res.result)
@@ -341,7 +358,26 @@ return view.extend({
window.URL.revokeObjectURL(url);
});
})
}, [ _('Generate and download') ])
}, [ _('Generate and download') ]);
const renderDiagnosticSection = s.render.bind(s);
s.render = function() {
return renderDiagnosticSection().then((el) => {
/* The gap before the button is the stylesheet's
* `margin-left`, not a text node - a spacer node would drag
* `document` into the test environment for nothing. */
const heading = el.querySelector('h3');
if (heading)
dom.append(heading, reportButton());
return el;
});
};
o = s.option(form.DummyValue, '_debug_report');
o.rawhtml = true;
o.render = function() {
return E('div', { 'class': 'cbi-value' }, [
E('p', {}, _('Collects system, dependency, routing, firewall and configuration state into one file. Credentials are masked by option name; public addresses and LAN topology are not. Read it before posting it anywhere.'))
]);
};
@@ -230,6 +230,38 @@ hp_prepare_ruleset_dir() {
|| log "Warning: failed to create ${hp_dir}/ruleset."
}
# hp_rule_set_current <source> <output>
# 0 when <output> is already the up-to-date rendering of <source>, 1 when it
# has to be generated.
#
# Generating china-domain.json measured 3540 ms on the x86-64 target
# (111,353 suffixes, 2.2 MB), and the source lists are replaced at most daily
# by update_resources.sh. Regenerating unconditionally therefore spent a
# third of every reload - every Save & Apply, every node switch - rebuilding a
# byte-identical file, which is the single largest cost in a reload.
#
# The three conditions below are exactly the cases the unconditional version
# had to cover:
#
# [ -f "$out" ] a fresh install predates the file, or a previous run
# produced nothing
# [ -s "$out" ] the previous run was interrupted after truncating it; an
# empty rule-set would make the route-side set match nothing,
# which is the silent inversion this whole file exists to
# prevent
# [ "$out" -nt "$src" ] the list was replaced while the service was stopped
#
# A newer-but-corrupt output is deliberately NOT detected here. It does not
# need to be: the generated config names the file, a rule-set sing-box cannot
# parse fails the start, and the health gate rolls back to the known-good
# configuration. The check is cheap (<1 ms) and cannot mask a real failure -
# it can only skip work that would rebuild identical bytes.
hp_rule_set_current() {
local src="$1" out="$2"
[ -f "$out" ] && [ -s "$out" ] && [ "$out" -nt "$src" ]
}
# hp_prepare_runtime_files <hp-dir> <run-dir> <routing-mode> <client> <server>
# Create the mode-specific working files, truncate the instance logs and hand
# every runtime file to the sing-box user. <client>/<server> are "1"/"0".
@@ -249,7 +281,12 @@ hp_prepare_runtime_files() {
# file and sing-box would refuse to start. A missing source list is not
# an error - the generator then emits no route-side rule at all.
if [ "$client_enabled" = "1" ] && [ -f "$hp_dir/resources/china_ip4.txt" ]; then
if ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
if hp_rule_set_current "$hp_dir/resources/china_ip4.txt" "$hp_dir/resources/china_ip4.json"; then
# Unchanged source: keep the file, but re-assert ownership anyway -
# chown costs nothing and survives an install that reset it.
chown sing-box:sing-box "$hp_dir/resources/china_ip4.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip4.json to sing-box."
elif ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
"$hp_dir/resources/china_ip4.txt" "$hp_dir/resources/china_ip4.json" >>"$LOG_PATH" 2>&1; then
chown sing-box:sing-box "$hp_dir/resources/china_ip4.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip4.json to sing-box."
@@ -275,7 +312,10 @@ hp_prepare_runtime_files() {
cn_ipv6="$(grep -cE '^[0-9a-fA-F:]+:[0-9a-fA-F:]*(/[0-9]{1,3})?[[:space:]]*$' \
"$hp_dir/resources/china_ip6.txt" 2>"/dev/null")"
[ -n "$cn_ipv6" ] || cn_ipv6=0
if [ "$cn_ipv6" -gt 0 ] && ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
if [ "$cn_ipv6" -gt 0 ] && hp_rule_set_current "$hp_dir/resources/china_ip6.txt" "$hp_dir/resources/china_ip6.json"; then
chown sing-box:sing-box "$hp_dir/resources/china_ip6.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip6.json to sing-box."
elif [ "$cn_ipv6" -gt 0 ] && ucode -S "$hp_dir/scripts/runtime/china_ip_ruleset.uc" \
"$hp_dir/resources/china_ip6.txt" "$hp_dir/resources/china_ip6.json" >>"$LOG_PATH" 2>&1; then
chown sing-box:sing-box "$hp_dir/resources/china_ip6.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china_ip6.json to sing-box."
@@ -304,7 +344,10 @@ hp_prepare_runtime_files() {
# DNS-side rule for it, and the split degrades to the resolver default
# rather than failing the start.
if [ "$client_enabled" = "1" ] && [ -f "$hp_dir/resources/china_list.txt" ]; then
if ucode -S "$hp_dir/scripts/runtime/domain_ruleset.uc" \
if hp_rule_set_current "$hp_dir/resources/china_list.txt" "$hp_dir/resources/china-domain.json"; then
chown sing-box:sing-box "$hp_dir/resources/china-domain.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china-domain.json to sing-box."
elif ucode -S "$hp_dir/scripts/runtime/domain_ruleset.uc" \
"$hp_dir/resources/china_list.txt" "$hp_dir/resources/china-domain.json" >>"$LOG_PATH" 2>&1; then
chown sing-box:sing-box "$hp_dir/resources/china-domain.json" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/resources/china-domain.json to sing-box."
@@ -83,11 +83,23 @@ class MockOption {
}
class MockSection {
constructor(kind, name) {
constructor(kind, name, title) {
this.__kind = kind;
this.__name = name;
this.title = title;
this.options = [];
}
/* form.js NamedSection.render() resolves with the finished section
* element, and status.js appends the report button into the h3 that
* render() built - the title itself cannot carry it, because form.js
* guards the heading with `typeof(this.title) == 'string'`. The mock
* reproduces exactly that much: a section element holding an h3. */
render() {
return Promise.resolve(E('div', {}, [
this.title != null && this.title !== '' ? E('h3', {}, this.title) : '',
...this.options.map((o) => E('div', { 'class': 'cbi-section-node' }, []))
]));
}
tab() {}
taboption(_tab, ...args) { return this.option(...args); }
option(kind, name, title, description) {
@@ -120,8 +132,8 @@ function makeForm() {
form.Map = class {
constructor(title) { this.title = title; this.sections = []; }
section(kind, name) {
const section = new MockSection(kind && kind.__name__ ? kind.__name__ : String(kind), name);
section(kind, name, title) {
const section = new MockSection(kind && kind.__name__ ? kind.__name__ : String(kind), name, title);
this.sections.push(section);
return section;
}
@@ -268,18 +280,29 @@ function testAccessDomainLists() {
* 3. status.js: each log view keeps its own basename
* ====================================================================== */
function E(tag, attrs, children) {
if (Array.isArray(tag) && attrs === undefined)
return { tag: null, attrs: {}, children: tag, appendChild() {} };
/* A node carries the two DOM operations a view can perform on an element it
* did not create itself: appendChild, and a tag lookup. The log views never
* need them - they build their own tree - but status.js reaches into the
* element the form layer returns in order to put the report button into the
* heading, so the mock has to offer the same two. findTag is a hoisted
* function declaration, so it is in scope here. */
function mockNode(tag, attrs, children) {
return {
tag,
attrs: attrs || {},
children: children === undefined ? [] : (Array.isArray(children) ? children : [ children ]),
appendChild() {}
appendChild(child) { this.children.push(child); return child; },
querySelector(t) { return findTag(this, t); }
};
}
function E(tag, attrs, children) {
if (Array.isArray(tag) && attrs === undefined)
return mockNode(null, {}, tag);
return mockNode(tag, attrs, children);
}
function findClickable(node) {
if (!node || typeof node !== 'object')
return null;
@@ -314,7 +337,13 @@ function testLogViewWiring() {
path.join(root, 'htdocs/luci-static/resources/view/homeproxy-pro/status.js'),
{
baseclass: {},
dom: { content: () => {} },
dom: {
content: () => {},
append: (parent, ...children) => {
for (const child of children)
parent.appendChild(child);
}
},
form,
fs: { read_direct: (p) => { reads.push(p); return Promise.resolve('log line'); } },
poll: { add: () => {} },
@@ -380,8 +409,36 @@ function testLogViewWiring() {
check('status.js: all three log views rendered', rendered === 3, String(rendered));
check('status.js: the log poll was registered', pollHandlers !== null && typeof pollHandlers.read === 'function');
/* --- the report button lives on the title line -----------------------
*
* form.js builds the heading as `E('h3', {}, this.title)` behind a
* `typeof(this.title) == 'string'` guard, so the button cannot be part of
* the title. status.js overrides the section's render() and appends the
* button into the h3 that render() already built. Assert on the h3's own
* subtree: a check that walked the whole section would still pass if the
* button fell back below the description, which is the exact regression
* this replaced. */
const report_section = map.sections.find((s) =>
s.options.some((o) => o.__name === '_debug_report'));
check('status.js: the report section exists', report_section !== null,
String(map.sections.map((s) => s.__name).join(',')));
return Promise.resolve(report_section ? report_section.render() : null).then((el) => {
const heading = el ? el.querySelector('h3') : null;
check('status.js: the report section renders a heading', heading !== null, String(el));
const in_heading = heading ? findClickable(heading) : null;
check('status.js: the download button sits inside the heading',
in_heading !== null, 'no clickable node under the h3');
/* And it must be gone from the body, or the page shows two of them. */
const body = findOption(report_section, '_debug_report');
const body_tree = body.render(0, 'cfg01', false);
check('status.js: the report body no longer holds the button',
findClickable(body_tree) === null, 'a button is still in the description block');
if (!pollHandlers)
return Promise.resolve();
return;
/* One tick reads one file per registered target, keyed by basename. */
return Promise.resolve(pollHandlers.read()).then(() => {
@@ -393,6 +450,7 @@ function testLogViewWiring() {
].sort()),
JSON.stringify(reads));
});
});
}
testNodesValidator();
@@ -862,6 +862,63 @@ else
echo "PASS: the dnsmasq snippets are gone after the client is switched off"
fi
# --- the rule-set mtime gate -------------------------------------------
#
# Regenerating china-domain.json measured 3540 ms on the target and the
# source lists are replaced at most daily, so hp_prepare_runtime_files skips
# the rebuild when the output is already up to date. Asserted against the
# function rather than through a scenario: every scenario in this file runs
# against a sandbox that has no pre-existing rule-set, so the skip branch is
# unreachable from a scenario and the regression it guards would be silent.
#
# The three conditions that must still rebuild are exactly the three the
# unconditional version existed for - a fresh install, a truncated previous
# run, and a list replaced while the service was stopped. All four cases are
# checked, because a gate that skips too eagerly is worse than the 3.5s it
# saves: it would keep a rule-set the source has moved past.
# `.` is a POSIX special builtin: sourcing a missing file terminates a
# non-interactive shell outright, and `||` does not get a chance to run. So
# this names the real path and never guards it.
. "$ROOT/root/etc/homeproxy-pro/scripts/runtime/service.sh"
hp_rs_dir="$(mktemp -d "${TMPDIR:-/tmp}/hp-rule-set.XXXXXX")"
hp_rs_src="$hp_rs_dir/src.txt"
hp_rs_out="$hp_rs_dir/out.json"
printf 'a.cn\n' > "$hp_rs_src"
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
echo "FAIL: a missing rule-set counted as current, so a fresh install would never get one"
exit 1
else
echo "PASS: a missing rule-set is regenerated"
fi
: > "$hp_rs_out"
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
echo "FAIL: an empty rule-set counted as current; it would match nothing and invert the split"
exit 1
else
echo "PASS: an empty rule-set is regenerated"
fi
printf '{"rules":[]}\n' > "$hp_rs_out"; sleep 1; touch "$hp_rs_src"
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
echo "FAIL: a rule-set older than its source counted as current"
exit 1
else
echo "PASS: a rule-set older than its source is regenerated"
fi
sleep 1; touch "$hp_rs_out"
if hp_rule_set_current "$hp_rs_src" "$hp_rs_out"; then
echo "PASS: an up-to-date rule-set is kept (the reload skip still works)"
else
echo "FAIL: an up-to-date rule-set would be rebuilt; the reload-time skip regressed"
exit 1
fi
rm -rf "$hp_rs_dir"
if cmp -s "$GOLDEN" "$TRACE.norm"; then
echo "PASS: runtime orchestration matches $(basename "$GOLDEN")"
# Counted from the golden rather than hardcoded: a scenario added without
+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=uugamebooster
PKG_VERSION:=14.6.24
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(ARCH).tar.gz
PKG_SOURCE_URL:=https://uurouter.gdl.netease.com/uuplugin/openwrt-$(ARCH)/v$(PKG_VERSION)/uu.tar.gz?
+32
View File
@@ -9,6 +9,18 @@ PROG="/usr/bin/uugamebooster"
UU_CONF="/usr/share/uugamebooster/uu.conf"
firewall_rule() {
json_add_object ""
json_add_string type rule
json_add_string src '*'
[ "$1" = forward ] && json_add_string dest '*'
json_add_string device tun163
json_add_string direction "$2"
json_add_string proto all
json_add_string target ACCEPT
json_close_object
}
start_service() {
config_load "$CONF"
local enabled
@@ -24,9 +36,29 @@ start_service() {
procd_set_param stdout 1
procd_set_param stderr 1
# UU's own nftables ACCEPT rules cannot bypass fw4's later base chains.
# Publish tunnel rules through procd so fw4 owns their full lifecycle.
if [ -x /sbin/fw4 ]; then
procd_open_data
json_add_array firewall
firewall_rule input in
firewall_rule forward in
firewall_rule forward out
json_close_array
procd_close_data
fi
procd_close_instance
}
service_started() {
[ ! -x /sbin/fw4 ] || procd_set_config_changed firewall
}
service_stopped() {
[ ! -x /sbin/fw4 ] || procd_set_config_changed firewall
}
reload_service() {
stop
start