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:
parent
e559b11b08
commit
d9087102fd
96
confguration.go
Normal file
96
confguration.go
Normal 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
31
errors.go
Normal 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")
|
229
main.go
229
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 <me@arav.top>")
|
||||
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("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.")
|
||||
}
|
||||
|
89
processlist.go
Normal file
89
processlist.go
Normal 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
90
server.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user