hermes/backend/externalbackendlauncher/main.go

308 lines
6.8 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"time"
"git.terah.dev/imterah/hermes/backend/backendlauncher"
"git.terah.dev/imterah/hermes/backend/commonbackend"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
)
type ProxyInstance struct {
SourceIP string `json:"sourceIP"`
SourcePort uint16 `json:"sourcePort"`
DestPort uint16 `json:"destPort"`
Protocol string `json:"protocol"`
}
type WriteLogger struct{}
func (writer WriteLogger) Write(p []byte) (n int, err error) {
logSplit := strings.Split(string(p), "\n")
for _, line := range logSplit {
if line == "" {
continue
}
log.Infof("application: %s", line)
}
return len(p), err
}
var (
tempDir string
logLevel string
)
func entrypoint(cCtx *cli.Context) error {
executablePath := cCtx.Args().Get(0)
if executablePath == "" {
return fmt.Errorf("executable file is not set")
}
executableParamsPath := cCtx.String("params-path")
if executablePath == "" {
return fmt.Errorf("executable parameters is not set")
}
proxyFilePath := cCtx.String("proxies")
proxies := []ProxyInstance{}
if proxyFilePath != "" {
proxyFile, err := os.ReadFile(proxyFilePath)
if err != nil {
return fmt.Errorf("failed to read proxy file: %s", err.Error())
}
err = json.Unmarshal(proxyFile, &proxies)
if err != nil {
return fmt.Errorf("failed to parse proxy file: %s", err.Error())
}
}
log.Debugf("discovered %d proxies.", len(proxies))
backendParameters, err := os.ReadFile(executableParamsPath)
if err != nil {
return fmt.Errorf("could not read backend parameters: %s", err.Error())
}
_, err = os.Stat(executablePath)
if err != nil {
return fmt.Errorf("failed to get backend executable information: %s", err.Error())
}
log.Debug("running socket acquisition")
sockPath, sockListener, err := backendlauncher.GetUnixSocket(tempDir)
if err != nil {
return fmt.Errorf("failed to acquire unix socket: %s", err.Error())
}
log.Debugf("acquisition was successful: %s", sockPath)
go func() {
log.Debug("entering execution loop (in auxiliary goroutine)...")
for {
log.Info("waiting for Unix socket connections...")
sock, err := sockListener.Accept()
log.Info("recieved connection. initializing...")
if err != nil {
log.Warnf("failed to accept socket connection: %s", err.Error())
continue
}
defer sock.Close()
startCommand := &commonbackend.Start{
Arguments: backendParameters,
}
startMarshalledCommand, err := commonbackend.Marshal(startCommand)
if err != nil {
log.Errorf("failed to generate start command: %s", err.Error())
continue
}
if _, err = sock.Write(startMarshalledCommand); err != nil {
log.Errorf("failed to write to socket: %s", err.Error())
continue
}
commandRaw, err := commonbackend.Unmarshal(sock)
if err != nil {
log.Errorf("failed to read from/unmarshal from socket: %s", err.Error())
continue
}
command, ok := commandRaw.(*commonbackend.BackendStatusResponse)
if !ok {
log.Error("failed to typecast response")
continue
}
if !command.IsRunning {
var status string
if command.StatusCode == commonbackend.StatusSuccess {
status = "Success"
} else {
status = "Failure"
}
log.Errorf("failed to start backend (status: %s): %s", status, command.Message)
continue
}
log.Info("successfully started backend.")
hasAnyFailed := false
for _, proxy := range proxies {
log.Infof("initializing proxy %s:%d -> remote:%d", proxy.SourceIP, proxy.SourcePort, proxy.DestPort)
proxyAddCommand := &commonbackend.AddProxy{
SourceIP: proxy.SourceIP,
SourcePort: proxy.SourcePort,
DestPort: proxy.DestPort,
Protocol: proxy.Protocol,
}
marshalledProxyCommand, err := commonbackend.Marshal(proxyAddCommand)
if err != nil {
log.Errorf("failed to generate start command: %s", err.Error())
hasAnyFailed = true
continue
}
if _, err = sock.Write(marshalledProxyCommand); err != nil {
log.Errorf("failed to write to socket: %s", err.Error())
hasAnyFailed = true
continue
}
commandRaw, err := commonbackend.Unmarshal(sock)
if err != nil {
log.Errorf("failed to read from/unmarshal from socket: %s", err.Error())
hasAnyFailed = true
continue
}
command, ok := commandRaw.(*commonbackend.ProxyStatusResponse)
if !ok {
log.Error("failed to typecast response")
hasAnyFailed = true
continue
}
if !command.IsActive {
log.Error("failed to activate: isActive is false in response to AddProxy{} call")
hasAnyFailed = true
continue
}
log.Infof("successfully initialized proxy %s:%d -> remote:%d", proxy.SourceIP, proxy.SourcePort, proxy.DestPort)
}
if hasAnyFailed {
log.Error("failed to initialize all proxies (read logs above)")
} else {
log.Info("successfully initialized all proxies")
}
log.Debug("entering infinite keepalive loop...")
for {
}
}
}()
log.Debug("entering execution loop (in main goroutine)...")
stdout := WriteLogger{}
stderr := WriteLogger{}
for {
log.Info("starting process...")
// TODO: can we reuse cmd?
cmd := exec.Command(executablePath)
cmd.Env = append(cmd.Env, fmt.Sprintf("HERMES_API_SOCK=%s", sockPath), fmt.Sprintf("HERMES_LOG_LEVEL=%s", logLevel))
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Run()
if err != nil {
if err, ok := err.(*exec.ExitError); ok {
log.Warnf("backend died with exit code '%d' and with error '%s'", err.ExitCode(), err.Error())
} else {
log.Warnf("backend died with error: %s", err.Error())
}
} else {
log.Info("process exited gracefully.")
}
log.Info("sleeping 5 seconds, and then restarting process")
time.Sleep(5 * time.Second)
}
}
func main() {
logLevel = os.Getenv("HERMES_LOG_LEVEL")
if logLevel == "" {
logLevel = "fatal"
}
switch logLevel {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warn":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
}
var err error
tempDir, err = os.MkdirTemp("", "hermes-sockets-")
if err != nil {
log.Fatalf("failed to create sockets directory: %s", err.Error())
}
app := &cli.App{
Name: "externalbackendlauncher",
Usage: "for development purposes only -- external backend launcher for Hermes",
Action: entrypoint,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "params-path",
Aliases: []string{"params", "pp"},
Usage: "file containing the parameters that are sent to the backend",
Required: true,
},
&cli.StringFlag{
Name: "proxies",
Aliases: []string{"p"},
Usage: "file that contains the list of proxies to setup in JSON format",
},
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}