1
0

Aside from JSON there is also XML available now. You need to provide an Accept: application/xml to get XML. Fixed removing from list, so it works now. Implemented reloading of list of procs by sending SIGHUP signal to process. Spread everything across multiple files for readability.

This commit is contained in:
Alexander Andreev 2021-12-24 23:28:50 +04:00
parent e559b11b08
commit d9087102fd
Signed by: Arav
GPG Key ID: 1327FE8A374CC86F
5 changed files with 360 additions and 175 deletions

96
confguration.go Normal file
View File

@ -0,0 +1,96 @@
/*
httpprocwatchd provides HTTP interface to a list of process' statuses
formatted as JSON.
Copyright (c) 2021 Alexander "Arav" Andreev <me@arav.top>
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 <https://www.gnu.org/licenses/>.
*/
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
}

31
errors.go Normal file
View File

@ -0,0 +1,31 @@
/*
httpprocwatchd provides HTTP interface to a list of process' statuses
formatted as JSON.
Copyright (c) 2021 Alexander "Arav" Andreev <me@arav.top>
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 <https://www.gnu.org/licenses/>.
*/
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")

225
main.go
View File

@ -20,173 +20,40 @@
package main package main
import ( import (
"context"
"encoding/json"
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
) )
// ErrPgrepNotFound occurs when pgrep program is not found. // listWatchedProcesses prints a list of processes being watched.
var ErrPgrepNotFound = errors.New("pgrep not found") func listWatchedProcesses(processes *[]string) {
var ErrNotFound = errors.New("process is not on list") for _, v := range *processes {
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) fmt.Printf("%s, ", v)
} }
} fmt.Println()
} }
// version prints information about program.
func version() { func version() {
fmt.Println("httpprocwatchd ver. 1.1") fmt.Println("httpprocwatchd ver. 1.2")
fmt.Println("Copyright (c) 2021 Alexander \"Arav\" Andreev <me@arav.top>") fmt.Println("Copyright (c) 2021 Alexander \"Arav\" Andreev <me@arav.top>")
fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.") fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.")
fmt.Println("This is free software, and you are welcome to change and redistribute it.") 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.") 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() { func main() {
var oConfigPath string
var oShowVersion bool
var oListProcesses bool
var oAddProcess string
var oRemoveProcess string
log.SetFlags(0) log.SetFlags(0)
flag.StringVar(&oConfigPath, "config", "processes.json", "path to configuration file") flag.StringVar(&oConfigPath, "config", "processes.json", "path to configuration file")
@ -220,7 +87,7 @@ func main() {
} }
if oListProcesses { if oListProcesses {
ListWatchedProcesses(&conf.Processes) listWatchedProcesses(&conf.Processes)
return return
} }
@ -229,7 +96,6 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Cannot add process: %s\n", err) log.Fatalf("Cannot add process: %s\n", err)
} }
return
} }
if oRemoveProcess != "" { if oRemoveProcess != "" {
@ -237,37 +103,50 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Cannot remove process: %s\n", err) 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 return
} }
router := http.NewServeMux()
router.HandleFunc("/processes", AreProcessesUp(&conf.Processes))
srv := &http.Server{ srv := createAndStartHTTPServer(conf)
Addr: conf.ListenAddress,
Handler: router,
}
done := make(chan os.Signal, 1) syssignal := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) signal.Notify(syssignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("ListenAndServe: %s\n", err)
}
}()
log.Printf("httpprocwatchd is running on \"%s\".", conf.ListenAddress) log.Printf("httpprocwatchd is running on \"%s\".", conf.ListenAddress)
<-done for {
switch <-syssignal {
log.Printf("Shutting down... ") case os.Interrupt:
fallthrough
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) case syscall.SIGINT | syscall.SIGTERM:
defer cancel() log.Println("Shutting down... ")
shutdownHTTPServer(srv)
if err := srv.Shutdown(ctx); err != nil { log.Println("Server shutted down.")
log.Fatalf("%s\n", err) 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.")
} }

89
processlist.go Normal file
View File

@ -0,0 +1,89 @@
/*
httpprocwatchd provides HTTP interface to a list of process' statuses
formatted as JSON.
Copyright (c) 2021 Alexander "Arav" Andreev <me@arav.top>
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 <https://www.gnu.org/licenses/>.
*/
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
}

90
server.go Normal file
View File

@ -0,0 +1,90 @@
/*
httpprocwatchd provides HTTP interface to a list of process' statuses
formatted as JSON.
Copyright (c) 2021 Alexander "Arav" Andreev <me@arav.top>
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 <https://www.gnu.org/licenses/>.
*/
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")
}
}
}