package main import ( "context" "errors" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "mfd-monitor-agent/internal/collector" "mfd-monitor-agent/internal/config" "mfd-monitor-agent/internal/httpapi" "mfd-monitor-agent/internal/reporter" "mfd-monitor-agent/internal/service" "mfd-monitor-agent/internal/snapshot" ) const version = "0.1.0" // main runs the CLI and exits non-zero when a command fails. func main() { if err := runCLI(os.Args[1:]); err != nil { _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // runCLI dispatches agent subcommands and loads config before invoking command handlers. func runCLI(args []string) error { if len(args) == 0 { args = []string{"run"} } switch args[0] { case "run": fs := flag.NewFlagSet("run", flag.ExitOnError) configPath := fs.String("config", "", "path to JSON config file") if err := fs.Parse(args[1:]); err != nil { return err } cfg, err := config.Load(*configPath) if err != nil { return err } return runAgent(cfg) case "install": cfg, executable, configPath, err := commandConfig(args[1:]) if err != nil { return err } return service.NewManager().Install(executable, cfg, service.InstallOptions{ConfigPath: configPath}) case "uninstall": cfg, _, _, err := commandConfig(args[1:]) if err != nil { return err } return service.NewManager().Uninstall(cfg) case "start": cfg, _, _, err := commandConfig(args[1:]) if err != nil { return err } return service.NewManager().Start(cfg) case "stop": cfg, _, _, err := commandConfig(args[1:]) if err != nil { return err } return service.NewManager().Stop(cfg) case "version": fmt.Println(version) return nil default: return fmt.Errorf("unknown command %q; expected run, install, uninstall, start, stop, version", args[0]) } } // commandConfig parses service command flags and resolves the executable path. func commandConfig(args []string) (config.Config, string, string, error) { fs := flag.NewFlagSet("service", flag.ExitOnError) configPath := fs.String("config", "", "path to JSON config file") executableFlag := fs.String("exe", "", "path to agent executable") if err := fs.Parse(args); err != nil { return config.Config{}, "", "", err } cfg, err := config.Load(*configPath) if err != nil { return cfg, "", "", err } executable := *executableFlag if executable == "" { executable, err = os.Executable() if err != nil { return cfg, "", "", err } } return cfg, executable, *configPath, nil } // runAgent starts collection, optional reporting, and the local HTTP API until signaled. func runAgent(cfg config.Config) error { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() startedAt := time.Now().UTC() cache := snapshot.NewCache(collector.New(cfg.HostID), version, startedAt) go cache.Run(ctx, cfg.Collect.Interval.Duration(), cfg.Collect.Timeout.Duration()) if cfg.Report.Enabled { client := reporter.NewClient(cfg.Report.Endpoint, cfg.Report.Timeout.Duration()) go reporter.Run(ctx, cache, client, cfg.Report.Interval.Duration(), logger) } server := &http.Server{ Addr: cfg.HTTP.ListenAddress, Handler: httpapi.New(cache), ReadHeaderTimeout: 5 * time.Second, } go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = server.Shutdown(shutdownCtx) }() logger.Info("agent listening", "address", cfg.HTTP.ListenAddress) err := server.ListenAndServe() if errors.Is(err, http.ErrServerClosed) { return nil } return err }