1
0
Fork 0

Compare commits

...

22 Commits

Author SHA1 Message Date
Alexander Andreev 77af8817fc
An AreProcessesUp func was turned into a lambda func. 2024-01-13 04:55:19 +04:00
Alexander Andreev 37bc8b0f1b
Also moved a Configuration struct to main.go. 2024-01-13 04:42:19 +04:00
Alexander Andreev 64b5966b48
Cause a proper server shutdown if a list reload fail by defering it. 2024-01-13 04:41:46 +04:00
Alexander Andreev 99d53b31a0
Version was set to 3.1.1, and a year got incremented. 2024-01-13 04:22:34 +04:00
Alexander Andreev dd2614102e
PKGBUILD moved out to a build dir. 2024-01-13 04:18:57 +04:00
Alexander Andreev 0cafa69cab
Print a version to stderr. 2024-01-13 04:03:29 +04:00
Alexander Andreev ffb401fd9b
Avoiding of junk in logs. 2024-01-13 04:02:56 +04:00
Alexander Andreev df3376bc69
A copyright has been moved to a next line. 2024-01-13 03:56:20 +04:00
Alexander Andreev 37105a9c8a
A Process type and a GetProcessesState() func was moved to main.go. 2024-01-13 03:54:34 +04:00
Alexander Andreev 4f6f018b54
Fixed a typo in a Makefile. 2023-12-16 02:34:10 +04:00
Alexander Andreev de180ef514
Version set to 3.1.0. 2023-12-16 02:22:46 +04:00
Alexander Andreev 97439561ee
Added a check for an empty process field. Also, added ability to leave just a process name that will be an alias simultaneously. 2023-12-16 02:22:27 +04:00
Alexander Andreev fd0b2a145c
Found another typo in a Makefile. 2023-12-15 04:47:31 +04:00
Alexander Andreev 6b47ea5a88
Fixed a typo in Makefile. 2023-12-15 04:44:33 +04:00
Alexander Andreev 72284c299a
In PKGBUILD noextract=() was removed. Added CGO_* and custom GOPATH env vars. Also added PREFIX var. 2023-12-15 04:24:33 +04:00
Alexander Andreev 8a6647f11a
Updated LICENSE, just year and an e-mail. 2023-12-15 04:23:12 +04:00
Alexander Andreev 8855cd9121
Made make clean useful. 2023-12-15 04:11:13 +04:00
Alexander Andreev 47fc30feeb
In systemd.service .conf was replaced with .json. Also changed description. 2023-12-15 04:10:00 +04:00
Alexander Andreev 6ba2e8a471
Well, actually, let's move the platform-independent parts out to a process.go file. 2023-12-15 04:07:37 +04:00
Alexander Andreev e53bf0b77d
process.go was renamed to process_unix.go and build tags for unix was added to it as well. That's to show that currently only unix and unix-like systems are supported. 2023-12-15 04:05:34 +04:00
Alexander Andreev e1f7dd81b6
Moved AreProcessesUp() handler to main.go. 2023-12-15 04:02:56 +04:00
Alexander Andreev df3714d071
Version 3.0.0. Throwed out everything not used but for whatever reason added previously. 2023-12-15 04:00:26 +04:00
15 changed files with 148 additions and 369 deletions

4
.gitignore vendored
View File

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

View File

@ -1,4 +1,4 @@
Copyright (c) 2021,2022 Alexander "Arav" Andreev <me@arav.top>
Copyright (c) 2021-2024 Alexander "Arav" Andreev <me@arav.su>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -2,27 +2,32 @@ TARGET=httpprocprobed
SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
SYSDDIR=${SYSDDIR_:/%=%}
DESTDIR=/
LDFLAGS=-ldflags "-s -w" -tags netgo
DESTDIR:=
PREFIX:=/usr/local
VERSION:=3.1.1
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 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
install -Dm 0755 ${TARGET} ${DESTDIR}${PREFIX}/bin/${TARGET}
install -Dm 0644 configs/config.example.json ${DESTDIR}/etc/${TARGET}.json
install -Dm 0644 LICENSE ${DESTDIR}${PREFIX}/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}${PREFIX}/bin/${TARGET}
rm ${DESTDIR}${PREFIX}/share/licenses/${TARGET}
rm ${DESTDIR}${SYSDDIR}/${TARGET}.service
clean:
go clean
rm httpprocprobed

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

