Version 3.0.0. Throwed out everything not used but for whatever reason added previously.
This commit is contained in:
parent
3de964388a
commit
df3714d071
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
.vscode
|
||||
*.conf
|
||||
!config.example.conf
|
||||
*.json
|
||||
!config.example.json
|
||||
httpprocprobed
|
15
Makefile
15
Makefile
@ -2,26 +2,29 @@ TARGET=httpprocprobed
|
||||
|
||||
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
|
||||
SYSDDIR=${SYSDDIR_:/%=%}
|
||||
DESTDIR=/
|
||||
DESTDIR:=
|
||||
|
||||
LDFLAGS=-ldflags "-s -w" -tags netgo
|
||||
VERSION:=3.0.0
|
||||
|
||||
FLAGS:=-buildmode=pie -modcacherw -mod=readonly -trimpath
|
||||
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags netgo
|
||||
|
||||
SOURCES := ${wildcard *.go}
|
||||
|
||||
all: ${TARGET}
|
||||
.PHONY: install uninstall clean
|
||||
|
||||
${TARGET}: ${SOURCES}
|
||||
go build ${LDFLAGS}
|
||||
go build ${LDFLAGS} ${FLAGS}
|
||||
|
||||
install:
|
||||
install -Dm 0755 ${TARGET} ${DESTDIR}usr/bin/${TARGET}
|
||||
install -Dm 0644 configs/config.example.conf ${DESTDIR}etc/${TARGET}.conf
|
||||
install -Dm 0644 configs/config.example.json ${DESTDIR}etc/${TARGET}.json
|
||||
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||
install -Dm 0644 init/systemd.service ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||
|
||||
uninstall:
|
||||
rm ${DESTDIR}usr/bin/${TARGET}
|
||||
rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
|
||||
rm ${DESTDIR}usr/share/licenses/${TARGET}
|
||||
rm ${DESTDIR}${SYSDDIR}/${TARGET}.service
|
||||
|
||||
clean:
|
||||
|
46
README.md
46
README.md
@ -1,46 +0,0 @@
|
||||
# httpprocprobed Ver 2.0.1
|
||||
|
||||
License: MIT+NIGGER.
|
||||
|
||||
This utility provides a HTTP `/processes` GET endpoint that returns a list of
|
||||
processes, and if they are currently running or not.
|
||||
|
||||
There are currently three output formats available: JSON, XML, and plain text.
|
||||
|
||||
JSON is a default format if `Accept` header didn't provided, or did with
|
||||
value `application/json`. Its form is `{"process":true|false, ...}`.
|
||||
|
||||
XML is provided if `Accept: application/xml` header was given. Its form is
|
||||
`<ProcessList><Process name="process">true|false</Process>...</ProcessList>`.
|
||||
|
||||
Plain text is provided if `Accept: text/plain` header was given. Its form is a
|
||||
comma separated list of ONLY running process' names.
|
||||
|
||||
Configuration file is a simple `key = value` storage consisting of
|
||||
`listen_address` string field in form `"[<ip|host>]:<port>"`. `indented_output`
|
||||
boolean in form `true|false`, to enable indentation of JSON and XML output.
|
||||
And `processes` is a space separated array of process names.
|
||||
|
||||
## Installation
|
||||
|
||||
### Manually
|
||||
|
||||
Run these commands one after the other.
|
||||
|
||||
```console
|
||||
$ make
|
||||
$ make install
|
||||
```
|
||||
|
||||
In order to uninstall run these commands:
|
||||
|
||||
```console
|
||||
# systemctl stop httpprocprobed
|
||||
# systemctl disable httpprocprobed
|
||||
$ make uninstall
|
||||
# systemctl daemon-reload
|
||||
```
|
||||
|
||||
### For ArchLinux
|
||||
|
||||
You can take a [PKGBUILD](/Arav/httpprocprobed/raw/branch/master/build/archlinux/PKGBUILD) file and in a directory with it run `makepkg -i`.
|
@ -1,21 +1,14 @@
|
||||
# Maintainer: Alexander "Arav" Andreev <me@arav.top>
|
||||
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
|
||||
pkgname=httpprocprobed
|
||||
pkgver=2.0.1
|
||||
pkgver=3.0.0
|
||||
pkgrel=1
|
||||
pkgdesc="HTTPProcProbeD hands out an HTTP endpoint to get if processes are running."
|
||||
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
|
||||
url="https://git.arav.top/Arav/httpprocprobed"
|
||||
url="https://git.arav.su/Arav/httpprocprobed"
|
||||
license=('MIT')
|
||||
groups=()
|
||||
depends=()
|
||||
makedepends=('go')
|
||||
provides=('httpprocprobed')
|
||||
conflicts=('httpprocprobed')
|
||||
replaces=()
|
||||
backup=('etc/httpprocprobed.conf')
|
||||
options=()
|
||||
install=
|
||||
source=('https://git.arav.top/Arav/httpprocprobed/archive/2.0.1.tar.gz')
|
||||
makedepends=('go>=1.17')
|
||||
backup=('etc/httpprocprobed.json')
|
||||
source=("https://git.arav.su/Arav/httpprocprobed/archive/$pkgver.tar.gz")
|
||||
noextract=()
|
||||
md5sums=('SKIP')
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
listen_address = :28010
|
||||
indented_output = false
|
||||
processes =
|
6
configs/config.example.json
Normal file
6
configs/config.example.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"listen-address": ":28010",
|
||||
"processes": [
|
||||
{"alias": "minecraft", "process": "fabric-server-mc"}
|
||||
]
|
||||
}
|
@ -1,98 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"log"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Configuration holds a configuration for the service.
|
||||
type Configuration struct {
|
||||
ListenAddress string
|
||||
IndentedOutput bool
|
||||
Processes []string
|
||||
ListenAddress string `json:"listen-address"`
|
||||
Processes []Process `json:"processes"`
|
||||
}
|
||||
|
||||
// LoadConfiguration loads configuration from a file.
|
||||
func LoadConfiguration(path string) (conf *Configuration, err error) {
|
||||
conf = &Configuration{}
|
||||
|
||||
file, err := os.Open(path)
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
defer f.Close()
|
||||
|
||||
s := bufio.NewScanner(file)
|
||||
s.Split(bufio.ScanLines)
|
||||
conf = &Configuration{}
|
||||
|
||||
for s.Scan() {
|
||||
kv := strings.Split(s.Text(), " = ")
|
||||
switch kv[0] {
|
||||
case "listen_address":
|
||||
conf.ListenAddress = kv[1]
|
||||
case "indented_output":
|
||||
v, err := strconv.ParseBool(kv[1])
|
||||
if err != nil {
|
||||
log.Printf("[WARN] could not parse \"indented_output\", valid values are true or false. Defaulted to false.\n")
|
||||
}
|
||||
conf.IndentedOutput = v
|
||||
case "processes":
|
||||
if kv[1] != "" {
|
||||
conf.Processes = append(conf.Processes, strings.Split(kv[1], " ")...)
|
||||
} else {
|
||||
log.Printf("[WARN] \"processes\" list is empty.\n")
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(f).Decode(conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// StoreConfiguration writes configuration into a file.
|
||||
func (conf *Configuration) StoreConfiguration(path string) (err error) {
|
||||
var config strings.Builder
|
||||
|
||||
config.WriteString("listen_address = ")
|
||||
config.WriteString(conf.ListenAddress)
|
||||
config.WriteByte('\n')
|
||||
config.WriteString("indented_output = ")
|
||||
config.WriteString(strconv.FormatBool(conf.IndentedOutput))
|
||||
config.WriteByte('\n')
|
||||
config.WriteString("processes = ")
|
||||
config.WriteString(strings.Join(conf.Processes, " "))
|
||||
if err := os.WriteFile(path, []byte(config.String()), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddProcess appends a new given process into a configuration file.
|
||||
func (conf *Configuration) AddProcess(process string, configPath string) error {
|
||||
for _, v := range conf.Processes {
|
||||
if v == process {
|
||||
return errors.New("process is already on list")
|
||||
}
|
||||
}
|
||||
|
||||
conf.Processes = append(conf.Processes, process)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveProcess removes a given process from a configuration file.
|
||||
func (conf *Configuration) RemoveProcess(process string, 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
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("process is not on list")
|
||||
}
|
||||
|
24
http.go
Normal file
24
http.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AreProcessesUp sends back status of watched processes.
|
||||
func AreProcessesUp(processes *[]Process) func(http.ResponseWriter, *http.Request) {
|
||||
return 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(processes)); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("Failed to encode a process list: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateAndStartHTTPServer(conf *Configuration) *http.Server {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("/processes", AreProcessesUp(&conf.Processes, conf.IndentedOutput))
|
||||
|
||||
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, indented bool) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
proclist := make(ProcessList)
|
||||
|
||||
for _, proc := range *processes {
|
||||
pids, err := GetProcessPIDs(proc)
|
||||
proclist[proc] = err == nil && len(pids) > 0
|
||||
}
|
||||
|
||||
switch r.Header.Get("Accept") {
|
||||
case "application/xml":
|
||||
w.Header().Add("Content-Type", "application/xml")
|
||||
enc := xml.NewEncoder(w)
|
||||
if indented {
|
||||
enc.Indent("", "\t")
|
||||
}
|
||||
enc.Encode(proclist)
|
||||
case "text/plain":
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
var s []string
|
||||
for k, v := range proclist {
|
||||
if v {
|
||||
s = append(s, k)
|
||||
}
|
||||
}
|
||||
w.Write([]byte(strings.Join(s, ",")))
|
||||
default:
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
if indented {
|
||||
enc.SetIndent("", "\t")
|
||||
}
|
||||
enc.Encode(proclist)
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
w.Header().Add("Allow", "GET")
|
||||
}
|
||||
}
|
||||
}
|
78
main.go
78
main.go
@ -1,85 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var configPath *string = flag.String("c", "config.conf", "path to configuration file")
|
||||
var configPath *string = flag.String("c", "config.json", "path to configuration file")
|
||||
var showVersion *bool = flag.Bool("v", false, "show version")
|
||||
var listProcesses *bool = flag.Bool("l", false, "list watched processes")
|
||||
|
||||
var addProcess *string = flag.String("a", "", "add process to list")
|
||||
var removeProcess *string = flag.String("r", "", "remove process from list")
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println("httpprocprobed ver. 2.0.1")
|
||||
fmt.Println("Copyright (c) 2021-2023 Alexander \"Arav\" Andreev <me@arav.top>")
|
||||
fmt.Println("This program is licensed under terms of MIT+NIGGER license.")
|
||||
fmt.Println("httpprocprobed ver.", version, "Copyright (c) 2021-2023 Alexander \"Arav\" Andreev <me@arav.su>")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
conf, err := LoadConfiguration(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERR] Cannot load configuration file: %s\n", err)
|
||||
log.Fatalf("Cannot load configuration file: %s\n", err)
|
||||
}
|
||||
|
||||
if *listProcesses {
|
||||
for _, v := range conf.Processes {
|
||||
fmt.Printf("%s, ", v)
|
||||
}
|
||||
fmt.Println()
|
||||
os.Exit(0)
|
||||
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,
|
||||
}
|
||||
|
||||
if *addProcess != "" {
|
||||
err := conf.AddProcess(*addProcess, *configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERR] Cannot add process: %s\n", err)
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *removeProcess != "" {
|
||||
err := conf.RemoveProcess(*removeProcess, *configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERR] Cannot remove process: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If we modified a list then let's look for a running program and
|
||||
// send SIGHUP to it to reload a list. Here we assume that there
|
||||
// is only one process running, so we just filter our PID.
|
||||
if *addProcess != "" || *removeProcess != "" {
|
||||
if err := conf.StoreConfiguration(*configPath); err != nil {
|
||||
log.Fatalf("[ERR] Cannot write configuration. Error: %s\n", err)
|
||||
}
|
||||
|
||||
pids, _ := GetProcessPIDs("httpprocprobed")
|
||||
if len(pids) > 1 {
|
||||
var trgt_pid int
|
||||
if pids[0] == os.Getpid() {
|
||||
trgt_pid = pids[1]
|
||||
} else {
|
||||
trgt_pid = pids[0]
|
||||
}
|
||||
|
||||
if proc, err := os.FindProcess(trgt_pid); err == nil {
|
||||
proc.Signal(syscall.SIGHUP)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
srv := CreateAndStartHTTPServer(conf)
|
||||
}()
|
||||
|
||||
syssignal := make(chan os.Signal, 1)
|
||||
signal.Notify(syssignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
@ -91,7 +58,10 @@ func main() {
|
||||
case os.Interrupt:
|
||||
fallthrough
|
||||
case syscall.SIGINT | syscall.SIGTERM:
|
||||
ShutdownHTTPServer(srv)
|
||||
srv.SetKeepAlivesEnabled(false)
|
||||
if err := srv.Shutdown(context.Background()); err != nil {
|
||||
log.Fatalf("%s\n", err)
|
||||
}
|
||||
log.Println("Server shutted down.")
|
||||
os.Exit(0)
|
||||
case syscall.SIGHUP:
|
||||
|
@ -1,5 +1,3 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@ -8,6 +6,27 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Process contains an alias that will be returned when queries, and a process
|
||||
// name to look for.
|
||||
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
|
||||
}
|
||||
|
||||
// GetProcessPIDs returns a list of PIDs found for a process.
|
||||
func GetProcessPIDs(name string) (pids []int, err error) {
|
||||
dir, err := os.ReadDir("/proc/")
|
@ -1,32 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// ProcessList is a map of processes' names and its statuses.
|
||||
type ProcessList map[string]bool
|
||||
|
||||
// MarshalXML implements XML 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(struct {
|
||||
XMLName xml.Name
|
||||
Name string `xml:"name,attr"`
|
||||
IsUp bool `xml:",chardata"`
|
||||
}{
|
||||
XMLName: xml.Name{Local: "Process"},
|
||||
Name: key,
|
||||
IsUp: val})
|
||||
}
|
||||
|
||||
return e.EncodeToken(start.End())
|
||||
}
|
Loading…
Reference in New Issue
Block a user