diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 0e2a693..1dfbef2 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "flag" "fmt" "log/slog" "os" "os/signal" + "runtime" "syscall" "github.com/ThreeDotsLabs/watermill" @@ -26,6 +28,16 @@ var ( ) func main() { + // Handle version flag first, before any service initialization + showVersion := flag.Bool("v", false, "Show version information") + showVersionLong := flag.Bool("version", false, "Show version information") + flag.Parse() + + if *showVersion || *showVersionLong { + printVersionInfo() + return + } + l := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ AddSource: true, Level: slog.LevelDebug, @@ -154,3 +166,13 @@ func main() { pubsub.Close() processor.Stop() } + +func printVersionInfo() { + fmt.Printf("shelltime-daemon %s\n", version) + fmt.Printf(" Commit: %s\n", commit) + fmt.Printf(" Build Date: %s\n", date) + fmt.Printf(" Go Version: %s\n", runtime.Version()) + fmt.Printf(" OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) + fmt.Println() + fmt.Println("Use 'shelltime daemon status' to check the running daemon status.") +} diff --git a/commands/daemon.status.go b/commands/daemon.status.go index 3f2ff63..13f35a5 100644 --- a/commands/daemon.status.go +++ b/commands/daemon.status.go @@ -1,12 +1,14 @@ package commands import ( + "encoding/json" "fmt" "net" "os" "time" "github.com/gookit/color" + "github.com/malamtime/cli/daemon" "github.com/malamtime/cli/model" "github.com/urfave/cli/v2" ) @@ -37,8 +39,9 @@ func commandDaemonStatus(c *cli.Context) error { printError(fmt.Sprintf("Socket file does not exist at %s", socketPath)) } - // Check 2: Socket connectivity - connected, latency, connErr := checkSocketConnection(socketPath, 2*time.Second) + // Check 2: Socket connectivity and get status + statusResp, latency, connErr := requestDaemonStatus(socketPath, 2*time.Second) + connected := statusResp != nil if connected { printSuccess(fmt.Sprintf("Daemon is responding (latency: %v)", latency.Round(time.Microsecond))) } else { @@ -59,6 +62,15 @@ func commandDaemonStatus(c *cli.Context) error { } } + // Daemon info section (if connected) + if statusResp != nil { + printSectionHeader("Daemon Info") + fmt.Printf(" Version: %s\n", statusResp.Version) + fmt.Printf(" Uptime: %s (since %s)\n", statusResp.Uptime, statusResp.StartedAt.Format("2006-01-02 15:04:05")) + fmt.Printf(" Go Version: %s\n", statusResp.GoVersion) + fmt.Printf(" Platform: %s\n", statusResp.Platform) + } + // Configuration section printSectionHeader("Configuration") fmt.Printf(" Socket Path: %s\n", socketPath) @@ -97,13 +109,30 @@ func checkSocketFileExists(socketPath string) bool { return err == nil } -func checkSocketConnection(socketPath string, timeout time.Duration) (bool, time.Duration, error) { +func requestDaemonStatus(socketPath string, timeout time.Duration) (*daemon.StatusResponse, time.Duration, error) { start := time.Now() conn, err := net.DialTimeout("unix", socketPath, timeout) if err != nil { - return false, 0, err + return nil, 0, err } defer conn.Close() + + // Send status request + msg := daemon.SocketMessage{ + Type: daemon.SocketMessageTypeStatus, + } + encoder := json.NewEncoder(conn) + if err := encoder.Encode(msg); err != nil { + return nil, 0, err + } + + // Read response + var response daemon.StatusResponse + decoder := json.NewDecoder(conn) + if err := decoder.Decode(&response); err != nil { + return nil, 0, err + } + latency := time.Since(start) - return true, latency, nil + return &response, latency, nil } diff --git a/daemon/base.go b/daemon/base.go index db7331d..82787c1 100644 --- a/daemon/base.go +++ b/daemon/base.go @@ -1,9 +1,14 @@ package daemon -import "github.com/malamtime/cli/model" +import ( + "time" + + "github.com/malamtime/cli/model" +) var stConfig model.ConfigService var version string +var startedAt time.Time const ( PubSubTopic = "socket" @@ -12,4 +17,13 @@ const ( func Init(cs model.ConfigService, vs string) { stConfig = cs version = vs + startedAt = time.Now() +} + +func GetStartedAt() time.Time { + return startedAt +} + +func GetVersion() string { + return version } diff --git a/daemon/ccotel_processor.go b/daemon/ccotel_processor.go index 0b562dc..d44300c 100644 --- a/daemon/ccotel_processor.go +++ b/daemon/ccotel_processor.go @@ -74,6 +74,7 @@ func (p *CCOtelProcessor) writeDebugFile(filename string, data interface{}) { if _, err := f.WriteString(fmt.Sprintf("\n--- %s ---\n%s\n", timestamp, jsonData)); err != nil { slog.Error("CCOtel: Failed to write debug data", "error", err) } + slog.Debug("CCOtel: Wrote debug data", "path", filePath) } // ProcessMetrics receives OTEL metrics and forwards to backend immediately diff --git a/daemon/socket.go b/daemon/socket.go index eec5c00..9d3e182 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -2,9 +2,12 @@ package daemon import ( "encoding/json" + "fmt" "log/slog" "net" "os" + "runtime" + "time" "github.com/ThreeDotsLabs/watermill" "github.com/ThreeDotsLabs/watermill/message" @@ -16,8 +19,18 @@ type SocketMessageType string const ( SocketMessageTypeSync SocketMessageType = "sync" SocketMessageTypeHeartbeat SocketMessageType = "heartbeat" + SocketMessageTypeStatus SocketMessageType = "status" ) +// StatusResponse contains daemon status information +type StatusResponse struct { + Version string `json:"version"` + StartedAt time.Time `json:"startedAt"` + Uptime string `json:"uptime"` + GoVersion string `json:"goVersion"` + Platform string `json:"platform"` +} + type SocketMessage struct { Type SocketMessageType `json:"type"` // if parse from buffer, it will be the map[any]any @@ -99,10 +112,8 @@ func (p *SocketHandler) handleConnection(conn net.Conn) { } switch msg.Type { - // case "status": - // p.handleStatus(conn) - // case "track": - // p.handleTrack(conn, msg.Payload) + case SocketMessageTypeStatus: + p.handleStatus(conn) case SocketMessageTypeSync: buf, err := json.Marshal(msg) if err != nil { @@ -133,3 +144,37 @@ func (p *SocketHandler) handleConnection(conn net.Conn) { slog.Error("Unknown message type:", slog.String("messageType", string(msg.Type))) } } + +func (p *SocketHandler) handleStatus(conn net.Conn) { + uptime := time.Since(startedAt) + response := StatusResponse{ + Version: version, + StartedAt: startedAt, + Uptime: formatDuration(uptime), + GoVersion: runtime.Version(), + Platform: runtime.GOOS + "/" + runtime.GOARCH, + } + + encoder := json.NewEncoder(conn) + if err := encoder.Encode(response); err != nil { + slog.Error("Error encoding status response", slog.Any("err", err)) + } +} + +func formatDuration(d time.Duration) string { + days := int(d.Hours() / 24) + hours := int(d.Hours()) % 24 + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + + if days > 0 { + return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds) + } + if hours > 0 { + return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds) + } + if minutes > 0 { + return fmt.Sprintf("%dm %ds", minutes, seconds) + } + return fmt.Sprintf("%ds", seconds) +} diff --git a/model/config.go b/model/config.go index f9b2bdb..bc0c837 100644 --- a/model/config.go +++ b/model/config.go @@ -182,6 +182,10 @@ func (cs *configService) ReadConfigFile(ctx context.Context, opts ...ReadConfigO if config.CCOtel != nil && config.CCOtel.GRPCPort == 0 { config.CCOtel.GRPCPort = 54027 // default OTEL gRPC port } + + if config.CCOtel != nil && config.CCOtel.Debug != nil && *config.CCOtel.Debug { + config.CCOtel.Debug = &truthy + } if config.SocketPath == "" { config.SocketPath = DefaultSocketPath }