refactor: remove v1 protocol and stabilize file transfer
This commit is contained in:
1 parent
46fd2af44c
commit
2ff7c11db0
27 files changed
+344
-611
No files matched your search
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/komari-monitor/komari/database/dbcore"
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
func GetClientUUIDByToken(token string) (clientUUID string, err error) {
|
||||
@@ -20,7 +20,7 @@ func GetClientUUIDByToken(token string) (clientUUID string, err error) {
|
||||
}
|
||||
|
||||
// 检查数据防止异常数据导致数据库损坏
|
||||
func ReportVerify(report v1.Report) error {
|
||||
func ReportVerify(report v2.Report) error {
|
||||
// 防止输入不合理范围
|
||||
if report.CPU.Usage < 0 || report.CPU.Usage > 100 {
|
||||
return fmt.Errorf("CPU.Usage must be between 0 and 100")
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/pkg/metric"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
func TestDefaultRollupPolicy(t *testing.T) {
|
||||
@@ -572,15 +572,15 @@ func TestGetRecordsByClientAndTimeReadsRollupsAfterRawCompaction(t *testing.T) {
|
||||
Connections: 321,
|
||||
ConnectionsUdp: 12,
|
||||
}
|
||||
if _, err := WriteReport(ctx, v1.Report{
|
||||
if _, err := WriteReport(ctx, v2.Report{
|
||||
UUID: rec.Client,
|
||||
UpdatedAt: ts,
|
||||
CPU: v1.CPUReport{Usage: float64(rec.Cpu)},
|
||||
Ram: v1.RamReport{Used: rec.Ram, Total: rec.RamTotal},
|
||||
Load: v1.LoadReport{Load1: float64(rec.Load)},
|
||||
Disk: v1.DiskReport{Used: rec.Disk, Total: rec.DiskTotal},
|
||||
CPU: v2.CPUReport{Usage: float64(rec.Cpu)},
|
||||
Ram: v2.RamReport{Used: rec.Ram, Total: rec.RamTotal},
|
||||
Load: v2.LoadReport{Load1: float64(rec.Load)},
|
||||
Disk: v2.DiskReport{Used: rec.Disk, Total: rec.DiskTotal},
|
||||
Process: rec.Process,
|
||||
Connections: v1.ConnectionsReport{
|
||||
Connections: v2.ConnectionsReport{
|
||||
TCP: rec.Connections,
|
||||
UDP: rec.ConnectionsUdp,
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/pkg/metric"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
type reportTrafficState struct {
|
||||
@@ -59,7 +59,7 @@ type reportBatchRequest struct {
|
||||
|
||||
type reportBatchWorker struct {
|
||||
mu sync.Mutex
|
||||
queue chan v1.Report
|
||||
queue chan v2.Report
|
||||
pingQueue chan models.PingRecord
|
||||
requests chan reportBatchRequest
|
||||
done chan struct{}
|
||||
@@ -75,7 +75,7 @@ func StartReportBatcher() {
|
||||
return
|
||||
}
|
||||
worker := &reportBatchWorker{
|
||||
queue: make(chan v1.Report, reportBatchQueueSize),
|
||||
queue: make(chan v2.Report, reportBatchQueueSize),
|
||||
pingQueue: make(chan models.PingRecord, reportBatchQueueSize),
|
||||
requests: make(chan reportBatchRequest, 1),
|
||||
done: make(chan struct{}),
|
||||
@@ -152,16 +152,16 @@ func FlushReportBatch(ctx context.Context) error {
|
||||
// WriteReport persists one agent report and adds it to in-memory minute
|
||||
// summaries using the same server receive time. Traffic deltas remain summable
|
||||
// after rollup.
|
||||
func WriteReport(ctx context.Context, report v1.Report) (v1.Report, error) {
|
||||
func WriteReport(ctx context.Context, report v2.Report) (v2.Report, error) {
|
||||
if report.UUID == "" {
|
||||
return v1.Report{}, fmt.Errorf("report UUID is required")
|
||||
return v2.Report{}, fmt.Errorf("report UUID is required")
|
||||
}
|
||||
if report.UpdatedAt.IsZero() {
|
||||
return v1.Report{}, fmt.Errorf("report receive time is required")
|
||||
return v2.Report{}, fmt.Errorf("report receive time is required")
|
||||
}
|
||||
report.UpdatedAt = report.UpdatedAt.UTC()
|
||||
if GetStore() == nil {
|
||||
return v1.Report{}, fmt.Errorf("metric store not enabled")
|
||||
return v2.Report{}, fmt.Errorf("metric store not enabled")
|
||||
}
|
||||
|
||||
reportBatcherMu.Lock()
|
||||
@@ -169,19 +169,19 @@ func WriteReport(ctx context.Context, report v1.Report) (v1.Report, error) {
|
||||
reportBatcherMu.Unlock()
|
||||
if worker != nil {
|
||||
if err := worker.enqueue(ctx, report); err != nil {
|
||||
return v1.Report{}, err
|
||||
return v2.Report{}, err
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
saved, err := writeReportBatch(ctx, []v1.Report{report})
|
||||
saved, err := writeReportBatch(ctx, []v2.Report{report})
|
||||
if err != nil {
|
||||
return v1.Report{}, err
|
||||
return v2.Report{}, err
|
||||
}
|
||||
return saved[0], nil
|
||||
}
|
||||
|
||||
func (w *reportBatchWorker) enqueue(ctx context.Context, report v1.Report) error {
|
||||
func (w *reportBatchWorker) enqueue(ctx context.Context, report v2.Report) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.stopping {
|
||||
@@ -202,7 +202,7 @@ func (w *reportBatchWorker) run() {
|
||||
ticker := time.NewTicker(reportBatchInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
var pending []v1.Report
|
||||
var pending []v2.Report
|
||||
var pendingPings []models.PingRecord
|
||||
for {
|
||||
select {
|
||||
@@ -252,7 +252,7 @@ func (w *reportBatchWorker) enqueuePing(ctx context.Context, record models.PingR
|
||||
}
|
||||
}
|
||||
|
||||
func drainReportQueue(queue <-chan v1.Report, limit int) []v1.Report {
|
||||
func drainReportQueue(queue <-chan v2.Report, limit int) []v2.Report {
|
||||
if limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func drainReportQueue(queue <-chan v1.Report, limit int) []v1.Report {
|
||||
if capacity == 0 {
|
||||
return nil
|
||||
}
|
||||
reports := make([]v1.Report, 0, capacity)
|
||||
reports := make([]v2.Report, 0, capacity)
|
||||
for len(reports) < limit {
|
||||
select {
|
||||
case report := <-queue:
|
||||
@@ -298,7 +298,7 @@ func drainPingQueue(queue <-chan models.PingRecord, limit int) []models.PingReco
|
||||
return records
|
||||
}
|
||||
|
||||
func writePendingReports(ctx context.Context, pending *[]v1.Report) error {
|
||||
func writePendingReports(ctx context.Context, pending *[]v2.Report) error {
|
||||
if len(*pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func writePendingPingRecords(ctx context.Context, pending *[]models.PingRecord)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeReportBatch(ctx context.Context, reports []v1.Report) ([]v1.Report, error) {
|
||||
func writeReportBatch(ctx context.Context, reports []v2.Report) ([]v2.Report, error) {
|
||||
if len(reports) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -349,7 +349,7 @@ func writeReportBatch(ctx context.Context, reports []v1.Report) ([]v1.Report, er
|
||||
return nil, fmt.Errorf("metric store not enabled")
|
||||
}
|
||||
|
||||
prepared := make([]v1.Report, len(reports))
|
||||
prepared := make([]v2.Report, len(reports))
|
||||
copy(prepared, reports)
|
||||
points := make([]metric.Point, 0, len(reports)*20)
|
||||
pendingStates := make(map[*reportTrafficState]reportTrafficValues)
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/pkg/metric"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
func reportMetricPoints(report v1.Report, trafficUp, trafficDown int64) []metric.Point {
|
||||
func reportMetricPoints(report v2.Report, trafficUp, trafficDown int64) []metric.Point {
|
||||
entityID := report.UUID
|
||||
ts := report.UpdatedAt
|
||||
points := []metric.Point{
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/pkg/metric"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
func useReportTestStore(t *testing.T, policy *metric.RollupPolicy) *metric.Store {
|
||||
@@ -51,20 +51,20 @@ func TestWriteReportStoresMinuteMetricsAndResetAwareTraffic(t *testing.T) {
|
||||
base := time.Now().UTC().Truncate(time.Minute).Add(5 * time.Second)
|
||||
now := base.Add(45 * time.Second)
|
||||
|
||||
report := v1.Report{
|
||||
report := v2.Report{
|
||||
UUID: "node-a",
|
||||
UpdatedAt: base,
|
||||
CPU: v1.CPUReport{Usage: 12.5},
|
||||
Ram: v1.RamReport{Used: 100, Total: 1000},
|
||||
Swap: v1.RamReport{Used: 20, Total: 200},
|
||||
Load: v1.LoadReport{Load1: 0.5},
|
||||
Disk: v1.DiskReport{Used: 300, Total: 3000},
|
||||
Network: v1.NetworkReport{Up: 3, Down: 4, TotalUp: 100, TotalDown: 200},
|
||||
CPU: v2.CPUReport{Usage: 12.5},
|
||||
Ram: v2.RamReport{Used: 100, Total: 1000},
|
||||
Swap: v2.RamReport{Used: 20, Total: 200},
|
||||
Load: v2.LoadReport{Load1: 0.5},
|
||||
Disk: v2.DiskReport{Used: 300, Total: 3000},
|
||||
Network: v2.NetworkReport{Up: 3, Down: 4, TotalUp: 100, TotalDown: 200},
|
||||
Process: 7,
|
||||
Connections: v1.ConnectionsReport{TCP: 8, UDP: 9},
|
||||
GPU: &v1.GPUDetailReport{
|
||||
Connections: v2.ConnectionsReport{TCP: 8, UDP: 9},
|
||||
GPU: &v2.GPUDetailReport{
|
||||
AverageUsage: 25,
|
||||
DetailedInfo: []v1.GPUDeviceInfo{{
|
||||
DetailedInfo: []v2.GPUDeviceInfo{{
|
||||
Name: "GPU 0", MemoryUsed: 400, MemoryTotal: 800, Utilization: 30, Temperature: 55,
|
||||
}},
|
||||
},
|
||||
@@ -126,7 +126,7 @@ func TestWriteReportSkipsMetricsWithoutAgentData(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := useReportTestStore(t, nil)
|
||||
timestamp := time.Now().UTC()
|
||||
if _, err := WriteReport(ctx, v1.Report{
|
||||
if _, err := WriteReport(ctx, v2.Report{
|
||||
UUID: "node-without-gpu", UpdatedAt: timestamp,
|
||||
}); err != nil {
|
||||
t.Fatalf("write report: %v", err)
|
||||
@@ -154,11 +154,11 @@ func TestReportBatcherFlushesQueuedReports(t *testing.T) {
|
||||
})
|
||||
|
||||
base := time.Now().UTC().Truncate(time.Minute).Add(10 * time.Second)
|
||||
first := v1.Report{
|
||||
first := v2.Report{
|
||||
UUID: "batched-node",
|
||||
UpdatedAt: base,
|
||||
CPU: v1.CPUReport{Usage: 10},
|
||||
Network: v1.NetworkReport{TotalUp: 100, TotalDown: 200},
|
||||
CPU: v2.CPUReport{Usage: 10},
|
||||
Network: v2.NetworkReport{TotalUp: 100, TotalDown: 200},
|
||||
}
|
||||
second := first
|
||||
second.UpdatedAt = base.Add(3 * time.Second)
|
||||
@@ -271,11 +271,11 @@ func TestReportBatchKeepsEverySample(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := useReportTestStore(t, nil)
|
||||
base := time.Now().UTC().Truncate(time.Second)
|
||||
pending := []v1.Report{
|
||||
{UUID: "node-a", UpdatedAt: base, CPU: v1.CPUReport{Usage: 10}, Network: v1.NetworkReport{TotalUp: 100}},
|
||||
{UUID: "node-a", UpdatedAt: base, CPU: v1.CPUReport{Usage: 20}, Network: v1.NetworkReport{TotalUp: 150}},
|
||||
{UUID: "node-b", UpdatedAt: base, CPU: v1.CPUReport{Usage: 30}, Network: v1.NetworkReport{TotalUp: 200}},
|
||||
{UUID: "node-b", UpdatedAt: base.Add(time.Second), CPU: v1.CPUReport{Usage: 40}, Network: v1.NetworkReport{TotalUp: 260}},
|
||||
pending := []v2.Report{
|
||||
{UUID: "node-a", UpdatedAt: base, CPU: v2.CPUReport{Usage: 10}, Network: v2.NetworkReport{TotalUp: 100}},
|
||||
{UUID: "node-a", UpdatedAt: base, CPU: v2.CPUReport{Usage: 20}, Network: v2.NetworkReport{TotalUp: 150}},
|
||||
{UUID: "node-b", UpdatedAt: base, CPU: v2.CPUReport{Usage: 30}, Network: v2.NetworkReport{TotalUp: 200}},
|
||||
{UUID: "node-b", UpdatedAt: base.Add(time.Second), CPU: v2.CPUReport{Usage: 40}, Network: v2.NetworkReport{TotalUp: 260}},
|
||||
}
|
||||
|
||||
if err := writePendingReports(ctx, &pending); err != nil {
|
||||
@@ -292,11 +292,11 @@ func TestReportQueueFullReturnsError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
useReportTestStore(t, nil)
|
||||
worker := &reportBatchWorker{
|
||||
queue: make(chan v1.Report, 1),
|
||||
queue: make(chan v2.Report, 1),
|
||||
requests: make(chan reportBatchRequest, 1),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
worker.queue <- v1.Report{UUID: "already-queued"}
|
||||
worker.queue <- v2.Report{UUID: "already-queued"}
|
||||
reportBatcherMu.Lock()
|
||||
reportBatcher = worker
|
||||
reportBatcherMu.Unlock()
|
||||
@@ -308,7 +308,7 @@ func TestReportQueueFullReturnsError(t *testing.T) {
|
||||
reportBatcherMu.Unlock()
|
||||
})
|
||||
|
||||
report := v1.Report{
|
||||
report := v2.Report{
|
||||
UUID: "realtime-node",
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
@@ -374,11 +374,11 @@ func TestWriteReportRebasesTrafficAfterAgentRestart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := useReportTestStore(t, nil)
|
||||
base := time.Now().UTC().Truncate(time.Minute).Add(5 * time.Second)
|
||||
report := v1.Report{
|
||||
report := v2.Report{
|
||||
UUID: "restarted-node",
|
||||
UpdatedAt: base,
|
||||
Uptime: 1000,
|
||||
Network: v1.NetworkReport{TotalUp: 100, TotalDown: 200},
|
||||
Network: v2.NetworkReport{TotalUp: 100, TotalDown: 200},
|
||||
}
|
||||
if _, err := WriteReport(ctx, report); err != nil {
|
||||
t.Fatalf("write first report: %v", err)
|
||||
@@ -417,11 +417,11 @@ func TestWriteReportNormalizesReceiveTimeToUTC(t *testing.T) {
|
||||
s := useReportTestStore(t, nil)
|
||||
local := time.FixedZone("UTC+8", 8*60*60)
|
||||
receiveTime := time.Now().In(local).Add(-10 * time.Second)
|
||||
report := v1.Report{
|
||||
report := v2.Report{
|
||||
UUID: "utc-report",
|
||||
UpdatedAt: receiveTime,
|
||||
CPU: v1.CPUReport{Usage: 10},
|
||||
Network: v1.NetworkReport{TotalUp: 1, TotalDown: 2},
|
||||
CPU: v2.CPUReport{Usage: 10},
|
||||
Network: v2.NetworkReport{TotalUp: 1, TotalDown: 2},
|
||||
}
|
||||
|
||||
saved, err := WriteReport(ctx, report)
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
// wsFrameHookTimeout bounds one frame-level callback. The agent read pumps
|
||||
// run on deadlines (v1: 11s, v2: readWait); a frame hook must never stall
|
||||
// run on the agent read deadline; a frame hook must never stall
|
||||
// them, so the wait is cut short and the frame passes through untouched.
|
||||
const wsFrameHookTimeout = time.Second
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package v1
|
||||
|
||||
import "time"
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Sender string `json:"sender"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type IPAddress struct {
|
||||
Ipv4 string `json:"ipv4"`
|
||||
Ipv6 string `json:"ipv6"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
CPU CPUReport `json:"cpu"`
|
||||
Ram RamReport `json:"ram"`
|
||||
Swap RamReport `json:"swap"`
|
||||
Load LoadReport `json:"load"`
|
||||
Disk DiskReport `json:"disk"`
|
||||
Network NetworkReport `json:"network"`
|
||||
Connections ConnectionsReport `json:"connections"`
|
||||
GPU *GPUDetailReport `json:"gpu,omitempty"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Process int `json:"process"`
|
||||
Message string `json:"message"`
|
||||
Method string `json:"method,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CPUReport struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Usage float64 `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type GPUDetailReport struct {
|
||||
Count int `json:"count"`
|
||||
AverageUsage float64 `json:"average_usage"`
|
||||
DetailedInfo []GPUDeviceInfo `json:"detailed_info"`
|
||||
}
|
||||
|
||||
type GPUDeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
MemoryTotal int64 `json:"memory_total"`
|
||||
MemoryUsed int64 `json:"memory_used"`
|
||||
Utilization float64 `json:"utilization"`
|
||||
Temperature int `json:"temperature"`
|
||||
}
|
||||
|
||||
// GPUReport is the legacy v1 single-GPU shape kept for wire compatibility.
|
||||
type GPUReport struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Usage float64 `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type RamReport struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type LoadReport struct {
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
Load15 float64 `json:"load15"`
|
||||
}
|
||||
|
||||
type DiskReport struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type NetworkReport struct {
|
||||
Up int64 `json:"up"`
|
||||
Down int64 `json:"down"`
|
||||
TotalUp int64 `json:"totalUp"`
|
||||
TotalDown int64 `json:"totalDown"`
|
||||
}
|
||||
|
||||
type ConnectionsReport struct {
|
||||
TCP int `json:"tcp"`
|
||||
UDP int `json:"udp"`
|
||||
}
|
||||
+87
-4
@@ -3,8 +3,6 @@ package v2
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -52,8 +50,86 @@ type RPCError struct {
|
||||
}
|
||||
|
||||
type ReportParams struct {
|
||||
Report v1.Report `json:"report"`
|
||||
AckEventIDs []string `json:"ack_event_ids,omitempty"`
|
||||
Report Report `json:"report"`
|
||||
AckEventIDs []string `json:"ack_event_ids,omitempty"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Sender string `json:"sender"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type IPAddress struct {
|
||||
Ipv4 string `json:"ipv4"`
|
||||
Ipv6 string `json:"ipv6"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
CPU CPUReport `json:"cpu"`
|
||||
Ram RamReport `json:"ram"`
|
||||
Swap RamReport `json:"swap"`
|
||||
Load LoadReport `json:"load"`
|
||||
Disk DiskReport `json:"disk"`
|
||||
Network NetworkReport `json:"network"`
|
||||
Connections ConnectionsReport `json:"connections"`
|
||||
GPU *GPUDetailReport `json:"gpu,omitempty"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Process int `json:"process"`
|
||||
Message string `json:"message"`
|
||||
Method string `json:"method,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CPUReport struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Cores int `json:"cores,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
Usage float64 `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type GPUDetailReport struct {
|
||||
Count int `json:"count"`
|
||||
AverageUsage float64 `json:"average_usage"`
|
||||
DetailedInfo []GPUDeviceInfo `json:"detailed_info"`
|
||||
}
|
||||
|
||||
type GPUDeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
MemoryTotal int64 `json:"memory_total"`
|
||||
MemoryUsed int64 `json:"memory_used"`
|
||||
Utilization float64 `json:"utilization"`
|
||||
Temperature int `json:"temperature"`
|
||||
}
|
||||
|
||||
type RamReport struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type LoadReport struct {
|
||||
Load1 float64 `json:"load1"`
|
||||
Load5 float64 `json:"load5"`
|
||||
Load15 float64 `json:"load15"`
|
||||
}
|
||||
|
||||
type DiskReport struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
}
|
||||
|
||||
type NetworkReport struct {
|
||||
Up int64 `json:"up"`
|
||||
Down int64 `json:"down"`
|
||||
TotalUp int64 `json:"totalUp"`
|
||||
TotalDown int64 `json:"totalDown"`
|
||||
}
|
||||
|
||||
type ConnectionsReport struct {
|
||||
TCP int `json:"tcp"`
|
||||
UDP int `json:"udp"`
|
||||
}
|
||||
|
||||
type BasicInfoParams struct {
|
||||
@@ -67,6 +143,13 @@ type PingResultParams struct {
|
||||
FinishedAt time.Time `json:"finished_at"`
|
||||
}
|
||||
|
||||
type TaskResultParams struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Result string `json:"result"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
FinishedAt time.Time `json:"finished_at"`
|
||||
}
|
||||
|
||||
type PullParams struct {
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
AckEventIDs []string `json:"ack_event_ids,omitempty"`
|
||||
|
||||
+1
-13
@@ -57,18 +57,6 @@ func (m *PingTaskManager) Reload(pingTasks []models.PingTask) error {
|
||||
|
||||
// executePingTask 执行单个PingTask
|
||||
func executePingTask(ctx context.Context, task models.PingTask) {
|
||||
var message struct {
|
||||
TaskID uint `json:"ping_task_id"`
|
||||
Message string `json:"message"`
|
||||
Type string `json:"ping_type"`
|
||||
Target string `json:"ping_target"`
|
||||
}
|
||||
|
||||
message.Message = "ping"
|
||||
message.TaskID = task.Id
|
||||
message.Type = task.Type
|
||||
message.Target = task.Target
|
||||
|
||||
for _, clientUUID := range targetPingClientUUIDs(task) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -78,7 +66,7 @@ func executePingTask(ctx context.Context, task models.PingTask) {
|
||||
// Context is still active, continue.
|
||||
}
|
||||
|
||||
agent_runtime.DispatchPing(clientUUID, message, v2.PingParams{TaskID: task.Id, Type: task.Type, Target: task.Target})
|
||||
agent_runtime.DispatchPing(clientUUID, v2.PingParams{TaskID: task.Id, Type: task.Type, Target: task.Target})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-19
@@ -5,15 +5,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
"github.com/komari-monitor/komari/web/connection"
|
||||
)
|
||||
|
||||
var (
|
||||
connectedClients = make(map[string]*connection.SafeConn)
|
||||
connectedClientV2 = make(map[string]bool)
|
||||
latestReport = make(map[string]*v1.Report)
|
||||
recentReports = make(map[string][]v1.Report)
|
||||
connectedClients = make(map[string]*connection.SafeConn)
|
||||
v2Clients = make(map[string]struct{})
|
||||
latestReport = make(map[string]*v2.Report)
|
||||
recentReports = make(map[string][]v2.Report)
|
||||
// presenceOnly stores online state for non-WebSocket agents.
|
||||
// value keeps connectionID and a soft expiration to avoid flicker
|
||||
presenceOnly = make(map[string]struct {
|
||||
@@ -41,16 +41,17 @@ func SetConnectedClients(uuid string, conn *connection.SafeConn) {
|
||||
connectedClients[uuid] = conn
|
||||
}
|
||||
|
||||
func SetClientProtocolVersion(uuid string, version int) {
|
||||
func MarkV2Client(uuid string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
connectedClientV2[uuid] = version >= 2
|
||||
v2Clients[uuid] = struct{}{}
|
||||
}
|
||||
|
||||
func IsV2Client(uuid string) bool {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return connectedClientV2[uuid]
|
||||
_, ok := v2Clients[uuid]
|
||||
return ok
|
||||
}
|
||||
|
||||
func DeleteClientConditionally(uuid string, connToRemove *connection.SafeConn) {
|
||||
@@ -60,7 +61,7 @@ func DeleteClientConditionally(uuid string, connToRemove *connection.SafeConn) {
|
||||
// 检查当前 map 里的 conn 是否就是要删除的这一个
|
||||
if currentConn, exists := connectedClients[uuid]; exists && currentConn == connToRemove {
|
||||
delete(connectedClients, uuid)
|
||||
delete(connectedClientV2, uuid)
|
||||
delete(v2Clients, uuid)
|
||||
}
|
||||
}
|
||||
func DeleteConnectedClients(uuid string) {
|
||||
@@ -68,7 +69,7 @@ func DeleteConnectedClients(uuid string) {
|
||||
defer mu.Unlock()
|
||||
// 只从 map 中删除,不再负责关闭连接
|
||||
delete(connectedClients, uuid)
|
||||
delete(connectedClientV2, uuid)
|
||||
delete(v2Clients, uuid)
|
||||
}
|
||||
|
||||
// SetPresence sets or clears presence for non-WebSocket agents.
|
||||
@@ -121,10 +122,10 @@ func GetAllOnlineUUIDs() []string {
|
||||
}
|
||||
return res
|
||||
}
|
||||
func GetLatestReport() map[string]*v1.Report {
|
||||
func GetLatestReport() map[string]*v2.Report {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
reportCopy := make(map[string]*v1.Report)
|
||||
reportCopy := make(map[string]*v2.Report)
|
||||
for k, v := range latestReport {
|
||||
if v == nil {
|
||||
continue
|
||||
@@ -137,7 +138,7 @@ func GetLatestReport() map[string]*v1.Report {
|
||||
|
||||
// RecordReport updates the latest runtime state and keeps only the short raw
|
||||
// window used by recent-status compatibility endpoints.
|
||||
func RecordReport(report v1.Report) {
|
||||
func RecordReport(report v2.Report) {
|
||||
if report.UUID == "" {
|
||||
return
|
||||
}
|
||||
@@ -161,30 +162,30 @@ func RecordReport(report v1.Report) {
|
||||
insertAt := sort.Search(len(reports), func(i int) bool {
|
||||
return reports[i].UpdatedAt.After(report.UpdatedAt)
|
||||
})
|
||||
reports = append(reports, v1.Report{})
|
||||
reports = append(reports, v2.Report{})
|
||||
copy(reports[insertAt+1:], reports[insertAt:])
|
||||
reports[insertAt] = report
|
||||
recentReports[report.UUID] = reports
|
||||
}
|
||||
|
||||
func GetRecentReports(uuid string) []v1.Report {
|
||||
func GetRecentReports(uuid string) []v2.Report {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
reports := reportsAfter(recentReports[uuid], time.Now().UTC().Add(-recentReportRetention))
|
||||
if len(reports) == 0 {
|
||||
delete(recentReports, uuid)
|
||||
return []v1.Report{}
|
||||
return []v2.Report{}
|
||||
}
|
||||
recentReports[uuid] = reports
|
||||
return append([]v1.Report(nil), reports...)
|
||||
return append([]v2.Report(nil), reports...)
|
||||
}
|
||||
|
||||
func reportsAfter(reports []v1.Report, cutoff time.Time) []v1.Report {
|
||||
func reportsAfter(reports []v2.Report, cutoff time.Time) []v2.Report {
|
||||
first := 0
|
||||
for first < len(reports) && reports[first].UpdatedAt.Before(cutoff) {
|
||||
first++
|
||||
}
|
||||
out := make([]v1.Report, len(reports)-first)
|
||||
out := make([]v2.Report, len(reports)-first)
|
||||
copy(out, reports[first:])
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
)
|
||||
|
||||
func TestRecordReportKeepsLatestAndShortRecentWindow(t *testing.T) {
|
||||
mu.Lock()
|
||||
previousLatest := latestReport
|
||||
previousRecent := recentReports
|
||||
latestReport = make(map[string]*v1.Report)
|
||||
recentReports = make(map[string][]v1.Report)
|
||||
latestReport = make(map[string]*v2.Report)
|
||||
recentReports = make(map[string][]v2.Report)
|
||||
mu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
mu.Lock()
|
||||
@@ -22,9 +22,9 @@ func TestRecordReportKeepsLatestAndShortRecentWindow(t *testing.T) {
|
||||
})
|
||||
|
||||
now := time.Now().UTC()
|
||||
RecordReport(v1.Report{UUID: "node-a", UpdatedAt: now.Add(-2 * time.Minute), CPU: v1.CPUReport{Usage: 10}})
|
||||
RecordReport(v1.Report{UUID: "node-a", UpdatedAt: now.Add(-30 * time.Second), CPU: v1.CPUReport{Usage: 20}})
|
||||
RecordReport(v1.Report{UUID: "node-a", UpdatedAt: now.Add(-45 * time.Second), CPU: v1.CPUReport{Usage: 15}})
|
||||
RecordReport(v2.Report{UUID: "node-a", UpdatedAt: now.Add(-2 * time.Minute), CPU: v2.CPUReport{Usage: 10}})
|
||||
RecordReport(v2.Report{UUID: "node-a", UpdatedAt: now.Add(-30 * time.Second), CPU: v2.CPUReport{Usage: 20}})
|
||||
RecordReport(v2.Report{UUID: "node-a", UpdatedAt: now.Add(-45 * time.Second), CPU: v2.CPUReport{Usage: 15}})
|
||||
|
||||
recent := GetRecentReports("node-a")
|
||||
if len(recent) != 2 || recent[0].CPU.Usage != 15 || recent[1].CPU.Usage != 20 {
|
||||
|
||||
@@ -54,12 +54,9 @@ func DispatchV2Event(uuid, method string, params any) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func DispatchPing(uuid string, legacy any, params v2.PingParams) bool {
|
||||
func DispatchPing(uuid string, params v2.PingParams) bool {
|
||||
if conn := GetConnectedClients()[uuid]; conn != nil {
|
||||
payload := legacy
|
||||
if IsV2Client(uuid) {
|
||||
payload = v2.Request{JSONRPC: v2.Version, Method: v2.MethodAgentPing, Params: params}
|
||||
}
|
||||
payload := v2.Request{JSONRPC: v2.Version, Method: v2.MethodAgentPing, Params: params}
|
||||
if conn.WriteJSON(payload) == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -5,21 +5,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/metricstore"
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/database/tasks"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
"github.com/komari-monitor/komari/internal/metricstore"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
)
|
||||
|
||||
// ingest.go
|
||||
// agent 上报数据的传输无关处理逻辑。v1 (REST/WS) 与 v2 (JSON-RPC) 两套上报入口
|
||||
// 经过各自的协议解析后,统一调用这里的函数落库并更新运行时状态,消除重复。
|
||||
// agent 上报数据的传输无关处理逻辑。v2 的 HTTP/WebSocket JSON-RPC 入口
|
||||
// 经过协议解析后,统一调用这里的函数落库并更新运行时状态。
|
||||
|
||||
// ingestReport 保存一次负载上报并刷新运行时状态。
|
||||
// protocolVersion 标记上报所用协议(1 或 2),用于运行时区分客户端能力。
|
||||
// markPresence 为 true 时按 POST 上报会话刷新在线状态(WS 连接自行管理在线状态,应传 false)。
|
||||
func ingestReport(uuid string, report v1.Report, protocolVersion int, markPresence bool) error {
|
||||
func ingestReport(uuid string, report v2.Report, markPresence bool) error {
|
||||
report.UUID = uuid
|
||||
report.UpdatedAt = time.Now().UTC()
|
||||
if err := clients.ReportVerify(report); err != nil {
|
||||
@@ -30,7 +29,7 @@ func ingestReport(uuid string, report v1.Report, protocolVersion int, markPresen
|
||||
return err
|
||||
}
|
||||
agent_runtime.RecordReport(savedReport)
|
||||
agent_runtime.SetClientProtocolVersion(uuid, protocolVersion)
|
||||
agent_runtime.MarkV2Client(uuid)
|
||||
if markPresence {
|
||||
refreshPostPresence(uuid)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/utils/notifier"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
)
|
||||
|
||||
const (
|
||||
// 如果超过这个时间没有收到任何消息,则认为连接已死
|
||||
readWait = 11 * time.Second
|
||||
postPresenceTTL = 35 * time.Second
|
||||
)
|
||||
|
||||
type postPresenceEntry struct {
|
||||
connID int64
|
||||
timer *time.Timer
|
||||
generation uint64
|
||||
}
|
||||
|
||||
var (
|
||||
postPresenceMu sync.Mutex
|
||||
postPresenceStates = make(map[string]*postPresenceEntry)
|
||||
)
|
||||
|
||||
// refreshPostPresence 管理 HTTP POST 上报者的在线/离线状态。
|
||||
func refreshPostPresence(uuid string) {
|
||||
postPresenceMu.Lock()
|
||||
defer postPresenceMu.Unlock()
|
||||
|
||||
if entry, exists := postPresenceStates[uuid]; exists {
|
||||
entry.generation++
|
||||
entry.timer.Stop()
|
||||
gen := entry.generation
|
||||
entry.timer = time.AfterFunc(postPresenceTTL, func() {
|
||||
postPresenceExpired(uuid, entry.connID, gen)
|
||||
})
|
||||
agent_runtime.KeepAlivePresence(uuid, entry.connID, postPresenceTTL)
|
||||
return
|
||||
}
|
||||
|
||||
connID := time.Now().UnixNano()
|
||||
agent_runtime.KeepAlivePresence(uuid, connID, postPresenceTTL)
|
||||
agent_runtime.MarkV2Client(uuid)
|
||||
go notifier.OnlineNotification(uuid, connID)
|
||||
|
||||
defaultGeneration := uint64(0)
|
||||
entry := &postPresenceEntry{connID: connID, generation: defaultGeneration}
|
||||
entry.timer = time.AfterFunc(postPresenceTTL, func() {
|
||||
postPresenceExpired(uuid, connID, defaultGeneration)
|
||||
})
|
||||
postPresenceStates[uuid] = entry
|
||||
}
|
||||
|
||||
func postPresenceExpired(uuid string, connID int64, gen uint64) {
|
||||
postPresenceMu.Lock()
|
||||
e, ok := postPresenceStates[uuid]
|
||||
if !ok || e.connID != connID || e.generation != gen {
|
||||
postPresenceMu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(postPresenceStates, uuid)
|
||||
postPresenceMu.Unlock()
|
||||
|
||||
agent_runtime.SetPresence(uuid, connID, false)
|
||||
notifier.OfflineNotification(uuid, connID)
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
logger "github.com/komari-monitor/komari/utils/log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/komari-monitor/komari/database/clients"
|
||||
v1 "github.com/komari-monitor/komari/protocol/v1"
|
||||
"github.com/komari-monitor/komari/utils/notifier"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
"github.com/komari-monitor/komari/web/api"
|
||||
"github.com/komari-monitor/komari/web/connection"
|
||||
)
|
||||
|
||||
const (
|
||||
// 如果超过这个时间没有收到任何消息,则认为连接已死
|
||||
// 因为目前server没有存agent的信息上报间隔。只有写一个默认的
|
||||
readWait = 11 * time.Second
|
||||
postPresenceTTL = 35 * time.Second
|
||||
)
|
||||
|
||||
// postPresenceEntry 保存单个客户端的 POST 上报会话状态
|
||||
type postPresenceEntry struct {
|
||||
connID int64
|
||||
timer *time.Timer
|
||||
generation uint64 // 每次 Reset 递增,用于回调中判断是否为过期的旧回调
|
||||
}
|
||||
|
||||
var (
|
||||
postPresenceMu sync.Mutex
|
||||
postPresenceStates = make(map[string]*postPresenceEntry)
|
||||
)
|
||||
|
||||
// refreshPostPresence 管理 HTTP POST 上报者的在线/离线状态。
|
||||
// 每次 POST 刷新 TTL 定时器;定时器到期后触发离线通知。
|
||||
func refreshPostPresence(uuid string) {
|
||||
postPresenceMu.Lock()
|
||||
defer postPresenceMu.Unlock()
|
||||
|
||||
if entry, exists := postPresenceStates[uuid]; exists {
|
||||
// 已在线:递增 generation 使可能正在执行的旧回调失效
|
||||
entry.generation++
|
||||
entry.timer.Stop()
|
||||
// 重新创建 AfterFunc 以在闭包中捕获新的 generation
|
||||
gen := entry.generation
|
||||
entry.timer = time.AfterFunc(postPresenceTTL, func() {
|
||||
postPresenceExpired(uuid, entry.connID, gen)
|
||||
})
|
||||
agent_runtime.KeepAlivePresence(uuid, entry.connID, postPresenceTTL)
|
||||
return
|
||||
}
|
||||
|
||||
// 新 POST 会话:生成 connID,标记在线,启动超时定时器
|
||||
connID := time.Now().UnixNano()
|
||||
agent_runtime.KeepAlivePresence(uuid, connID, postPresenceTTL)
|
||||
agent_runtime.SetClientProtocolVersion(uuid, 1)
|
||||
go notifier.OnlineNotification(uuid, connID)
|
||||
|
||||
defaultGeneration := uint64(0)
|
||||
|
||||
entry := &postPresenceEntry{
|
||||
connID: connID,
|
||||
generation: defaultGeneration,
|
||||
}
|
||||
|
||||
entry.timer = time.AfterFunc(postPresenceTTL, func() {
|
||||
postPresenceExpired(uuid, connID, defaultGeneration)
|
||||
})
|
||||
|
||||
postPresenceStates[uuid] = entry
|
||||
}
|
||||
|
||||
// postPresenceExpired 是定时器到期的回调。
|
||||
// 只有当 connID 和 generation 都与当前 entry 匹配时才执行离线清理,
|
||||
// 避免 timer.Reset 竞态导致过期回调错误地清除仍活跃的会话。
|
||||
func postPresenceExpired(uuid string, connID int64, gen uint64) {
|
||||
postPresenceMu.Lock()
|
||||
e, ok := postPresenceStates[uuid]
|
||||
if !ok || e.connID != connID || e.generation != gen {
|
||||
postPresenceMu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(postPresenceStates, uuid)
|
||||
postPresenceMu.Unlock()
|
||||
|
||||
agent_runtime.SetPresence(uuid, connID, false)
|
||||
notifier.OfflineNotification(uuid, connID)
|
||||
}
|
||||
|
||||
func UploadReport(c *gin.Context) {
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
logger.ErrorArgs("client-api", "Failed to read request body:", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal(bodyBytes, &data)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
// Save report to database
|
||||
var report v1.Report
|
||||
err = json.Unmarshal(bodyBytes, &report)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
||||
return
|
||||
}
|
||||
// 优先使用 body 中的 UUID,若为空则从中间件注入的上下文中获取
|
||||
uuid := report.UUID
|
||||
if uuid == "" {
|
||||
if v, ok := c.Get("client_uuid"); ok {
|
||||
uuid, _ = v.(string)
|
||||
}
|
||||
}
|
||||
if uuid == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "UUID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// POST 上报:落库、更新运行时状态并刷新在线状态
|
||||
if err := ingestReport(uuid, report, 1, true); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore the body for further use
|
||||
c.JSON(200, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
func WebSocketReport(c *gin.Context) {
|
||||
// 升级ws
|
||||
if !api.IsWebSocketUpgrade(c) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Require WebSocket upgrade"})
|
||||
return
|
||||
}
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := api.UpgradeSafeConn(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "Failed to upgrade to WebSocket." + err.Error()})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
logger.ErrorArgs("client-api", "Error reading message:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 第一次数据拿token
|
||||
data := map[string]interface{}{}
|
||||
err = json.Unmarshal(message, &data)
|
||||
if err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Invalid JSON"})
|
||||
return
|
||||
}
|
||||
// it should ok,token was verfied in the middleware
|
||||
token := ""
|
||||
var errMsg string
|
||||
|
||||
// 优先检查查询参数中的 token
|
||||
token = c.Query("token")
|
||||
|
||||
// 如果 token 为空,返回错误
|
||||
if token == "" {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": errMsg})
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := clients.GetClientUUIDByToken(token)
|
||||
if err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": errMsg})
|
||||
return
|
||||
}
|
||||
|
||||
// 接受新连接,并处理旧连接
|
||||
if oldConn, exists := agent_runtime.GetConnectedClients()[uuid]; exists {
|
||||
logger.Infof("client-api", "Client %s is reconnecting. Closing the old connection.", uuid)
|
||||
|
||||
// 强制关闭旧连接。这将导致旧连接的 ReadMessage() 循环出错退出。
|
||||
go oldConn.Close()
|
||||
}
|
||||
agent_runtime.SetConnectedClients(uuid, conn)
|
||||
logger.Infof("client-api", "Client %s is reconnect success, connID: %d", uuid, conn.ID)
|
||||
go notifier.OnlineNotification(uuid, conn.ID)
|
||||
defer func() {
|
||||
agent_runtime.DeleteClientConditionally(uuid, conn)
|
||||
notifier.OfflineNotification(uuid, conn.ID)
|
||||
}()
|
||||
|
||||
// 首先处理第一次ws conn收到的消息
|
||||
processMessage(conn, message, uuid)
|
||||
|
||||
for {
|
||||
conn.SetReadDeadline(time.Now().Add(readWait))
|
||||
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
logger.Errorf("client-api", "Client %s connection error: %v", uuid, err)
|
||||
}
|
||||
break // 任何读错误(包括超时)都意味着连接已断开,退出循环
|
||||
}
|
||||
processMessage(conn, message, uuid)
|
||||
}
|
||||
}
|
||||
|
||||
// 将消息处理逻辑提取到一个函数中,方便复用
|
||||
func processMessage(conn *connection.SafeConn, message []byte, uuid string) {
|
||||
type MessageType struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
var msgType MessageType
|
||||
err := json.Unmarshal(message, &msgType)
|
||||
if err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Invalid JSON"})
|
||||
return
|
||||
}
|
||||
|
||||
switch msgType.Type {
|
||||
case "", "report":
|
||||
report := v1.Report{}
|
||||
err = json.Unmarshal(message, &report)
|
||||
if err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Invalid report format"})
|
||||
return
|
||||
}
|
||||
// WS 连接自行管理在线状态,无需刷新 POST presence
|
||||
if err := ingestReport(uuid, report, 1, false); err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": fmt.Sprintf("%v", err)})
|
||||
return
|
||||
}
|
||||
case "ping_result":
|
||||
var reqBody struct {
|
||||
PingTaskID uint `json:"task_id"`
|
||||
PingResult int `json:"value"`
|
||||
PingType string `json:"ping_type"`
|
||||
}
|
||||
err = json.Unmarshal(message, &reqBody)
|
||||
if err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Invalid ping result format"})
|
||||
return
|
||||
}
|
||||
if err := ingestPingResult(uuid, reqBody.PingTaskID, reqBody.PingResult); err != nil {
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Failed to save ping result"})
|
||||
}
|
||||
default:
|
||||
logger.Warnf("client-api", "Unknown message type: %s", msgType.Type)
|
||||
conn.WriteJSON(gin.H{"status": "error", "error": "Unknown message type"})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/komari-monitor/komari/database/clients"
|
||||
"github.com/komari-monitor/komari/database/tasks"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
"github.com/komari-monitor/komari/utils/notifier"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
@@ -52,7 +53,7 @@ func handleV2RPC(uuid string, req v2.Request, allowWait bool) v2.Response {
|
||||
if err := bindV2Params(req.Params, ¶ms); err != nil {
|
||||
return v2.Error(req.ID, -32602, "invalid report params", err.Error())
|
||||
}
|
||||
if err := ingestReport(uuid, params.Report, 2, true); err != nil {
|
||||
if err := ingestReport(uuid, params.Report, true); err != nil {
|
||||
return v2.Error(req.ID, -32000, "failed to save report", err.Error())
|
||||
}
|
||||
return v2.Success(req.ID, gin.H{
|
||||
@@ -77,13 +78,26 @@ func handleV2RPC(uuid string, req v2.Request, allowWait bool) v2.Response {
|
||||
return v2.Error(req.ID, -32000, "failed to save ping result", err.Error())
|
||||
}
|
||||
return v2.Success(req.ID, gin.H{"status": "success"})
|
||||
case v2.MethodAgentTaskResult:
|
||||
var params v2.TaskResultParams
|
||||
if err := bindV2Params(req.Params, ¶ms); err != nil {
|
||||
return v2.Error(req.ID, -32602, "invalid task result params", err.Error())
|
||||
}
|
||||
finishedAt := params.FinishedAt
|
||||
if finishedAt.IsZero() {
|
||||
finishedAt = time.Now().UTC()
|
||||
}
|
||||
if err := tasks.SaveTaskResult(params.TaskID, uuid, params.Result, params.ExitCode, finishedAt); err != nil {
|
||||
return v2.Error(req.ID, -32000, "failed to save task result", err.Error())
|
||||
}
|
||||
return v2.Success(req.ID, gin.H{"status": "success"})
|
||||
case v2.MethodAgentPull:
|
||||
var params v2.PullParams
|
||||
if err := bindV2Params(req.Params, ¶ms); err != nil {
|
||||
return v2.Error(req.ID, -32602, "invalid pull params", err.Error())
|
||||
}
|
||||
refreshPostPresence(uuid)
|
||||
agent_runtime.SetClientProtocolVersion(uuid, 2)
|
||||
agent_runtime.MarkV2Client(uuid)
|
||||
timeout := 0 * time.Second
|
||||
if allowWait {
|
||||
timeout = 25 * time.Second
|
||||
@@ -151,7 +165,7 @@ func WebSocketV2RPC(c *gin.Context) {
|
||||
go oldConn.Close()
|
||||
}
|
||||
agent_runtime.SetConnectedClients(uuid, conn)
|
||||
agent_runtime.SetClientProtocolVersion(uuid, 2)
|
||||
agent_runtime.MarkV2Client(uuid)
|
||||
go notifierOnline(uuid, conn.ID)
|
||||
defer func() {
|
||||
agent_runtime.DeleteClientConditionally(uuid, conn)
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/komari-monitor/komari/database/clients"
|
||||
"github.com/komari-monitor/komari/internal/config"
|
||||
"github.com/komari-monitor/komari/utils/geoip"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func getClientIPType(ip net.IP) int {
|
||||
@@ -80,25 +78,3 @@ func appendClientRegionFromGeoIP(info map[string]interface{}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func UploadBasicInfo(c *gin.Context) {
|
||||
var cbi = map[string]interface{}{}
|
||||
if err := c.ShouldBindJSON(&cbi); err != nil {
|
||||
c.JSON(400, gin.H{"status": "error", "error": "Invalid or missing data"})
|
||||
return
|
||||
}
|
||||
|
||||
token := c.Query("token")
|
||||
uuid, err := clients.GetClientUUIDByToken(token)
|
||||
if uuid == "" || err != nil {
|
||||
c.JSON(400, gin.H{"status": "error", "error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := ingestBasicInfo(uuid, cbi, c.ClientIP()); err != nil {
|
||||
c.JSON(500, gin.H{"status": "error", "error": err})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"status": "success"})
|
||||
}
|
||||
@@ -14,17 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func dispatchTerminalRequest(uuid, id string) bool {
|
||||
if agent_runtime.IsV2Client(uuid) {
|
||||
return agent_runtime.DispatchV2Event(uuid, v2.MethodAgentTerminal, v2.TerminalRequestParams{RequestID: id})
|
||||
}
|
||||
agent := agent_runtime.GetConnectedClients()[uuid]
|
||||
if agent == nil {
|
||||
return false
|
||||
}
|
||||
return agent.WriteJSON(gin.H{
|
||||
"message": "terminal",
|
||||
"request_id": id,
|
||||
}) == nil
|
||||
return agent_runtime.DispatchV2Event(uuid, v2.MethodAgentTerminal, v2.TerminalRequestParams{RequestID: id})
|
||||
}
|
||||
|
||||
func RequestTerminal(c *gin.Context) {
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/komari-monitor/komari/database/accounts"
|
||||
"github.com/komari-monitor/komari/database/dbcore"
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
)
|
||||
|
||||
@@ -53,11 +53,11 @@ func GetClients(c *gin.Context) {
|
||||
for {
|
||||
var resp struct {
|
||||
Online []string `json:"online"` // 已建立连接的客户端uuid列表
|
||||
Data map[string]v1.Report `json:"data"` // 最后上报的数据
|
||||
Data map[string]v2.Report `json:"data"` // 最后上报的数据
|
||||
}
|
||||
|
||||
resp.Online = []string{}
|
||||
resp.Data = map[string]v1.Report{}
|
||||
resp.Data = map[string]v2.Report{}
|
||||
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
|
||||
@@ -280,6 +280,7 @@ func serveUploadTransfer(c *gin.Context, transfer *streamTransfer) {
|
||||
}
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Length", strconv.FormatInt(transfer.size, 10))
|
||||
c.Header("Content-Encoding", "identity")
|
||||
c.Header("Cache-Control", "no-store, no-transform")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
c.Status(http.StatusOK)
|
||||
@@ -325,6 +326,7 @@ func streamDownloadRelay(c *gin.Context, clientUUID, path string, fileSize int64
|
||||
for offset < end {
|
||||
length := min(currentChunkSize, end-offset)
|
||||
var chunkErr error
|
||||
var lastCopyResult streamCopyResult
|
||||
for {
|
||||
resized := false
|
||||
chunkErr = nil
|
||||
@@ -334,6 +336,7 @@ func streamDownloadRelay(c *gin.Context, clientUUID, path string, fileSize int64
|
||||
commit = nil
|
||||
}
|
||||
copyResult, streamErr := streamDownloadChunk(c, clientUUID, path, fileSize, modifiedAt, offset, length, chunkIndex, commit)
|
||||
lastCopyResult = copyResult
|
||||
if copyResult.err == nil && copyResult.bytes == length {
|
||||
// The request has proven that this logical block size is
|
||||
// accepted. Remember it immediately so concurrent/follow-up
|
||||
@@ -373,6 +376,18 @@ func streamDownloadRelay(c *gin.Context, clientUUID, path string, fileSize int64
|
||||
if copyResult.bytes == 0 && isNonRetryableStreamError(chunkErr) {
|
||||
return chunkErr
|
||||
}
|
||||
if copyResult.bytes == 0 && attempt == streamChunkAttempts && isChunkTransportError(chunkErr) {
|
||||
adaptiveUpperBound = min(adaptiveUpperBound, length)
|
||||
if next, ok := nextReducedTransferChunkSize(length, adaptiveUpperBound); ok && next < length {
|
||||
previousLength := length
|
||||
currentChunkSize = next
|
||||
c.Set(downloadChunkSizeContextKey, currentChunkSize)
|
||||
length = min(currentChunkSize, end-offset)
|
||||
logger.Warnf("file-transfer", "reducing download chunk size after transport failure client=%s path=%q from=%d to=%d: %v", clientUUID, path, previousLength, currentChunkSize, chunkErr)
|
||||
resized = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Once a byte of this logical chunk reached the browser, retrying
|
||||
// would overlap an already committed HTTP response. Only retry when
|
||||
// the failure happened before the first byte.
|
||||
@@ -389,6 +404,16 @@ func streamDownloadRelay(c *gin.Context, clientUUID, path string, fileSize int64
|
||||
break
|
||||
}
|
||||
if chunkErr != nil {
|
||||
// A downstream browser/proxy can close a long response after some
|
||||
// bytes were delivered. The current HTTP response cannot be resumed,
|
||||
// but the next Range request can avoid repeating the same oversized
|
||||
// logical block by remembering a smaller probe size.
|
||||
if lastCopyResult.bytes > 0 && isClientDisconnectError(lastCopyResult.err) {
|
||||
if next, ok := nextReducedTransferChunkSize(currentChunkSize, currentChunkSize); ok && next < currentChunkSize {
|
||||
rememberDownloadChunkSize(clientUUID, next)
|
||||
logger.Warnf("file-transfer", "reducing download chunk size after downstream disconnect client=%s from=%d to=%d", clientUUID, currentChunkSize, next)
|
||||
}
|
||||
}
|
||||
return chunkErr
|
||||
}
|
||||
offset += length
|
||||
@@ -518,6 +543,46 @@ func isRelayPipeError(err error) bool {
|
||||
return strings.Contains(message, "closed pipe") || strings.Contains(message, "file transfer is closed")
|
||||
}
|
||||
|
||||
func isClientDisconnectError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
message := strings.ToLower(err.Error())
|
||||
for _, marker := range []string{
|
||||
"broken pipe",
|
||||
"connection reset by peer",
|
||||
"connection reset",
|
||||
"write: closed network connection",
|
||||
"client disconnected",
|
||||
} {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isChunkTransportError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
message := strings.ToLower(err.Error())
|
||||
for _, marker := range []string{
|
||||
" 502 ",
|
||||
" 503 ",
|
||||
" 504 ",
|
||||
"broken pipe",
|
||||
"connection reset",
|
||||
"unexpected eof",
|
||||
"timeout",
|
||||
} {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNonRetryableStreamError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -565,6 +565,7 @@ func downloadFile(c *gin.Context, clientUUID, path string, options downloadRespo
|
||||
c.Header("Content-Disposition", formatDownloadContentDisposition(disposition, name, options.OfficeCompatible))
|
||||
c.Header("Accept-Ranges", "bytes")
|
||||
c.Header("Content-Length", strconv.FormatInt(contentLength, 10))
|
||||
c.Header("Content-Encoding", "identity")
|
||||
c.Header("Cache-Control", "no-store, no-transform")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
c.Header("X-Komari-Transfer-Chunk-Size", strconv.FormatInt(headerChunkSize, 10))
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestRestrictedControllersRegisterUnifiedRoutes(t *testing.T) {
|
||||
if routes["POST "+base+"/discard"] != test.discard {
|
||||
t.Fatalf("POST %s/discard presence = %v, want %v", base, routes["POST "+base+"/discard"], test.discard)
|
||||
}
|
||||
if routes["GET /api/public"] || routes["GET /api/rpc2"] || routes["POST /api/clients/report"] {
|
||||
if routes["GET /api/public"] || routes["GET /api/rpc2"] || routes["GET /api/clients/v2/rpc"] {
|
||||
t.Fatalf("ordinary APIs leaked into restricted routes: %#v", routes)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,21 +68,13 @@ func registerAgentRoutes(r *gin.Engine) {
|
||||
|
||||
tokenAuthorized := r.Group("/api/clients", api.RequireRole(api.RoleAdmin, api.RoleClient))
|
||||
{
|
||||
// 上报类(WS / 原始流 / 兼容协议)保留 REST handler。
|
||||
tokenAuthorized.GET("/report", client.WebSocketReport)
|
||||
tokenAuthorized.POST("/uploadBasicInfo", client.UploadBasicInfo)
|
||||
tokenAuthorized.POST("/report", client.UploadReport)
|
||||
// Agent 上报统一使用 v2 JSON-RPC。
|
||||
tokenAuthorized.GET("/v2/rpc", client.WebSocketV2RPC)
|
||||
tokenAuthorized.POST("/v2/rpc", client.UploadV2RPC)
|
||||
// File data uses a short-lived, raw HTTP stream opened by a file RPC.
|
||||
tokenAuthorized.GET("/transfer/:id", filemanager.AgentTransfer)
|
||||
tokenAuthorized.POST("/transfer/:id", filemanager.AgentTransfer)
|
||||
tokenAuthorized.GET("/terminal", terminal.EstablishConnection)
|
||||
|
||||
// JSON 接口 -> RPC2 (client: 命名空间)。
|
||||
tokenAuthorized.POST("/task/result", jsonRpc.Bind("client:taskResult", jsonRpc.WithRaw()))
|
||||
tokenAuthorized.GET("/ping/tasks", jsonRpc.Bind("client:getPingTasks", jsonRpc.WithRaw()))
|
||||
tokenAuthorized.POST("/ping/result", jsonRpc.Bind("client:uploadPingResult", jsonRpc.WithRaw()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ handler 退化为薄适配层:解析 gin 参数 → 调 RPC → 把响应映
|
||||
| --- | --- |
|
||||
| `admin` | client CRUD、ping task、session/settings/weight、notification(load/offline/traffic)、clipboard、provider(messageSender/oidc)、task 查询、system(logs/cloudflared/exec/test)、xtermjs |
|
||||
| `public` | getMe、getNodesInformation、getPublicSettings、getVersion、getClientRecentRecords、getRecordsByUUID、getPingRecords、getPublicPingTasks、recordVisitorEvent |
|
||||
| `client` | getPingTasks、uploadPingResult、taskResult |
|
||||
| Agent v2 | `agent.report`、`agent.basicInfo`、`agent.pingResult`、`agent.taskResult`、`agent.pull`(`/api/clients/v2/rpc`) |
|
||||
|
||||
### 声明式路由桥 `Bind`
|
||||
|
||||
@@ -136,6 +136,6 @@ r.GET("/api/admin/client/:uuid", jsonRpc.Bind("admin:getClient", jsonRpc.WithPat
|
||||
### 保留为 REST 的接口(不走 RPC 桥)
|
||||
|
||||
二进制/流/重定向/特殊鉴权类,集中在 `web/api/admin`(2fa/theme/backup/update/oauth 绑定)、
|
||||
`web/api/public`(login/logout/oauth/plugin)、`web/api/client`(report WS+POST、v2 RPC、uploadBasicInfo、terminal、AutoDiscovery 注册)。
|
||||
`web/api/public`(login/logout/oauth/plugin)、`web/api/client`(v2 RPC、terminal、AutoDiscovery 注册)。
|
||||
|
||||
agent v1/v2 上报的核心逻辑已统一到 `web/api/client/ingest.go`。
|
||||
agent v2 上报的核心逻辑统一在 `web/api/client/ingest.go`。
|
||||
@@ -131,15 +131,7 @@ func adminExec(ctx context.Context, req *rpc.JsonRpcRequest) (any, *rpc.JsonRpcE
|
||||
return nil, rpc.MakeError(rpc.InternalError, "Failed to create task: "+err.Error(), nil)
|
||||
}
|
||||
for _, uuid := range onlineClients {
|
||||
legacy := struct {
|
||||
Message string `json:"message"`
|
||||
Command string `json:"command"`
|
||||
TaskId string `json:"task_id"`
|
||||
}{Message: "exec", Command: params.Command, TaskId: taskId}
|
||||
payload, _ := json.Marshal(legacy)
|
||||
if agent_runtime.IsV2Client(uuid) {
|
||||
payload, _ = json.Marshal(v2.Request{JSONRPC: v2.Version, Method: v2.MethodAgentExec, Params: v2.ExecParams{TaskID: taskId, Command: params.Command}})
|
||||
}
|
||||
payload, _ := json.Marshal(v2.Request{JSONRPC: v2.Version, Method: v2.MethodAgentExec, Params: v2.ExecParams{TaskID: taskId, Command: params.Command}})
|
||||
client := agent_runtime.GetConnectedClients()[uuid]
|
||||
if client == nil {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "Client connection is null: "+uuid, nil)
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package jsonrpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/database/models"
|
||||
"github.com/komari-monitor/komari/database/tasks"
|
||||
"github.com/komari-monitor/komari/pkg/rpc"
|
||||
)
|
||||
|
||||
// client.go
|
||||
// 客户端(agent)面向的 RPC2 方法(client 命名空间,client/admin 可调用)。
|
||||
// 这些方法依赖 meta.ClientUUID 标识调用方客户端。
|
||||
|
||||
func init() {
|
||||
regClient("getPingTasks", clientGetPingTasks, "Get ping tasks assigned to the calling client")
|
||||
regClient("uploadPingResult", clientUploadPingResult, "Upload a ping result")
|
||||
regClient("taskResult", clientTaskResult, "Upload an exec task result")
|
||||
}
|
||||
|
||||
func regClient(name string, h rpc.Handler, summary string) {
|
||||
RegisterWithGroupAndMeta(name, rpc.RoleClient, h, &rpc.MethodMeta{Name: "client:" + name, Summary: summary})
|
||||
}
|
||||
|
||||
// callingClientUUID 返回发起调用的客户端 UUID。
|
||||
func callingClientUUID(ctx context.Context) string {
|
||||
if meta := rpc.MetaFromContext(ctx); meta != nil {
|
||||
return meta.ClientUUID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func clientGetPingTasks(ctx context.Context, _ *rpc.JsonRpcRequest) (any, *rpc.JsonRpcError) {
|
||||
uuid := callingClientUUID(ctx)
|
||||
if uuid == "" {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "client_uuid not found", nil)
|
||||
}
|
||||
return tasks.GetPingTasksByClient(uuid), nil
|
||||
}
|
||||
|
||||
func clientUploadPingResult(ctx context.Context, req *rpc.JsonRpcRequest) (any, *rpc.JsonRpcError) {
|
||||
uuid := callingClientUUID(ctx)
|
||||
if uuid == "" {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "client_uuid not found", nil)
|
||||
}
|
||||
var params struct {
|
||||
TaskID uint `json:"task_id"`
|
||||
Value int `json:"value"`
|
||||
PingType string `json:"ping_type"`
|
||||
}
|
||||
if err := req.BindParams(¶ms); err != nil {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "Invalid request: "+err.Error(), nil)
|
||||
}
|
||||
record := models.PingRecord{
|
||||
Client: uuid,
|
||||
TaskId: params.TaskID,
|
||||
Value: params.Value,
|
||||
Time: time.Now().UTC(),
|
||||
}
|
||||
if err := tasks.SavePingRecord(record); err != nil {
|
||||
return nil, rpc.MakeError(rpc.InternalError, "Failed to save ping result: "+err.Error(), nil)
|
||||
}
|
||||
return map[string]any{"status": "success"}, nil
|
||||
}
|
||||
|
||||
func clientTaskResult(ctx context.Context, req *rpc.JsonRpcRequest) (any, *rpc.JsonRpcError) {
|
||||
uuid := callingClientUUID(ctx)
|
||||
if uuid == "" {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "Invalid or missing token", nil)
|
||||
}
|
||||
var params struct {
|
||||
TaskId string `json:"task_id"`
|
||||
Result string `json:"result"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
}
|
||||
if err := req.BindParams(¶ms); err != nil {
|
||||
return nil, rpc.MakeError(rpc.InvalidParams, "Invalid request", nil)
|
||||
}
|
||||
if err := tasks.SaveTaskResult(params.TaskId, uuid, params.Result, params.ExitCode, time.Now().UTC()); err != nil {
|
||||
return nil, rpc.MakeError(rpc.InternalError, "Failed to update task result: "+err.Error(), nil)
|
||||
}
|
||||
return map[string]any{"status": "success", "message": "Task result updated successfully"}, nil
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/komari-monitor/komari/database/tasks"
|
||||
"github.com/komari-monitor/komari/internal/config"
|
||||
"github.com/komari-monitor/komari/pkg/rpc"
|
||||
"github.com/komari-monitor/komari/protocol/v1"
|
||||
v2 "github.com/komari-monitor/komari/protocol/v2"
|
||||
"github.com/komari-monitor/komari/utils"
|
||||
agent_runtime "github.com/komari-monitor/komari/web/agent"
|
||||
|
||||
@@ -285,7 +285,7 @@ func getNodesLatestStatus(ctx context.Context, req *rpc.JsonRpcRequest) (any, *r
|
||||
req.BindParams(¶ms)
|
||||
|
||||
meta := rpc.MetaFromContext(ctx)
|
||||
latest := agent_runtime.GetLatestReport() // map[string]*v1.Report (copy)
|
||||
latest := agent_runtime.GetLatestReport()
|
||||
onlineUUIDs := agent_runtime.GetAllOnlineUUIDs()
|
||||
onlineSet := make(map[string]bool, len(onlineUUIDs))
|
||||
for _, uuid := range onlineUUIDs {
|
||||
@@ -350,7 +350,7 @@ func getNodesLatestStatus(ctx context.Context, req *rpc.JsonRpcRequest) (any, *r
|
||||
// 预取所有 ping 任务
|
||||
pingTasks, _ := tasks.GetAllPingTasks()
|
||||
|
||||
appendOne := func(uuid string, rep *v1.Report) {
|
||||
appendOne := func(uuid string, rep *v2.Report) {
|
||||
if rep == nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user