Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 0aaedf3

Browse files
Reworked update mechanism
2 parents a035250 + 4236226 commit 0aaedf3

14 files changed

Lines changed: 513 additions & 164 deletions

File tree

Makefile

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# Build variables
44
BINARY_NAME=patchmon-agent
55
BUILD_DIR=build
6-
# Get version from git tags, fallback to "dev" if not available
7-
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
6+
# Use hardcoded version instead of git tags
7+
VERSION=1.3.0
88
# Strip debug info and set version variable
99
LDFLAGS=-ldflags "-s -w -X patchmon-agent/internal/version.Version=$(VERSION)"
10+
# Disable VCS stamping
11+
BUILD_FLAGS=-buildvcs=false
1012

1113
# Go variables
1214
GOBASE=$(shell pwd)
@@ -21,17 +23,17 @@ all: build
2123
build:
2224
@echo "Building $(BINARY_NAME)..."
2325
@mkdir -p $(BUILD_DIR)
24-
@go build $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
26+
@go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME) ./cmd/patchmon-agent
2527

2628
# Build for multiple architectures
2729
.PHONY: build-all
2830
build-all:
2931
@echo "Building for multiple architectures..."
3032
@mkdir -p $(BUILD_DIR)
31-
@GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-amd64 ./cmd/patchmon-agent
32-
@GOOS=linux GOARCH=386 go build $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-386 ./cmd/patchmon-agent
33-
@GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
34-
@GOOS=linux GOARCH=arm go build $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
33+
@GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-amd64 ./cmd/patchmon-agent
34+
@GOOS=linux GOARCH=386 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-386 ./cmd/patchmon-agent
35+
@GOOS=linux GOARCH=arm64 go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm64 ./cmd/patchmon-agent
36+
@GOOS=linux GOARCH=arm go build $(BUILD_FLAGS) $(LDFLAGS) -o $(GOBIN)/$(BINARY_NAME)-linux-arm ./cmd/patchmon-agent
3537

3638
# Install dependencies
3739
.PHONY: deps

cmd/patchmon-agent/commands/diagnostics.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"runtime"
99
"strings"
1010

