1
0

Version 3.0.0. Throwed out everything not used but for whatever reason added previously.

This commit is contained in:
Alexander Andreev 2023-12-15 04:00:26 +04:00
parent 3de964388a
commit df3714d071
Signed by: Arav
GPG Key ID: D22A817D95815393
13 changed files with 101 additions and 324 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
.vscode
*.conf
!config.example.conf
*.json
!config.example.json
httpprocprobed

View File

@ -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:

View File

@ -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`.

View File

@ -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')

View File

@ -1,3 +0,0 @@
listen_address = :28010
indented_output = false
processes =

View File

@ -0,0 +1,6 @@
{
"listen-address": ":28010",
"processes": [
{"alias": "minecraft", "process": "fabric-server-mc"}
]
}

View File

@ -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")
}

2
go.mod
View File

@ -1,3 +1,3 @@
module httpprocprobed
go 1.20
go 1.17

24
http.go Normal file
View 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)
}
}
}

View File

@ -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
View File

@ -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:

View File

@ -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/")

View File

@ -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())
}