package main import ( "context" "encoding/json" "flag" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" ) var configPath *string = flag.String("c", "config.json", "path to configuration file") var showVersion *bool = flag.Bool("v", false, "show version") var version string // Process contains an alias that will be returned when queried, and a process // name to look for. // A process is effectively a substring that is being looked in a cmdline file // in /proc/ dir on unix-like systems. type Process struct { Alias string `json:"alias"` Process string `json:"process"` } // ProcessesState is a map of processes' aliases and its statuses. type ProcessesState map[string]bool func GetProcessesState(procs *[]Process) (ps ProcessesState) { ps = make(ProcessesState) for _, proc := range *procs { pids, err := GetProcessPIDs(proc.Process) ps[proc.Alias] = err == nil && len(pids) > 0 } return } type Configuration struct { ListenAddress string `json:"listen-address"` Processes []Process `json:"processes"` } func LoadConfiguration(path string) (conf *Configuration, err error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() conf = &Configuration{} if err := json.NewDecoder(f).Decode(conf); err != nil { return nil, err } for i := 0; i < len(conf.Processes); i++ { if conf.Processes[i].Process == "" { return nil, fmt.Errorf("an empty process field found") } if conf.Processes[i].Alias == "" { conf.Processes[i].Alias = conf.Processes[i].Process } } return conf, nil } func main() { log.SetFlags(0) flag.Parse() if *showVersion { fmt.Fprintln(os.Stderr, "httpprocprobed ver.", version, "\nCopyright (c) 2021-2024 Alexander \"Arav\" Andreev ") os.Exit(0) } conf, err := LoadConfiguration(*configPath) if err != nil { log.Fatalf("Cannot load configuration file: %s\n", err) } router := http.NewServeMux() router.HandleFunc("/processes", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) w.Header().Add("Allow", "GET") return } w.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(GetProcessesState(&conf.Processes)); err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("Failed to encode a process list: %s\n", err) } }) srv := &http.Server{ Addr: conf.ListenAddress, Handler: router, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 10 * time.Second, } go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("ListenAndServe: %s\n", err) } }() defer func() { srv.SetKeepAlivesEnabled(false) if err := srv.Shutdown(context.Background()); err != nil { log.Fatalf("%s\n", err) } }() syssignal := make(chan os.Signal, 1) signal.Notify(syssignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) for { switch <-syssignal { case os.Interrupt: fallthrough case syscall.SIGINT | syscall.SIGTERM: return case syscall.SIGHUP: newconf, err := LoadConfiguration(*configPath) if err != nil { log.Fatalf("Failed to reload a list of processes from configuration: %s\n", err) } conf.Processes = newconf.Processes } } }