重新整理物联网服务,分离业务逻辑。
This commit is contained in:
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>linux</w>
|
||||
<w>从海康sdk获取图片以及信息</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="github.com/pkg/errors" />
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/sentinel.iml" filepath="$PROJECT_DIR$/.idea/sentinel.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true">
|
||||
<buildTags>
|
||||
<option name="os" value="linux" />
|
||||
<option name="arch" value="arm64" />
|
||||
<option name="cgo" value="NO" />
|
||||
</buildTags>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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."
|
||||
@@ -0,0 +1,34 @@
|
||||
module sentinel
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // 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/philhofer/fwd v1.2.0 // 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
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
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/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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=
|
||||
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=
|
||||
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sentinel/pkg/config"
|
||||
"sentinel/pkg/log"
|
||||
model2 "sentinel/pkg/model"
|
||||
"sentinel/pkg/utils"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BusinessService struct {
|
||||
mqtt *MQTTService
|
||||
deviceID string
|
||||
deptId string
|
||||
cmdTopic string
|
||||
deviceType string
|
||||
subscriptions map[string]struct{} // 记录已订阅 topic
|
||||
}
|
||||
|
||||
func NewBusinessService(m *MQTTService, deviceID string) *BusinessService {
|
||||
return &BusinessService{
|
||||
mqtt: m,
|
||||
deviceID: deviceID,
|
||||
subscriptions: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeTopic 订阅指定 topic,并记录可取消
|
||||
func (b *BusinessService) SubscribeTopic(topic string, qos byte) error {
|
||||
if err := b.mqtt.Subscribe(topic, qos); err != nil {
|
||||
return err
|
||||
}
|
||||
b.subscriptions[topic] = struct{}{}
|
||||
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) Start() error {
|
||||
if err := b.SubscribeTopic(getInitTopic(b.deviceID), 1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mqtt.SetMessageHandler(b.onMQTTMessage)
|
||||
|
||||
// 第一次连接就发送状态信息
|
||||
b.SendStatusInfo()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsubscribeTopic 取消订阅指定 topic
|
||||
func (b *BusinessService) UnsubscribeTopic(topic string) error {
|
||||
token := b.mqtt.client.Unsubscribe(topic)
|
||||
if token.Wait() && token.Error() != nil {
|
||||
return token.Error()
|
||||
}
|
||||
delete(b.subscriptions, topic)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsubscribeAll 取消所有已订阅 topic
|
||||
func (b *BusinessService) UnsubscribeAll() {
|
||||
for topic := range b.subscriptions {
|
||||
_ = b.mqtt.client.Unsubscribe(topic)
|
||||
delete(b.subscriptions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息处理
|
||||
func (b *BusinessService) onMQTTMessage(topic string, payload []byte) {
|
||||
model := model2.FromStringToMqttTopic(topic)
|
||||
// 指令
|
||||
if model.Domain == "cmd" && model.DeviceType == b.deviceType {
|
||||
log.Println("收到指令:", model.Resource)
|
||||
switch model.Resource {
|
||||
case "ping":
|
||||
log.Println("pong")
|
||||
case "shutdown":
|
||||
b.handleShutdown()
|
||||
case "restart":
|
||||
b.handleRestart()
|
||||
case "check_update":
|
||||
b.handleCheckUpdate()
|
||||
default:
|
||||
log.Println("未知的命令:", model.Resource)
|
||||
}
|
||||
} else if model.Domain == "status" && model.Resource == "receipt" {
|
||||
b.deviceType = model.DeviceType
|
||||
b.deptId = model.DeptId
|
||||
// 取消订阅之前的初始化主题
|
||||
if b.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
|
||||
log.Error("无法取消初始化主题")
|
||||
return
|
||||
}
|
||||
// 新订阅属于自己的主题
|
||||
if b.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
|
||||
log.Error("无法定于属于自己的主题")
|
||||
return
|
||||
}
|
||||
log.Println("设备初始化成功:所属项目:", model.DeptId, "\t设备类型:", model.DeviceType)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BusinessService) SendStatusInfo() {
|
||||
info := map[string]interface{}{
|
||||
"version": config.APP_VERSION,
|
||||
"online": true,
|
||||
"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),
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(info)
|
||||
topic := "x/status/x/" + b.deviceID + "/info"
|
||||
qos := byte(1)
|
||||
retained := true
|
||||
log.Println("发送消息:", topic)
|
||||
|
||||
if err := b.mqtt.Publish(topic, qos, retained, payload); err != nil {
|
||||
log.Println("发送状态信息出错:", err)
|
||||
} else {
|
||||
log.Println("发送状态信息:", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭程序(立即退出)
|
||||
func (b *BusinessService) handleShutdown() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 重启程序
|
||||
func (b *BusinessService) handleRestart() {
|
||||
exe, _ := os.Executable()
|
||||
cmd := exec.Command(exe)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
_ = cmd.Start()
|
||||
os.Exit(0)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// handleCheckUpdate 触发更新流程(主程序侧)
|
||||
//func (b *BusinessService) handleCheckUpdate() {
|
||||
//
|
||||
// args := []string{
|
||||
// "--version", strconv.Itoa(config.APP_VERSION),
|
||||
// }
|
||||
//
|
||||
// cmd := exec.Command("./updater.exe", args...)
|
||||
// cmd.Stdout = os.Stdout
|
||||
// cmd.Stderr = os.Stderr
|
||||
//
|
||||
// // OS 级脱离父进程
|
||||
// switch runtime.GOOS {
|
||||
// case "windows":
|
||||
// cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
// CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if err := cmd.Start(); err != nil {
|
||||
// log.Println("[BUS] failed to start updater:", err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// log.Println(
|
||||
// "[BUS] updater started (pid=%d), exiting main program\n",
|
||||
// cmd.Process.Pid,
|
||||
// )
|
||||
//
|
||||
// // 给 updater 留出启动窗口(尤其是 systemd / docker 环境)
|
||||
// time.Sleep(500 * time.Millisecond)
|
||||
//
|
||||
// os.Exit(0)
|
||||
//}
|
||||
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"sentinel/pkg/config"
|
||||
"sentinel/pkg/device"
|
||||
"sentinel/pkg/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
banner := `
|
||||
==========================================================================
|
||||
|
||||
██████ █████ █████ ██████ ██ █████ ██████ ██ ██ ██████
|
||||
██ ██ ██ ██ ██ ████ ██ ██ ███ ██ ██
|
||||
█████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ████ ██ ██
|
||||
██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██
|
||||
██████ █████ █████ ██████ ██ ██ █████ ██████ ██ ██ ██
|
||||
|
||||
==========================================================================
|
||||
`
|
||||
fmt.Println(banner)
|
||||
deviceID := device.GetDeviceID()
|
||||
log.Init(config.Log_file_dic) // 初始化日志目录
|
||||
log.Info("Device id: " + deviceID) // 第一次启动记录
|
||||
log.Println("版本号: ", config.APP_VERSION) // 第一次启动记录
|
||||
|
||||
var mqttSvc *MQTTService
|
||||
firstFail := true // 标记是否第一次失败
|
||||
for {
|
||||
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
|
||||
err := mqttSvc.Connect()
|
||||
if err != nil {
|
||||
if firstFail {
|
||||
log.Error("物联网服务连接失败,如未注册设备,请先注册: " + deviceID)
|
||||
firstFail = false
|
||||
}
|
||||
time.Sleep(3 * time.Second) // 5秒后重试
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
defer mqttSvc.Close()
|
||||
|
||||
biz := NewBusinessService(mqttSvc, deviceID)
|
||||
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
|
||||
}
|
||||
|
||||
// 主线程循环,可做心跳或状态上报
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "sentinel/pkg/platform"
|
||||
|
||||
func newUpdaterLauncher() platform.UpdaterLauncher {
|
||||
return &updaterLauncher{}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sentinel/pkg/log"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
)
|
||||
|
||||
type MQTTService struct {
|
||||
client mqtt.Client
|
||||
handler func(topic string, payload []byte)
|
||||
}
|
||||
|
||||
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.SetKeepAlive(time.Duration(keepalive) * time.Second)
|
||||
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
||||
|
||||
ms := &MQTTService{}
|
||||
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("物联网服务已连接")
|
||||
}
|
||||
opts.OnConnectionLost = func(c mqtt.Client, err error) {
|
||||
log.Println("物联网服务已断开:", 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.Println("物联网服务消息订阅:", 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
// 变动
|
||||
|
||||
// 常量
|
||||
const (
|
||||
// 版本号
|
||||
APP_VERSION = 1
|
||||
|
||||
BASE_URL = "https://ai.ronsunny.cn:8090"
|
||||
//BASE_URL = "http://127.0.0.1:13011"
|
||||
Log_file_dic = "./logs"
|
||||
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
|
||||
PASSWORD = "123456"
|
||||
)
|
||||
|
||||
var (
|
||||
// DeviceType string
|
||||
// DeptId string
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetDeviceID 返回本机原始唯一ID(Linux /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 ""
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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)
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type BaseResponse struct {
|
||||
Status bool `json:"status"` // 是否成功
|
||||
Message string `json:"message"` // 提示信息
|
||||
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
|
||||
}
|
||||
@@ -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, "#")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type UpdateInfo struct {
|
||||
Version int `json:"version"`
|
||||
DownloadURL string `json:"url"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package platform
|
||||
|
||||
type UpdaterLauncher interface {
|
||||
Start(args []string) error
|
||||
}
|
||||
|
||||
type MainProgramStarter interface {
|
||||
Start(targetExe string) error
|
||||
|
||||
GetMainName() string
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"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)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sentinel/pkg/device"
|
||||
"sentinel/pkg/log"
|
||||
"sentinel/pkg/net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 定义命令行参数
|
||||
version := flag.String("version", "", "current version of main program")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *version == "" {
|
||||
// updater 视角:-1 表示“未知版本”,一定触发更新检测
|
||||
*version = "0"
|
||||
log.Println("[updater] --version not provided, fallback to -1")
|
||||
fmt.Println("[updater] 主程序版本号:", *version)
|
||||
}
|
||||
|
||||
deviceID := device.GetDeviceID()
|
||||
fmt.Printf("[updater] 当前设备id: %s\n", deviceID)
|
||||
versionInt, err := strconv.Atoi(*version)
|
||||
if err != nil {
|
||||
log.Println("[updater] invalid version:", *version)
|
||||
versionInt = 0
|
||||
}
|
||||
if err := RunUpdate(deviceID, versionInt); err != nil {
|
||||
log.Fatalf("[updater] 更新失败: %v", err)
|
||||
}
|
||||
fmt.Println("[updater] 更新程序结束")
|
||||
}
|
||||
func RunUpdate(deviceID string, version int) error {
|
||||
// 1. 检查更新
|
||||
info, err := api.CheckUpdate(deviceID)
|
||||
if err != nil {
|
||||
fmt.Println("[updater] 请求错误,请检查网络")
|
||||
return err
|
||||
}
|
||||
fmt.Println("[updater] 新版本:", info.Version)
|
||||
fmt.Println("[updater] 新内容:", info.Notes)
|
||||
fmt.Println("[updater] 下载地址:", info.DownloadURL)
|
||||
|
||||
// 获取主程序路径
|
||||
selfPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selfDir := filepath.Dir(selfPath)
|
||||
|
||||
starter := newMainProgramStarter()
|
||||
targetExe := filepath.Join(selfDir, starter.GetMainName())
|
||||
|
||||
// 2. 对比版本号,没有新版本则直接启动原程序
|
||||
if info.Version <= version {
|
||||
fmt.Println("[updater] 暂未发现新版本,启动原程序")
|
||||
if err := starter.Start(targetExe); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 3. 有新版本则先备份到 ./tmp/old_app/
|
||||
backupDir := filepath.Join(selfDir, "tmp", "old_app")
|
||||
_ = os.MkdirAll(backupDir, 0755)
|
||||
backupFile := filepath.Join(backupDir, "main_"+strconv.Itoa(version)+".bak")
|
||||
if err := os.Rename(targetExe, backupFile); err != nil {
|
||||
fmt.Println("[updater] 备份失败,但继续更新:", err)
|
||||
}
|
||||
|
||||
// 4. 下载新版本到 ./tmp
|
||||
tmpDir := filepath.Join(selfDir, "tmp")
|
||||
_ = os.MkdirAll(tmpDir, 0755)
|
||||
|
||||
u, err := url.Parse(info.DownloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := filepath.Base(u.Path)
|
||||
ext := filepath.Ext(base)
|
||||
|
||||
tmpFile, err := os.CreateTemp(tmpDir, "main_*"+ext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(info.DownloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 重命名新文件到 ./main.exe
|
||||
tmpFile.Close() // 关闭临时文件才能重命名
|
||||
maxRetry := 20
|
||||
for i := 0; i < maxRetry; i++ {
|
||||
err := os.Rename(tmpFile.Name(), targetExe)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
fmt.Println("[updater] 文件被占用,等待 500ms 再尝试...")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if i == maxRetry-1 {
|
||||
return fmt.Errorf("替换失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 启动主程序,同时完全退出自己
|
||||
if err := starter.Start(targetExe); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("[updater] 更新完成,新程序已启动,退出更新程序")
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "sentinel/pkg/platform"
|
||||
|
||||
func newMainProgramStarter() platform.MainProgramStarter {
|
||||
return &mainProgramStarter{}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user