v0.1.17: UI fixes, Linux improvements, documentation overhaul
UI/UX: - Fix heartbeat auto-refresh and rate-limiting page - Add navigation breadcrumbs to settings pages - New screenshots added Linux Agent v0.1.17: - Fix disk detection for multiple mount points - Improve installer idempotency - Prevent duplicate registrations Documentation: - README rewrite: 538→229 lines, homelab-focused - Split docs: API.md, CONFIGURATION.md, DEVELOPMENT.md - Add NOTICE for Apache 2.0 attribution
This commit is contained in:
13
aggregator-agent/NOTICE
Normal file
13
aggregator-agent/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
RedFlag Agent
|
||||
Copyright 2024-2025
|
||||
|
||||
This software includes code from the following third-party projects:
|
||||
|
||||
---
|
||||
|
||||
windowsupdate
|
||||
Copyright 2022 Zheng Dayu
|
||||
Licensed under the Apache License, Version 2.0
|
||||
https://github.com/ceshihao/windowsupdate
|
||||
|
||||
Included in: aggregator-agent/pkg/windowsupdate/
|
||||
@@ -16,12 +16,13 @@ import (
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/display"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/installer"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/scanner"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/service"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
AgentVersion = "0.1.16" // Enhanced configuration system with proxy support and registration tokens
|
||||
AgentVersion = "0.1.17" // Fixed Linux disk detection to show all physical mount points (/, /home, etc.)
|
||||
)
|
||||
|
||||
// getConfigPath returns the platform-specific config path
|
||||
@@ -86,6 +87,13 @@ func main() {
|
||||
displayName := flag.String("name", "", "Display name for agent")
|
||||
insecureTLS := flag.Bool("insecure-tls", false, "Skip TLS certificate verification")
|
||||
exportFormat := flag.String("export", "", "Export format: json, csv")
|
||||
|
||||
// Windows service management commands
|
||||
installServiceCmd := flag.Bool("install-service", false, "Install as Windows service")
|
||||
removeServiceCmd := flag.Bool("remove-service", false, "Remove Windows service")
|
||||
startServiceCmd := flag.Bool("start-service", false, "Start Windows service")
|
||||
stopServiceCmd := flag.Bool("stop-service", false, "Stop Windows service")
|
||||
serviceStatusCmd := flag.Bool("service-status", false, "Show Windows service status")
|
||||
flag.Parse()
|
||||
|
||||
// Handle version command
|
||||
@@ -95,6 +103,48 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Handle Windows service management commands (only on Windows)
|
||||
if runtime.GOOS == "windows" {
|
||||
if *installServiceCmd {
|
||||
if err := service.InstallService(); err != nil {
|
||||
log.Fatalf("Failed to install service: %v", err)
|
||||
}
|
||||
fmt.Println("RedFlag service installed successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *removeServiceCmd {
|
||||
if err := service.RemoveService(); err != nil {
|
||||
log.Fatalf("Failed to remove service: %v", err)
|
||||
}
|
||||
fmt.Println("RedFlag service removed successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *startServiceCmd {
|
||||
if err := service.StartService(); err != nil {
|
||||
log.Fatalf("Failed to start service: %v", err)
|
||||
}
|
||||
fmt.Println("RedFlag service started successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *stopServiceCmd {
|
||||
if err := service.StopService(); err != nil {
|
||||
log.Fatalf("Failed to stop service: %v", err)
|
||||
}
|
||||
fmt.Println("RedFlag service stopped successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *serviceStatusCmd {
|
||||
if err := service.ServiceStatus(); err != nil {
|
||||
log.Fatalf("Failed to get service status: %v", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags from comma-separated string
|
||||
var tags []string
|
||||
if *tagsFlag != "" {
|
||||
@@ -197,7 +247,16 @@ func main() {
|
||||
log.Fatal("Agent not registered. Run with -register flag first.")
|
||||
}
|
||||
|
||||
// Start agent service
|
||||
// Check if running as Windows service
|
||||
if runtime.GOOS == "windows" && service.IsService() {
|
||||
// Run as Windows service
|
||||
if err := service.RunService(cfg); err != nil {
|
||||
log.Fatal("Service failed:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start agent service (console mode)
|
||||
if err := runAgent(cfg); err != nil {
|
||||
log.Fatal("Agent failed:", err)
|
||||
}
|
||||
@@ -221,7 +280,8 @@ func registerAgent(cfg *config.Config, serverURL string) error {
|
||||
}
|
||||
}
|
||||
|
||||
apiClient := client.NewClient(serverURL, "")
|
||||
// Use registration token from config if available
|
||||
apiClient := client.NewClient(serverURL, cfg.RegistrationToken)
|
||||
|
||||
// Create metadata with system information
|
||||
metadata := map[string]string{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
module github.com/Fimeg/RedFlag/aggregator-agent
|
||||
|
||||
go 1.21
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/docker/docker v27.4.1+incompatible
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/scjalliance/comshim v0.0.0-20250111221056-b2ef9d8d7e0f
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -16,7 +18,6 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
@@ -24,7 +25,6 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/scjalliance/comshim v0.0.0-20250111221056-b2ef9d8d7e0f // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
@@ -32,6 +32,6 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
@@ -105,8 +105,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
|
||||
@@ -56,6 +56,13 @@ install_binary() {
|
||||
chmod 755 "$AGENT_BINARY"
|
||||
chown root:root "$AGENT_BINARY"
|
||||
echo "✓ Agent binary installed"
|
||||
|
||||
# Set SELinux context for binary if SELinux is enabled
|
||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||
echo "SELinux detected, setting file context for binary..."
|
||||
restorecon -v "$AGENT_BINARY" 2>/dev/null || true
|
||||
echo "✓ SELinux context set for binary"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to install sudoers configuration
|
||||
@@ -167,6 +174,13 @@ register_agent() {
|
||||
# Create config directory
|
||||
mkdir -p /etc/aggregator
|
||||
|
||||
# Set SELinux context for config directory if SELinux is enabled
|
||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||
echo "Setting SELinux context for config directory..."
|
||||
restorecon -Rv /etc/aggregator 2>/dev/null || true
|
||||
echo "✓ SELinux context set for config directory"
|
||||
fi
|
||||
|
||||
# Register agent (run as regular binary, not as service)
|
||||
if "$AGENT_BINARY" -register -server "$server_url"; then
|
||||
echo "✓ Agent registered successfully"
|
||||
|
||||
@@ -46,12 +46,13 @@ func (c *Client) SetToken(token string) {
|
||||
|
||||
// RegisterRequest is the payload for agent registration
|
||||
type RegisterRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Hostname string `json:"hostname"`
|
||||
OSType string `json:"os_type"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
AgentVersion string `json:"agent_version"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // Fallback method
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// RegisterResponse is returned after successful registration
|
||||
@@ -66,6 +67,12 @@ type RegisterResponse struct {
|
||||
func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/agents/register", c.baseURL)
|
||||
|
||||
// If we have a registration token, include it in the request
|
||||
// Registration tokens are longer than regular JWT tokens (usually 64 chars vs JWT ~400 chars)
|
||||
if c.token != "" && len(c.token) > 40 {
|
||||
req.RegistrationToken = c.token
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -77,6 +84,12 @@ func (c *Client) Register(req RegisterRequest) (*RegisterResponse, error) {
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add Authorization header if we have a registration token (preferred method)
|
||||
// Registration tokens are longer than regular JWT tokens (usually 64 chars vs JWT ~400 chars)
|
||||
if c.token != "" && len(c.token) > 40 {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -349,6 +349,12 @@ func (c *Config) Save(configPath string) error {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
// Create parent directory if it doesn't exist
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
52
aggregator-agent/internal/service/service_stub.go
Normal file
52
aggregator-agent/internal/service/service_stub.go
Normal file
@@ -0,0 +1,52 @@
|
||||
//go:build !windows
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"github.com/Fimeg/RedFlag/aggregator-agent/internal/config"
|
||||
)
|
||||
|
||||
// Stub implementations for non-Windows platforms
|
||||
|
||||
// RunService executes the agent as a Windows service (stub for non-Windows)
|
||||
func RunService(cfg *config.Config) error {
|
||||
return fmt.Errorf("Windows service mode is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// IsService returns true if running as Windows service (stub for non-Windows)
|
||||
func IsService() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// InstallService installs the agent as a Windows service (stub for non-Windows)
|
||||
func InstallService() error {
|
||||
return fmt.Errorf("Windows service installation is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// RemoveService removes the Windows service (stub for non-Windows)
|
||||
func RemoveService() error {
|
||||
return fmt.Errorf("Windows service removal is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// StartService starts the Windows service (stub for non-Windows)
|
||||
func StartService() error {
|
||||
return fmt.Errorf("Windows service management is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// StopService stops the Windows service (stub for non-Windows)
|
||||
func StopService() error {
|
||||
return fmt.Errorf("Windows service management is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// ServiceStatus returns the current status of the Windows service (stub for non-Windows)
|
||||
func ServiceStatus() error {
|
||||
return fmt.Errorf("Windows service management is only available on Windows, current OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
// RunConsole runs the agent in console mode with signal handling
|
||||
func RunConsole(cfg *config.Config) error {
|
||||
// For non-Windows, just run normally
|
||||
return fmt.Errorf("Console mode is handled by main application logic on %s", runtime.GOOS)
|
||||
}
|
||||
1329
aggregator-agent/internal/service/windows.go
Normal file
1329
aggregator-agent/internal/service/windows.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -262,9 +262,42 @@ func getDiskInfo() ([]DiskInfo, error) {
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 6 {
|
||||
mountpoint := fields[0]
|
||||
filesystem := fields[5]
|
||||
|
||||
// Filter out pseudo-filesystems and only show physical/important mounts
|
||||
// Skip tmpfs, devtmpfs, overlay, squashfs, etc.
|
||||
if strings.HasPrefix(filesystem, "tmpfs") ||
|
||||
strings.HasPrefix(filesystem, "devtmpfs") ||
|
||||
strings.HasPrefix(filesystem, "overlay") ||
|
||||
strings.HasPrefix(filesystem, "squashfs") ||
|
||||
strings.HasPrefix(filesystem, "udev") ||
|
||||
strings.HasPrefix(filesystem, "proc") ||
|
||||
strings.HasPrefix(filesystem, "sysfs") ||
|
||||
strings.HasPrefix(filesystem, "cgroup") ||
|
||||
strings.HasPrefix(filesystem, "devpts") ||
|
||||
strings.HasPrefix(filesystem, "securityfs") ||
|
||||
strings.HasPrefix(filesystem, "pstore") ||
|
||||
strings.HasPrefix(filesystem, "bpf") ||
|
||||
strings.HasPrefix(filesystem, "configfs") ||
|
||||
strings.HasPrefix(filesystem, "fusectl") ||
|
||||
strings.HasPrefix(filesystem, "hugetlbfs") ||
|
||||
strings.HasPrefix(filesystem, "mqueue") ||
|
||||
strings.HasPrefix(filesystem, "debugfs") ||
|
||||
strings.HasPrefix(filesystem, "tracefs") {
|
||||
continue // Skip virtual/pseudo filesystems
|
||||
}
|
||||
|
||||
// Skip container/snap mounts unless they're important
|
||||
if strings.Contains(mountpoint, "/snap/") ||
|
||||
strings.Contains(mountpoint, "/var/lib/docker") ||
|
||||
strings.Contains(mountpoint, "/run") {
|
||||
continue
|
||||
}
|
||||
|
||||
disk := DiskInfo{
|
||||
Mountpoint: fields[0],
|
||||
Filesystem: fields[5],
|
||||
Mountpoint: mountpoint,
|
||||
Filesystem: filesystem,
|
||||
}
|
||||
|
||||
// Parse sizes (df outputs in human readable format, we'll parse the numeric part)
|
||||
|
||||
@@ -14,20 +14,34 @@ import (
|
||||
func getWindowsInfo() string {
|
||||
// Try using wmic for detailed Windows version info
|
||||
if cmd, err := exec.LookPath("wmic"); err == nil {
|
||||
if data, err := exec.Command(cmd, "os", "get", "Caption,Version,BuildNumber,SKU").Output(); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Microsoft Windows") {
|
||||
// Clean up the output
|
||||
line = strings.TrimSpace(line)
|
||||
// Remove extra spaces
|
||||
for strings.Contains(line, " ") {
|
||||
line = strings.ReplaceAll(line, " ", " ")
|
||||
}
|
||||
return line
|
||||
}
|
||||
// Get Caption (e.g., "Microsoft Windows 10 Pro")
|
||||
caption := ""
|
||||
if data, err := exec.Command(cmd, "os", "get", "Caption", "/value").Output(); err == nil {
|
||||
output := strings.TrimSpace(string(data))
|
||||
if strings.HasPrefix(output, "Caption=") {
|
||||
caption = strings.TrimPrefix(output, "Caption=")
|
||||
caption = strings.TrimSpace(caption)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Version and Build Number
|
||||
version := ""
|
||||
if data, err := exec.Command(cmd, "os", "get", "Version", "/value").Output(); err == nil {
|
||||
output := strings.TrimSpace(string(data))
|
||||
if strings.HasPrefix(output, "Version=") {
|
||||
version = strings.TrimPrefix(output, "Version=")
|
||||
version = strings.TrimSpace(version)
|
||||
}
|
||||
}
|
||||
|
||||
// Combine caption and version for clean output
|
||||
if caption != "" && version != "" {
|
||||
return fmt.Sprintf("%s (Build %s)", caption, version)
|
||||
} else if caption != "" {
|
||||
return caption
|
||||
} else if version != "" {
|
||||
return fmt.Sprintf("Windows %s", version)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to basic version detection
|
||||
@@ -180,31 +194,50 @@ func getWindowsDiskInfo() ([]DiskInfo, error) {
|
||||
var disks []DiskInfo
|
||||
|
||||
if cmd, err := exec.LookPath("wmic"); err == nil {
|
||||
// Get logical disk information
|
||||
if data, err := exec.Command(cmd, "logicaldisk", "get", "DeviceID,Size,FreeSpace,FileSystem").Output(); err == nil {
|
||||
// Get logical disk information - use /value format for reliable parsing
|
||||
if data, err := exec.Command(cmd, "logicaldisk", "get", "DeviceID,Size,FreeSpace,FileSystem", "/format:csv").Output(); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" && !strings.Contains(line, "DeviceID") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 4 {
|
||||
disk := DiskInfo{
|
||||
Mountpoint: strings.TrimSpace(fields[0]),
|
||||
Filesystem: strings.TrimSpace(fields[3]),
|
||||
}
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Skip header and empty lines
|
||||
if i == 0 || line == "" || !strings.Contains(line, ",") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse sizes (wmic outputs in bytes)
|
||||
if total, err := strconv.ParseUint(strings.TrimSpace(fields[1]), 10, 64); err == nil {
|
||||
disk.Total = total
|
||||
}
|
||||
if available, err := strconv.ParseUint(strings.TrimSpace(fields[2]), 10, 64); err == nil {
|
||||
disk.Available = available
|
||||
}
|
||||
// CSV format: Node,DeviceID,FileSystem,FreeSpace,Size
|
||||
fields := strings.Split(line, ",")
|
||||
if len(fields) >= 5 {
|
||||
deviceID := strings.TrimSpace(fields[1])
|
||||
filesystem := strings.TrimSpace(fields[2])
|
||||
freeSpaceStr := strings.TrimSpace(fields[3])
|
||||
sizeStr := strings.TrimSpace(fields[4])
|
||||
|
||||
// Skip if no size info (e.g., CD-ROM drives)
|
||||
if sizeStr == "" || freeSpaceStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
disk := DiskInfo{
|
||||
Mountpoint: deviceID,
|
||||
Filesystem: filesystem,
|
||||
}
|
||||
|
||||
// Parse sizes (wmic outputs in bytes)
|
||||
if total, err := strconv.ParseUint(sizeStr, 10, 64); err == nil {
|
||||
disk.Total = total
|
||||
}
|
||||
if available, err := strconv.ParseUint(freeSpaceStr, 10, 64); err == nil {
|
||||
disk.Available = available
|
||||
}
|
||||
|
||||
// Calculate used space
|
||||
if disk.Total > 0 && disk.Available <= disk.Total {
|
||||
disk.Used = disk.Total - disk.Available
|
||||
if disk.Total > 0 {
|
||||
disk.UsedPercent = float64(disk.Used) / float64(disk.Total) * 100
|
||||
}
|
||||
disk.UsedPercent = float64(disk.Used) / float64(disk.Total) * 100
|
||||
}
|
||||
|
||||
// Only add disks with valid size info
|
||||
if disk.Total > 0 {
|
||||
disks = append(disks, disk)
|
||||
}
|
||||
}
|
||||
@@ -238,36 +271,35 @@ func getWindowsProcessCount() (int, error) {
|
||||
func getWindowsUptime() (string, error) {
|
||||
// Try PowerShell first for more accurate uptime
|
||||
if cmd, err := exec.LookPath("powershell"); err == nil {
|
||||
// Get uptime in seconds for precise calculation
|
||||
if data, err := exec.Command(cmd, "-Command",
|
||||
"(Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime | Select-Object TotalDays").Output(); err == nil {
|
||||
// Parse the output to get days
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "TotalDays") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
if days, err := strconv.ParseFloat(fields[len(fields)-1], 64); err == nil {
|
||||
return formatUptimeFromDays(days), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
"(New-TimeSpan -Start (Get-CimInstance Win32_OperatingSystem).LastBootUpTime -End (Get-Date)).TotalSeconds").Output(); err == nil {
|
||||
secondsStr := strings.TrimSpace(string(data))
|
||||
if seconds, err := strconv.ParseFloat(secondsStr, 64); err == nil {
|
||||
return formatUptimeFromSeconds(seconds), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to wmic
|
||||
// Fallback to wmic with manual parsing
|
||||
if cmd, err := exec.LookPath("wmic"); err == nil {
|
||||
if data, err := exec.Command(cmd, "os", "get", "LastBootUpTime").Output(); err == nil {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) != "" && !strings.Contains(line, "LastBootUpTime") {
|
||||
// Parse WMI datetime format: 20231201123045.123456-300
|
||||
wmiTime := strings.TrimSpace(line)
|
||||
if len(wmiTime) >= 14 {
|
||||
// Extract just the date part for basic calculation
|
||||
// This is a simplified approach - in production you'd want proper datetime parsing
|
||||
return fmt.Sprintf("Since %s", wmiTime[:8]), nil
|
||||
}
|
||||
if data, err := exec.Command(cmd, "os", "get", "LastBootUpTime", "/value").Output(); err == nil {
|
||||
output := strings.TrimSpace(string(data))
|
||||
if strings.HasPrefix(output, "LastBootUpTime=") {
|
||||
wmiTime := strings.TrimPrefix(output, "LastBootUpTime=")
|
||||
wmiTime = strings.TrimSpace(wmiTime)
|
||||
// Parse WMI datetime format: 20251025123045.123456-300
|
||||
if len(wmiTime) >= 14 {
|
||||
// Extract date/time components: YYYYMMDDHHmmss
|
||||
year := wmiTime[0:4]
|
||||
month := wmiTime[4:6]
|
||||
day := wmiTime[6:8]
|
||||
hour := wmiTime[8:10]
|
||||
minute := wmiTime[10:12]
|
||||
second := wmiTime[12:14]
|
||||
|
||||
bootTimeStr := fmt.Sprintf("%s-%s-%s %s:%s:%s", year, month, day, hour, minute, second)
|
||||
return fmt.Sprintf("Since %s", bootTimeStr), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +308,27 @@ func getWindowsUptime() (string, error) {
|
||||
return "Unknown", nil
|
||||
}
|
||||
|
||||
// formatUptimeFromSeconds formats uptime from seconds into human readable format
|
||||
func formatUptimeFromSeconds(seconds float64) string {
|
||||
days := int(seconds / 86400)
|
||||
hours := int((seconds - float64(days*86400)) / 3600)
|
||||
minutes := int((seconds - float64(days*86400) - float64(hours*3600)) / 60)
|
||||
|
||||
if days > 0 {
|
||||
if hours > 0 {
|
||||
return fmt.Sprintf("%d days, %d hours", days, hours)
|
||||
}
|
||||
return fmt.Sprintf("%d days", days)
|
||||
} else if hours > 0 {
|
||||
if minutes > 0 {
|
||||
return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%d hours", hours)
|
||||
} else {
|
||||
return fmt.Sprintf("%d minutes", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
// formatUptimeFromDays formats uptime from days into human readable format
|
||||
func formatUptimeFromDays(days float64) string {
|
||||
if days < 1 {
|
||||
|
||||
Reference in New Issue
Block a user