diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1aaad2b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + release: + types: [created] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + artifact_name: komari-agent-linux-amd64 + goos: linux + goarch: amd64 + - os: ubuntu-latest + artifact_name: komari-agent-linux-arm64 + goos: linux + goarch: arm64 + - os: windows-latest + artifact_name: komari-agent-windows-amd64.exe + goos: windows + goarch: amd64 + - os: windows-latest + artifact_name: komari-agent-windows-arm64.exe + goos: windows + goarch: arm64 + - os: macos-latest + artifact_name: komari-agent-darwin-amd64 + goos: darwin + goarch: amd64 + - os: macos-latest + artifact_name: komari-agent-darwin-arm64 + goos: darwin + goarch: arm64 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.23' + + - name: Build + run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o ${{ matrix.artifact_name }} ./cmd/komari-agent + + - name: Generate changelog + id: changelog + run: | + echo "CHANGELOG<> $GITHUB_ENV + git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Upload release asset + uses: softprops/action-gh-release@v1 + with: + files: ${{ matrix.artifact_name }} + body: ${{ env.CHANGELOG }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2c86d1b..ed217ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ agent.json -.vscode/ \ No newline at end of file +.vscode/ +komari-agent.exe +komari-agent \ No newline at end of file diff --git a/config/local.go b/config/local.go index dd31a6c..b0f28de 100644 --- a/config/local.go +++ b/config/local.go @@ -7,12 +7,13 @@ import ( ) type LocalConfig struct { - Endpoint string `json:"endpoint"` - Token string `json:"token"` - Terminal bool `json:"terminal"` - MaxRetries int `json:"maxRetries"` - ReconnectInterval int `json:"reconnectInterval"` - IgnoreUnsafeCert bool `json:"ignoreUnsafeCert"` + Endpoint string `json:"endpoint"` + Token string `json:"token"` + Terminal bool `json:"terminal"` + MaxRetries int `json:"maxRetries"` + ReconnectInterval int `json:"reconnectInterval"` + IgnoreUnsafeCert bool `json:"ignoreUnsafeCert"` + Interval float64 `json:"interval"` } func LoadConfig() (LocalConfig, error) { @@ -24,7 +25,8 @@ func LoadConfig() (LocalConfig, error) { path string maxRetries int reconnectInterval int - ignoreUnsafeCert bool + ignoreUnsafeCert bool + interval float64 ) flag.StringVar(&endpoint, "e", "", "The endpoint URL") @@ -33,7 +35,8 @@ func LoadConfig() (LocalConfig, error) { flag.StringVar(&path, "c", "agent.json", "Path to the configuration file") flag.IntVar(&maxRetries, "maxRetries", 10, "Maximum number of retries for WebSocket connection") flag.IntVar(&reconnectInterval, "reconnectInterval", 5, "Reconnect interval in seconds") - flag.BoolVar(&ignoreUnsafeCert,"ignoreUnsafeCert", false, "Ignore unsafe certificate errors") + flag.Float64Var(&interval, "interval", 1.1, "Interval in seconds for sending data to the server") + flag.BoolVar(&ignoreUnsafeCert, "ignoreUnsafeCert", false, "Ignore unsafe certificate errors") flag.Parse() // Ensure -c cannot coexist with other flags @@ -63,6 +66,7 @@ func LoadConfig() (LocalConfig, error) { Terminal: terminal, MaxRetries: maxRetries, ReconnectInterval: reconnectInterval, - IgnoreUnsafeCert: ignoreUnsafeCert, + IgnoreUnsafeCert: ignoreUnsafeCert, + Interval: interval, }, nil } diff --git a/config/remote.go b/config/remote.go deleted file mode 100644 index 33dcc58..0000000 --- a/config/remote.go +++ /dev/null @@ -1,70 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "time" -) - -type RemoteConfig struct { - Cpu bool `json:"cpu"` - Gpu bool `json:"gpu"` - Ram bool `json:"ram"` - Swap bool `json:"swap"` - Load bool `json:"load"` - Uptime bool `json:"uptime"` - Temperature bool `json:"temperature"` - Os bool `json:"os"` - Disk bool `json:"disk"` - Network bool `json:"network"` - Process bool `json:"process"` - Interval int `json:"interval"` - Connections bool `json:"connections"` -} - -// 使用HTTP GET请求远程配置 -// -// GET /api/getRemoteConfig -// -// Request the remote configuration -func LoadRemoteConfig(endpoint string, token string) (RemoteConfig, error) { - const maxRetry = 3 - endpoint = strings.TrimSuffix(endpoint, "/") + "/api/clients/getRemoteConfig" + "?token=" + token - - var resp *http.Response - var err error - - for attempt := 1; attempt <= maxRetry; attempt++ { - resp, err = http.Get(endpoint) - if err == nil && resp.StatusCode == http.StatusOK { - break - } - if resp != nil { - resp.Body.Close() - } - if attempt == maxRetry { - if err != nil { - return RemoteConfig{}, fmt.Errorf("failed to fetchafter %d attempts: %v", maxRetry, err) - } - return RemoteConfig{}, fmt.Errorf("failed to fetch after %d attempts: %s", maxRetry, resp.Status) - } - time.Sleep(time.Second * time.Duration(attempt)) // Exponential backoff - } - - defer resp.Body.Close() - - response, err := io.ReadAll(resp.Body) - if err != nil { - return RemoteConfig{}, err - } - - var remoteConfig RemoteConfig - if err := json.Unmarshal(response, &remoteConfig); err != nil { - return RemoteConfig{}, err - } - - return remoteConfig, nil -} diff --git a/go.mod b/go.mod index 6422d80..d958644 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module komari +module github.com/komari-monitor/komari-agent go 1.23.2 diff --git a/main.go b/main.go index cd87ea3..d08e962 100644 --- a/main.go +++ b/main.go @@ -5,13 +5,14 @@ import ( "encoding/json" "fmt" "io" - "komari/config" - "komari/monitoring" "log" "net/http" "strings" "time" + "github.com/komari-monitor/komari-agent/config" + "github.com/komari-monitor/komari-agent/monitoring" + "github.com/gorilla/websocket" ) @@ -24,13 +25,6 @@ func main() { http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } - remoteConfig, err := config.LoadRemoteConfig(localConfig.Endpoint, localConfig.Token) - if err != nil { - log.Fatalln("Failed to load remote config:", err) - } - - //log.Println("Remote Config:", remoteConfig) - err = uploadBasicInfo(localConfig.Endpoint, localConfig.Token) if err != nil { log.Fatalln("Failed to upload basic info:", err) @@ -46,7 +40,7 @@ func main() { } }() - ticker := time.NewTicker(time.Duration(remoteConfig.Interval * int(time.Second))) + ticker := time.NewTicker(time.Duration(localConfig.Interval * float64(time.Second))) defer ticker.Stop() for range ticker.C { @@ -58,7 +52,7 @@ func main() { conn, err = connectWebSocket(websocketEndpoint, localConfig.Endpoint, localConfig.Token) if err == nil { log.Println("WebSocket connected") - go handleWebSocketMessages(localConfig, remoteConfig, conn, make(chan struct{})) + go handleWebSocketMessages(localConfig, conn, make(chan struct{})) break } retry++ @@ -68,7 +62,7 @@ func main() { if retry >= localConfig.MaxRetries { log.Println("Max retries reached, falling back to POST") // Send report via POST and continue - data := report(localConfig, remoteConfig) + data := report(localConfig) if err := reportWithPOST(localConfig.Endpoint, data); err != nil { log.Println("Failed to send POST report:", err) } @@ -77,7 +71,7 @@ func main() { } // Send report via WebSocket - data := report(localConfig, remoteConfig) + data := report(localConfig) err = conn.WriteMessage(websocket.TextMessage, data) if err != nil { log.Println("Failed to send WebSocket message:", err) @@ -108,7 +102,7 @@ func connectWebSocket(websocketEndpoint, endpoint, token string) (*websocket.Con return conn, nil } -func handleWebSocketMessages(localConfig config.LocalConfig, remoteConfig config.RemoteConfig, conn *websocket.Conn, done chan<- struct{}) { +func handleWebSocketMessages(localConfig config.LocalConfig, conn *websocket.Conn, done chan<- struct{}) { defer close(done) for { _, message_raw, err := conn.ReadMessage() @@ -152,15 +146,18 @@ func reportWithPOST(endpoint string, data []byte) error { func uploadBasicInfo(endpoint string, token string) error { cpu := monitoring.Cpu() + osname := monitoring.OSName() + ipv4, ipv6, _ := monitoring.GetIPAddress() + data := map[string]interface{}{ - "token": token, - "cpu": map[string]interface{}{ - "name": cpu.CPUName, - "cores": cpu.CPUCores, - "arch": cpu.CPUArchitecture, - }, - "os": osname, + "cpu_name": cpu.CPUName, + "cpu_cores": cpu.CPUCores, + "arch": cpu.CPUArchitecture, + "os": osname, + "ipv4": ipv4, + "ipv6": ipv6, + "gpu_name": "Unknown", } endpoint = strings.TrimSuffix(endpoint, "/") + "/api/clients/uploadBasicInfo?token=" + token @@ -195,79 +192,68 @@ func uploadBasicInfo(endpoint string, token string) error { return nil } -func report(localConfig config.LocalConfig, remoteConfig config.RemoteConfig) []byte { +func report(localConfig config.LocalConfig) []byte { message := "" - data := map[string]interface{}{ - "token": localConfig.Token, + data := map[string]interface{}{} + + cpu := monitoring.Cpu() + data["cpu"] = map[string]interface{}{ + "usage": cpu.CPUUsage, } - if remoteConfig.Cpu { - cpu := monitoring.Cpu() - data["cpu"] = map[string]interface{}{ - "usage": cpu.CPUUsage, - } + + ram := monitoring.Ram() + data["ram"] = map[string]interface{}{ + "total": ram.Total, + "used": ram.Used, } - if remoteConfig.Ram { - ram := monitoring.Ram() - data["ram"] = map[string]interface{}{ - "total": ram.Total, - "used": ram.Used, - } + + swap := monitoring.Swap() + data["swap"] = map[string]interface{}{ + "total": swap.Total, + "used": swap.Used, } - if remoteConfig.Swap { - swap := monitoring.Swap() - data["swap"] = map[string]interface{}{ - "total": swap.Total, - "used": swap.Used, - } + load := monitoring.Load() + data["load"] = map[string]interface{}{ + "load1": load.Load1, + "load5": load.Load5, + "load15": load.Load15, } - if remoteConfig.Load { - load := monitoring.Load() - data["load"] = map[string]interface{}{ - "load1": load.Load1, - "load5": load.Load5, - "load15": load.Load15, - } + + disk := monitoring.Disk() + data["disk"] = map[string]interface{}{ + "total": disk.Total, + "used": disk.Used, } - if remoteConfig.Disk { - disk := monitoring.Disk() - data["disk"] = map[string]interface{}{ - "total": disk.Total, - "used": disk.Used, - } + + totalUp, totalDown, networkUp, networkDown, err := monitoring.NetworkSpeed(int(localConfig.Interval)) + if err != nil { + message += fmt.Sprintf("failed to get network speed: %v\n", err) } - if remoteConfig.Network { - totalUp, totalDown, networkUp, networkDown, err := monitoring.NetworkSpeed(remoteConfig.Interval) - if err != nil { - message += fmt.Sprintf("failed to get network speed: %v\n", err) - } - data["network"] = map[string]interface{}{ - "up": networkUp, - "down": networkDown, - "totalUp": totalUp, - "totalDown": totalDown, - } + data["network"] = map[string]interface{}{ + "up": networkUp, + "down": networkDown, + "totalUp": totalUp, + "totalDown": totalDown, } - if remoteConfig.Connections { - tcpCount, udpCount, err := monitoring.ConnectionsCount() - if err != nil { - message += fmt.Sprintf("failed to get connections: %v\n", err) - } - data["connections"] = map[string]interface{}{ - "tcp": tcpCount, - "udp": udpCount, - } + + tcpCount, udpCount, err := monitoring.ConnectionsCount() + if err != nil { + message += fmt.Sprintf("failed to get connections: %v\n", err) } - if remoteConfig.Uptime { - uptime, err := monitoring.Uptime() - if err != nil { - message += fmt.Sprintf("failed to get uptime: %v\n", err) - } - data["uptime"] = uptime + data["connections"] = map[string]interface{}{ + "tcp": tcpCount, + "udp": udpCount, } - if remoteConfig.Process { - processcount := monitoring.ProcessCount() - data["process"] = processcount + + uptime, err := monitoring.Uptime() + if err != nil { + message += fmt.Sprintf("failed to get uptime: %v\n", err) } + data["uptime"] = uptime + + processcount := monitoring.ProcessCount() + data["process"] = processcount + data["message"] = message s, err := json.Marshal(data) diff --git a/monitoring/ip.go b/monitoring/ip.go new file mode 100644 index 0000000..1c363b1 --- /dev/null +++ b/monitoring/ip.go @@ -0,0 +1,80 @@ +package monitoring + +import ( + "io" + "net/http" + "regexp" +) + +var userAgent = "curl/8.0.1" + +func GetIPv4Address() (string, error) { + webAPIs := []string{"https://api.bilibili.com/x/web-interface/zone", "https://ip.sb", "https://api.ipify.org?format=json"} + + for _, api := range webAPIs { + // get ipv4 + req, err := http.NewRequest("GET", api, nil) + if err != nil { + continue + } + req.Header.Set("User-Agent", userAgent) + resp, err := http.DefaultClient.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + continue + } + // 使用正则表达式从响应体中提取IPv4地址 + re := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`) + ipv4 := re.FindString(string(body)) + if ipv4 != "" { + return ipv4, nil + } + } + return "", nil +} + +func GetIPv6Address() (string, error) { + webAPIs := []string{"https://api6.ipify.org?format=json", "https://ipv6.icanhazip.com"} + + for _, api := range webAPIs { + // get ipv6 + req, err := http.NewRequest("GET", api, nil) + if err != nil { + continue + } + req.Header.Set("User-Agent", userAgent) + resp, err := http.DefaultClient.Do(req) + if err != nil { + continue + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + continue + } + // 使用正则表达式从响应体中提取IPv6地址 + re := regexp.MustCompile(`([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])`) + ipv6 := re.FindString(string(body)) + if ipv6 != "" { + return ipv6, nil + } + } + return "", nil +} + +func GetIPAddress() (ipv4, ipv6 string, err error) { + ipv4, err = GetIPv4Address() + if err != nil { + ipv4 = "" + } + ipv6, err = GetIPv6Address() + if err != nil { + ipv6 = "" + } + + return ipv4, ipv6, nil +} diff --git a/monitoring/os.go b/monitoring/os.go deleted file mode 100644 index ae680e6..0000000 --- a/monitoring/os.go +++ /dev/null @@ -1,49 +0,0 @@ -package monitoring - -import ( - "bufio" - "os" - "runtime" - "strings" - - "golang.org/x/sys/windows/registry" -) - -func OSName() string { - if runtime.GOOS == "windows" { - key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) - if err != nil { - return "Microsoft Windows" - } - defer key.Close() - - productName, _, err := key.GetStringValue("ProductName") - if err != nil { - return "Microsoft Windows" - } - - return productName - } else if runtime.GOOS == "linux" { - file, err := os.Open("/etc/os-release") - if err != nil { - return "Linux" - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "PRETTY_NAME=") { - return strings.Trim(line[len("PRETTY_NAME="):], `"`) - } - } - - if err := scanner.Err(); err != nil { - return "Linux" - } - - return "Linux" - } - - return "Unknown" -} diff --git a/monitoring/os_linux.go b/monitoring/os_linux.go new file mode 100644 index 0000000..0a1e2c8 --- /dev/null +++ b/monitoring/os_linux.go @@ -0,0 +1,32 @@ +//go:build linux +// +build linux + +package monitoring + +import ( + "bufio" + "os" + "strings" +) + +func OSName() string { + file, err := os.Open("/etc/os-release") + if err != nil { + return "Linux" + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "PRETTY_NAME=") { + return strings.Trim(line[len("PRETTY_NAME="):], `"`) + } + } + + if err := scanner.Err(); err != nil { + return "Linux" + } + + return "Linux" +} diff --git a/monitoring/os_windows.go b/monitoring/os_windows.go new file mode 100644 index 0000000..c3e9897 --- /dev/null +++ b/monitoring/os_windows.go @@ -0,0 +1,23 @@ +//go:build windows +// +build windows + +package monitoring + +import ( + "golang.org/x/sys/windows/registry" +) + +func OSName() string { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return "Microsoft Windows" + } + defer key.Close() + + productName, _, err := key.GetStringValue("ProductName") + if err != nil { + return "Microsoft Windows" + } + + return productName +} diff --git a/monitoring/process_linux.go b/monitoring/process_linux.go new file mode 100644 index 0000000..6760e7b --- /dev/null +++ b/monitoring/process_linux.go @@ -0,0 +1,33 @@ +//go:build linux +// +build linux + +package monitoring + +import ( + "os" + "strconv" +) + +// ProcessCount returns the number of running processes +func ProcessCount() (count int) { + return processCountLinux() +} + +// processCountLinux counts processes by reading /proc directory +func processCountLinux() (count int) { + procDir := "/proc" + + entries, err := os.ReadDir(procDir) + if err != nil { + return 0 + } + + for _, entry := range entries { + if _, err := strconv.ParseInt(entry.Name(), 10, 64); err == nil { + //if _, err := filepath.ParseInt(entry.Name(), 10, 64); err == nil { + count++ + } + } + + return count +} diff --git a/monitoring/process.go b/monitoring/process_windows.go similarity index 67% rename from monitoring/process.go rename to monitoring/process_windows.go index 83d814f..89381ff 100644 --- a/monitoring/process.go +++ b/monitoring/process_windows.go @@ -1,38 +1,17 @@ +//go:build windows +// +build windows + package monitoring import ( - "os" - "runtime" - "strconv" "syscall" "unsafe" ) // ProcessCount returns the number of running processes func ProcessCount() (count int) { - if runtime.GOOS == "windows" { - return processCountWindows() - } - return processCountLinux() -} + return processCountWindows() -// processCountLinux counts processes by reading /proc directory -func processCountLinux() (count int) { - procDir := "/proc" - - entries, err := os.ReadDir(procDir) - if err != nil { - return 0 - } - - for _, entry := range entries { - if _, err := strconv.ParseInt(entry.Name(), 10, 64); err == nil { - //if _, err := filepath.ParseInt(entry.Name(), 10, 64); err == nil { - count++ - } - } - - return count } // processCountWindows counts processes using Windows API diff --git a/update/update.go b/update/update.go new file mode 100644 index 0000000..7a7e4d4 --- /dev/null +++ b/update/update.go @@ -0,0 +1 @@ +package update