mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 14:43:56 +08:00
🔥 Sync 2026-09-07 20:49:02
This commit is contained in:
1 parent
c05bdbe2bc
commit
6e956beb69
160 files changed
+152126
-957
No files matched your search
@@ -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
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user