27
build/PKGBUILD Normal file
View File

@ -0,0 +1,27 @@
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
pkgname=httpprocprobed
pkgver=3.1.1
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.su/Arav/httpprocprobed"
license=('MIT')
makedepends=('go>=1.17')
backup=('etc/httpprocprobed.json')
source=("https://git.arav.su/Arav/httpprocprobed/archive/$pkgver.tar.gz")
md5sums=('SKIP')
build() {
cd "$srcdir/$pkgname"
export GOPATH="$srcdir"/gopath
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
make VERSION=$pkgver DESTDIR="$pkgdir/" PREFIX="/usr"
}
package() {
cd "$srcdir/$pkgname"
make DESTDIR="$pkgdir/" PREFIX="/usr" install
}

View File

@ -1,30 +0,0 @@
# Maintainer: Alexander "Arav" Andreev <me@arav.top>
pkgname=httpprocprobed
pkgver=2.0.1
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"
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')
noextract=()
md5sums=('SKIP')
build() {
cd "$srcdir/$pkgname"
make DESTDIR="$pkgdir/"
}
package() {
cd "$srcdir/$pkgname"
make DESTDIR="$pkgdir/" install
}

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 +0,0 @@
package main
import (
"bufio"
"errors"
"log"
"os"
"strconv"
"strings"
)
// Configuration holds a configuration for the service.
type Configuration struct {
ListenAddress string
IndentedOutput bool
Processes []string
}
// LoadConfiguration loads configuration from a file.
func LoadConfiguration(path string) (conf *Configuration, err error) {
conf = &Configuration{}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
s := bufio.NewScanner(file)
s.Split(bufio.ScanLines)
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")
}
}
}
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

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

View File

@ -1,12 +1,12 @@
[Unit]
Description=HTTPProcProbeD
Description=HTTP Process Prober Daemon
After=network.target
[Service]
Type=simple
DynamicUser=yes
Restart=on-failure
ExecStart=/usr/bin/httpprocprobed -c /etc/httpprocprobed.conf
ExecStart=/usr/bin/httpprocprobed -c /etc/httpprocprobed.json
ExecReload=kill -HUP $MAINPID
ReadOnlyPaths=/

149
main.go
View File

@ -1,106 +1,141 @@
package main
import (
"context"
"encoding/json"
"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
// Process contains an alias that will be returned when queried, and a process
// name to look for.
// A process is effectively a substring that is being looked in a cmdline file
// in /proc/ dir on unix-like systems.
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
}
type Configuration struct {
ListenAddress string `json:"listen-address"`
Processes []Process `json:"processes"`
}
func LoadConfiguration(path string) (conf *Configuration, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
conf = &Configuration{}
if err := json.NewDecoder(f).Decode(conf); err != nil {
return nil, err
}
for i := 0; i < len(conf.Processes); i++ {
if conf.Processes[i].Process == "" {
return nil, fmt.Errorf("an empty process field found")
}
if conf.Processes[i].Alias == "" {
conf.Processes[i].Alias = conf.Processes[i].Process
}
}
return conf, nil
}
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.Fprintln(os.Stderr, "httpprocprobed ver.", version, "\nCopyright (c) 2021-2024 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)
router := http.NewServeMux()
router.HandleFunc("/processes", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
w.Header().Add("Allow", "GET")
return
}
fmt.Println()
os.Exit(0)
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(GetProcessesState(&conf.Processes)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("Failed to encode a process list: %s\n", err)
}
})
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)
}()
defer func() {
srv.SetKeepAlivesEnabled(false)
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatalf("%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)
log.Printf("httpprocprobed is running on \"%s\".", conf.ListenAddress)
for {
switch <-syssignal {
case os.Interrupt:
fallthrough
case syscall.SIGINT | syscall.SIGTERM:
ShutdownHTTPServer(srv)
log.Println("Server shutted down.")
os.Exit(0)
return
case syscall.SIGHUP:
newconf, err := LoadConfiguration(*configPath)
if err != nil {
log.Fatalf("Failed to reload a list of processes from configuration: %s\n", err)
}
conf.Processes = newconf.Processes
log.Println("Successfully reloaded a list of watched processes.")
}
}
}

View File

@ -1,4 +1,5 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
//go:build unix
// +build unix
package main

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