diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49ccfbe..1cb9fb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: release: uses: itzg/github-workflows/.github/workflows/go-with-releaser-image.yml@main with: - go-version: "1.20.10" + go-version: "1.21.6" secrets: image-registry-username: ${{ secrets.DOCKERHUB_USERNAME }} image-registry-password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 9f49809..6bd26d4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -10,13 +10,12 @@ builds: - goos: - linux goarch: - - 386 - amd64 - arm - arm64 goarm: - - 6 - - 7 + - "6" + - "7" main: . env: - CGO_ENABLED=0 @@ -25,8 +24,6 @@ archives: - format_overrides: - goos: windows format: zip -snapshot: - name_template: SNAPSHOT-{{ .Commit }} changelog: filters: exclude: diff --git a/main.go b/main.go index b74e2b5..76493e9 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "io" @@ -67,14 +68,43 @@ func main() { logger.Error("Unable to get stdin", zap.Error(err)) } - stdout, err := cmd.StdoutPipe() - if err != nil { - logger.Error("Unable to get stdout", zap.Error(err)) - } + if args.RemoteConsole { + stdout, err := cmd.StdoutPipe() + if err != nil { + logger.Error("Unable to get stdout", zap.Error(err)) + } - stderr, err := cmd.StderrPipe() - if err != nil { - logger.Error("Unable to get stderr", zap.Error(err)) + stderr, err := cmd.StderrPipe() + if err != nil { + logger.Error("Unable to get stderr", zap.Error(err)) + } + + console := makeConsole(stdin, stdout, stderr) + + // Relay stdin between outside and server + if !args.DetachStdin { + go consoleInRoutine(os.Stdin, console, logger) + } + + go consoleOutRoutine(os.Stdout, console, stdOutTarget, logger) + go consoleOutRoutine(os.Stderr, console, stdErrTarget, logger) + + go runRemoteShellServer(console, logger) + + logger.Info("Running with remote console support") + } else { + logger.Debug("Directly assigning stdout/stderr") + // directly assign stdout/err to pass through terminal, if applicable + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if hasRconCli() { + logger.Debug("Directly assigning stdin") + cmd.Stdin = os.Stdin + stdin = os.Stdin + } else { + go relayStdin(logger, stdin) + } } err = cmd.Start() @@ -93,26 +123,11 @@ func main() { } } - console := makeConsole(stdin, stdout, stderr) - - // Relay stdin between outside and server - if !args.DetachStdin { - go consoleInRoutine(os.Stdin, console, logger) - } - - go consoleOutRoutine(os.Stdout, console, stdOutTarget, logger) - go consoleOutRoutine(os.Stderr, console, stdErrTarget, logger) - - // Start the remote server if intended - if args.RemoteConsole { - go startRemoteShellServer(console, logger) - } - ctx, cancel := context.WithCancel(context.Background()) - errors := make(chan error, 1) + errorChan := make(chan error, 1) if args.NamedPipe != "" { - err2 := handleNamedPipe(ctx, args.NamedPipe, stdin, errors) + err2 := handleNamedPipe(ctx, args.NamedPipe, stdin, errorChan) if err2 != nil { logger.Fatal("Failed to setup named pipe", zap.Error(err2)) } @@ -123,14 +138,12 @@ func main() { go func() { waitErr := cmd.Wait() if waitErr != nil { - if exitErr, ok := waitErr.(*exec.ExitError); ok { + var exitErr *exec.ExitError + if errors.As(waitErr, &exitErr) { exitCode := exitErr.ExitCode() logger.Warn("Minecraft server failed. Inspect logs above for errors that indicate cause. DO NOT report this line as an error.", zap.Int("exitCode", exitCode)) cmdExitChan <- exitCode - } else { - logger.Error("Command failed abnormally", zap.Error(waitErr)) - cmdExitChan <- 1 } return } else { @@ -142,7 +155,7 @@ func main() { select { case <-signalChan: if args.StopServerAnnounceDelay > 0 { - announceStopViaConsole(logger, stdin, args.StopServerAnnounceDelay) + announceStop(logger, stdin, args.StopServerAnnounceDelay) logger.Info("Sleeping before server stop", zap.Duration("sleepTime", args.StopServerAnnounceDelay)) time.Sleep(args.StopServerAnnounceDelay) } @@ -168,7 +181,7 @@ func main() { }) } - case namedPipeErr := <-errors: + case namedPipeErr := <-errorChan: logger.Error("Error during named pipe handling", zap.Error(namedPipeErr)) case exitCode := <-cmdExitChan: @@ -180,6 +193,13 @@ func main() { } +func relayStdin(logger *zap.Logger, stdin io.WriteCloser) { + _, err := io.Copy(stdin, os.Stdin) + if err != nil { + logger.Error("Failed to relay standard input", zap.Error(err)) + } +} + func hasRconCli() bool { if strings.ToUpper(os.Getenv("ENABLE_RCON")) == "TRUE" { _, err := exec.LookPath("rcon-cli") @@ -189,9 +209,7 @@ func hasRconCli() bool { } } -func stopWithRconCli() error { - log.Println("Stopping with rcon-cli") - +func sendRconCommand(cmd ...string) error { rconConfigFile := os.Getenv("RCON_CONFIG_FILE") if rconConfigFile == "" { port := os.Getenv("RCON_PORT") @@ -204,23 +222,39 @@ func stopWithRconCli() error { password = "minecraft" } - rconCliCmd := exec.Command("rcon-cli", - "--port", port, - "--password", password, - "stop") + args := []string{"--port", port, + "--password", password} + args = append(args, cmd...) + + rconCliCmd := exec.Command("rcon-cli", args...) return rconCliCmd.Run() } else { - rconCliCmd := exec.Command("rcon-cli", - "--config", rconConfigFile, - "stop") + + args := []string{"--config", rconConfigFile} + args = append(args, cmd...) + + rconCliCmd := exec.Command("rcon-cli", args...) return rconCliCmd.Run() } } -func announceStopViaConsole(logger *zap.Logger, stdin io.Writer, shutdownDelay time.Duration) { +func stopWithRconCli() error { + log.Println("Stopping with rcon-cli") + + return sendRconCommand("stop") +} + +func announceStop(logger *zap.Logger, stdin io.Writer, shutdownDelay time.Duration) { logger.Info("Sending shutdown announce 'say' to Minecraft server") + if hasRconCli() { + err := sendRconCommand("say", fmt.Sprintf("Server shutting down in %0.f seconds", shutdownDelay.Seconds())) + if err != nil { + logger.Error("Failed to send 'say' command", zap.Error(err)) + } + } + _, err := stdin.Write([]byte(fmt.Sprintf("say Server shutting down in %0.f seconds\n", shutdownDelay.Seconds()))) if err != nil { logger.Error("Failed to write say command to server console", zap.Error(err)) diff --git a/remote_shell_service.go b/remote_shell_service.go index a39d367..b7f0bc1 100644 --- a/remote_shell_service.go +++ b/remote_shell_service.go @@ -183,7 +183,7 @@ func consoleOutRoutine(output io.Writer, console *Console, target ConsoleTarget, // Use os.Stdin for console. func consoleInRoutine(stdIn io.Reader, console *Console, logger *zap.Logger) { - scanner := bufio.NewScanner(os.Stdin) + scanner := bufio.NewScanner(stdIn) for scanner.Scan() { text := scanner.Text() outBytes := []byte(fmt.Sprintf("%s\n", text)) @@ -229,7 +229,7 @@ func ensureHostKey(logger *zap.Logger) (string, error) { return keyfilePath, err } -func startRemoteShellServer(console *Console, logger *zap.Logger) { +func runRemoteShellServer(console *Console, logger *zap.Logger) { logger.Info("Starting remote shell server on 2222...") ssh.Handle(func(s ssh.Session) { handleSession(s, console, logger) })