物联网软件

This commit is contained in:
BBIT-Kai
2026-02-04 14:02:57 +08:00
parent 646e312a4c
commit 1c11f3f06c
28 changed files with 4526 additions and 271 deletions
@@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoResourceLeak" enabled="true" level="WARNING" enabled_by_default="true">
<functions>
<function importPath="github.com/docker/docker/client" name="NewClientWithOpts" />
</functions>
</inspection_tool>
</profile>
</component>
+5
View File
@@ -0,0 +1,5 @@
version_code: 0
pid: 0
need_update: false
control_state: CONNECTED
last_alive_at: 1767926721
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
version_code: 26
pid: 26612
need_update: false
control_state: CONNECTED
last_alive_at: 1769564518
Binary file not shown.
Binary file not shown.
+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
+19 -1
View File
@@ -3,9 +3,18 @@ module sentinel
go 1.25.5
require (
github.com/dustin/go-humanize v1.0.1 // indirect
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
@@ -16,7 +25,11 @@ require (
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
@@ -25,6 +38,11 @@ require (
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
+43
View File
@@ -1,9 +1,30 @@
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=
@@ -26,8 +47,20 @@ 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=
@@ -44,6 +77,16 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
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=
+60 -106
View File
@@ -1,109 +1,89 @@
package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"fmt"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/docker"
"sentinel/pkg/log"
model2 "sentinel/pkg/model"
"sentinel/pkg/utils"
"strconv"
"time"
)
type BusinessService struct {
mqtt *MQTTService
dockerManager docker.DockerManager
deviceID string
deptId string
cmdTopic string
deviceType string
subscriptions map[string]struct{} // 记录已订阅 topic
}
func NewBusinessService(m *MQTTService, deviceID string) *BusinessService {
func NewBusinessService(m *MQTTService, deviceID string, dm docker.DockerManager) *BusinessService {
return &BusinessService{
mqtt: m,
dockerManager: dm,
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 {
func (b *BusinessService) Start() error {
if err := b.mqtt.SubscribeTopic(getInitTopic(b.deviceID), 1); err != nil {
return err
}
b.subscriptions[topic] = struct{}{}
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) 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)
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 "ping":
log.Println("pong")
case "shutdown":
b.handleShutdown()
payload_json["massage"] = b.handleShutdown()
case "restart":
b.handleRestart()
payload_json["massage"] = b.handleRestart()
case "check_update":
b.handleCheckUpdate()
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.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
if b.mqtt.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
log.Error("无法取消初始化主题")
return
}
// 新订阅属于自己的主题
if b.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
if b.mqtt.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
log.Error("无法定于属于自己的主题")
return
}
@@ -111,10 +91,10 @@ func (b *BusinessService) onMQTTMessage(topic string, payload []byte) {
}
}
func (b *BusinessService) SendStatusInfo() {
func getStatusInfoTopicPayload(isOnline bool) (string, string) {
info := map[string]interface{}{
"version": config.APP_VERSION,
"online": true,
"online": isOnline,
"ip": utils.GetLocalIP(),
"hostname": utils.GetHostname(),
"mac": utils.GetMacAddress(),
@@ -124,86 +104,60 @@ func (b *BusinessService) SendStatusInfo() {
"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))
}
payloadBytes, _ := json.Marshal(info)
payload := string(payloadBytes) // 转成 string
return "x/status/x/" + device.GetDeviceID() + "/info", payload
}
// 关闭程序(立即退出)
func (b *BusinessService) handleShutdown() {
os.Exit(0)
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() {
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),
func (b *BusinessService) handleRestart() string {
log.Println("正在拉取镜像:", config.DOCKER_IMAGE)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err := b.dockerManager.PullImage(ctx, config.DOCKER_IMAGE); err != nil {
log.Fatalf("镜像拉取失败:%v", err)
}
launcher := newUpdaterLauncher()
if err := launcher.Start(args); err != nil {
log.Println("[BUS] failed to start updater:", err)
return
err := b.dockerManager.RunContainer(ctx)
if err != nil {
log.Fatal(err)
}
log.Println(
"[BUS] updater started, exiting main program",
)
time.Sleep(500 * time.Millisecond)
os.Exit(0)
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 "程序更新机制已触发"
}
// 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
// launcher := newUpdaterLauncher()
//
// // OS 级脱离父进程
// switch runtime.GOOS {
// case "windows":
// cmd.SysProcAttr = &syscall.SysProcAttr{
// CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
// }
// }
//
// if err := cmd.Start(); err != nil {
// if err := launcher.Start(args); 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,
// "[BUS] updater started, exiting main program",
// )
//
// // 给 updater 留出启动窗口(尤其是 systemd / docker 环境)
// time.Sleep(500 * time.Millisecond)
//
// os.Exit(0)
//}
+79 -21
View File
@@ -1,40 +1,69 @@
package main
import (
"context"
"fmt"
"os"
"time"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/docker"
"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) // 第一次启动记录
// 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 // 标记是否第一次失败
for {
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
for {
err := mqttSvc.Connect()
if err != nil {
if firstFail {
log.Error("物联网服务连接失败,如未注册设备,请先注册: " + deviceID)
log.Error("物联网服务连接失败,如未注册设备,请先注册: " + deviceID)
firstFail = false
}
time.Sleep(3 * time.Second) // 5秒后重试
@@ -44,7 +73,13 @@ func main() {
}
defer mqttSvc.Close()
biz := NewBusinessService(mqttSvc, deviceID)
dm, err := docker.NewDockerManager()
if err != nil {
log.Fatal(err)
}
biz := NewBusinessService(mqttSvc, deviceID, *dm)
for {
// MQTT业务
err := biz.Start()
@@ -56,9 +91,32 @@ func main() {
}
break
}
// 第一次运行直接启动
biz.handleRestart()
// 主线程循环,可做心跳或状态上报
for {
time.Sleep(10 * time.Second)
// 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{})
}
+77 -5
View File
@@ -2,6 +2,7 @@ package main
import (
"crypto/tls"
"sentinel/pkg/config"
"sentinel/pkg/log"
"time"
@@ -11,6 +12,7 @@ import (
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 {
@@ -19,20 +21,45 @@ func NewMQTTService(broker, clientID, username, password string, keepalive int)
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})
ms := &MQTTService{}
// 设置遗嘱消息
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("物联网服务已连接")
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)
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)
@@ -52,7 +79,7 @@ func (m *MQTTService) Subscribe(topic string, qos byte) error {
if token.Wait() && token.Error() != nil {
return token.Error()
}
log.Println("物联网服务消息订阅:", topic)
log.Debug("物联网服务消息订阅:", topic)
return nil
}
@@ -71,3 +98,48 @@ func (m *MQTTService) Close() {
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))
}
}
+92 -7
View File
@@ -1,20 +1,105 @@
package config
// 变动
import (
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
// 常量
const (
// 版本号
APP_VERSION = 1
APP_VERSION = 26
BASE_URL = "https://ai.ronsunny.cn:8090"
//BASE_URL = "http://127.0.0.1:13011"
Log_file_dic = "./logs"
//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 (
// DeviceType string
// DeptId string
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)
}
+165
View File
@@ -0,0 +1,165 @@
package docker
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"sentinel/pkg/config"
"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,
Healthcheck: &container.HealthConfig{
Test: []string{"CMD-SHELL", "echo ok"},
Interval: 5 * time.Second,
Timeout: 2 * time.Second,
Retries: 3,
},
},
&container.HostConfig{
Binds: config.DOCKER_CONTAINER_BINDS,
NetworkMode: "host", // <-- 使用宿主机网络
},
nil, // NetworkConfig
nil, // Platform
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
}
+13
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"sentinel/pkg/config"
"time"
)
@@ -17,7 +18,14 @@ func Init(dir string) {
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天的日志文件
@@ -73,6 +81,11 @@ 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)
}
@@ -8,4 +8,8 @@ type MainProgramStarter interface {
Start(targetExe string) error
GetMainName() string
IsProcessRunning(pid int) (bool, error)
KillProcess(pid int) error
}
+17
View File
@@ -1,6 +1,7 @@
package utils
import (
"encoding/json"
"fmt"
"net"
"os"
@@ -98,3 +99,19 @@ func GetDisk() string {
}
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)
}
+148 -91
View File
@@ -1,132 +1,189 @@
package main
import (
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sentinel/pkg/config"
"sentinel/pkg/device"
"sentinel/pkg/log"
"sentinel/pkg/net"
"strconv"
"sentinel/pkg/platform"
"time"
)
func main() {
// 定义命令行参数
version := flag.String("version", "", "current version of main program")
log.Println("[daemon] 设备ID:", device.GetDeviceID())
flag.Parse()
log.Init(config.LOG_FILE_DIC)
if *version == "" {
// updater 视角:-1 表示“未知版本”,一定触发更新检测
*version = "0"
log.Println("[updater] --version not provided, fallback to -1")
fmt.Println("[updater] 主程序版本号:", *version)
}
starter := newMainProgramStarter()
// 启动主程序
initMainProcess(starter)
ticker := time.NewTicker(config.DAEMON_UPDATE_CHECK_INTERVAL_SECONDS * time.Second)
defer ticker.Stop()
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
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)
}
if err := RunUpdate(deviceID, versionInt); err != nil {
log.Fatalf("[updater] 更新失败: %v", err)
handleUpdate(starter)
}
fmt.Println("[updater] 更新程序结束")
}
}()
select {}
}
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)
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 err
return fmt.Errorf("获取自身路径失败: %w", 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)
outFile, err := os.Create(targetExe)
if err != nil {
return err
return fmt.Errorf("创建目标文件失败: %w", 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()
defer outFile.Close()
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(info.DownloadURL)
resp, err := client.Get(downloadURL)
if err != nil {
return err
return fmt.Errorf("下载新程序失败: %w", err)
}
defer resp.Body.Close()
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
return err
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载失败, HTTP状态码: %d", resp.StatusCode)
}
// 5. 重命名新文件到 ./main.exe
tmpFile.Close() // 关闭临时文件才能重命名
maxRetry := 20
for i := 0; i < maxRetry; i++ {
err := os.Rename(tmpFile.Name(), targetExe)
if err == nil {
break
if _, err := io.Copy(outFile, resp.Body); err != nil {
return fmt.Errorf("写入目标文件失败: %w", err)
}
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
}
+27
View File
@@ -6,6 +6,7 @@ package main
import (
"os"
"os/exec"
"strconv"
)
type mainProgramStarter struct{}
@@ -25,3 +26,29 @@ func (s *mainProgramStarter) Start(targetExe string) error {
// 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()
}
+34
View File
@@ -6,7 +6,10 @@ package main
import (
"os"
"os/exec"
"strconv"
"syscall"
"golang.org/x/sys/windows"
)
type mainProgramStarter struct{}
@@ -27,3 +30,34 @@ func (s *mainProgramStarter) Start(targetExe string) error {
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()
}
+8 -32
View File
@@ -1,36 +1,12 @@
# ==========================
# Stage 1: Build the Go binary
# ==========================
FROM golang:1.21-alpine AS builder
FROM scratch
# 设置工作目录
WORKDIR /build
# 复制 go.mod 和 go.sum,先做依赖缓存
COPY go.mod go.sum ./
# 下载依赖(缓存层)
RUN go mod download
# 复制源代码
COPY app/ ./app
# 编译可执行文件
RUN go build -o sentinel ./app/main.go
# ==========================
# Stage 2: 生成最终运行镜像
# ==========================
FROM alpine:3.18
# 安装 ca-certificates,如果程序需要访问 HTTPS
RUN apk add --no-cache ca-certificates
# 设置工作目录
WORKDIR /app
# 复制从 builder 构建好的二进制
COPY --from=builder /build/sentinel .
# buildx 自动注入
ARG TARGETOS
ARG TARGETARCH
# 设置默认执行命令
CMD ["./sentinel"]
COPY build/release/${TARGETOS}_${TARGETARCH} /app
COPY ca-certificates.crt /etc/ssl/certs/
CMD ["./main"]
+4
View File
@@ -30,6 +30,10 @@ func main() {
uploadFile(record.LicensePlateImage, record.VehicleImage)
// 2. 调用分析请求
analytics(record)
<-make(chan struct{})
//fatal error: all goroutines are asleep - deadlock!
}
func loadData(deviceID string) model.Record {
+40
View File
@@ -0,0 +1,40 @@
# 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/release/win"; MainOut="main.exe"; },
@{ OS="linux"; ARCH="amd64"; Dir="build/release/linux_amd64"; MainOut="main"; },
@{ OS="linux"; ARCH="arm64"; Dir="build/release/linux_arm64"; MainOut="main"; }
)
# -------------------------
# 循环打包
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)" ./app
Write-Host "$($t.OS)-$($t.ARCH) Build complete, Output dir: $($t.Dir)"
Write-Host ""
}
Write-Host "All builds finished."
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
# -----------------------------
# 版本号
# -----------------------------
$env:VERSION = "0.0.1"
# 仓库与镜像名
$IMAGE = "ai.ronsunny.cn:13011/bbit_iot/ce_sentinel"
# -----------------------------
# 确保 buildx builder 存在
# -----------------------------
docker buildx inspect multiarch-builder > $null 2>&1
if ($LASTEXITCODE -ne 0) {
docker buildx create --name multiarch-builder --use
} else {
docker buildx use multiarch-builder
}
# -----------------------------
# 构建 + 推送 多架构镜像
# -----------------------------
docker buildx build `
--platform linux/amd64,linux/arm64 `
-t ${IMAGE}:$env:VERSION `
-t ${IMAGE}:latest `
--push `
.