diff --git a/.gitignore b/.gitignore
index 5a23055..e11fa7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
.vscode
-*.conf
-!config.example.conf
+*.json
+!config.example.json
httpprocprobed
\ No newline at end of file
diff --git a/Makefile b/Makefile
index c3bc600..930db28 100644
--- a/Makefile
+++ b/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:
diff --git a/README.md b/README.md
deleted file mode 100644
index 27ac396..0000000
--- a/README.md
+++ /dev/null
@@ -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
-`true|false...`.
-
-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 `"[]:"`. `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`.
\ No newline at end of file
diff --git a/build/archlinux/PKGBUILD b/build/archlinux/PKGBUILD
index 943d78a..524d85e 100644
--- a/build/archlinux/PKGBUILD
+++ b/build/archlinux/PKGBUILD
@@ -1,21 +1,14 @@
-# Maintainer: Alexander "Arav" Andreev
+# Maintainer: Alexander "Arav" Andreev
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')
diff --git a/configs/config.example.conf b/configs/config.example.conf
deleted file mode 100644
index 76e8d40..0000000
--- a/configs/config.example.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-listen_address = :28010
-indented_output = false
-processes =
\ No newline at end of file
diff --git a/configs/config.example.json b/configs/config.example.json
new file mode 100644
index 0000000..891d91e
--- /dev/null
+++ b/configs/config.example.json
@@ -0,0 +1,6 @@
+{
+ "listen-address": ":28010",
+ "processes": [
+ {"alias": "minecraft", "process": "fabric-server-mc"}
+ ]
+}
\ No newline at end of file
diff --git a/configuration.go b/configuration.go
index 06fc0fb..81a15d5 100644
--- a/configuration.go
+++ b/configuration.go
@@ -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")
-}
diff --git a/go.mod b/go.mod
index 3afa6aa..3a53e42 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
module httpprocprobed
-go 1.20
+go 1.17
diff --git a/http.go b/http.go
new file mode 100644
index 0000000..33e15f0
--- /dev/null
+++ b/http.go
@@ -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)
+ }
+ }
+}
diff --git a/httpserver.go b/httpserver.go
deleted file mode 100644
index d162ec0..0000000
--- a/httpserver.go
+++ /dev/null
@@ -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")
- }
- }
-}
diff --git a/main.go b/main.go
index 5e2f518..cdebd0a 100644
--- a/main.go
+++ b/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 ")
- fmt.Println("This program is licensed under terms of MIT+NIGGER license.")
+ fmt.Println("httpprocprobed ver.", version, "Copyright (c) 2021-2023 Alexander \"Arav\" Andreev ")
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:
diff --git a/proc.go b/process.go
similarity index 61%
rename from proc.go
rename to process.go
index 00dcc73..b5108a1 100644
--- a/proc.go
+++ b/process.go
@@ -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/")
diff --git a/processlist.go b/processlist.go
deleted file mode 100644
index 96152fe..0000000
--- a/processlist.go
+++ /dev/null
@@ -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())
-}