初始化项目

This commit is contained in:
BBIT-Kai
2026-05-26 13:53:23 +08:00
commit 7e803e2cdb
27 changed files with 1820 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.idea/
build/
logs/
*.exe
+5
View File
@@ -0,0 +1,5 @@
version_code: 27
pid: 29412
need_update: false
control_state: CONTROL_LOST
last_alive_at: 1771987569
+44
View File
@@ -0,0 +1,44 @@
# build_all.ps1
# 一键打包 main 和 updater,三平台: Windows 64, Linux amd64, Linux arm64
# -------------------------
# 项目根目录
$RootDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Set-Location $RootDir
# -------------------------
# 定义要打包的平台
$targets = @(
@{ OS="windows"; ARCH="amd64"; Dir="build/win"; MainOut="main.exe"; UpdaterOut="updater.exe" },
@{ OS="linux"; ARCH="amd64"; Dir="build/linux_amd64"; MainOut="main"; UpdaterOut="updater" },
@{ OS="linux"; ARCH="arm64"; Dir="build/linux_arm64"; MainOut="main"; UpdaterOut="updater" }
)
# -------------------------
# 循环打包
foreach ($t in $targets) {
Write-Host "=== Build $($t.OS)-$($t.ARCH) Version ==="
# 设置环境变量
$env:GOOS = $t.OS
$env:GOARCH = $t.ARCH
$env:CGO_ENABLED = "0"
# 创建输出目录
if (!(Test-Path $t.Dir)) {
New-Item -ItemType Directory -Path $t.Dir | Out-Null
}
# 编译 main
Write-Host "Building main..."
go build -ldflags="-s -w" -o "$($t.Dir)/$($t.MainOut)" ./main
# 编译 updater
Write-Host "Building updater..."
go build -ldflags="-s -w" -o "$($t.Dir)/$($t.UpdaterOut)" ./updater
Write-Host "$($t.OS)-$($t.ARCH) Build complete, Output dir: $($t.Dir)"
Write-Host ""
}
Write-Host "All builds finished."
+31
View File
@@ -0,0 +1,31 @@
[Unit]
Description=Sentinel Updater Service
After=network.target
Wants=network.target
[Service]
Type=simple
# 启动前确保可执行权限
ExecStartPre=/bin/chmod +x /opt/edge/updater
ExecStart=/opt/edge/updater
WorkingDirectory=/opt/edge/
User=root
Group=root
# 崩溃自动拉起
Restart=always
RestartSec=10
# 日志直接进 journalctl
StandardOutput=journal
StandardError=journal
# 可选:限制资源
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
+52
View File
@@ -0,0 +1,52 @@
module sentinel
go 1.25.5
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.97 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+107
View File
@@ -0,0 +1,107 @@
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+174
View File
@@ -0,0 +1,174 @@
package main
import (
"context"
"encoding/json"
"fmt"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/docker"
"sentinel/pkg/log"
model2 "sentinel/pkg/model"
"sentinel/pkg/utils"
"time"
)
type BusinessService struct {
mqtt *MQTTService
dockerManager docker.DockerManager
deviceID string
deptId string
cmdTopic string
deviceType string
}
func NewBusinessService(m *MQTTService, deviceID string, dm docker.DockerManager) *BusinessService {
return &BusinessService{
mqtt: m,
dockerManager: dm,
deviceID: deviceID,
}
}
func (b *BusinessService) Start() error {
if err := b.mqtt.SubscribeTopic(getInitTopic(b.deviceID), 1); err != nil {
return err
}
b.mqtt.SetMessageHandler(b.onMQTTMessage)
return nil
}
func getInitTopic(deviceID string) string {
return "+/+/+/" + deviceID + "/#"
}
func (b *BusinessService) getOwnTopic(deviceID string) string {
return b.deptId + "/cmd/" + b.deviceType + "/" + deviceID + "/#"
}
// 消息处理
func (b *BusinessService) onMQTTMessage(topic string, payload []byte) {
model := model2.FromStringToMqttTopic(topic)
payload_json, err := utils.PayloadToMap(payload)
if err != nil {
fmt.Println("解析失败:", err)
return
}
// 指令
if model.Domain == "cmd" && model.DeviceType == b.deviceType {
log.Println("收到指令:", model.Resource)
switch model.Resource {
case "shutdown":
payload_json["massage"] = b.handleShutdown()
case "restart":
payload_json["massage"] = b.handleRestart()
case "check_update":
payload_json["massage"] = b.handleCheckUpdate()
default:
log.Println("未知的命令:", model.Resource)
payload_json["massage"] = "未知的命令"
}
topic := "x/receipt/x/" + device.GetDeviceID() + "/info"
payload := utils.JSONToString(payload_json)
b.mqtt.PublicMsg(topic, payload, false) // 回执
} else if model.Domain == "status" && model.Resource == "receipt" {
b.deviceType = model.DeviceType
b.deptId = model.DeptId
// 取消订阅之前的初始化主题
if b.mqtt.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
log.Error("无法取消初始化主题")
return
}
// 新订阅属于自己的主题
if b.mqtt.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
log.Error("无法定于属于自己的主题")
return
}
log.Println("设备初始化成功:所属项目:", model.DeptId, "\t设备类型:", model.DeviceType)
}
}
func getStatusInfoTopicPayload(isOnline bool) (string, string) {
info := map[string]interface{}{
"version": config.APP_VERSION,
"online": isOnline,
"ip": utils.GetLocalIP(),
"hostname": utils.GetHostname(),
"mac": utils.GetMacAddress(),
"os": utils.GetOSInfo(),
"cpu": utils.GetCPUInfo(),
"memory_total": utils.GetMemory(),
"disk_total": utils.GetDisk(),
"last_seen": time.Now().UTC().Format(time.RFC3339),
}
payloadBytes, _ := json.Marshal(info)
payload := string(payloadBytes) // 转成 string
return "x/status/x/" + device.GetDeviceID() + "/info", payload
}
// 关闭程序(立即退出)
func (b *BusinessService) handleShutdown() string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
b.dockerManager.StopAndRemoveContainer(ctx)
return "程序已退出"
}
// 重启程序
// 重启程序
func (b *BusinessService) handleRestart() string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// 先停止并移除旧容器
if err := b.dockerManager.StopAndRemoveContainer(ctx); err != nil {
log.Println("警告:移除容器失败,可能容器不存在:", err)
}
// 拉取最新镜像
log.Println("正在拉取镜像:", config.DOCKER_IMAGE)
if err := b.dockerManager.PullImage(ctx, config.DOCKER_IMAGE); err != nil {
log.Println("镜像拉取失败:", err)
return "程序重启失败,拉取镜像失败"
}
// 启动新容器
if err := b.dockerManager.RunContainer(ctx); err != nil {
log.Println("容器启动失败:", err)
return "程序重启失败,容器启动失败"
}
return "程序已重启"
}
func (b *BusinessService) handleCheckUpdate() string {
cfg := config.LoadConfig()
cfg.NeedUpdate = true
if err := config.WriteLocalConfig(&cfg); err != nil {
log.Println("load config failed:", err)
}
return "程序更新机制已触发"
}
//func (b *BusinessService) handleCheckUpdate() {
// args := []string{
// "--version", strconv.Itoa(config.APP_VERSION),
// }
//
// launcher := newUpdaterLauncher()
//
// if err := launcher.Start(args); err != nil {
// log.Println("[BUS] failed to start updater:", err)
// return
// }
//
// log.Println(
// "[BUS] updater started, exiting main program",
// )
//
// time.Sleep(500 * time.Millisecond)
// os.Exit(0)
//}
+122
View File
@@ -0,0 +1,122 @@
package main
import (
"context"
"fmt"
"os"
"time"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/docker"
"sentinel/pkg/log"
)
func main() {
deviceID := device.GetDeviceID()
// APP Logo
InitPrint(deviceID)
// Config
InitConfigFile()
// MQTT
InitMQTT(deviceID)
}
func InitConfigFile() {
// 第一次写入默认配置
cfg := config.AppConfig{
VersionCode: config.APP_VERSION,
PID: os.Getpid(),
NeedUpdate: false,
ControlState: "DEGRADED",
LastAliveAt: time.Now().Unix(),
}
// 写入文件
if err := config.WriteLocalConfig(&cfg); err != nil {
log.Println("[config] 写入初始配置失败:", err)
}
}
func InitPrint(deviceID string) {
banner := `
==============================================================================
██████ █████ ██████ ██████ ██ ██████ ██████ ██ ██ ██████
██ ██ ██ ██ ██ ████ ██ ██ ███ ██ ██
█████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ████ ██ ██
██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██
██████ █████ █████ ██████ ██ ██ █████ ██████ ██ ██ ██
==============================================================================
`
log.Println(banner)
log.Println("Device id: "+deviceID, "\t版本号: ", config.APP_VERSION) // 第一次启动记录
}
func InitMQTT(deviceID string) {
var mqttSvc *MQTTService
firstFail := true // 标记是否第一次失败
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
for {
err := mqttSvc.Connect()
if err != nil {
if firstFail {
log.Error("物联网服务器连接失败,如未注册设备,请先注册: " + deviceID)
firstFail = false
}
time.Sleep(3 * time.Second) // 5秒后重试
continue
}
break
}
defer mqttSvc.Close()
dm, err := docker.NewDockerManager()
if err != nil {
log.Fatal(err)
}
biz := NewBusinessService(mqttSvc, deviceID, *dm)
for {
// MQTT业务
err := biz.Start()
if err != nil {
log.Error("business service start failed: " + err.Error())
fmt.Println("业务启动失败,5秒后重试...")
time.Sleep(5 * time.Second)
continue
}
break
}
// 第一次运行直接启动
biz.handleRestart()
// 4️⃣ T2 超时自毁检查
tickerLost := time.NewTicker(10 * time.Second)
defer tickerLost.Stop()
go func() {
for range tickerLost.C {
cfg := config.LoadConfig()
now := time.Now().Unix()
if cfg.ControlState == "DEGRADED" && now-cfg.LastAliveAt > config.DAEMON_ALIVE_GAP_SECONDS {
log.Println("[main] MQTT 长期失联,进入 CONTROL_LOST")
cfg.ControlState = "CONTROL_LOST"
cfg.LastAliveAt = time.Now().Unix()
if err := config.WriteLocalConfig(cfg); err != nil {
log.Println("[main] 写状态失败:", err)
}
// 停止业务容器
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
dm.StopAndRemoveContainer(ctx)
// 自杀退出,让 daemon 重启
os.Exit(1)
}
}
}()
<-make(chan struct{})
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "sentinel/pkg/platform"
func newUpdaterLauncher() platform.UpdaterLauncher {
return &updaterLauncher{}
}
+20
View File
@@ -0,0 +1,20 @@
//go:build linux
// +build linux
package main
import (
"os"
"os/exec"
)
type updaterLauncher struct{}
func (u *updaterLauncher) Start(args []string) error {
cmd := exec.Command("./updater", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Linux 下先不做进程组处理,保证能跑
return cmd.Start()
}
+24
View File
@@ -0,0 +1,24 @@
//go:build windows
// +build windows
package main
import (
"os"
"os/exec"
"syscall"
)
type updaterLauncher struct{}
func (u *updaterLauncher) Start(args []string) error {
cmd := exec.Command("./updater.exe", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
return cmd.Start()
}
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"crypto/tls"
"sentinel/pkg/config"
"sentinel/pkg/log"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
type MQTTService struct {
client mqtt.Client
handler func(topic string, payload []byte)
subscriptions map[string]struct{} // 记录已订阅 topic
}
func NewMQTTService(broker, clientID, username, password string, keepalive int) *MQTTService {
opts := mqtt.NewClientOptions()
opts.AddBroker(broker)
opts.SetClientID(clientID)
opts.SetUsername(username)
opts.SetPassword(password)
opts.AutoReconnect = true
opts.OnReconnecting = func(c mqtt.Client, opts *mqtt.ClientOptions) {
log.Println("正在重连到物联网服务器...")
}
opts.SetKeepAlive(time.Duration(keepalive) * time.Second)
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
// 设置遗嘱消息
lwtTopic, lwtPayload := getStatusInfoTopicPayload(false)
opts.SetWill(
lwtTopic, // topic
lwtPayload, // payload
1, // qos
false, // retained
)
ms := &MQTTService{subscriptions: make(map[string]struct{})}
opts.SetDefaultPublishHandler(func(c mqtt.Client, m mqtt.Message) {
if ms.handler != nil {
ms.handler(m.Topic(), m.Payload())
}
})
opts.OnConnect = func(c mqtt.Client) {
log.Println("物联网服务器已连接")
cfg := config.LoadConfig()
cfg.ControlState = "CONNECTED"
cfg.LastAliveAt = time.Now().Unix()
config.WriteLocalConfig(cfg)
// 连接成功 发送状态信息
topic, payload := getStatusInfoTopicPayload(true)
ms.PublicMsg(topic, payload, false)
}
opts.OnConnectionLost = func(c mqtt.Client, err error) {
log.Println("物联网服务器已断开:", err)
cfg := config.LoadConfig()
cfg.ControlState = "DEGRADED" // 表示短暂断开
cfg.LastAliveAt = time.Now().Unix() // 更新这个时间戳,让 daemon 与 主程序看到尚可忍耐
if err := config.WriteLocalConfig(cfg); err != nil {
log.Println("[main] 写状态失败:", err)
}
}
ms.client = mqtt.NewClient(opts)
return ms
}
func (m *MQTTService) Connect() error {
token := m.client.Connect()
if token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}
func (m *MQTTService) Subscribe(topic string, qos byte) error {
token := m.client.Subscribe(topic, qos, nil)
if token.Wait() && token.Error() != nil {
return token.Error()
}
log.Debug("物联网服务消息订阅:", topic)
return nil
}
func (m *MQTTService) Publish(topic string, qos byte, retained bool, payload interface{}) error {
token := m.client.Publish(topic, qos, retained, payload)
token.Wait()
return token.Error()
}
func (m *MQTTService) SetMessageHandler(h func(topic string, payload []byte)) {
m.handler = h
}
func (m *MQTTService) Close() {
if m.client != nil {
m.client.Disconnect(250)
}
}
func (m *MQTTService) UnsubscribeTopic(topic string) error {
token := m.client.Unsubscribe(topic)
go func() {
if token.Wait() && token.Error() != nil {
log.Println("取消订阅失败:", token.Error())
} else {
delete(m.subscriptions, topic)
log.Debug("取消订阅成功:", topic)
}
}()
return nil
}
// UnsubscribeAll 取消所有已订阅 topic
func (m *MQTTService) UnsubscribeAll() {
for topic := range m.subscriptions {
_ = m.client.Unsubscribe(topic)
delete(m.subscriptions, topic)
}
}
// SubscribeTopic 订阅指定 topic,并记录可取消
func (m *MQTTService) SubscribeTopic(topic string, qos byte) error {
token := m.client.Subscribe(topic, qos, nil)
go func() {
if token.Wait() && token.Error() != nil {
log.Println("订阅失败:", token.Error())
} else {
m.subscriptions[topic] = struct{}{}
log.Debug("订阅成功:", topic)
}
}()
return nil
}
func (m *MQTTService) PublicMsg(topic string, payload string, retained bool) {
qos := byte(1)
log.Debug("发送消息:", topic)
if err := m.Publish(topic, qos, retained, payload); err != nil {
log.Println("发送信息出错:", err)
} else {
log.Debug("发送信息:", string(payload))
}
}
+105
View File
@@ -0,0 +1,105 @@
package config
import (
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
// 常量
const (
// 版本号
APP_VERSION = 29
BASE_URL = "https://ai.ronsunny.cn:8090"
//BASE_URL = "http://127.0.0.1:13011"
LOG_FILE_DIC = "./logs"
LOG_CHECK_INTERVAL_HOURS = 10 // 文件轮询检查间隔(秒)
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
PASSWORD = "123456"
DOCKER_REGISTRY = "ai.ronsunny.cn:13011"
DOCKER_USERNAME = "iot_device"
DOCKER_PASSWORD = "Bbit000000"
DOCKER_IMAGE = "ai.ronsunny.cn:13011/bbit_iot/ce_sentinel"
DOCKER_CONTAINER_NAME = "BBIT_Project"
DOCKER_TIME_OUT = 30 * time.Second
DAEMON_UPDATE_CHECK_INTERVAL_SECONDS = 5 // 文件轮询检查间隔(秒)
DAEMON_ALIVE_GAP_SECONDS = 30
)
var (
DOCKER_CONTAINER_BINDS = []string{
"/opt/sentinel/logs:/app/logs:rw",
}
)
type AppConfig struct {
VersionCode int `yaml:"version_code"`
PID int `yaml:"pid"`
NeedUpdate bool `yaml:"need_update"`
/**
状态 语义
CONNECTED MQTT 已连,设备可控
DEGRADED MQTT 短暂失联,容忍期
CONTROL_LOST MQTT 长期失联,不应继续运行
*/
ControlState string `yaml:"control_state"` // CONNECTED / DEGRADED / CONTROL_LOST
LastAliveAt int64 `yaml:"last_alive_at"` // Unix 秒
}
func LoadConfig() *AppConfig {
cfg := &AppConfig{}
if err := loadLocalConfig(cfg); err != nil {
print("[daemon] 加载配置失败,当作新状态处理:", err)
}
return cfg
}
func loadLocalConfig[T any](cfg *T) error {
exePath, err := os.Executable()
if err != nil {
return err
}
dir := filepath.Dir(exePath)
cfgPath := filepath.Join(dir, "config.yaml")
// 不存在就创建一个默认的
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
cfg := &AppConfig{
VersionCode: -1,
PID: -1,
NeedUpdate: false,
}
return WriteLocalConfig(cfg)
}
data, err := os.ReadFile(cfgPath)
if err != nil {
return err
}
if err := yaml.Unmarshal(data, cfg); err != nil {
return err
}
return nil
}
func WriteLocalConfig(cfg any) error {
exePath, err := os.Executable()
if err != nil {
return err
}
dir := filepath.Dir(exePath)
cfgPath := filepath.Join(dir, "config.yaml")
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return os.WriteFile(cfgPath, data, 0644)
}
+40
View File
@@ -0,0 +1,40 @@
package device
import (
"net"
"os"
"strings"
)
// GetDeviceID 返回本机原始唯一IDLinux /etc/machine-id 或 hostname+MAC
func GetDeviceID() string {
// 尝试读取 Linux /etc/machine-id
if data, err := os.ReadFile("/etc/machine-id"); err == nil {
s := strings.TrimSpace(string(data))
if s != "" {
return s
}
}
// fallback: hostname + first non零MAC
hn, _ := os.Hostname()
mac := getFirstMac()
return hn + "|" + mac
}
func getFirstMac() string {
ifaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, it := range ifaces {
if len(it.HardwareAddr) == 0 {
continue
}
mac := it.HardwareAddr.String()
if mac != "" && mac != "00:00:00:00:00:00" {
return mac
}
}
return ""
}
+184
View File
@@ -0,0 +1,184 @@
package docker
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"sentinel/pkg/config"
"sentinel/pkg/device"
"time"
"sentinel/pkg/log"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
)
type DockerManager struct {
registry string
username string
password string
cli *client.Client
timeout time.Duration
}
func NewDockerManager() (*DockerManager, error) {
cli, err := client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
if err != nil {
return nil, err
}
return &DockerManager{
cli: cli,
registry: config.DOCKER_REGISTRY,
username: config.DOCKER_USERNAME,
password: config.DOCKER_PASSWORD,
timeout: config.DOCKER_TIME_OUT,
}, nil
}
func (d *DockerManager) PullImage(ctx context.Context, refStr string) error {
auth := registry.AuthConfig{
ServerAddress: d.registry,
Username: d.username,
Password: d.password,
}
authBytes, _ := json.Marshal(auth)
authStr := base64.StdEncoding.EncodeToString(authBytes)
reader, err := d.cli.ImagePull(ctx, refStr, image.PullOptions{
RegistryAuth: authStr,
})
if err != nil {
return err
}
defer reader.Close()
// 消费输出,避免挂起
_, _ = io.Copy(io.Discard, reader)
return nil
}
func (d *DockerManager) RunContainer(ctx context.Context) error {
// 停止并删除已有容器
if err := d.StopAndRemoveContainer(ctx); err != nil {
return err
}
// 创建容器
log.Println("正在启动名为<", config.DOCKER_CONTAINER_NAME, ">的容器")
resp, err := d.cli.ContainerCreate(
ctx,
&container.Config{
Image: config.DOCKER_IMAGE,
Env: []string{
"DEVICE_ID=" + device.GetDeviceID(),
"LD_LIBRARY_PATH=/opt/nvidia/deepstream/deepstream/lib" +
":/opt/nvidia/deepstream/deepstream/lib/triton" +
":/opt/nvidia/deepstream/deepstream/lib/rivermax" +
":/opt/nvidia/vpi3/lib/aarch64-linux-gnu" +
":/usr/lib/aarch64-linux-gnu" +
":/usr/lib/aarch64-linux-gnu/nvidia" +
":/usr/local/cuda-12.6/lib64",
"GST_PLUGIN_PATH=/opt/nvidia/deepstream/deepstream/lib/gst-plugins",
},
Healthcheck: &container.HealthConfig{
Test: []string{"CMD-SHELL", "echo ok"},
Interval: 5 * time.Second,
Timeout: 2 * time.Second,
Retries: 3,
},
},
&container.HostConfig{
Runtime: "nvidia",
Privileged: true,
Binds: []string{
"/usr/lib/aarch64-linux-gnu:/usr/lib/aarch64-linux-gnu:ro",
"/opt/nvidia/deepstream/deepstream/lib:/opt/nvidia/deepstream/deepstream/lib:ro",
"/opt/nvidia/vpi3/lib/aarch64-linux-gnu/:/opt/nvidia/vpi3/lib/aarch64-linux-gnu/:ro",
"/usr/local/cuda-12.6/lib64/:/usr/local/cuda-12.6/lib64/:ro",
"/tmp/argus_socket:/tmp/argus_socket",
},
NetworkMode: "host",
},
nil,
nil,
config.DOCKER_CONTAINER_NAME,
)
if err != nil {
return err
}
// 启动容器
if err := d.cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
return err
}
log.Println("容器已成功运行")
return nil
}
// StopAndRemoveContainer 停止并删除指定容器
func (d *DockerManager) StopAndRemoveContainer(ctx context.Context) error {
containers, err := d.cli.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "name", Value: config.DOCKER_CONTAINER_NAME},
),
})
if err != nil {
return err
}
for _, c := range containers {
log.Println("正在停止名为<", config.DOCKER_CONTAINER_NAME, ">的容器:", c.ID)
timeout := 10 * time.Second
seconds := int(timeout.Seconds())
if err := d.cli.ContainerStop(ctx, c.ID, container.StopOptions{
Timeout: &seconds,
}); err != nil {
log.Println("Failed to stop container %s: %v", c.ID, err)
return err
}
if err := d.cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{}); err != nil {
log.Println("Failed to remove container %s: %v", c.ID, err)
return err
}
}
return nil
}
func (d *DockerManager) CheckContainerHealth(ctx context.Context, containerName string) (string, error) {
if containerName == "" {
return "", errors.New("containerName must not be empty")
}
containers, err := d.cli.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "name", Value: containerName},
),
})
if err != nil {
return "", err
}
if len(containers) == 0 {
return "", errors.New("container not found")
}
inspect, err := d.cli.ContainerInspect(ctx, containers[0].ID)
if err != nil {
return "", err
}
if inspect.State.Health != nil {
return inspect.State.Health.Status, nil
}
return "unknown", nil
}
+110
View File
@@ -0,0 +1,110 @@
package log
import (
"fmt"
"os"
"path/filepath"
"sentinel/pkg/config"
"time"
)
var logDir = "./logs" // 日志目录,可根据需要修改
// 初始化日志目录
func Init(dir string) {
if dir != "" {
logDir = dir
}
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Println("create log dir failed:", err)
}
go func() {
ticker := time.NewTicker(config.LOG_CHECK_INTERVAL_HOURS * time.Hour)
defer ticker.Stop()
for range ticker.C {
cleanupOldLogs()
}
}()
}
// Cleanup 删除超过7天的日志文件
func cleanupOldLogs() {
files, err := os.ReadDir(logDir)
if err != nil {
return
}
cutoff := time.Now().AddDate(0, 0, -7)
for _, f := range files {
if f.IsDir() {
continue
}
info, err := f.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
_ = os.Remove(filepath.Join(logDir, f.Name()))
}
}
}
// log 内部写文件
func logToFile(level, msg string) {
fmt.Println(msg)
t := time.Now()
// 确保日志目录存在
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Println("create log dir failed:", err)
return
}
filename := filepath.Join(logDir, t.Format("2006-01-02")+".log")
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Println("open log file failed:", err)
return
}
defer f.Close()
line := fmt.Sprintf("%s [%s] %s\n", t.Format("2006-01-02 15:04:05"), level, msg)
_, _ = f.WriteString(line)
}
// 对外接口
func Info(msg string) {
logToFile("INFO", msg)
}
// Println 支持多个参数拼接,写 INFO 日志
func Println(v ...interface{}) {
msg := fmt.Sprint(v...)
logToFile("INFO", msg)
}
func Debug(v ...interface{}) {
//msg := fmt.Sprint(v...)
//logToFile("DEBUF", msg)
}
func Warn(msg string) {
logToFile("WARN", msg)
}
func Error(msg string) {
logToFile("ERROR", msg)
}
func Fatal(err error) {
if err == nil {
return
}
logToFile("ERROR", err.Error())
}
// Fatal 打印错误日志并退出程序
func Fatalf(msg string, args ...interface{}) {
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
logToFile("FATAL", msg)
os.Exit(1)
}
+7
View File
@@ -0,0 +1,7 @@
package model
type BaseResponse struct {
Status bool `json:"status"` // 是否成功
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
}
+64
View File
@@ -0,0 +1,64 @@
package model
import (
"errors"
"strings"
)
type MqttTopic struct {
DeptId string
Domain string
DeviceType string
DeviceID string
Resource string
}
// 从字符串解析成 MqttTopic
func FromStringToMqttTopic(topic string) *MqttTopic {
parts := strings.Split(topic, "/")
// 补齐不足的部分
for len(parts) < 5 {
parts = append(parts, "")
}
return &MqttTopic{
DeptId: parts[0],
Domain: parts[1],
DeviceType: parts[2],
DeviceID: parts[3],
Resource: parts[4],
}
}
// 从结构体生成 topic 字符串,可用 "+" 表示通配符
func (m *MqttTopic) ToString() string {
toVal := func(s string) string {
if s == "" {
return "+"
}
return s
}
return strings.Join([]string{
toVal(m.DeptId),
toVal(m.Domain),
toVal(m.DeviceType),
toVal(m.DeviceID),
toVal(m.Resource),
}, "/")
}
// 严格生成 topic,不允许 "+" 或空
func (m *MqttTopic) Build() (string, error) {
parts := []string{m.DeptId, m.Domain, m.DeviceType, m.DeviceID, m.Resource}
for _, p := range parts {
if p == "" || p == "+" {
return "", errors.New("cannot build strict topic, wildcard exists")
}
}
return strings.Join(parts, "/"), nil
}
// 判断是否为通配符 topic
func (m *MqttTopic) IsWildcard() bool {
topic := m.ToString()
return strings.Contains(topic, "+") || strings.Contains(topic, "#")
}
+7
View File
@@ -0,0 +1,7 @@
package model
type UpdateInfo struct {
Version int `json:"version"`
DownloadURL string `json:"url"`
Notes string `json:"notes"`
}
+22
View File
@@ -0,0 +1,22 @@
package api
import "sentinel/pkg/model"
const (
updateCheckURL = "/iot/common/update/check"
)
func CheckUpdate(deviceID string) (*model.UpdateInfo, error) {
var resp model.UpdateInfo
err := Get(
updateCheckURL,
map[string]string{
"deviceID": deviceID,
},
&resp,
)
if err != nil {
return nil, err
}
return &resp, nil
}
+101
View File
@@ -0,0 +1,101 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sentinel/pkg/model"
"time"
"sentinel/pkg/config"
"sentinel/pkg/log"
)
var client = &http.Client{
Timeout: 5 * time.Second,
}
func Get(path string, query map[string]string, out any) error {
return do(http.MethodGet, path, query, nil, out)
}
func Post(path string, body any, out any) error {
return do(http.MethodPost, path, nil, body, out)
}
func do(method, path string, query map[string]string, body any, out any) error {
u, err := url.Parse(config.BASE_URL + path)
if err != nil {
log.Error("parse url failed: " + err.Error())
return err
}
if len(query) > 0 {
q := u.Query()
for k, v := range query {
q.Set(k, v)
}
u.RawQuery = q.Encode()
}
var reqBody *bytes.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
log.Error("marshal body failed: " + err.Error())
return err
}
reqBody = bytes.NewReader(b)
} else {
reqBody = bytes.NewReader(nil)
}
req, err := http.NewRequest(method, u.String(), reqBody)
if err != nil {
log.Error("create request failed: " + err.Error())
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("apikey", "NzusyzcLIUoZ22tflHN2sOjHrry3W7zJ")
resp, err := client.Do(req)
if err != nil {
log.Error("request failed: " + err.Error())
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Error("http status error: " + resp.Status)
return fmt.Errorf("http status %d", resp.StatusCode)
}
// 解析成 BaseResponse
var baseResp model.BaseResponse
if err := json.NewDecoder(resp.Body).Decode(&baseResp); err != nil {
log.Error("decode base response failed: " + err.Error())
return err
}
if !baseResp.Status {
log.Error("server returned error: " + baseResp.Message)
return fmt.Errorf(baseResp.Message)
}
if out != nil && baseResp.Data != nil {
// 将 Data 转成业务类型
b, err := json.Marshal(baseResp.Data)
if err != nil {
log.Error("marshal data failed: " + err.Error())
return err
}
if err := json.Unmarshal(b, out); err != nil {
log.Error("unmarshal data to out failed: " + err.Error())
return err
}
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
package platform
type UpdaterLauncher interface {
Start(args []string) error
}
type MainProgramStarter interface {
Start(targetExe string) error
GetMainName() string
IsProcessRunning(pid int) (bool, error)
KillProcess(pid int) error
}
+117
View File
@@ -0,0 +1,117 @@
package utils
import (
"encoding/json"
"fmt"
"net"
"os"
"runtime"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
)
func GetLocalIP() string {
ifaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // 接口未启用
}
if iface.Flags&net.FlagLoopback != 0 {
continue // 忽略回环
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue
}
return ip.String()
}
}
return ""
}
func GetHostname() string {
name, err := os.Hostname()
if err != nil {
return ""
}
return name
}
func GetMacAddress() string {
ifaces, err := net.Interfaces()
if err != nil {
return ""
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
mac := iface.HardwareAddr.String()
if mac != "" {
return mac
}
}
return ""
}
func GetOSInfo() string {
return runtime.GOOS // windows, linux, darwin
}
func GetCPUInfo() string {
info, err := cpu.Info()
if err != nil || len(info) == 0 {
return ""
}
return info[0].ModelName
}
func GetMemory() string {
v, err := mem.VirtualMemory()
if err != nil {
return ""
}
return fmt.Sprintf("%dMB", v.Total/1024/1024)
}
func GetDisk() string {
usage, err := disk.Usage("/")
if err != nil {
return ""
}
return fmt.Sprintf("%dGB", usage.Total/1024/1024/1024)
}
func PayloadToMap(payload []byte) (map[string]interface{}, error) {
var result map[string]interface{}
err := json.Unmarshal(payload, &result)
if err != nil {
return nil, err
}
return result, nil
}
func JSONToString(data map[string]interface{}) string {
jsonBytes, err := json.Marshal(data)
if err != nil {
return ""
}
return string(jsonBytes)
}
+189
View File
@@ -0,0 +1,189 @@
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/log"
"sentinel/pkg/net"
"sentinel/pkg/platform"
"time"
)
func main() {
log.Println("[daemon] 设备ID:", device.GetDeviceID())
log.Init(config.LOG_FILE_DIC)
starter := newMainProgramStarter()
// 启动主程序
initMainProcess(starter)
ticker := time.NewTicker(config.DAEMON_UPDATE_CHECK_INTERVAL_SECONDS * time.Second)
defer ticker.Stop()
go func() {
for range ticker.C {
//log.Println("[daemon] 定时检查主程序运行状态")
handleProcessCheck(starter)
cfg := config.LoadConfig()
if cfg.NeedUpdate {
log.Println("[daemon] 配置文件标记需要更新,触发更新")
cfg.NeedUpdate = false
if err := config.WriteLocalConfig(cfg); err != nil {
log.Println("[daemon] 写回配置文件失败:", err)
}
handleUpdate(starter)
}
}
}()
select {}
}
func initMainProcess(starter platform.MainProgramStarter) {
if err := startMainProgram(starter); err != nil {
log.Println("[daemon] 启动失败,回退到更新:", err)
handleUpdate(starter)
}
}
var lastState string
var lastPID int
func handleProcessCheck(starter platform.MainProgramStarter) {
cfg := config.LoadConfig()
// 1. PID 是否存在
running, err := starter.IsProcessRunning(cfg.PID)
if cfg.PID <= 0 || err != nil || !running {
if lastState != "NOT_RUNNING" {
log.Println("[daemon] 主程序未运行,尝试启动")
lastState = "NOT_RUNNING"
lastPID = 0
}
restartMain(starter)
return
}
// 2. 语义健康判断
switch cfg.ControlState {
case "DEGRADED":
if lastState != "DEGRADED" {
log.Println("[daemon] 主程序存在错误,等待主程序自行处理")
lastState = "DEGRADED"
lastPID = cfg.PID
}
return
case "CONTROL_LOST":
if lastState != "CONTROL_LOST" {
log.Println("[daemon] 主程序心跳超时,已失控,强制重启")
lastState = "CONTROL_LOST"
lastPID = cfg.PID
}
restartMain(starter)
return
default:
// 3. 健康
if lastState != "HEALTHY" || lastPID != cfg.PID {
log.Println("[daemon] 主程序健康运行,PID:", cfg.PID)
lastState = "HEALTHY"
lastPID = cfg.PID
}
}
}
func restartMain(starter platform.MainProgramStarter) {
cfg := config.LoadConfig()
if cfg.PID > 0 {
_ = starter.KillProcess(cfg.PID)
time.Sleep(300 * time.Millisecond)
}
_ = startMainProgram(starter)
}
func handleUpdate(starter platform.MainProgramStarter) {
cfg := config.LoadConfig()
info, err := api.CheckUpdate(device.GetDeviceID())
if err != nil {
log.Println("[daemon] 检查更新失败:", err)
return
}
if info.Version <= cfg.VersionCode {
log.Println("[daemon] 没有新版本,跳过")
return
}
log.Println("[daemon] 发现新版本 %d\n", info.Version)
// 停止主程序
if cfg.PID > 0 {
_ = starter.KillProcess(cfg.PID)
time.Sleep(500 * time.Millisecond)
}
if err := downloadAndReplace(starter, info.DownloadURL); err != nil {
log.Println("[daemon] 更新失败:", err)
return
}
if err := startMainProgram(starter); err != nil {
log.Println("[daemon] 更新后启动失败:", err)
return
}
log.Println("[daemon] 更新完成")
}
func startMainProgram(starter platform.MainProgramStarter) error {
selfPath, _ := os.Executable()
selfDir := filepath.Dir(selfPath)
targetExe := filepath.Join(selfDir, starter.GetMainName())
if _, err := os.Stat(targetExe); os.IsNotExist(err) {
log.Println("[daemon] 主程序文件不存在,触发更新")
handleUpdate(starter)
return nil // 先不启动,等更新完再启动
}
err := starter.Start(targetExe)
return err
}
func downloadAndReplace(
starter platform.MainProgramStarter,
downloadURL string,
) error {
// 获取主程序路径
selfPath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取自身路径失败: %w", err)
}
selfDir := filepath.Dir(selfPath)
targetExe := filepath.Join(selfDir, starter.GetMainName())
outFile, err := os.Create(targetExe)
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer outFile.Close()
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(downloadURL)
if err != nil {
return fmt.Errorf("下载新程序失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败, HTTP状态码: %d", resp.StatusCode)
}
if _, err := io.Copy(outFile, resp.Body); err != nil {
return fmt.Errorf("写入目标文件失败: %w", err)
}
return nil
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "sentinel/pkg/platform"
func newMainProgramStarter() platform.MainProgramStarter {
return &mainProgramStarter{}
}
+54
View File
@@ -0,0 +1,54 @@
//go:build linux
// +build linux
package main
import (
"os"
"os/exec"
"strconv"
)
type mainProgramStarter struct{}
func (s *mainProgramStarter) GetMainName() string {
return "main"
}
func (s *mainProgramStarter) Start(targetExe string) error {
if err := os.Chmod(targetExe, 0755); err != nil {
return err
}
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Linux 下:先保持最简单,保证能跑
return cmd.Start()
}
// 判断进程是否在运行
func (s *mainProgramStarter) IsProcessRunning(pid int) (bool, error) {
if pid <= 0 {
return false, nil
}
procPath := "/proc/" + strconv.Itoa(pid)
_, err := os.Stat(procPath)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
// 其他错误(例如权限问题)
return false, err
}
// 杀掉进程
func (l *mainProgramStarter) KillProcess(pid int) error {
cmd := exec.Command("kill", "-9", strconv.Itoa(pid))
return cmd.Run()
}
+63
View File
@@ -0,0 +1,63 @@
//go:build windows
// +build windows
package main
import (
"os"
"os/exec"
"strconv"
"syscall"
"golang.org/x/sys/windows"
)
type mainProgramStarter struct{}
func (s *mainProgramStarter) GetMainName() string {
return "main.exe"
}
func (s *mainProgramStarter) Start(targetExe string) error {
cmd := exec.Command(targetExe)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Windows:新进程组,脱离 Ctrl+C / 父进程
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
return cmd.Start()
}
// 判断进程是否在运行
func (s *mainProgramStarter) IsProcessRunning(pid int) (bool, error) {
if pid <= 0 {
return false, nil
}
// 打开进程句柄
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
if err != nil {
// 权限不足,但进程存在
if err == windows.ERROR_ACCESS_DENIED {
return true, nil
}
// 进程不存在
if err == windows.ERROR_INVALID_PARAMETER {
return false, nil
}
// 其他错误
return false, err
}
defer windows.CloseHandle(handle)
return true, nil
}
// 杀掉进程
func (w *mainProgramStarter) KillProcess(pid int) error {
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
return cmd.Run()
}