🔥 Sync 2026-09-07 20:49:02

This commit is contained in:
github-actions[bot] committed 2026-09-07 20:49:02 +08:00
1 parent c05bdbe2bc
commit 6e956beb69
160 files changed
+152126 -957

No files matched your search

+6 -13
View File
@@ -1,20 +1,10 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 luci-app-sysctl contributors
#
# LuCI application: manage kernel sysctl parameters from the web UI.
#
# Build inside OpenWrt 24.10 buildroot/SDK:
# - copy this directory to package/luci-app-sysctl (needs luci feed installed), or
# - copy to feeds/luci/applications/luci-app-sysctl and re-run feeds update/install
# then: make menuconfig -> LuCI -> Applications -> luci-app-sysctl
# make package/luci-app-sysctl/compile V=s
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-sysctl
PKG_VERSION:=1.5.9
PKG_RELEASE:=1
PKG_VERSION:=1.7.1
PKG_RELEASE:=2
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=luci-app-sysctl contributors
@@ -25,9 +15,12 @@ LUCI_DESCRIPTION:=View live kernel parameters and manage custom sysctl entries \
Supports browsing /proc/sys, searching parameters, one-click apply and \
immediate runtime activation of single parameters.
LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-ucode
LUCI_CONFFILES:=/etc/sysctl.d/99-luci-sysctl.conf
LUCI_PKGARCH:=all
define Package/luci-app-sysctl/conffiles
/etc/sysctl.d/99-luci-sysctl.conf
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
-115
View File
@@ -1,115 +0,0 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: Apache-2.0
#
# build-ipk.sh - pack luci-app-sysctl into an opkg-installable .ipk
# without requiring the OpenWrt SDK/buildroot.
#
# Usage:
# ./build-ipk.sh [output-directory]
#
# The produced .ipk targets OpenWrt 24.10 (opkg, "all" architecture):
# opkg install luci-app-sysctl_<version>-<release>_all.ipk
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PKG_DIR="$SCRIPT_DIR"
OUT_DIR="${1:-$PKG_DIR}"
# --- read package identity from the OpenWrt Makefile -----------------------
get_var() {
sed -n "s/^$1:=//p" "$PKG_DIR/Makefile" | head -1
}
PKG_NAME="$(get_var PKG_NAME)"
PKG_VERSION="$(get_var PKG_VERSION)"
PKG_RELEASE="$(get_var PKG_RELEASE)"
for v in PKG_NAME PKG_VERSION PKG_RELEASE; do
if [ -z "$(eval "echo \$$v")" ]; then
echo "ERROR: $v not found in $PKG_DIR/Makefile" >&2
exit 1
fi
done
IPK_NAME="${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk"
# --- sanity checks ----------------------------------------------------------
for f in \
"$PKG_DIR/htdocs/luci-static/resources/view/sysctl.js" \
"$PKG_DIR/root/usr/share/rpcd/ucode/luci.sysctl" \
"$PKG_DIR/root/usr/share/luci/menu.d/$PKG_NAME.json" \
"$PKG_DIR/root/usr/share/rpcd/acl.d/$PKG_NAME.json" \
"$PKG_DIR/root/etc/sysctl.d/99-luci-sysctl.conf"
do
if [ ! -f "$f" ]; then
echo "ERROR: required file missing: $f" >&2
exit 1
fi
done
# --- staging ----------------------------------------------------------------
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
DATA="$WORK/data"
CTRL="$WORK/control"
mkdir -p "$DATA" "$CTRL"
# root/ maps to /
cp -a "$PKG_DIR/root/." "$DATA/"
# htdocs/ maps to /www
mkdir -p "$DATA/www"
cp -a "$PKG_DIR/htdocs/." "$DATA/www/"
# uniform ownership/permissions; rpcd refuses world-writable ucode plugins
find "$DATA" -type d -exec chmod 755 {} +
find "$DATA" -type f -exec chmod 644 {} +
# --- control metadata ---------------------------------------------------------
cat > "$CTRL/control" <<EOF
Package: $PKG_NAME
Version: $PKG_VERSION-$PKG_RELEASE
Architecture: all
Maintainer: luci-app-sysctl contributors
Section: luci
Priority: optional
Depends: libc, luci-base, rpcd, rpcd-mod-ucode
Description: LuCI application to manage kernel sysctl parameters
View live kernel parameters and manage custom sysctl entries
in /etc/sysctl.d/99-luci-sysctl.conf from the LuCI web interface.
EOF
cat > "$CTRL/conffiles" <<'EOF'
/etc/sysctl.d/99-luci-sysctl.conf
EOF
cat > "$CTRL/postinst" <<'EOF'
#!/bin/sh
# register the new ubus object and invalidate the LuCI menu cache
if [ -x /etc/init.d/rpcd ]; then
/etc/init.d/rpcd restart
fi
rm -f /tmp/luci-indexcache* 2>/dev/null
exit 0
EOF
chmod 755 "$CTRL/postinst"
chmod 644 "$CTRL/control" "$CTRL/conffiles"
# --- archive ----------------------------------------------------------------
TAR_FLAGS=(--owner=0 --group=0 --numeric-owner)
tar "${TAR_FLAGS[@]}" -czf "$WORK/data.tar.gz" -C "$DATA" ./etc ./usr ./www
tar "${TAR_FLAGS[@]}" -czf "$WORK/control.tar.gz" -C "$CTRL" ./control ./conffiles ./postinst
printf '2.0\n' > "$WORK/debian-binary"
IPK_PATH="$OUT_DIR/$IPK_NAME"
mkdir -p "$OUT_DIR"
rm -f "$IPK_PATH"
tar -czf "$IPK_PATH" -C "$WORK" debian-binary control.tar.gz data.tar.gz
echo "OK: $IPK_PATH"
@@ -40,6 +40,7 @@ var callPresetList = rpc.declare({
var callFileView = rpc.declare({ object: 'luci.sysctl', method: 'file_view', params: [ 'path' ] });
var callFileSet = rpc.declare({ object: 'luci.sysctl', method: 'file_set', params: [ 'path', 'key', 'value', 'disabled' ] });
var callFileDelete = rpc.declare({ object: 'luci.sysctl', method: 'file_delete', params: [ 'path', 'key' ] });
var callDupCheck = rpc.declare({ object: 'luci.sysctl', method: 'dup_check', params: [ 'key' ] });
var KEY_RE = /^[A-Za-z0-9_][A-Za-z0-9_.\/-]*$/;
@@ -75,7 +76,8 @@ var LSC_CSS = [
'.lsc-root .cbi-button:hover { filter: brightness(1.06); }',
'.lsc-root h2, .lsc-root h3, .lsc-root h4, .lsc-root th, .lsc-root .lsc-badge, .lsc-root code, .lsc-root .lsc-muted, .lsc-root .cbi-section-descr, .lsc-root .lsc-card * { text-transform: none !important; }',
'.lsc-root .lsc-muted { color: #777; }',
'.lsc-root .cbi-page-actions { float: none !important; text-align: center !important; }'
'.lsc-root .cbi-page-actions { float: none !important; text-align: center !important; display: flex !important; flex-wrap: wrap; justify-content: center !important; align-items: center; gap: .5rem; }',
'.lsc-root .cbi-page-actions > br { display: none; }'
].join('\n');
return view.extend({
@@ -179,6 +181,7 @@ return view.extend({
}, _('暂无自定义参数,点击下方“添加参数”开始。'));
this.editFormBox = E('div', { 'style': 'display:none' });
this.fileViewBox = E('div', { 'style': 'display:none' });
this.dupBox = E('div', { 'style': 'display:none' });
this.applyResultBox = E('div', { 'style': 'display:none' });
this.sourceChipsBox = E('div', { 'class': 'lsc-chips' });
@@ -205,11 +208,12 @@ return view.extend({
return E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('自定义参数')),
E('div', { 'class': 'cbi-section-descr' }, descr),
this.applyResultBox,
this.editFormBox,
this.tableWrapBox,
this.customEmptyBox,
this.fileViewBox,
this.applyResultBox,
this.dupBox,
E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-add',
@@ -221,6 +225,11 @@ return view.extend({
'click': ui.createHandlerFn(this, 'applyConfig')
}, _('应用配置')),
' ',
E('button', {
'class': 'cbi-button',
'click': ui.createHandlerFn(this, 'showDupCheck')
}, _('检测重复')),
' ',
E('button', {
'class': 'cbi-button cbi-button-negative',
'click': ui.createHandlerFn(this, 'reloadList')
@@ -294,7 +303,11 @@ return view.extend({
},
showEditForm: function(entry) {
this.editing = (entry != null) ? entry : null;
/* "添加参数" button calls this with null -> use a blank new-entry
* state, otherwise renderEditForm would bail out and the form would
* never open (latent bug since v1.0, other entries always passed an
* object so it never surfaced until now). */
this.editing = (entry != null) ? entry : { isNew: true };
this.editFromBrowse = (entry != null && entry.fromBrowse === true);
this.renderEditForm();
@@ -350,6 +363,20 @@ return view.extend({
saveBtn.addEventListener('click', function() { self.saveEditForm(keyInput, valInput, applyChk, disabledChk, errBox); });
var editDupBox = E('div', { 'style': 'display:none' });
this.editDupBox = editDupBox;
keyInput.addEventListener('input', function() {
if (self.editDupTimer != null)
window.clearTimeout(self.editDupTimer);
var v = keyInput.value;
self.editDupTimer = window.setTimeout(function() {
self.refreshEditDup((v || '').trim());
}, 400);
});
dom.content(this.editFormBox, [
E('div', { 'class': 'lsc-card' }, [
E('strong', {}, title),
@@ -369,6 +396,7 @@ return view.extend({
])
]),
E('div', { 'style': 'color:#666;margin-top:4px' }, note),
editDupBox,
errBox,
E('div', { 'style': 'margin-top:8px' }, [
saveBtn, ' ',
@@ -376,10 +404,19 @@ return view.extend({
])
])
]);
if (key != '')
this.refreshEditDup(key);
},
hideEditForm: function() {
this.editing = null;
if (this.editDupTimer != null) {
window.clearTimeout(this.editDupTimer);
this.editDupTimer = null;
}
this.renderEditForm();
/* edit was started from the browse section: scroll back there so the
@@ -392,6 +429,83 @@ return view.extend({
}
},
/* Summarize where a key is defined besides the custom config, and whether
* any of those files load AFTER the custom config (=> would override it). */
summarizeDup: function(d) {
var customPath = '/etc/sysctl.d/99-luci-sysctl.conf';
var files = (this.statusData != null && this.statusData.files != null) ? this.statusData.files : [];
var paths = [];
var i;
for (i = 0; i < files.length; i++)
paths.push(files[i].path);
var customIdx = paths.indexOf(customPath);
var others = [];
var shadowed = false;
for (i = 0; i < d.entries.length; i++) {
var e = d.entries[i];
if (e.path == customPath)
continue;
others.push(e.path.replace('/etc/', '') + ' (' + e.value + ')');
if (!e.disabled && customIdx >= 0 && paths.indexOf(e.path) > customIdx)
shadowed = true;
}
return { others: others, shadowed: shadowed };
},
/* Inline warning inside the add/edit form: warn BEFORE saving that the
* key is also defined in other config files (and whether those files
* would override the custom entry). */
refreshEditDup: function(key) {
var self = this;
var box = this.editDupBox;
if (box == null)
return;
if (key == null || key == '' || !KEY_RE.test(key)) {
box.style.display = 'none';
dom.content(box, []);
return;
}
callDupCheck(key).then(function(res) {
var dups = (res != null && res.dups != null) ? res.dups : [];
if (dups.length == 0 || self.editing == null) {
box.style.display = 'none';
dom.content(box, []);
return;
}
var sum = self.summarizeDup(dups[0]);
if (sum.others.length == 0) {
box.style.display = 'none';
dom.content(box, []);
return;
}
var msg = sum.shadowed
? _('注意:该参数在自定义配置之后加载的文件中也有定义(%s),保存后会被覆盖、不会生效。请直接编辑对应文件或删除其同名条目。').format(sum.others.join('、'))
: _('该参数在其他文件中也有定义(%s)。自定义配置加载顺序靠后,保存后以这里的值为准。').format(sum.others.join('、'));
box.style.display = '';
dom.content(box, [
E('div', { 'style': (sum.shadowed ? 'color:#b3261e' : 'color:#a35a00') + ';margin-top:4px' }, msg)
]);
}).catch(function() {
box.style.display = 'none';
dom.content(box, []);
});
},
saveEditForm: function(keyInput, valInput, applyChk, disabledChk, errBox) {
var self = this;
var st = this.editing;
@@ -458,14 +572,36 @@ return view.extend({
else if (applyNow && res.applied === false)
notes.push(E('p', { 'style': 'color:#a80' }, _('参数 %s 已写入配置,但运行时校验不一致,请检查取值格式。').format(key)));
var hasWarn = (res.rename_leftover != null) || (!res.exists) || (applyNow && (res.applied === 'readonly' || res.applied === false));
var saveAutoHide = hasWarn ? 8000 : 4000;
if (notes.length == 0)
notes.push(E('p', {}, (st.fromMain && isNew) ? _('已创建覆盖条目:%s。') : _('参数 %s 已保存。').format(key)));
self.hideEditForm();
self.reloadList();
dom.content(self.applyResultBox, [ E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, notes) ]);
self.applyResultBox.style.display = '';
self.showApplyResult([ E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, notes) ], saveAutoHide);
/* post-save heads-up: same key also defined in other files? */
callDupCheck(key).then(function(dres) {
var dups = (dres != null && dres.dups != null) ? dres.dups : [];
if (dups.length == 0)
return;
var sum = self.summarizeDup(dups[0]);
if (sum.others.length == 0)
return;
var msg = sum.shadowed
? _('注意:该参数在自定义配置之后加载的文件中也有定义(%s),你的取值会被覆盖。').format(sum.others.join('、'))
: _('该参数在其他文件中也有定义(%s),自定义配置加载靠后,以你的值为准。').format(sum.others.join('、'));
notes.push(E('p', { 'style': 'color:' + (sum.shadowed ? '#b3261e' : '#a35a00') }, msg));
self.showApplyResult([ E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, notes) ], 8000);
}).catch(function() {});
}).catch(function(e) {
errBox.textContent = '';
errBox.appendChild(self.errorBox(_('保存失败:%s').format(e.message), self.backendErrorHint(e.message)));
@@ -486,6 +622,12 @@ return view.extend({
self.refreshCustomTable();
self.refreshPresetStatus();
self.refreshSourceChips();
/* re-render an open file panel so its "当前值" column reflects
* the kernel state AFTER an apply/refresh (values were stale
* otherwise, making applied settings look ineffective) */
if (self.fileViewPath != null)
self.renderFilePanel();
}).catch(function(e) {
ui.addNotification(null, E('p', {}, _('加载失败:%s').format(e.message)), 'error');
});
@@ -556,11 +698,44 @@ return view.extend({
});
},
/* Unified result-bar display: shows nodes, optional auto-hide after
* autoHideMs (0 = stay until replaced). Re-calling resets the timer. */
showApplyResult: function(nodes, autoHideMs) {
if (this.applyResultTimer != null) {
window.clearTimeout(this.applyResultTimer);
this.applyResultTimer = null;
}
dom.content(this.applyResultBox, nodes);
this.applyResultBox.style.display = '';
if (autoHideMs > 0) {
var box = this.applyResultBox;
this.applyResultTimer = window.setTimeout(function() {
box.style.display = 'none';
dom.content(box, []);
}, autoHideMs);
}
},
hideApplyResult: function() {
if (this.applyResultTimer != null) {
window.clearTimeout(this.applyResultTimer);
this.applyResultTimer = null;
}
this.applyResultBox.style.display = 'none';
dom.content(this.applyResultBox, []);
},
applyConfig: function() {
var self = this;
dom.content(this.applyResultBox, [ E('p', { 'style': 'color:#777;padding:6px' }, _('正在应用全部配置,请稍候…')) ]);
this.applyResultBox.style.display = '';
this.showApplyResult([ E('p', { 'style': 'color:#777;padding:6px' }, _('正在应用全部配置,请稍候…')) ], 0);
if (this.applyResultBox.scrollIntoView)
this.applyResultBox.scrollIntoView({ block: 'nearest' });
return callApply().then(function(res) {
var errors = (res != null && res.errors != null) ? res.errors : [];
@@ -571,28 +746,55 @@ return view.extend({
}
else if (errors.length == 0) {
content = E('div', { 'class': 'alert-message', 'style': 'margin:6px 0' }, [
E('p', {}, _('全部配置已成功应用。'))
E('p', {}, _('全部配置已成功应用。')),
E('p', { 'style': 'color:#777;font-size:12px;margin:2px 0 0' }, _('(本提示将在数秒后自动消失)'))
]);
}
else {
var items = [];
for (var i = 0; i < errors.length; i++)
items.push(E('li', {}, [
E('strong', {}, errors[i].file),
E('pre', { 'style': 'white-space:pre-wrap;margin:4px 0' }, errors[i].output || '')
]));
for (var i = 0; i < errors.length; i++) {
var er = errors[i];
var parts = [ E('strong', {}, er.file) ];
/* real command errors (permission denied etc.) */
if (er.output)
parts.push(E('pre', { 'style': 'white-space:pre-wrap;margin:4px 0' }, er.output));
/* kernel did not accept these values (echo was silent,
* but read-back differs) */
if (er.not_applied != null && er.not_applied.length > 0) {
var naItems = [];
for (var j = 0; j < er.not_applied.length; j++) {
var na = er.not_applied[j];
naItems.push(E('li', {}, _('%s:写入 %s,内核当前 %s').format(
na.key, na.value, na.current)));
}
parts.push(E('p', { 'style': 'margin:4px 0' },
_('以下参数内核未接受(可能被限制、随后被覆盖或需要重启生效):')));
parts.push(E('ul', { 'style': 'margin:2px 0' }, naItems));
}
items.push(E('li', {}, parts));
}
content = E('div', { 'class': 'alert-message warning', 'style': 'margin:6px 0' }, [
E('p', {}, _('应用完成,以下文件存在报错(常见原因:参数不存在、只读或权限不足):')),
E('p', {}, _('应用完成,以下内容需要留意(常见原因:参数不存在、只读、被内核拒绝或被其他文件覆盖):')),
E('ul', {}, items)
]);
}
dom.content(self.applyResultBox, [ content ]);
self.showApplyResult([ content ], (res != null && res.code == 0 && errors.length == 0) ? 4000 : 0);
if (self.applyResultBox.scrollIntoView)
self.applyResultBox.scrollIntoView({ block: 'nearest' });
self.reloadList();
}).catch(function(e) {
dom.content(self.applyResultBox, [ self.errorBox(_('应用失败:%s').format(e.message), self.backendErrorHint(e.message)) ]);
self.showApplyResult([ self.errorBox(_('应用失败:%s').format(e.message), self.backendErrorHint(e.message)) ], 0);
});
},
@@ -676,7 +878,11 @@ return view.extend({
rows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, (e.disabled ? '# ' : '') + e.key) ]),
E('td', {}, [ E('code', {}, e.value) ]),
E('td', {}, [ E('code', {}, (e.disabled || e.current == null) ? '—' : e.current) ]),
E('td', {}, [ E('code', {
/* value differs from kernel state: likely overridden by a
* later config file (or write failed) -> highlight it */
'style': (!e.disabled && e.current != null && e.current != e.value) ? 'color:#a35a00;font-weight:600' : ''
}, (e.disabled || e.current == null) ? '—' : e.current) ]),
E('td', { 'style': 'white-space:nowrap' }, ops)
]));
}
@@ -814,6 +1020,139 @@ return view.extend({
this.renderFilePanel();
},
/* ---------- duplicate-key detection & cleanup ---------- */
showDupCheck: function() {
var self = this;
this.dupBox.style.display = '';
dom.content(this.dupBox, [
E('h3', { 'style': 'text-align:center' }, _('同名参数检测')),
E('p', { 'style': 'color:#777;text-align:center' }, _('扫描中…'))
]);
if (this.dupBox.scrollIntoView)
this.dupBox.scrollIntoView({ block: 'start' });
callDupCheck('').then(function(res) {
self.renderDupResults((res != null && res.dups != null) ? res.dups : []);
}).catch(function(e) {
dom.content(self.dupBox, [
E('h3', { 'style': 'text-align:center' }, _('同名参数检测')),
self.errorBox(_('扫描失败:%s').format(e.message), self.backendErrorHint(e.message))
]);
});
},
hideDupCheck: function() {
this.dupBox.style.display = 'none';
dom.content(this.dupBox, []);
},
renderDupResults: function(dups) {
var self = this;
var rows = [];
var customPath = '/etc/sysctl.d/99-luci-sysctl.conf';
var collapseBtn = E('div', { 'style': 'text-align:center;margin-top:8px' }, [
E('button', { 'class': 'btn', 'click': ui.createHandlerFn(self, 'hideDupCheck') }, _('收起'))
]);
if (dups.length == 0) {
dom.content(this.dupBox, [
E('h3', { 'style': 'text-align:center' }, _('同名参数检测')),
E('p', { 'style': 'text-align:center;color:#777;padding:8px' },
_('未发现在多个文件中重复定义的参数。')),
collapseBtn
]);
return;
}
for (var i = 0; i < dups.length; i++) {
var d = dups[i];
var entRows = [];
var finalEntry = null;
for (var j = 0; j < d.entries.length; j++) {
var e = d.entries[j];
if (!e.disabled)
finalEntry = e;
entRows.push(E('div', {
'style': 'display:flex;align-items:center;justify-content:center;gap:8px;margin:3px 0'
}, [
E('code', {}, e.path.replace('/etc/', '')),
E('span', { 'style': 'color:#999' }, '='),
E('code', {}, e.value),
e.disabled
? badge(_('已禁用'), 'off')
: E('span', { 'style': 'color:#18794e;font-size:12px' }, _('生效来源')),
(e.path != customPath)
? E('button', {
'class': 'cbi-button cbi-button-remove',
'title': _('从该文件删除此条目'),
'click': ui.createHandlerFn(self, 'dupDeleteEntry', e, d.key)
}, _('删除'))
: E('span', { 'style': 'color:#34618e;font-size:12px' }, _('自定义'))
]));
}
/* files order == application order; compare against custom conf */
var files = (this.statusData != null && this.statusData.files != null) ? this.statusData.files : [];
var paths = [];
for (var k = 0; k < files.length; k++)
paths.push(files[k].path);
var customIdx = paths.indexOf(customPath);
var shadowed = (finalEntry != null && customIdx >= 0 && paths.indexOf(finalEntry.path) > customIdx);
var warnText = (shadowed && finalEntry != null)
? _('生效来源(%s)加载在自定义配置之后:“添加参数”里设置该参数不会生效,请直接编辑该文件或删除其同名条目。').format(finalEntry.path.replace('/etc/', ''))
: _('自定义配置加载顺序靠后,“添加参数”设置该参数时会覆盖以上取值。');
rows.push(E('tr', {}, [
E('td', {}, [ E('code', {}, d.key) ]),
E('td', {}, E('div', {}, entRows)),
E('td', { 'style': 'max-width:300px' }, E('div', {
'style': 'color:' + (shadowed ? '#b3261e' : '#666')
}, warnText))
]));
}
dom.content(this.dupBox, [
E('h3', { 'style': 'text-align:center' }, _('同名参数检测')),
E('p', { 'style': 'color:#666;margin:4px 0;text-align:center' },
_('以下参数在多个配置文件中重复定义,按应用顺序以最后一个为准:')),
E('div', { 'class': 'lsc-tablewrap' }, E('table', { 'class': 'lsc-table' }, [
E('thead', {}, E('tr', {}, [
E('th', {}, _('参数名')),
E('th', {}, _('各文件定义(按应用顺序)')),
E('th', {}, _('说明'))
])),
E('tbody', rows)
])),
collapseBtn
]);
},
dupDeleteEntry: function(e, key) {
var self = this;
return callFileDelete(e.path, key).then(function(res) {
if (res == null || res.code != 0) {
ui.addNotification(null,
E('p', {}, _('删除失败:%s').format((res != null && res.error) ? res.error : _('未知错误'))),
'error');
return;
}
self.reloadList();
self.showDupCheck();
}).catch(function(err) {
ui.addNotification(null, E('p', {}, _('删除失败:%s').format(err.message)), 'error');
});
},
/* ---------- section 2: online preset ---------- */
renderPresetSection: function() {
@@ -11,8 +11,9 @@
"preset_status",
"preset_fetch",
"preset_check",
"file_view",
"preset_list"
"file_view",
"preset_list",
"dup_check"
]
}
},
@@ -549,6 +549,32 @@ function file_delete_key(path, key) {
return { code: 0, removed: removed };
}
// Strip the normal "key = value" echo lines that sysctl -p prints to stdout
// for every successfully applied entry. What remains (if anything) is a
// genuine error message like "sysctl: permission denied on key '...'".
// NOTE: ucode regex has no non-capturing groups; plain pattern is fine here.
function strip_echo(out) {
if (out == null || length(out) == 0)
return '';
let lines = split(out, '\n');
let rest = [];
for (let i = 0; i < length(lines); i++) {
let t = trim(lines[i]);
if (t == null || length(t) == 0)
continue;
if (match(t, /^[A-Za-z0-9_][A-Za-z0-9_.\/-]*[ \t]*=[ \t]*\S/) != null)
continue;
push(rest, t);
}
return join('\n', rest);
}
const methods = {
status: {
args: {},
@@ -763,10 +789,39 @@ const methods = {
let out = (p != null) ? p.read('all') : null;
let rc = (p != null) ? p.close() : -1;
out = (out == null) ? '' : trim(out);
out = strip_echo((out == null) ? '' : trim(out));
if (rc != 0 || length(out) > 0)
push(errors, { file: confs[i], code: rc, output: out });
/* busybox `sysctl -e -p` echoes every line and silently
* swallows write failures -> echo alone proves nothing.
* Read back each enabled entry from /proc/sys instead:
* anything that differs was NOT accepted by the kernel. */
let not_applied = [];
let entries = parse_conf(confs[i]);
for (let j = 0; j < length(entries); j++) {
if (entries[j].disabled)
continue;
let cur = read_current(entries[j].key);
if (cur == null)
continue; // key unknown to this kernel, -e skips it by design
if (cur != entries[j].value)
push(not_applied, {
key: entries[j].key,
value: entries[j].value,
current: cur
});
}
if (rc != 0 || length(out) > 0 || length(not_applied) > 0)
push(errors, {
file: confs[i],
code: rc,
output: out,
not_applied: not_applied
});
}
return { code: 0, errors: errors };
@@ -1012,6 +1067,57 @@ const methods = {
return file_delete_key(p, key);
}
},
// Locate a sysctl key across ALL config sources, in application order.
// Without args: return only keys defined in >= 2 files (duplicate map).
// With args.key: return that key's definitions even if defined once
// (used by the add/edit form to warn about shadowing overrides).
// Entries are ordered by file load order -> the LAST enabled entry wins.
dup_check: {
args: { key: '' },
call: function(request) {
let a = (request.args != null) ? request.args : {};
let want = trim(a.key);
let confs = collect_confs();
let by_key = {};
let order = [];
for (let i = 0; i < length(confs); i++) {
let path = confs[i];
let entries = parse_conf(path);
for (let j = 0; j < length(entries); j++) {
let k = entries[j].key;
if (want != null && length(want) > 0 && k != want)
continue;
if (by_key[k] == null) {
by_key[k] = [];
push(order, k);
}
push(by_key[k], {
path: path,
value: entries[j].value,
disabled: entries[j].disabled
});
}
}
let min_defs = (want != null && length(want) > 0) ? 1 : 2;
let dups = [];
for (let i = 0; i < length(order); i++) {
let k = order[i];
if (length(by_key[k]) >= min_defs)
push(dups, { key: k, entries: by_key[k] });
}
return { code: 0, dups: dups };
}
}
};