mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 03:07:52 +08:00
✨ Sync 2026-10-20 03:34:23
This commit is contained in:
1 parent
d210fa85f8
commit
cbd3ce4959
21 files changed
+2002
-90
No files matched your search
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OneNAS.space
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+51
-23
@@ -1,49 +1,80 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=beszel-agent
|
||||
PKG_NAME:=beszel
|
||||
PKG_VERSION:=0.21.0
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)_linux_$(or $(BESZEL_GOARCH),arm64).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/henrygd/beszel/releases/download/v$(PKG_VERSION)/
|
||||
PKG_SOURCE:=beszel-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/henrygd/beszel/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Jackie264 <OneNAS-space>
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/beszel-$(PKG_VERSION)
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
|
||||
GO_PKG:=github.com/henrygd/beszel
|
||||
GO_PKG_BUILD_PKG:=$(GO_PKG)/internal/cmd/agent
|
||||
GO_PKG_TAGS:=openwrt
|
||||
GO_PKG_ENABLE_CGO:=0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/beszel-agent
|
||||
SECTION:=admin
|
||||
CATEGORY:=Administration
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=Beszel monitoring agent
|
||||
URL:=https://github.com/henrygd/beszel
|
||||
URL:=https://beszel.dev
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS)
|
||||
endef
|
||||
|
||||
define Package/beszel-agent/description
|
||||
Lightweight server monitoring agent for Beszel hub
|
||||
Beszel is a lightweight server monitoring hub.
|
||||
This package contains the agent that runs on the monitored system.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
tar -xzf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR)
|
||||
$(call Build/Prepare/Default)
|
||||
$(CP) ./files/openwrt_services.go $(PKG_BUILD_DIR)/agent/
|
||||
$(CP) ./files/openwrt_intel_gpu.go $(PKG_BUILD_DIR)/agent/
|
||||
$(SED) 's|//go:build linux|//go:build linux \&\& !openwrt|' $(PKG_BUILD_DIR)/agent/systemd.go
|
||||
rm -f $(PKG_BUILD_DIR)/agent/gpu_intel.go
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
if command -v upx >/dev/null 2>&1; then \
|
||||
if [ "$(BESZEL_GOARCH)" = "mips" ] || [ "$(BESZEL_GOARCH)" = "mipsle" ]; then \
|
||||
echo "Skipping UPX compression for MIPS architectures (prevents execution hang)..."; \
|
||||
else \
|
||||
upx --best --lzma $(PKG_BUILD_DIR)/beszel-agent || true; \
|
||||
fi; \
|
||||
define Package/beszel-agent/conffiles
|
||||
/etc/config/beszel-agent
|
||||
endef
|
||||
|
||||
define Package/beszel-agent/preinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
if [ -f "/etc/init.d/beszel-agent" ]; then
|
||||
echo "Stopping old Beszel Agent instance before upgrade..."
|
||||
/etc/init.d/beszel-agent stop
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/beszel-agent/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
echo "Beszel Agent updated successfully. Starting service..."
|
||||
/etc/init.d/beszel-agent enable
|
||||
/etc/init.d/beszel-agent start
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/beszel-agent/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/beszel-agent $(1)/usr/bin/
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/agent $(1)/usr/bin/beszel-agent
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/beszel-agent.init $(1)/etc/init.d/beszel-agent
|
||||
@@ -52,8 +83,5 @@ define Package/beszel-agent/install
|
||||
$(INSTALL_CONF) ./files/beszel-agent.config $(1)/etc/config/beszel-agent
|
||||
endef
|
||||
|
||||
define Package/beszel-agent/conffiles
|
||||
/etc/config/beszel-agent
|
||||
endef
|
||||
|
||||
$(eval $(call GoPackage,beszel-agent))
|
||||
$(eval $(call BuildPackage,beszel-agent))
|
||||
@@ -0,0 +1,26 @@
|
||||
# beszel-agent
|
||||
The lightweight telemetry agent adapted to OpenWrt is used to report system and Docker indicators to your Beszel Hub.
|
||||
|
||||
## luci-app-beszel-agent
|
||||
LuCI UI configuration interface of Beszel Agent
|
||||
- [Right-click on this and then "Open Link in New Tab"](https://github.com/OneNAS-space/luci-app-beszel-agent)
|
||||
|
||||
#### (Based on the official source code)
|
||||
Automatically synchronize and track the latest version of upstream
|
||||
|
||||
- Support OpenWrt's procd so that Service detail can display the status correctly.
|
||||
<img width="1460" height="755" alt="Screenshot 2026-07-10 at 21 54 34" src="https://github.com/user-attachments/assets/09109fd9-4811-412e-9a45-11660054b481" />
|
||||
|
||||
- Support AMD/Intel GPU
|
||||
<img width="1446" height="419" alt="Screenshot 2026-07-17 at 23 36 34" src="https://github.com/user-attachments/assets/cca94b20-489b-4a83-9432-30ed7f9071be" />
|
||||
|
||||
### Attribution
|
||||
#### Reference
|
||||
- [beszel official](https://github.com/henrygd/beszel)
|
||||
|
||||
#### Acknowledgment
|
||||
Inspired by
|
||||
- [@vernette beszel-agent-openwrt](https://github.com/vernette/beszel-agent-openwrt)
|
||||
|
||||
#### Notice
|
||||
If the author of the original code has any objections, please contact me to deal with it.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
@@ -1,9 +1,17 @@
|
||||
config agent 'agent'
|
||||
option enabled '0'
|
||||
option port '45876'
|
||||
option public_key ''
|
||||
option hub_url '' # optional, for outbound mode (websockets)
|
||||
option token '' # optional, for outbound mode (websockets)
|
||||
option extra_filesystems '' # optional, comma-separated, e.g. 'sda1__NAS'
|
||||
option fingerprint '' # auto-populated on first start, do not edit
|
||||
config beszel-agent 'main'
|
||||
option boot_delay '15'
|
||||
option enable '0'
|
||||
option data_dir '/etc/beszel-agent'
|
||||
option docker_host ''
|
||||
option hub_url ''
|
||||
option intel_gpu_device ''
|
||||
option key ''
|
||||
option listen '0.0.0.0:45876'
|
||||
option log_level 'info'
|
||||
option system_name 'openwrt'
|
||||
option token ''
|
||||
|
||||
# Load additional file systems (optional)
|
||||
# Add multiple mount points using 'list' syntax
|
||||
# list extra_filesystems '/mnt/sda1'
|
||||
# list extra_filesystems '/mnt/sdb1'
|
||||
@@ -1,75 +1,124 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
USE_PROCD=1
|
||||
NAME=beszel-agent
|
||||
PROG=/usr/bin/beszel-agent
|
||||
DATA_DIR=/var/lib/beszel-agent
|
||||
FP_FILE="$DATA_DIR/fingerprint"
|
||||
OLD_FP_FILE=/usr/share/beszel-agent/fingerprint
|
||||
|
||||
append_fs() {
|
||||
local value="$1"
|
||||
if [ -z "$FS_LIST" ]; then
|
||||
FS_LIST="$value"
|
||||
else
|
||||
FS_LIST="$FS_LIST,$value"
|
||||
fi
|
||||
}
|
||||
|
||||
append_nic() {
|
||||
local value="$1"
|
||||
if [ -z "$NIC_LIST" ]; then
|
||||
NIC_LIST="$value"
|
||||
else
|
||||
NIC_LIST="$NIC_LIST,$value"
|
||||
fi
|
||||
}
|
||||
|
||||
boot() {
|
||||
local delay
|
||||
delay="$(uci -q get $NAME.@main.boot_delay)"
|
||||
sleep "${delay:-15}"
|
||||
start "$@"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled port key url token extra_fs fp
|
||||
local enable data_dir docker_host gpu_collector hub_url intel_gpu_device key listen log_level port token skip_gpu skip_systemd system_name
|
||||
|
||||
config_load 'beszel-agent'
|
||||
config_get_bool enabled 'agent' 'enabled' '0'
|
||||
config_get port 'agent' 'port' '45876'
|
||||
config_get key 'agent' 'public_key' ''
|
||||
config_get url 'agent' 'hub_url' ''
|
||||
config_get token 'agent' 'token' ''
|
||||
config_get extra_fs 'agent' 'extra_filesystems' ''
|
||||
config_get fp 'agent' 'fingerprint' ''
|
||||
config_load "$NAME"
|
||||
|
||||
[ "$enabled" -eq 0 ] && return 0
|
||||
config_get_bool enable "main" "enable" 0
|
||||
[ "$enable" -eq 1 ] || return 0
|
||||
|
||||
[ -z "$key" ] && {
|
||||
logger -t beszel-agent "Public key not set!"
|
||||
return 1
|
||||
}
|
||||
config_get data_dir "main" "data_dir" "/etc/beszel-agent"
|
||||
config_get docker_host "main" "docker_host" ""
|
||||
config_get gpu_collector "main" "gpu_collector" ""
|
||||
config_get hub_url "main" "hub_url" ""
|
||||
config_get intel_gpu_device "main" "intel_gpu_device" ""
|
||||
config_get key "main" "key" ""
|
||||
config_get listen "main" "listen" ""
|
||||
config_get log_level "main" "log_level" "info"
|
||||
config_get port "main" "port" "45876"
|
||||
config_get skip_gpu "main" "skip_gpu" "false"
|
||||
case "$(uname -m)" in
|
||||
x86_64|i?86)
|
||||
if ! ls /sys/class/drm/card[0-9]* >/dev/null 2>&1; then
|
||||
skip_gpu="true"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
skip_gpu="true"
|
||||
;;
|
||||
esac
|
||||
config_get skip_systemd "main" "skip_systemd" "false"
|
||||
config_get system_name "main" "system_name" ""
|
||||
config_get token "main" "token" ""
|
||||
|
||||
if [ -n "$url" ] || [ -n "$token" ]; then
|
||||
if [ -z "$url" ] || [ -z "$token" ]; then
|
||||
logger -t beszel-agent "Outbound mode requires both hub_url and token, but only one set!"
|
||||
return 1
|
||||
if [ -n "$data_dir" ]; then
|
||||
mkdir -p "$data_dir"
|
||||
if [ -f /etc/sysupgrade.conf ]; then
|
||||
if ! grep -qFx "$data_dir" /etc/sysupgrade.conf; then
|
||||
echo "$data_dir" >> /etc/sysupgrade.conf
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$DATA_DIR"
|
||||
FS_LIST=""
|
||||
config_list_foreach "main" "extra_filesystems" append_fs
|
||||
|
||||
# migrate from pre-UCI storage on flash
|
||||
[ -z "$fp" ] && [ -s "$OLD_FP_FILE" ] && fp=$(cat "$OLD_FP_FILE")
|
||||
NIC_LIST=""
|
||||
config_list_foreach "main" "nics" append_nic
|
||||
|
||||
if [ -z "$fp" ]; then
|
||||
DATA_DIR="$DATA_DIR" "$PROG" fingerprint > /dev/null 2>&1
|
||||
[ -s "$FP_FILE" ] && fp=$(cat "$FP_FILE")
|
||||
fi
|
||||
procd_open_instance "beszel-agent"
|
||||
procd_set_param command "$PROG"
|
||||
|
||||
if [ -n "$fp" ]; then
|
||||
printf '%s' "$fp" > "$FP_FILE"
|
||||
[ "$(uci -q get beszel-agent.agent.fingerprint)" = "$fp" ] || {
|
||||
uci set beszel-agent.agent.fingerprint="$fp"
|
||||
uci commit beszel-agent
|
||||
logger -t beszel-agent "Fingerprint saved to UCI: $fp"
|
||||
}
|
||||
else
|
||||
logger -t beszel-agent "Failed to obtain fingerprint, agent will generate one"
|
||||
fi
|
||||
procd_set_param env HOST_PROC="/proc" HOST_SYS="/sys"
|
||||
|
||||
procd_open_instance beszel-agent
|
||||
procd_set_param command "$PROG" -listen "$port" -key "$key"
|
||||
[ -n "$url" ] && [ -n "$token" ] && {
|
||||
procd_append_param command -url "$url" -token "$token"
|
||||
}
|
||||
if [ -n "$listen" ]; then
|
||||
procd_append_param env LISTEN="$listen"
|
||||
else
|
||||
procd_append_param env PORT="$port"
|
||||
fi
|
||||
|
||||
procd_set_param env DATA_DIR="$DATA_DIR"
|
||||
[ -n "$extra_fs" ] && procd_append_param env EXTRA_FILESYSTEMS="$extra_fs"
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
[ -n "$docker_host" ] && procd_append_param env DOCKER_HOST="$docker_host"
|
||||
[ -n "$data_dir" ] && procd_append_param env DATA_DIR="$data_dir"
|
||||
[ -n "$FS_LIST" ] && procd_append_param env EXTRA_FILESYSTEMS="$FS_LIST"
|
||||
[ -n "$hub_url" ] && procd_append_param env HUB_URL="$hub_url"
|
||||
[ -n "$key" ] && procd_append_param env KEY="$key"
|
||||
[ -n "$log_level" ] && procd_append_param env LOG_LEVEL="$log_level"
|
||||
[ -n "$NIC_LIST" ] && procd_append_param env NICS="$NIC_LIST"
|
||||
[ -n "$skip_gpu" ] && procd_append_param env SKIP_GPU="$skip_gpu"
|
||||
[ -n "$skip_systemd" ] && procd_append_param env SKIP_SYSTEMD="$skip_systemd"
|
||||
[ -n "$system_name" ] && procd_append_param env SYSTEM_NAME="$system_name"
|
||||
[ -n "$token" ] && procd_append_param env TOKEN="$token"
|
||||
|
||||
if [ "$skip_gpu" = "false" ]; then
|
||||
[ -n "$gpu_collector" ] && procd_append_param env GPU_COLLECTOR="$gpu_collector"
|
||||
if [ "$gpu_collector" = "intel_gpu_top" ] || [ -z "$gpu_collector" ]; then
|
||||
[ -n "$intel_gpu_device" ] && procd_append_param env INTEL_GPU_DEVICE="$intel_gpu_device"
|
||||
fi
|
||||
fi
|
||||
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'beszel-agent'
|
||||
procd_add_reload_trigger "beszel-agent"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//go:build linux && openwrt
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
)
|
||||
|
||||
const intelGpuStatsCmd = "sh"
|
||||
|
||||
type intelGpuStats struct {
|
||||
PowerGPU float64
|
||||
PowerPkg float64
|
||||
Engines map[string]float64
|
||||
}
|
||||
|
||||
var (
|
||||
gpuStateMu sync.Mutex
|
||||
lastRc6 uint64
|
||||
lastGpuEnergy uint64
|
||||
lastPkgEnergy uint64
|
||||
lastTime time.Time
|
||||
)
|
||||
|
||||
func getEnergyPaths() (gpuPath string, pkgPath string) {
|
||||
pkgBase := "/sys/class/powercap/intel-rapl/intel-rapl:0"
|
||||
gpuBase := "/sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:1"
|
||||
|
||||
if _, err := os.Stat(pkgBase + "/energy_uj"); err == nil {
|
||||
pkgPath = pkgBase + "/energy_uj"
|
||||
}
|
||||
if _, err := os.Stat(gpuBase + "/energy_uj"); err == nil {
|
||||
gpuPath = gpuBase + "/energy_uj"
|
||||
}
|
||||
return gpuPath, pkgPath
|
||||
}
|
||||
|
||||
func readUint64(path string) uint64 {
|
||||
if path == "" {
|
||||
return 0
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
val, _ := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
|
||||
return val
|
||||
}
|
||||
|
||||
func (gm *GPUManager) updateIntelFromStats(sample *intelGpuStats) bool {
|
||||
gm.Lock()
|
||||
defer gm.Unlock()
|
||||
|
||||
id := "0"
|
||||
gpuData, ok := gm.GpuDataMap[id]
|
||||
if !ok {
|
||||
gpuData = &system.GPUData{Name: "GPU", Engines: make(map[string]float64)}
|
||||
gm.GpuDataMap[id] = gpuData
|
||||
}
|
||||
|
||||
gpuData.Power += sample.PowerGPU
|
||||
gpuData.PowerPkg += sample.PowerPkg
|
||||
|
||||
if gpuData.Engines == nil {
|
||||
gpuData.Engines = make(map[string]float64, len(sample.Engines))
|
||||
}
|
||||
for name, engine := range sample.Engines {
|
||||
gpuData.Engines[name] += engine
|
||||
}
|
||||
|
||||
gpuData.Count++
|
||||
return true
|
||||
}
|
||||
|
||||
func (gm *GPUManager) collectIntelStats() (err error) {
|
||||
gpuStateMu.Lock()
|
||||
defer gpuStateMu.Unlock()
|
||||
|
||||
device := os.Getenv("INTEL_GPU_DEVICE")
|
||||
if device == "" {
|
||||
device = "card0"
|
||||
}
|
||||
device = filepath.Base(device)
|
||||
|
||||
rc6Path := "/sys/class/drm/" + device + "/power/rc6_residency_ms"
|
||||
gpuEnergyPath, pkgEnergyPath := getEnergyPaths()
|
||||
|
||||
currRc6 := readUint64(rc6Path)
|
||||
currGpuEnergy := readUint64(gpuEnergyPath)
|
||||
currPkgEnergy := readUint64(pkgEnergyPath)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
if !lastTime.IsZero() {
|
||||
timeDelta := uint64(now.Sub(lastTime).Milliseconds())
|
||||
if timeDelta > 500 {
|
||||
rc6Delta := currRc6 - lastRc6
|
||||
usage := 100.0 - (float64(rc6Delta) / float64(timeDelta) * 100.0)
|
||||
if usage < 0 { usage = 0 }
|
||||
|
||||
var powerGPU float64
|
||||
if currGpuEnergy > 0 && currGpuEnergy >= lastGpuEnergy {
|
||||
energyDelta := currGpuEnergy - lastGpuEnergy
|
||||
powerGPU = float64(energyDelta) / float64(timeDelta) / 1000.0
|
||||
}
|
||||
|
||||
var powerPkg float64
|
||||
if currPkgEnergy > 0 && currPkgEnergy >= lastPkgEnergy {
|
||||
energyDelta := currPkgEnergy - lastPkgEnergy
|
||||
powerPkg = float64(energyDelta) / float64(timeDelta) / 1000.0
|
||||
}
|
||||
|
||||
sample := intelGpuStats{
|
||||
PowerGPU: powerGPU,
|
||||
PowerPkg: powerPkg,
|
||||
Engines: map[string]float64{
|
||||
"Render/3D": usage,
|
||||
},
|
||||
}
|
||||
gm.updateIntelFromStats(&sample)
|
||||
}
|
||||
}
|
||||
|
||||
lastRc6 = currRc6
|
||||
lastGpuEnergy = currGpuEnergy
|
||||
lastPkgEnergy = currPkgEnergy
|
||||
lastTime = now
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//go:build linux && openwrt
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/dbus"
|
||||
"github.com/henrygd/beszel/agent/utils"
|
||||
"github.com/henrygd/beszel/internal/entities/systemd"
|
||||
)
|
||||
|
||||
var errNoActiveTime = errors.New("no active time")
|
||||
|
||||
type systemdManager struct {
|
||||
sync.Mutex
|
||||
serviceStatsMap map[string]*systemd.Service
|
||||
isRunning bool
|
||||
hasFreshStats bool
|
||||
patterns []string
|
||||
logsEnabled bool
|
||||
}
|
||||
|
||||
func isSystemdAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func newSystemdManager() (*systemdManager, error) {
|
||||
if skipSystemd, _ := utils.GetEnv("SKIP_SYSTEMD"); skipSystemd == "true" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
manager := &systemdManager{
|
||||
serviceStatsMap: make(map[string]*systemd.Service),
|
||||
patterns: getServicePatterns(),
|
||||
}
|
||||
|
||||
manager.startWorker(nil)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (sm *systemdManager) startWorker(conn *dbus.Conn) {
|
||||
if sm.isRunning {
|
||||
return
|
||||
}
|
||||
sm.isRunning = true
|
||||
_ = sm.getServiceStats(nil, true)
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Minute * 10)
|
||||
_ = sm.getServiceStats(nil, true)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (sm *systemdManager) getServiceStatsCount() int {
|
||||
return len(sm.serviceStatsMap)
|
||||
}
|
||||
|
||||
func (sm *systemdManager) getFailedServiceCount() uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (sm *systemdManager) getServiceStats(conn *dbus.Conn, refresh bool) []*systemd.Service {
|
||||
var services []*systemd.Service
|
||||
|
||||
if !refresh {
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
for _, service := range sm.serviceStatsMap {
|
||||
services = append(services, service)
|
||||
}
|
||||
sm.hasFreshStats = false
|
||||
return services
|
||||
}
|
||||
|
||||
out, err := exec.Command("ubus", "call", "service", "list").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
type UbusInstance struct {
|
||||
Running bool `json:"running"`
|
||||
Pid int `json:"pid"`
|
||||
}
|
||||
type UbusService struct {
|
||||
Instances map[string]UbusInstance `json:"instances"`
|
||||
}
|
||||
|
||||
var ubusData map[string]UbusService
|
||||
if err := json.Unmarshal(out, &ubusData); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentUnits := make(map[string]struct{})
|
||||
pageSize := uint64(os.Getpagesize())
|
||||
if pageSize == 0 {
|
||||
pageSize = 4096
|
||||
}
|
||||
|
||||
sm.Lock()
|
||||
defer sm.Unlock()
|
||||
|
||||
for sName, sData := range ubusData {
|
||||
unitName := sName + ".service"
|
||||
currentUnits[unitName] = struct{}{}
|
||||
|
||||
pid := 0
|
||||
isRunning := false
|
||||
for _, inst := range sData.Instances {
|
||||
if inst.Running && inst.Pid > 0 {
|
||||
pid = inst.Pid
|
||||
isRunning = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
service, exists := sm.serviceStatsMap[unitName]
|
||||
if !exists {
|
||||
service = &systemd.Service{Name: sName}
|
||||
sm.serviceStatsMap[unitName] = service
|
||||
}
|
||||
|
||||
if isRunning {
|
||||
service.State = systemd.ParseServiceStatus("active")
|
||||
service.Sub = systemd.ParseServiceSubState("running")
|
||||
|
||||
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil {
|
||||
binEnd := strings.LastIndex(string(data), ")")
|
||||
if binEnd != -1 && len(string(data)) > binEnd+2 {
|
||||
fields := strings.Fields(string(data)[binEnd+2:])
|
||||
if len(fields) > 21 {
|
||||
utime, _ := strconv.ParseUint(fields[11], 10, 64)
|
||||
stime, _ := strconv.ParseUint(fields[12], 10, 64)
|
||||
rss, _ := strconv.ParseUint(fields[21], 10, 64)
|
||||
|
||||
service.Mem = rss * pageSize
|
||||
if service.Mem > service.MemPeak {
|
||||
service.MemPeak = service.Mem
|
||||
}
|
||||
service.UpdateCPUPercent((utime + stime) * 10000000)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
service.State = systemd.ParseServiceStatus("active")
|
||||
service.Sub = systemd.ParseServiceSubState("exited")
|
||||
service.Mem = 0
|
||||
service.UpdateCPUPercent(0)
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
for unitName := range sm.serviceStatsMap {
|
||||
if _, exists := currentUnits[unitName]; !exists {
|
||||
delete(sm.serviceStatsMap, unitName)
|
||||
}
|
||||
}
|
||||
|
||||
sm.hasFreshStats = true
|
||||
return services
|
||||
}
|
||||
|
||||
func (sm *systemdManager) updateServiceStats(conn *dbus.Conn, unit dbus.UnitStatus) (*systemd.Service, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (sm *systemdManager) getServiceDetails(serviceName string) (systemd.ServiceDetails, error) {
|
||||
details := make(systemd.ServiceDetails)
|
||||
uName := strings.TrimSuffix(serviceName, ".service")
|
||||
initScript := "/etc/init.d/" + uName
|
||||
|
||||
// 1. Get cache data (used to obtain historical memory peaks)
|
||||
sm.Lock()
|
||||
service, exists := sm.serviceStatsMap[serviceName]
|
||||
if !exists {
|
||||
service, exists = sm.serviceStatsMap[uName]
|
||||
}
|
||||
sm.Unlock()
|
||||
|
||||
// Initialize the default value
|
||||
details["ActiveState"] = "inactive"
|
||||
details["SubState"] = "dead"
|
||||
details["Result"] = "success"
|
||||
details["MemoryCurrent"] = uint64(0)
|
||||
details["MemoryPeak"] = uint64(0)
|
||||
details["MainPID"] = uint64(0)
|
||||
details["TasksCurrent"] = uint64(0)
|
||||
|
||||
// 2. Real-time query Ubus to get the status, PID, number of concurrent threads and memory RSS
|
||||
var isLoadedInProcd bool = false
|
||||
var isRunningRealTime bool = false
|
||||
var currentMem uint64 = 0
|
||||
|
||||
out, err := exec.Command("ubus", "call", "service", "list", fmt.Sprintf(`{"name":"%s"}`, uName)).Output()
|
||||
if err == nil {
|
||||
type UbusInstance struct {
|
||||
Running bool `json:"running"`
|
||||
Pid int `json:"pid"`
|
||||
}
|
||||
type UbusService struct {
|
||||
Instances map[string]UbusInstance `json:"instances"`
|
||||
}
|
||||
var ubusData map[string]UbusService
|
||||
if json.Unmarshal(out, &ubusData) == nil {
|
||||
if sData, ok := ubusData[uName]; ok {
|
||||
isLoadedInProcd = true
|
||||
for _, inst := range sData.Instances {
|
||||
if inst.Running {
|
||||
isRunningRealTime = true
|
||||
if inst.Pid > 0 {
|
||||
details["MainPID"] = uint64(inst.Pid)
|
||||
|
||||
// Accurate real-time reading of memory (RSS)
|
||||
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", inst.Pid)); err == nil {
|
||||
binEnd := strings.LastIndex(string(data), ")")
|
||||
if binEnd != -1 && len(string(data)) > binEnd+2 {
|
||||
fields := strings.Fields(string(data)[binEnd+2:])
|
||||
if len(fields) > 21 {
|
||||
pageSize := uint64(os.Getpagesize())
|
||||
if pageSize == 0 {
|
||||
pageSize = 4096
|
||||
}
|
||||
rss, _ := strconv.ParseUint(fields[21], 10, 64)
|
||||
currentMem = rss * pageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accurately read the number of threads in real time (Tasks)
|
||||
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", inst.Pid)); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Threads:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
tasks, _ := strconv.ParseUint(fields[1], 10, 64)
|
||||
details["TasksCurrent"] = tasks
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Core indicator status calibration and memory peak calculation
|
||||
if isRunningRealTime {
|
||||
details["ActiveState"] = "active"
|
||||
details["SubState"] = "running"
|
||||
details["MemoryCurrent"] = currentMem
|
||||
|
||||
if exists && service.MemPeak > currentMem {
|
||||
details["MemoryPeak"] = service.MemPeak
|
||||
} else {
|
||||
details["MemoryPeak"] = currentMem
|
||||
}
|
||||
} else if isLoadedInProcd {
|
||||
details["ActiveState"] = "active"
|
||||
details["SubState"] = "exited"
|
||||
details["MemoryCurrent"] = uint64(0)
|
||||
if exists { details["MemoryPeak"] = service.MemPeak }
|
||||
} else {
|
||||
details["ActiveState"] = "inactive"
|
||||
details["SubState"] = "dead"
|
||||
details["MemoryCurrent"] = uint64(0)
|
||||
if exists { details["MemoryPeak"] = service.MemPeak }
|
||||
}
|
||||
|
||||
// 4. Complete Systemd-compatible metadata
|
||||
details["Id"] = serviceName
|
||||
details["Description"] = uName + " (OpenWrt Procd)"
|
||||
details["LoadState"] = "loaded"
|
||||
details["FragmentPath"] = initScript
|
||||
details["NRestarts"] = uint64(0)
|
||||
details["StatusText"] = ""
|
||||
|
||||
// Check the self-starting status of the service
|
||||
unitFileState := "disabled"
|
||||
if info, err := os.Stat(initScript); err == nil && !info.Mode().IsDir() {
|
||||
if err := exec.Command(initScript, "enabled").Run(); err == nil {
|
||||
unitFileState = "enabled"
|
||||
}
|
||||
}
|
||||
details["UnitFileState"] = unitFileState
|
||||
|
||||
// Dynamic detection service control capability (Capabilities)
|
||||
canStart, canStop, canReload := false, false, false
|
||||
if info, err := os.Stat(initScript); err == nil && !info.Mode().IsDir() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
out, _ := exec.CommandContext(ctx, initScript).CombinedOutput()
|
||||
outputStr := strings.ToLower(string(out))
|
||||
if strings.Contains(outputStr, "start") { canStart = true }
|
||||
if strings.Contains(outputStr, "stop") { canStop = true }
|
||||
if strings.Contains(outputStr, "reload") { canReload = true }
|
||||
}
|
||||
details["CanStart"] = canStart
|
||||
details["CanStop"] = canStop
|
||||
details["CanReload"] = canReload
|
||||
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func unescapeServiceName(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func getServicePatterns() []string {
|
||||
patterns := []string{}
|
||||
if envPatterns, _ := utils.GetEnv("SERVICE_PATTERNS"); envPatterns != "" {
|
||||
for pattern := range strings.SplitSeq(envPatterns, ",") {
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
if pattern == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(pattern, "timer") && !strings.HasSuffix(pattern, ".service") {
|
||||
pattern += ".service"
|
||||
}
|
||||
patterns = append(patterns, pattern)
|
||||
}
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
patterns = []string{"*.service"}
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
func (sm *systemdManager) getServiceLogs(serviceName string) ([]byte, error) {
|
||||
/*
|
||||
未来进阶提示:如果你想在前端也看到 OpenWrt 的服务日志,
|
||||
可以将 serviceName 去掉后缀,然后通过执行类似以下的命令返回真实的日志字节流:
|
||||
out, err := exec.Command("logread", "-e", strings.TrimSuffix(serviceName, ".service")).Output()
|
||||
return out, err
|
||||
*/
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
From 128555126155413ce32b134435c425ad9bb24501 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie Han <jackie.han@gmail.com>
|
||||
Date: Wed, 14 Oct 2026 07:40:03 +0800
|
||||
Subject: [PATCH] Refactor collect function to handle wireless devices
|
||||
|
||||
---
|
||||
agent/wifi/wifi_linux.go | 111 ++++++++++++++++++++++++++++++++++++---
|
||||
1 file changed, 105 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/agent/wifi/wifi_linux.go b/agent/wifi/wifi_linux.go
|
||||
index 926d658fe..ea42b1f2b 100644
|
||||
--- a/agent/wifi/wifi_linux.go
|
||||
+++ b/agent/wifi/wifi_linux.go
|
||||
@@ -5,8 +5,12 @@ package wifi
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
+ "math"
|
||||
"net"
|
||||
"os"
|
||||
+ "os/exec"
|
||||
+ "regexp"
|
||||
+ "strconv"
|
||||
"time"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/system"
|
||||
@@ -99,15 +103,110 @@ func hasWirelessDevice(dir string) bool {
|
||||
}
|
||||
|
||||
func collect(ctx context.Context) map[string]system.WiFi {
|
||||
- if !hasWirelessDevice(wiphyClassDir) {
|
||||
- return nil
|
||||
+ results := make(map[string]system.WiFi)
|
||||
+
|
||||
+ if hasWirelessDevice(wiphyClassDir) {
|
||||
+ if client, err := newNL80211Client(); err == nil {
|
||||
+ for k, v := range collectLinux(ctx, client) {
|
||||
+ results[k] = v
|
||||
+ }
|
||||
+ client.Close()
|
||||
+ }
|
||||
}
|
||||
- client, err := newNL80211Client()
|
||||
- if err != nil {
|
||||
+
|
||||
+ if _, err := exec.LookPath("iwinfo"); err == nil {
|
||||
+ for k, v := range collectOpenWrtAP(ctx) {
|
||||
+ results[k] = v
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if len(results) == 0 {
|
||||
return nil
|
||||
}
|
||||
- defer client.Close()
|
||||
- return collectLinux(ctx, client)
|
||||
+ return results
|
||||
+}
|
||||
+
|
||||
+var (
|
||||
+ iwinfoIfaceRe = regexp.MustCompile(`(?m)^(\S+)[ \t]+ESSID:`)
|
||||
+ iwinfoModeRe = regexp.MustCompile(`Mode:[ \t]*Master`)
|
||||
+ iwinfoSSIDRe = regexp.MustCompile(`ESSID:\s*"([^"]+)"`)
|
||||
+ iwinfoAssocRe = regexp.MustCompile(`(?m)^(?:[0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}.*?(-?\d+)\s+dBm`)
|
||||
+)
|
||||
+
|
||||
+type iwinfoBlock struct {
|
||||
+ name string
|
||||
+ text string
|
||||
+}
|
||||
+
|
||||
+// splitIwinfoBlocks splits bare `iwinfo` output into one block per interface.
|
||||
+func splitIwinfoBlocks(out string) []iwinfoBlock {
|
||||
+ idx := iwinfoIfaceRe.FindAllStringSubmatchIndex(out, -1)
|
||||
+ blocks := make([]iwinfoBlock, 0, len(idx))
|
||||
+ for i, m := range idx {
|
||||
+ end := len(out)
|
||||
+ if i+1 < len(idx) {
|
||||
+ end = idx[i+1][0]
|
||||
+ }
|
||||
+ blocks = append(blocks, iwinfoBlock{name: out[m[2]:m[3]], text: out[m[0]:end]})
|
||||
+ }
|
||||
+ return blocks
|
||||
+}
|
||||
+
|
||||
+// parseIwinfoInfo reports whether a block is a Master (AP) interface and its SSID.
|
||||
+func parseIwinfoInfo(block string) (ssid string, isAP bool) {
|
||||
+ if !iwinfoModeRe.MatchString(block) {
|
||||
+ return "", false
|
||||
+ }
|
||||
+ if m := iwinfoSSIDRe.FindStringSubmatch(block); len(m) > 1 {
|
||||
+ ssid = m[1]
|
||||
+ }
|
||||
+ return ssid, true
|
||||
+}
|
||||
+
|
||||
+// parseAssocRSSI returns the rounded average RSSI of associated clients.
|
||||
+// ok is false when no valid RSSI value was found.
|
||||
+func parseAssocRSSI(out string) (avg float64, ok bool) {
|
||||
+ sum, count := 0, 0
|
||||
+ for _, m := range iwinfoAssocRe.FindAllStringSubmatch(out, -1) {
|
||||
+ rssi, err := strconv.Atoi(m[1])
|
||||
+ if err != nil || rssi >= 0 || rssi < -150 {
|
||||
+ continue
|
||||
+ }
|
||||
+ sum += rssi
|
||||
+ count++
|
||||
+ }
|
||||
+ if count == 0 {
|
||||
+ return 0, false
|
||||
+ }
|
||||
+ return math.Round(float64(sum) / float64(count)), true
|
||||
+}
|
||||
+
|
||||
+func collectOpenWrtAP(ctx context.Context) map[string]system.WiFi {
|
||||
+ results := make(map[string]system.WiFi)
|
||||
+
|
||||
+ out, err := exec.CommandContext(ctx, "iwinfo").Output()
|
||||
+ if err != nil && len(out) == 0 {
|
||||
+ return results
|
||||
+ }
|
||||
+
|
||||
+ for _, b := range splitIwinfoBlocks(string(out)) {
|
||||
+ if ctx.Err() != nil {
|
||||
+ return results
|
||||
+ }
|
||||
+ ssid, isAP := parseIwinfoInfo(b.text)
|
||||
+ if !isAP {
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ reading := system.WiFi{SSID: validSSID(ssid)}
|
||||
+ if assocOut, err := exec.CommandContext(ctx, "iwinfo", b.name, "assoclist").Output(); err == nil {
|
||||
+ if avg, ok := parseAssocRSSI(string(assocOut)); ok {
|
||||
+ reading.Signal = &avg
|
||||
+ }
|
||||
+ }
|
||||
+ results[b.name] = reading
|
||||
+ }
|
||||
+ return results
|
||||
}
|
||||
|
||||
func collectLinux(ctx context.Context, client linuxClient) map[string]system.WiFi {
|
||||
Reference in new issue
Block a user