11-
"patchmon-agent/internal/crontab"
1211
"patchmon-agent/internal/system"
1312
"patchmon-agent/internal/utils"
1413
"patchmon-agent/internal/version"
@@ -39,8 +38,6 @@ func showDiagnostics() error {
3938
osType, osVersion, err := systemDetector.DetectOS()
4039
if err != nil {
4140
fmt.Printf(" OS: %s (detection failed: %v)\n", runtime.GOOS, err)
42-
osType = runtime.GOOS
43-
osVersion = "unknown"
4441
} else {
4542
fmt.Printf(" OS: %s %s\n", osType, osVersion)
4643
}
@@ -84,16 +81,6 @@ func showDiagnostics() error {
8481
}
8582
fmt.Printf("\n")
8683

87-
// Crontab Status
88-
fmt.Printf("Crontab Status:\n")
89-
cronManager := crontab.New(logger)
90-
if crontabSchedule := cronManager.GetSchedule(); crontabSchedule != "" {
91-
fmt.Printf(" ✅ Schedule installed: %s\n", crontabSchedule)
92-
} else {
93-
fmt.Printf(" ❌ Schedule not installed\n")
94-
}
95-
fmt.Printf("\n")
96-
9784
// Network Connectivity & API Credentials
9885
fmt.Printf("Network Connectivity & API Credentials:\n")
9986
fmt.Printf(" Server URL: %s\n", cfg.PatchmonServer)

cmd/patchmon-agent/commands/report.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"patchmon-agent/internal/client"
89
"patchmon-agent/internal/hardware"
@@ -32,6 +33,8 @@ var reportCmd = &cobra.Command{
3233
}
3334

3435
func sendReport() error {
36+
// Start tracking execution time
37+
startTime := time.Now()
3538
logger.Debug("Starting report process")
3639

3740
// Load API credentials to send report
@@ -80,7 +83,7 @@ func sendReport() error {
8083

8184
// Get package information
8285
logger.Info("Collecting package information...")
83-
packageList, err := packageMgr.GetPackages(osType)
86+
packageList, err := packageMgr.GetPackages()
8487
if err != nil {
8588
return fmt.Errorf("failed to get packages: %w", err)
8689
}
@@ -117,7 +120,7 @@ func sendReport() error {
117120

118121
// Get repository information
119122
logger.Info("Collecting repository information...")
120-
repoList, err := repoMgr.GetRepositories(osType)
123+
repoList, err := repoMgr.GetRepositories()
121124
if err != nil {
122125
logger.WithError(err).Warn("Failed to get repositories")
123126
repoList = []models.Repository{}
@@ -132,6 +135,10 @@ func sendReport() error {
132135
}).Debug("Repository info")
133136
}
134137

138+
// Calculate execution time (in seconds, with millisecond precision)
139+
executionTime := time.Since(startTime).Seconds()
140+
logger.WithField("execution_time_seconds", executionTime).Debug("Data collection completed")
141+
135142
// Create payload
136143
payload := &models.ReportPayload{
137144
Packages: packageList,
@@ -155,6 +162,7 @@ func sendReport() error {
155162
GatewayIP: networkInfo.GatewayIP,
156163
DNSServers: networkInfo.DNSServers,
157164
NetworkInterfaces: networkInfo.NetworkInterfaces,
165+
ExecutionTime: executionTime,
158166
}
159167

160168
// Send report

cmd/patchmon-agent/commands/root.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package commands
22

33
import (
4-
"fmt"
5-
"os"
4+
"fmt"
5+
"os"
6+
"path/filepath"
67

7-
"patchmon-agent/internal/config"
8-
"patchmon-agent/internal/constants"
9-
"patchmon-agent/internal/version"
8+
"patchmon-agent/internal/config"
9+
"patchmon-agent/internal/constants"
10+
"patchmon-agent/internal/version"
1011

11-
"github.com/sirupsen/logrus"
12-
"github.com/spf13/cobra"
12+
"github.com/sirupsen/logrus"
13+
"github.com/spf13/cobra"
14+
lumberjack "gopkg.in/natefinch/lumberjack.v2"
1315
)
1416

1517
var (
@@ -51,8 +53,7 @@ func init() {
5153
rootCmd.AddCommand(pingCmd)
5254
rootCmd.AddCommand(configCmd)
5355
rootCmd.AddCommand(checkVersionCmd)
54-
rootCmd.AddCommand(updateAgentCmd)
55-
rootCmd.AddCommand(updateCrontabCmd)
56+
rootCmd.AddCommand(updateAgentCmd)
5657
rootCmd.AddCommand(diagnosticsCmd)
5758
rootCmd.AddCommand(uninstallCmd)
5859
}
@@ -67,9 +68,18 @@ func initialiseAgent() {
6768
TimestampFormat: "2006-01-02T15:04:05",
6869
})
6970

70-
// Initialise configuration manager
71-
cfgManager = config.New()
72-
cfgManager.SetConfigFile(configFile)
71+
// Initialise configuration manager
72+
cfgManager = config.New()
73+
cfgManager.SetConfigFile(configFile)
74+
75+
// Load config early to determine log file path
76+
_ = cfgManager.LoadConfig()
77+
logFile := cfgManager.GetConfig().LogFile
78+
if logFile == "" {
79+
logFile = config.DefaultLogFile
80+
}
81+
_ = os.MkdirAll(filepath.Dir(logFile), 0755)
82+
logger.SetOutput(&lumberjack.Logger{Filename: logFile, MaxSize: 10, MaxBackups: 5, MaxAge: 14, Compress: true})
7383
}
7484

7585
// updateLogLevel sets the logger level based on the flag value
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"strings"
8+
"time"
9+
10+
"patchmon-agent/internal/client"
11+
12+
"github.com/gorilla/websocket"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// serveCmd runs the agent as a long-lived service
17+
var serveCmd = &cobra.Command{
18+
Use: "serve",
19+
Short: "Run the agent as a service with async updates",
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
if err := checkRoot(); err != nil {
22+
return err
23+
}
24+
return runService()
25+
},
26+
}
27+
28+
func init() {
29+
rootCmd.AddCommand(serveCmd)
30+
}
31+
32+
func runService() error {
33+
if err := cfgManager.LoadCredentials(); err != nil {
34+
return err
35+
}
36+
37+
httpClient := client.New(cfgManager, logger)
38+
ctx := context.Background()
39+
40+
// obtain initial interval
41+
intervalMinutes := 60
42+
if resp, err := httpClient.GetUpdateInterval(ctx); err == nil && resp.UpdateInterval > 0 {
43+
intervalMinutes = resp.UpdateInterval
44+
}
45+
46+
ticker := time.NewTicker(time.Duration(intervalMinutes) * time.Minute)
47+
defer ticker.Stop()
48+
49+
// initial report on boot
50+
if err := sendReport(); err != nil {
51+
logger.WithError(err).Warn("initial report failed")
52+
}
53+
54+
// start websocket loop
55+
messages := make(chan wsMsg, 10)
56+
go wsLoop(messages)
57+
58+
for {
59+
select {
60+
case <-ticker.C:
61+
if err := sendReport(); err != nil {
62+
logger.WithError(err).Warn("periodic report failed")
63+
}
64+
case m := <-messages:
65+
switch m.kind {
66+
case "settings_update":
67+
if m.interval > 0 {
68+
ticker.Stop()
69+
ticker = time.NewTicker(time.Duration(m.interval) * time.Minute)
70+
logger.WithField("new_interval", m.interval).Info("interval updated, no report sent")
71+
}
72+
case "report_now":
73+
if err := sendReport(); err != nil {
74+
logger.WithError(err).Warn("report_now failed")
75+
}
76+
case "update_agent":
77+
if err := updateAgent(); err != nil {
78+
logger.WithError(err).Warn("update_agent failed")
79+
}
80+
case "update_notification":
81+
logger.WithField("version", m.version).Info("Update notification received from server")
82+
if m.force {
83+
logger.Info("Force update requested, updating agent now")
84+
if err := updateAgent(); err != nil {
85+
logger.WithError(err).Warn("forced update failed")
86+
}
87+
} else {
88+
logger.Info("Update available, run 'patchmon-agent update-agent' to update")
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
type wsMsg struct {
96+
kind string
97+
interval int
98+
version string
99+
force bool
100+
}
101+
102+
func wsLoop(out chan<- wsMsg) {
103+
backoff := time.Second
104+
for {
105+
if err := connectOnce(out); err != nil {
106+
logger.WithError(err).Warn("ws disconnected; retrying")
107+
}
108+
time.Sleep(backoff)
109+
if backoff < 30*time.Second {
110+
backoff *= 2
111+
}
112+
}
113+
}
114+
115+
func connectOnce(out chan<- wsMsg) error {
116+
server := cfgManager.GetConfig().PatchmonServer
117+
if server == "" {
118+
return nil
119+
}
120+
apiID := cfgManager.GetCredentials().APIID
121+
apiKey := cfgManager.GetCredentials().APIKey
122+
123+
// Convert http(s) -> ws(s)
124+
wsURL := server
125+
if strings.HasPrefix(wsURL, "https://") {
126+
wsURL = "wss://" + strings.TrimPrefix(wsURL, "https://")
127+
} else if strings.HasPrefix(wsURL, "http://") {
128+
wsURL = "ws://" + strings.TrimPrefix(wsURL, "http://")
129+
}
130+
if strings.HasSuffix(wsURL, "/") {
131+
wsURL = strings.TrimRight(wsURL, "/")
132+
}
133+
wsURL = wsURL + "/api/" + cfgManager.GetConfig().APIVersion + "/agents/ws"
134+
header := http.Header{}
135+
header.Set("X-API-ID", apiID)
136+
header.Set("X-API-KEY", apiKey)
137+
138+
conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
139+
if err != nil {
140+
return err
141+
}
142+
defer func() { _ = conn.Close() }()
143+
144+
// ping loop
145+
go func() {
146+
t := time.NewTicker(30 * time.Second)
147+
defer t.Stop()
148+
for range t.C {
149+
_ = conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(5*time.Second))
150+
}
151+
}()
152+
153+
// Set read deadlines and extend them on pong frames to avoid idle timeouts
154+
_ = conn.SetReadDeadline(time.Now().Add(90 * time.Second))
155+
conn.SetPongHandler(func(string) error {
156+
return conn.SetReadDeadline(time.Now().Add(90 * time.Second))
157+
})
158+
159+
logger.WithField("url", wsURL).Info("WebSocket connected")
160+
for {
161+
_, data, err := conn.ReadMessage()
162+
if err != nil {
163+
return err
164+
}
165+
var payload struct {
166+
Type string `json:"type"`
167+
UpdateInterval int `json:"update_interval"`
168+
Version string `json:"version"`
169+
Force bool `json:"force"`
170+
Message string `json:"message"`
171+
}
172+
if json.Unmarshal(data, &payload) == nil {
173+
switch payload.Type {
174+
case "settings_update":
175+
logger.WithField("interval", payload.UpdateInterval).Info("settings_update received")
176+
out <- wsMsg{kind: "settings_update", interval: payload.UpdateInterval}
177+
case "report_now":
178+
logger.Info("report_now received")
179+
out <- wsMsg{kind: "report_now"}
180+
case "update_agent":
181+
logger.Info("update_agent received")
182+
out <- wsMsg{kind: "update_agent"}
183+
case "update_notification":
184+
logger.WithFields(map[string]interface{}{
185+
"version": payload.Version,
186+
"force": payload.Force,
187+
"message": payload.Message,
188+
}).Info("update_notification received")
189+
out <- wsMsg{
190+
kind: "update_notification",
191+
version: payload.Version,
192+
force: payload.Force,
193+
}
194+
}
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)