diff --git a/confguration.go b/confguration.go new file mode 100644 index 0000000..4632b17 --- /dev/null +++ b/confguration.go @@ -0,0 +1,96 @@ +/* + httpprocwatchd provides HTTP interface to a list of process' statuses + formatted as JSON. + Copyright (c) 2021 Alexander "Arav" Andreev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "encoding/json" + "io/ioutil" +) + +// Configuration holds a list of process names to be tracked and a listen address. +type Configuration struct { + ListenAddress string `json:"listen_address"` + Processes []string `json:"processes"` +} + +// LoadConfiguration loads configuration from a JSON file. +func LoadConfiguration(path string) (conf *Configuration, err error) { + conf = &Configuration{} + + file, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(file, conf); err != nil { + return nil, err + } + + return conf, nil +} + +// StoreConfiguration writes Configuration into a JSON file. +func StoreConfiguration(path string, conf *Configuration) (err error) { + config, err := json.Marshal(*conf) + if err != nil { + return err + } + + err = ioutil.WriteFile(path, config, 0644) + if err != nil { + return err + } + + return nil +} + +// AddProcessToList appends a new given process into a configuration file. +func AddProcessToList(process string, conf *Configuration, configPath string) error { + for _, v := range conf.Processes { + if v == process { + return ErrIsOnList + } + } + + conf.Processes = append(conf.Processes, process) + if err := StoreConfiguration(configPath, conf); err != nil { + return err + } + + return nil +} + +// RemoveProcessFromList removes a given process from a configuration file. +func RemoveProcessFromList(process string, conf *Configuration, configPath string) error { + for k, v := range conf.Processes { + if v == process { + newlist := make([]string, len(conf.Processes)-1) + newlist = append(conf.Processes[:k], conf.Processes[k+1:]...) + conf.Processes = newlist + if err := StoreConfiguration(configPath, conf); err != nil { + return err + } + + return nil + } + } + + return ErrNotFound +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..be1ef36 --- /dev/null +++ b/errors.go @@ -0,0 +1,31 @@ +/* + httpprocwatchd provides HTTP interface to a list of process' statuses + formatted as JSON. + Copyright (c) 2021 Alexander "Arav" Andreev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package main + +import "errors" + +// ErrPgrepNotFound occurs when pgrep program is not found. +var ErrPgrepNotFound = errors.New("pgrep not found") + +// ErrNotFound occurs when a process is not presented in a list +var ErrNotFound = errors.New("process is not on list") + +// ErrIsOnList occurs when a process is already presented in a list +var ErrIsOnList = errors.New("process is already on list") diff --git a/main.go b/main.go index dcceffa..22e457b 100644 --- a/main.go +++ b/main.go @@ -20,173 +20,40 @@ package main import ( - "context" - "encoding/json" - "errors" "flag" "fmt" - "io/ioutil" "log" - "net/http" "os" "os/exec" "os/signal" "syscall" - "time" ) -// ErrPgrepNotFound occurs when pgrep program is not found. -var ErrPgrepNotFound = errors.New("pgrep not found") -var ErrNotFound = errors.New("process is not on list") -var ErrIsOnList = errors.New("process is already on list") - -// Configuration holds a list of process names to be tracked and a listen address. -type Configuration struct { - ListenAddress string `json:"listen_address"` - Processes []string `json:"processes"` -} - -// LoadConfiguration loads configuration from a JSON file. -func LoadConfiguration(path string) (conf *Configuration, err error) { - conf = &Configuration{} - - file, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(file, conf); err != nil { - return nil, err - } - - return conf, nil -} - -func StoreConfiguration(path string, conf *Configuration) (err error) { - config, err := json.Marshal(*conf) - if err != nil { - return err - } - - err = ioutil.WriteFile(path, config, 0644) - if err != nil { - return err - } - - return nil -} - -// ProcessList is a map of processes' statuses. -type ProcessList map[string]bool - -// NewProcessList returns a ProcessList with initialised map. -func NewProcessList() *ProcessList { - pl := make(ProcessList) - return &pl -} - -// AddProcess appends a process to a ProcessList -func (pl *ProcessList) AddProcess(name string, isup bool) { - (*pl)[name] = isup -} - -// IsProcessUp returns true if process is up. Uses pgrep to get PID of a process -// and if a process is not working, then pgrep returns nothing. -func IsProcessUp(name string) (bool, error) { - pgrep := exec.Command("pgrep", "-nf", name) - - out, err := pgrep.CombinedOutput() - if err != nil { - return false, err - } - - if len(out) == 0 { - return false, nil - } - - return true, nil -} - -// AreProcessesUp handles a GET /processes request and sends back status of given -// processes. -func AreProcessesUp(processes *[]string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - proclist := NewProcessList() - - for _, proc := range *processes { - isup, _ := IsProcessUp(proc) - - proclist.AddProcess(proc, isup) - } - - w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(proclist) - } else { - w.WriteHeader(http.StatusMethodNotAllowed) - w.Header().Add("Allow", "GET") - } - } -} - -func AddProcessToList(process string, conf *Configuration, configPath string) error { - for _, v := range conf.Processes { - if v == process { - return ErrIsOnList - } - } - - conf.Processes = append(conf.Processes, process) - if err := StoreConfiguration(configPath, conf); err != nil { - return err - } - - return nil -} - -func RemoveProcessFromList(process string, conf *Configuration, configPath string) error { - for k, v := range conf.Processes { - if v == process { - copy(conf.Processes[:k], conf.Processes[k+1:]) - - if err := StoreConfiguration(configPath, conf); err != nil { - return err - } - - return nil - } - } - - return ErrNotFound -} - -func ListWatchedProcesses(processes *[]string) { - lastidx := len(*processes) - 1 - for k, v := range *processes { - if k == lastidx { - fmt.Printf("%s\n", v) - } else { - fmt.Printf("%s, ", v) - } - } +// listWatchedProcesses prints a list of processes being watched. +func listWatchedProcesses(processes *[]string) { + for _, v := range *processes { + fmt.Printf("%s, ", v) + } + fmt.Println() } +// version prints information about program. func version() { - fmt.Println("httpprocwatchd ver. 1.1") + fmt.Println("httpprocwatchd ver. 1.2") fmt.Println("Copyright (c) 2021 Alexander \"Arav\" Andreev ") fmt.Println("License GPLv3+: GNU GPL version 3 or later .") fmt.Println("This is free software, and you are welcome to change and redistribute it.") fmt.Println("There is NO WARRANTY, to the extent permitted by law.") } -var oConfigPath string -var oShowVersion bool -var oListProcesses bool - -var oAddProcess string -var oRemoveProcess string - func main() { + var oConfigPath string + var oShowVersion bool + var oListProcesses bool + + var oAddProcess string + var oRemoveProcess string + log.SetFlags(0) flag.StringVar(&oConfigPath, "config", "processes.json", "path to configuration file") @@ -220,7 +87,7 @@ func main() { } if oListProcesses { - ListWatchedProcesses(&conf.Processes) + listWatchedProcesses(&conf.Processes) return } @@ -229,7 +96,6 @@ func main() { if err != nil { log.Fatalf("Cannot add process: %s\n", err) } - return } if oRemoveProcess != "" { @@ -237,37 +103,50 @@ func main() { if err != nil { log.Fatalf("Cannot remove process: %s\n", err) } + } + + // If we modified a list then let's look for a running program and + // send SIGHUP for it to reload a list. + if oAddProcess != "" || oRemoveProcess != "" { + isup, pid, _ := IsProcessUp("httpprocwatchd") + if isup && pid != nil && len(pid) > 1 { + var trgt_pid int + if pid[0] == os.Getpid() { + trgt_pid = pid[1] + } else { + trgt_pid = pid[0] + } + proc, err := os.FindProcess(trgt_pid) + if err == nil { + proc.Signal(syscall.SIGHUP) + } + } return } - router := http.NewServeMux() - router.HandleFunc("/processes", AreProcessesUp(&conf.Processes)) - srv := &http.Server{ - Addr: conf.ListenAddress, - Handler: router, - } + srv := createAndStartHTTPServer(conf) - done := make(chan os.Signal, 1) - signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - go func() { - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("ListenAndServe: %s\n", err) - } - }() + syssignal := make(chan os.Signal, 1) + signal.Notify(syssignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) log.Printf("httpprocwatchd is running on \"%s\".", conf.ListenAddress) - <-done - - log.Printf("Shutting down... ") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("%s\n", err) + for { + switch <-syssignal { + case os.Interrupt: + fallthrough + case syscall.SIGINT | syscall.SIGTERM: + log.Println("Shutting down... ") + shutdownHTTPServer(srv) + log.Println("Server shutted down.") + os.Exit(0) + case syscall.SIGHUP: + newconf, err := LoadConfiguration(oConfigPath) + if err != nil { + log.Fatalf("Failed to reload configuration: %s\n", err) + } + conf.Processes = newconf.Processes + log.Println("Successfully reloaded a list of watched processes.") + } } - - log.Println("Done.") } diff --git a/processlist.go b/processlist.go new file mode 100644 index 0000000..8d1a5f7 --- /dev/null +++ b/processlist.go @@ -0,0 +1,89 @@ +/* + httpprocwatchd provides HTTP interface to a list of process' statuses + formatted as JSON. + Copyright (c) 2021 Alexander "Arav" Andreev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "encoding/xml" + "os/exec" + "strconv" + "strings" +) + +// ProcessList is a map of processes' statuses. +type ProcessList map[string]bool + +// NewProcessList returns a ProcessList with initialised map. +func NewProcessList() *ProcessList { + pl := make(ProcessList) + return &pl +} + +// AddProcess appends a process to a ProcessList +func (pl *ProcessList) AddProcess(name string, isup bool) { + (*pl)[name] = isup +} + +// ProcessXMLEntry is a XML representation of a process stored in ProcessList. +type ProcessXMLEntry struct { + XMLName xml.Name + Name string `xml:"name,attr"` + IsUp bool `xml:",chardata"` +} + +// MarshalXML implements Marshaler interface for a ProcessList. +func (l ProcessList) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if len(l) == 0 { + return nil + } + + if err := e.EncodeToken(start); err != nil { + return err + } + + for key, val := range l { + e.Encode(ProcessXMLEntry{ + XMLName: xml.Name{Local: "Process"}, + Name: key, + IsUp: val}) + } + + return e.EncodeToken(start.End()) +} + +// IsProcessUp returns true if process is up, and a list of PIDs for +// that process name. Uses pgrep to get PID of a process and if +// a process is not working, then pgrep returns nothing. +func IsProcessUp(name string) (bool, []int, error) { + out, err := exec.Command("pgrep", "-f", name).Output() + if err != nil || len(out) < 2 { + return false, nil, err + } + + spids := strings.Split(string(out[:len(out)-1]), "\n") + pids := make([]int, len(spids)) + for i, v := range spids { + pids[i], err = strconv.Atoi(v) + if err != nil { + return false, nil, nil + } + } + + return true, pids, nil +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..bcab470 --- /dev/null +++ b/server.go @@ -0,0 +1,90 @@ +/* + httpprocwatchd provides HTTP interface to a list of process' statuses + formatted as JSON. + Copyright (c) 2021 Alexander "Arav" Andreev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "context" + "encoding/json" + "encoding/xml" + "log" + "net/http" + "time" +) + +func createAndStartHTTPServer(conf *Configuration) *http.Server { + router := http.NewServeMux() + router.HandleFunc("/processes", AreProcessesUp(&conf.Processes)) + + 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) + } + }() + + return srv +} + +func shutdownHTTPServer(srv *http.Server) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + srv.SetKeepAlivesEnabled(false) + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("%s\n", err) + } +} + +// AreProcessesUp handles a GET /processes request and sends back status of given +// processes. +func AreProcessesUp(processes *[]string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + proclist := NewProcessList() + + for _, proc := range *processes { + isup, _, _ := IsProcessUp(proc) + proclist.AddProcess(proc, isup) + } + + if hdr := r.Header.Get("Accept"); hdr == "application/xml" { + w.Header().Add("Content-Type", "application/xml") + enc := xml.NewEncoder(w) + enc.Indent("", "\t") + enc.Encode(proclist) + } else { + w.Header().Add("Content-Type", "application/json") + enc := json.NewEncoder(w) + enc.SetIndent("", "\t") + enc.Encode(proclist) + } + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Header().Add("Allow", "GET") + } + } +}