-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HTTP API for receiving signal requests and passing it to the wrapped …
…command
- Loading branch information
Showing
10 changed files
with
376 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,90 +1,94 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"path" | ||
"os/signal" | ||
|
||
"jarnfast/signalman/pkg" | ||
"jarnfast/signalman/pkg/api" | ||
"jarnfast/signalman/pkg/cmdwrapper" | ||
"jarnfast/signalman/pkg/utl" | ||
|
||
"go.uber.org/zap" | ||
) | ||
|
||
var version string = "1.0" | ||
var build string = "n/a" | ||
func createLogger() *zap.SugaredLogger { | ||
var config zap.Config | ||
|
||
type handlers struct { | ||
} | ||
configFilename := os.Getenv("SIGNALMAN_LOG_CONFIGFILE") | ||
|
||
type adminPortal struct { | ||
} | ||
if configFilename != "" { | ||
configFile, err := os.Open(configFilename) | ||
if err != nil { | ||
panic(fmt.Sprintf("Unable to open config file: %v", err)) | ||
|
||
func newAdminPortal() *adminPortal { | ||
return &adminPortal{} | ||
} | ||
} | ||
rawJSON, err := io.ReadAll(configFile) | ||
if err != nil { | ||
panic(fmt.Sprintf("Unable to read config file: %v", err)) | ||
} | ||
|
||
func (h *handlers) status(response http.ResponseWriter, request *http.Request) { | ||
response.WriteHeader(http.StatusOK) | ||
response.Write([]byte(versionString())) | ||
response.Write([]byte(fmt.Sprintf("Args: %d, %s", len(os.Args[1:]), os.Args[1:]))) | ||
} | ||
if err := json.Unmarshal(rawJSON, &config); err != nil { | ||
panic(fmt.Sprintf("Unable to parse config file as JSON: %v", err)) | ||
} | ||
|
||
func (h *handlers) signal(response http.ResponseWriter, request *http.Request) { | ||
if request.Method != "GET" { | ||
response.WriteHeader(http.StatusMethodNotAllowed) | ||
return | ||
} else { | ||
config = zap.NewProductionConfig() | ||
var err error | ||
config.Level, err = zap.ParseAtomicLevel(utl.GetenvDefault("SIGNALMAN_LOG_LEVEL", "info")) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
fmt.Println("Get request", request.URL) | ||
} | ||
func (h *handlers) kill(response http.ResponseWriter, request *http.Request) { | ||
os.Exit(1337) | ||
} | ||
func versionString() string { | ||
//return fmt.Sprintf("%s version %s (build %s)", path.Base(os.Args[0]), version, build) | ||
return fmt.Sprintf("%s version %s (build %s)", path.Base(os.Args[0]), pkg.Version, build) | ||
|
||
logger, _ := config.Build() | ||
|
||
defer logger.Sync() | ||
|
||
sugar := logger.Sugar() | ||
return sugar | ||
} | ||
|
||
func main() { | ||
fmt.Println("Starting", versionString()) | ||
logger := createLogger() | ||
|
||
addr := os.Getenv("SIGNALMAN_LISTEN_ADDRESS") | ||
if addr == "" { | ||
addr = "localhost:30000" | ||
} | ||
logger.Infof("Starting %s", pkg.VersionString()) | ||
|
||
h := &handlers{} | ||
http.HandleFunc("/status", h.status) | ||
http.HandleFunc("/kill", h.kill) | ||
http.HandleFunc("/signal/", h.signal) | ||
sigs := make(chan os.Signal, 10) | ||
|
||
go func() { | ||
err := http.ListenAndServe(addr, nil) | ||
if err != nil { | ||
panic(err) | ||
} | ||
}() | ||
// Relay incoming signals to process | ||
signal.Notify(sigs) | ||
|
||
fmt.Println("Listening for commands on", addr) | ||
a := api.NewApi(logger) | ||
// Relay signals received on HTTP | ||
a.Notify(sigs) | ||
a.ListenAndServe() | ||
|
||
args := os.Args[1:] | ||
binary, err := exec.LookPath(args[0]) | ||
if err != nil { | ||
fmt.Println("Command not found", binary) | ||
panic(err) | ||
} | ||
c := cmdwrapper.NewCmdWrapper(logger) | ||
// Subscribe on the received signals | ||
c.Subscribe(sigs) | ||
c.Start() | ||
|
||
exitCode, err := c.Wait() | ||
|
||
cmd := exec.Command(binary, args[1:]...) | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
cmd.Stdin = os.Stdin | ||
close(sigs) | ||
|
||
err = cmd.Start() | ||
if err != nil { | ||
fmt.Println("Unable to run command", err) | ||
panic(err) | ||
exitCode := -1 | ||
// try to get the original exit code | ||
var exitError *exec.ExitError | ||
if errors.As(err, &exitError) { | ||
exitCode = exitError.ExitCode() | ||
} | ||
logger.Warnf("Wrapped command finished with error: %v", err) | ||
os.Exit(exitCode) | ||
} else { | ||
logger.Info("Wrapped command finished without errors") | ||
os.Exit(exitCode) | ||
} | ||
|
||
cmd.Wait() | ||
|
||
fmt.Println("Command terminated") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,10 @@ | ||
module jarnfast/signalman | ||
|
||
go 1.19 | ||
|
||
require ( | ||
github.com/spf13/cast v1.5.0 // indirect | ||
go.uber.org/atomic v1.7.0 // indirect | ||
go.uber.org/multierr v1.6.0 // indirect | ||
go.uber.org/zap v1.24.0 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= | ||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= | ||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package api | ||
|
||
import ( | ||
"fmt" | ||
"jarnfast/signalman/pkg" | ||
"jarnfast/signalman/pkg/utl" | ||
"os" | ||
"strconv" | ||
"strings" | ||
"syscall" | ||
"time" | ||
|
||
"net/http" | ||
|
||
"github.com/spf13/cast" | ||
"go.uber.org/zap" | ||
) | ||
|
||
type Api struct { | ||
addr string | ||
logger *zap.SugaredLogger | ||
sigs chan<- os.Signal | ||
} | ||
|
||
func NewApi(logger *zap.SugaredLogger) *Api { | ||
addr := utl.GetenvDefault("SIGNALMAN_LISTEN_ADDRESS", "localhost:30000") | ||
|
||
return &Api{ | ||
addr: addr, | ||
logger: logger, | ||
} | ||
} | ||
|
||
// Notify causes api to relay signal received on HTTP to c. | ||
func (a *Api) Notify(c chan<- os.Signal) { | ||
a.sigs = c | ||
} | ||
|
||
func (a *Api) debugLogRequest(r *http.Request) { | ||
a.logger.Debugf("HTTP Request: %s %s", r.Method, r.URL) | ||
} | ||
|
||
func (a *Api) handleStatus(w http.ResponseWriter, r *http.Request) { | ||
a.debugLogRequest(r) | ||
|
||
if r.Method != "GET" { | ||
w.WriteHeader(http.StatusMethodNotAllowed) | ||
return | ||
} | ||
w.WriteHeader(http.StatusOK) | ||
w.Write([]byte(pkg.VersionString())) | ||
} | ||
|
||
func (a *Api) handleSignal(w http.ResponseWriter, r *http.Request) { | ||
a.debugLogRequest(r) | ||
|
||
if r.Method != "POST" { | ||
w.WriteHeader(http.StatusMethodNotAllowed) | ||
return | ||
} | ||
|
||
parts := strings.Split(r.URL.String(), "/") | ||
|
||
if len(parts) != 3 { | ||
w.WriteHeader(http.StatusBadRequest) | ||
return | ||
} | ||
|
||
i, err := strconv.Atoi(parts[2]) | ||
if err != nil { | ||
m := fmt.Sprintf("Unable to parse signal as int: %s", parts[2]) | ||
a.logger.Debug(m) | ||
w.WriteHeader(http.StatusBadRequest) | ||
w.Write([]byte(m)) | ||
return | ||
} | ||
|
||
w.WriteHeader(http.StatusAccepted) | ||
|
||
s := syscall.Signal(i) | ||
|
||
a.sigs <- s | ||
|
||
w.Write([]byte(fmt.Sprintf("Got signal (%d) %s\r\n", len(parts), parts[2]))) | ||
w.Write([]byte("This signal is: ")) | ||
w.Write([]byte(strconv.Itoa(i))) | ||
} | ||
|
||
func (a *Api) handleTerm(w http.ResponseWriter, r *http.Request) { | ||
a.debugLogRequest(r) | ||
|
||
w.WriteHeader(http.StatusAccepted) | ||
|
||
a.sigs <- syscall.SIGTERM | ||
|
||
timeval := cast.ToDuration(utl.GetenvDefault("SIGNALMAN_TERM_TIMEOUT", "10")) | ||
<-time.After(timeval * time.Second) | ||
|
||
a.logger.Warnf("Wrapped command did not react to TERM within %d seconds. Sending KILL.", timeval) | ||
|
||
a.sigs <- syscall.SIGKILL | ||
} | ||
|
||
func (a *Api) handleKill(w http.ResponseWriter, r *http.Request) { | ||
a.debugLogRequest(r) | ||
|
||
w.WriteHeader(http.StatusAccepted) | ||
a.sigs <- syscall.SIGKILL | ||
} | ||
|
||
// ListenAndServe listens on the configured TCP network address and relays received | ||
// signals to the configured channel | ||
func (a *Api) ListenAndServe() { | ||
|
||
if a.sigs == nil { | ||
a.logger.Panic("No signal channel found") | ||
} | ||
|
||
mux := http.NewServeMux() | ||
mux.HandleFunc("/status", a.handleStatus) | ||
mux.HandleFunc("/signal/", a.handleSignal) | ||
mux.HandleFunc("/term", a.handleTerm) | ||
mux.HandleFunc("/kill", a.handleKill) | ||
|
||
go func() { | ||
err := http.ListenAndServe(a.addr, mux) | ||
if err != nil { | ||
a.logger.Panic(err) | ||
} | ||
}() | ||
|
||
a.logger.Infof("Listening for commands on %s", a.addr) | ||
} |
Oops, something went wrong.