物联网软件
This commit is contained in:
@@ -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>
|
||||||
@@ -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.
@@ -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.
@@ -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
@@ -3,9 +3,18 @@ module sentinel
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
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/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-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/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // 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/crc64nvme v1.1.0 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/minio-go/v7 v7.0.97 // 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/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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // 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/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // 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/crypto v0.42.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.44.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
|||||||
@@ -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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
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 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
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/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/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 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
|
||||||
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
|
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 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
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 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/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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
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 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
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 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
|
|||||||
@@ -1,109 +1,89 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"fmt"
|
||||||
"os/exec"
|
|
||||||
"sentinel/pkg/config"
|
"sentinel/pkg/config"
|
||||||
|
"sentinel/pkg/device"
|
||||||
|
"sentinel/pkg/docker"
|
||||||
"sentinel/pkg/log"
|
"sentinel/pkg/log"
|
||||||
model2 "sentinel/pkg/model"
|
model2 "sentinel/pkg/model"
|
||||||
"sentinel/pkg/utils"
|
"sentinel/pkg/utils"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BusinessService struct {
|
type BusinessService struct {
|
||||||
mqtt *MQTTService
|
mqtt *MQTTService
|
||||||
|
dockerManager docker.DockerManager
|
||||||
deviceID string
|
deviceID string
|
||||||
deptId string
|
deptId string
|
||||||
cmdTopic string
|
cmdTopic string
|
||||||
deviceType 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{
|
return &BusinessService{
|
||||||
mqtt: m,
|
mqtt: m,
|
||||||
|
dockerManager: dm,
|
||||||
deviceID: deviceID,
|
deviceID: deviceID,
|
||||||
subscriptions: make(map[string]struct{}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeTopic 订阅指定 topic,并记录可取消
|
func (b *BusinessService) Start() error {
|
||||||
func (b *BusinessService) SubscribeTopic(topic string, qos byte) error {
|
if err := b.mqtt.SubscribeTopic(getInitTopic(b.deviceID), 1); err != nil {
|
||||||
if err := b.mqtt.Subscribe(topic, qos); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b.subscriptions[topic] = struct{}{}
|
|
||||||
|
b.mqtt.SetMessageHandler(b.onMQTTMessage)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getInitTopic(deviceID string) string {
|
func getInitTopic(deviceID string) string {
|
||||||
return "+/+/+/" + deviceID + "/#"
|
return "+/+/+/" + deviceID + "/#"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BusinessService) getOwnTopic(deviceID string) string {
|
func (b *BusinessService) getOwnTopic(deviceID string) string {
|
||||||
return b.deptId + "/cmd/" + b.deviceType + "/" + deviceID + "/#"
|
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) {
|
func (b *BusinessService) onMQTTMessage(topic string, payload []byte) {
|
||||||
model := model2.FromStringToMqttTopic(topic)
|
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 {
|
if model.Domain == "cmd" && model.DeviceType == b.deviceType {
|
||||||
log.Println("收到指令:", model.Resource)
|
log.Println("收到指令:", model.Resource)
|
||||||
switch model.Resource {
|
switch model.Resource {
|
||||||
case "ping":
|
|
||||||
log.Println("pong")
|
|
||||||
case "shutdown":
|
case "shutdown":
|
||||||
b.handleShutdown()
|
payload_json["massage"] = b.handleShutdown()
|
||||||
case "restart":
|
case "restart":
|
||||||
b.handleRestart()
|
payload_json["massage"] = b.handleRestart()
|
||||||
case "check_update":
|
case "check_update":
|
||||||
b.handleCheckUpdate()
|
payload_json["massage"] = b.handleCheckUpdate()
|
||||||
default:
|
default:
|
||||||
log.Println("未知的命令:", model.Resource)
|
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" {
|
} else if model.Domain == "status" && model.Resource == "receipt" {
|
||||||
b.deviceType = model.DeviceType
|
b.deviceType = model.DeviceType
|
||||||
b.deptId = model.DeptId
|
b.deptId = model.DeptId
|
||||||
// 取消订阅之前的初始化主题
|
// 取消订阅之前的初始化主题
|
||||||
if b.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
|
if b.mqtt.UnsubscribeTopic(getInitTopic(b.deviceID)) != nil {
|
||||||
log.Error("无法取消初始化主题")
|
log.Error("无法取消初始化主题")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 新订阅属于自己的主题
|
// 新订阅属于自己的主题
|
||||||
if b.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
|
if b.mqtt.SubscribeTopic(b.getOwnTopic(b.deviceID), 1) != nil {
|
||||||
log.Error("无法定于属于自己的主题")
|
log.Error("无法定于属于自己的主题")
|
||||||
return
|
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{}{
|
info := map[string]interface{}{
|
||||||
"version": config.APP_VERSION,
|
"version": config.APP_VERSION,
|
||||||
"online": true,
|
"online": isOnline,
|
||||||
"ip": utils.GetLocalIP(),
|
"ip": utils.GetLocalIP(),
|
||||||
"hostname": utils.GetHostname(),
|
"hostname": utils.GetHostname(),
|
||||||
"mac": utils.GetMacAddress(),
|
"mac": utils.GetMacAddress(),
|
||||||
@@ -124,86 +104,60 @@ func (b *BusinessService) SendStatusInfo() {
|
|||||||
"disk_total": utils.GetDisk(),
|
"disk_total": utils.GetDisk(),
|
||||||
"last_seen": time.Now().UTC().Format(time.RFC3339),
|
"last_seen": time.Now().UTC().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
payloadBytes, _ := json.Marshal(info)
|
||||||
payload, _ := json.Marshal(info)
|
payload := string(payloadBytes) // 转成 string
|
||||||
topic := "x/status/x/" + b.deviceID + "/info"
|
return "x/status/x/" + device.GetDeviceID() + "/info", payload
|
||||||
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() {
|
func (b *BusinessService) handleShutdown() string {
|
||||||
os.Exit(0)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
b.dockerManager.StopAndRemoveContainer(ctx)
|
||||||
|
return "程序已退出"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启程序
|
// 重启程序
|
||||||
func (b *BusinessService) handleRestart() {
|
func (b *BusinessService) handleRestart() string {
|
||||||
exe, _ := os.Executable()
|
log.Println("正在拉取镜像:", config.DOCKER_IMAGE)
|
||||||
cmd := exec.Command(exe)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
cmd.Stdout = os.Stdout
|
defer cancel()
|
||||||
cmd.Stderr = os.Stderr
|
if err := b.dockerManager.PullImage(ctx, config.DOCKER_IMAGE); err != nil {
|
||||||
_ = cmd.Start()
|
log.Fatalf("镜像拉取失败:%v", err)
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
func (b *BusinessService) handleCheckUpdate() {
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"--version", strconv.Itoa(config.APP_VERSION),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
launcher := newUpdaterLauncher()
|
err := b.dockerManager.RunContainer(ctx)
|
||||||
|
if err != nil {
|
||||||
if err := launcher.Start(args); err != nil {
|
log.Fatal(err)
|
||||||
log.Println("[BUS] failed to start updater:", err)
|
}
|
||||||
return
|
return "程序已重启"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(
|
func (b *BusinessService) handleCheckUpdate() string {
|
||||||
"[BUS] updater started, exiting main program",
|
cfg := config.LoadConfig()
|
||||||
)
|
cfg.NeedUpdate = true
|
||||||
|
if err := config.WriteLocalConfig(&cfg); err != nil {
|
||||||
time.Sleep(500 * time.Millisecond)
|
log.Println("load config failed:", err)
|
||||||
os.Exit(0)
|
}
|
||||||
|
return "程序更新机制已触发"
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCheckUpdate 触发更新流程(主程序侧)
|
|
||||||
//func (b *BusinessService) handleCheckUpdate() {
|
//func (b *BusinessService) handleCheckUpdate() {
|
||||||
//
|
|
||||||
// args := []string{
|
// args := []string{
|
||||||
// "--version", strconv.Itoa(config.APP_VERSION),
|
// "--version", strconv.Itoa(config.APP_VERSION),
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// cmd := exec.Command("./updater.exe", args...)
|
// launcher := newUpdaterLauncher()
|
||||||
// cmd.Stdout = os.Stdout
|
|
||||||
// cmd.Stderr = os.Stderr
|
|
||||||
//
|
//
|
||||||
// // OS 级脱离父进程
|
// if err := launcher.Start(args); err != nil {
|
||||||
// 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)
|
// log.Println("[BUS] failed to start updater:", err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// log.Println(
|
// log.Println(
|
||||||
// "[BUS] updater started (pid=%d), exiting main program\n",
|
// "[BUS] updater started, exiting main program",
|
||||||
// cmd.Process.Pid,
|
|
||||||
// )
|
// )
|
||||||
//
|
//
|
||||||
// // 给 updater 留出启动窗口(尤其是 systemd / docker 环境)
|
|
||||||
// time.Sleep(500 * time.Millisecond)
|
// time.Sleep(500 * time.Millisecond)
|
||||||
//
|
|
||||||
// os.Exit(0)
|
// os.Exit(0)
|
||||||
//}
|
//}
|
||||||
|
|||||||
+73
-15
@@ -1,40 +1,69 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sentinel/pkg/config"
|
"sentinel/pkg/config"
|
||||||
"sentinel/pkg/device"
|
"sentinel/pkg/device"
|
||||||
|
"sentinel/pkg/docker"
|
||||||
"sentinel/pkg/log"
|
"sentinel/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
banner := `
|
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 := `
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
██████ █████ ██████ ██████ ██ ██████ ██████ ██ ██ ██████
|
||||||
██ ██ ██ ██ ██ ████ ██ ██ ███ ██ ██
|
██ ██ ██ ██ ██ ████ ██ ██ ███ ██ ██
|
||||||
█████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ████ ██ ██
|
█████ ██ ██ ██ ██ █████ ██ ██ ██ ██ █████ ████ ██ ██
|
||||||
██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██
|
██ ██ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██
|
||||||
██████ █████ █████ ██████ ██ ██ █████ ██████ ██ ██ ██
|
██████ █████ █████ ██████ ██ ██ █████ ██████ ██ ██ ██
|
||||||
|
|
||||||
==========================================================================
|
==============================================================================
|
||||||
`
|
`
|
||||||
fmt.Println(banner)
|
log.Println(banner)
|
||||||
deviceID := device.GetDeviceID()
|
log.Println("Device id: "+deviceID, "\t版本号: ", config.APP_VERSION) // 第一次启动记录
|
||||||
log.Init(config.Log_file_dic) // 初始化日志目录
|
}
|
||||||
log.Info("Device id: " + deviceID) // 第一次启动记录
|
|
||||||
log.Println("版本号: ", config.APP_VERSION) // 第一次启动记录
|
|
||||||
|
|
||||||
|
func InitMQTT(deviceID string) {
|
||||||
var mqttSvc *MQTTService
|
var mqttSvc *MQTTService
|
||||||
firstFail := true // 标记是否第一次失败
|
firstFail := true // 标记是否第一次失败
|
||||||
for {
|
|
||||||
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
|
mqttSvc = NewMQTTService(config.MQTT_BROKER, deviceID, deviceID, config.PASSWORD, 60)
|
||||||
|
for {
|
||||||
err := mqttSvc.Connect()
|
err := mqttSvc.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if firstFail {
|
if firstFail {
|
||||||
log.Error("物联网服务连接失败,如未注册设备,请先注册: " + deviceID)
|
log.Error("物联网服务器连接失败,如未注册设备,请先注册: " + deviceID)
|
||||||
firstFail = false
|
firstFail = false
|
||||||
}
|
}
|
||||||
time.Sleep(3 * time.Second) // 5秒后重试
|
time.Sleep(3 * time.Second) // 5秒后重试
|
||||||
@@ -44,7 +73,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer mqttSvc.Close()
|
defer mqttSvc.Close()
|
||||||
|
|
||||||
biz := NewBusinessService(mqttSvc, deviceID)
|
dm, err := docker.NewDockerManager()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
biz := NewBusinessService(mqttSvc, deviceID, *dm)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// MQTT业务
|
// MQTT业务
|
||||||
err := biz.Start()
|
err := biz.Start()
|
||||||
@@ -56,9 +91,32 @@ func main() {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
// 第一次运行直接启动
|
||||||
|
biz.handleRestart()
|
||||||
|
|
||||||
// 主线程循环,可做心跳或状态上报
|
// 4️⃣ T2 超时自毁检查
|
||||||
for {
|
tickerLost := time.NewTicker(10 * time.Second)
|
||||||
time.Sleep(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{})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"sentinel/pkg/config"
|
||||||
"sentinel/pkg/log"
|
"sentinel/pkg/log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
type MQTTService struct {
|
type MQTTService struct {
|
||||||
client mqtt.Client
|
client mqtt.Client
|
||||||
handler func(topic string, payload []byte)
|
handler func(topic string, payload []byte)
|
||||||
|
subscriptions map[string]struct{} // 记录已订阅 topic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMQTTService(broker, clientID, username, password string, keepalive int) *MQTTService {
|
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.SetClientID(clientID)
|
||||||
opts.SetUsername(username)
|
opts.SetUsername(username)
|
||||||
opts.SetPassword(password)
|
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.SetKeepAlive(time.Duration(keepalive) * time.Second)
|
||||||
opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
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) {
|
opts.SetDefaultPublishHandler(func(c mqtt.Client, m mqtt.Message) {
|
||||||
if ms.handler != nil {
|
if ms.handler != nil {
|
||||||
ms.handler(m.Topic(), m.Payload())
|
ms.handler(m.Topic(), m.Payload())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
opts.OnConnect = func(c mqtt.Client) {
|
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) {
|
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)
|
ms.client = mqtt.NewClient(opts)
|
||||||
@@ -52,7 +79,7 @@ func (m *MQTTService) Subscribe(topic string, qos byte) error {
|
|||||||
if token.Wait() && token.Error() != nil {
|
if token.Wait() && token.Error() != nil {
|
||||||
return token.Error()
|
return token.Error()
|
||||||
}
|
}
|
||||||
log.Println("物联网服务消息订阅:", topic)
|
log.Debug("物联网服务消息订阅:", topic)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,3 +98,48 @@ func (m *MQTTService) Close() {
|
|||||||
m.client.Disconnect(250)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,105 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// 变动
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
// 常量
|
// 常量
|
||||||
const (
|
const (
|
||||||
// 版本号
|
// 版本号
|
||||||
APP_VERSION = 1
|
APP_VERSION = 26
|
||||||
|
|
||||||
BASE_URL = "https://ai.ronsunny.cn:8090"
|
//BASE_URL = "https://ai.ronsunny.cn:8090"
|
||||||
//BASE_URL = "http://127.0.0.1:13011"
|
BASE_URL = "http://127.0.0.1:13011"
|
||||||
Log_file_dic = "./logs"
|
LOG_FILE_DIC = "./logs"
|
||||||
|
LOG_CHECK_INTERVAL_HOURS = 10 // 文件轮询检查间隔(秒)
|
||||||
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
|
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
|
||||||
PASSWORD = "123456"
|
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 (
|
var (
|
||||||
// DeviceType string
|
DOCKER_CONTAINER_BINDS = []string{
|
||||||
// DeptId 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sentinel/pkg/config"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,8 +18,15 @@ func Init(dir string) {
|
|||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
fmt.Println("create log dir failed:", err)
|
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()
|
cleanupOldLogs()
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup 删除超过7天的日志文件
|
// Cleanup 删除超过7天的日志文件
|
||||||
func cleanupOldLogs() {
|
func cleanupOldLogs() {
|
||||||
@@ -73,6 +81,11 @@ func Println(v ...interface{}) {
|
|||||||
msg := fmt.Sprint(v...)
|
msg := fmt.Sprint(v...)
|
||||||
logToFile("INFO", msg)
|
logToFile("INFO", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Debug(v ...interface{}) {
|
||||||
|
//msg := fmt.Sprint(v...)
|
||||||
|
//logToFile("DEBUF", msg)
|
||||||
|
}
|
||||||
func Warn(msg string) {
|
func Warn(msg string) {
|
||||||
logToFile("WARN", msg)
|
logToFile("WARN", msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,8 @@ type MainProgramStarter interface {
|
|||||||
Start(targetExe string) error
|
Start(targetExe string) error
|
||||||
|
|
||||||
GetMainName() string
|
GetMainName() string
|
||||||
|
|
||||||
|
IsProcessRunning(pid int) (bool, error)
|
||||||
|
|
||||||
|
KillProcess(pid int) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@@ -98,3 +99,19 @@ func GetDisk() string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%dGB", usage.Total/1024/1024/1024)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+144
-87
@@ -1,132 +1,189 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sentinel/pkg/config"
|
||||||
"sentinel/pkg/device"
|
"sentinel/pkg/device"
|
||||||
"sentinel/pkg/log"
|
"sentinel/pkg/log"
|
||||||
"sentinel/pkg/net"
|
"sentinel/pkg/net"
|
||||||
"strconv"
|
"sentinel/pkg/platform"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 定义命令行参数
|
log.Println("[daemon] 设备ID:", device.GetDeviceID())
|
||||||
version := flag.String("version", "", "current version of main program")
|
|
||||||
|
|
||||||
flag.Parse()
|
log.Init(config.LOG_FILE_DIC)
|
||||||
|
|
||||||
if *version == "" {
|
starter := newMainProgramStarter()
|
||||||
// updater 视角:-1 表示“未知版本”,一定触发更新检测
|
// 启动主程序
|
||||||
*version = "0"
|
initMainProcess(starter)
|
||||||
log.Println("[updater] --version not provided, fallback to -1")
|
ticker := time.NewTicker(config.DAEMON_UPDATE_CHECK_INTERVAL_SECONDS * time.Second)
|
||||||
fmt.Println("[updater] 主程序版本号:", *version)
|
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 {}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceID := device.GetDeviceID()
|
func initMainProcess(starter platform.MainProgramStarter) {
|
||||||
fmt.Printf("[updater] 当前设备id: %s\n", deviceID)
|
if err := startMainProgram(starter); err != nil {
|
||||||
versionInt, err := strconv.Atoi(*version)
|
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 {
|
if err != nil {
|
||||||
log.Println("[updater] invalid version:", *version)
|
log.Println("[daemon] 检查更新失败:", err)
|
||||||
versionInt = 0
|
return
|
||||||
}
|
}
|
||||||
if err := RunUpdate(deviceID, versionInt); err != nil {
|
|
||||||
log.Fatalf("[updater] 更新失败: %v", err)
|
if info.Version <= cfg.VersionCode {
|
||||||
|
log.Println("[daemon] 没有新版本,跳过")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("[updater] 更新程序结束")
|
|
||||||
|
log.Println("[daemon] 发现新版本 %d\n", info.Version)
|
||||||
|
|
||||||
|
// 停止主程序
|
||||||
|
if cfg.PID > 0 {
|
||||||
|
_ = starter.KillProcess(cfg.PID)
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
func RunUpdate(deviceID string, version int) error {
|
|
||||||
// 1. 检查更新
|
if err := downloadAndReplace(starter, info.DownloadURL); err != nil {
|
||||||
info, err := api.CheckUpdate(deviceID)
|
log.Println("[daemon] 更新失败:", err)
|
||||||
if err != nil {
|
return
|
||||||
fmt.Println("[updater] 请求错误,请检查网络")
|
}
|
||||||
|
|
||||||
|
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
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("[updater] 新版本:", info.Version)
|
|
||||||
fmt.Println("[updater] 新内容:", info.Notes)
|
|
||||||
fmt.Println("[updater] 下载地址:", info.DownloadURL)
|
|
||||||
|
|
||||||
|
func downloadAndReplace(
|
||||||
|
starter platform.MainProgramStarter,
|
||||||
|
downloadURL string,
|
||||||
|
) error {
|
||||||
// 获取主程序路径
|
// 获取主程序路径
|
||||||
selfPath, err := os.Executable()
|
selfPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("获取自身路径失败: %w", err)
|
||||||
}
|
}
|
||||||
selfDir := filepath.Dir(selfPath)
|
selfDir := filepath.Dir(selfPath)
|
||||||
|
|
||||||
starter := newMainProgramStarter()
|
|
||||||
targetExe := filepath.Join(selfDir, starter.GetMainName())
|
targetExe := filepath.Join(selfDir, starter.GetMainName())
|
||||||
|
outFile, err := os.Create(targetExe)
|
||||||
// 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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||||
}
|
}
|
||||||
base := filepath.Base(u.Path)
|
defer outFile.Close()
|
||||||
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}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Get(info.DownloadURL)
|
resp, err := client.Get(downloadURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("下载新程序失败: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return err
|
return fmt.Errorf("下载失败, HTTP状态码: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 重命名新文件到 ./main.exe
|
if _, err := io.Copy(outFile, resp.Body); err != nil {
|
||||||
tmpFile.Close() // 关闭临时文件才能重命名
|
return fmt.Errorf("写入目标文件失败: %w", err)
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mainProgramStarter struct{}
|
type mainProgramStarter struct{}
|
||||||
@@ -25,3 +26,29 @@ func (s *mainProgramStarter) Start(targetExe string) error {
|
|||||||
// Linux 下:先保持最简单,保证能跑
|
// Linux 下:先保持最简单,保证能跑
|
||||||
return cmd.Start()
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mainProgramStarter struct{}
|
type mainProgramStarter struct{}
|
||||||
@@ -27,3 +30,34 @@ func (s *mainProgramStarter) Start(targetExe string) error {
|
|||||||
|
|
||||||
return cmd.Start()
|
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
@@ -1,36 +1,12 @@
|
|||||||
# ==========================
|
FROM scratch
|
||||||
# Stage 1: Build the Go binary
|
|
||||||
# ==========================
|
|
||||||
FROM golang:1.21-alpine AS builder
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
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
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制从 builder 构建好的二进制
|
# buildx 自动注入
|
||||||
COPY --from=builder /build/sentinel .
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
# 设置默认执行命令
|
COPY build/release/${TARGETOS}_${TARGETARCH} /app
|
||||||
CMD ["./sentinel"]
|
COPY ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
|
CMD ["./main"]
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func main() {
|
|||||||
uploadFile(record.LicensePlateImage, record.VehicleImage)
|
uploadFile(record.LicensePlateImage, record.VehicleImage)
|
||||||
// 2. 调用分析请求
|
// 2. 调用分析请求
|
||||||
analytics(record)
|
analytics(record)
|
||||||
|
<-make(chan struct{})
|
||||||
|
|
||||||
|
//fatal error: all goroutines are asleep - deadlock!
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadData(deviceID string) model.Record {
|
func loadData(deviceID string) model.Record {
|
||||||
|
|||||||
@@ -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
@@ -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 `
|
||||||
|
.
|
||||||
Reference in New Issue
Block a user