diff --git a/cli/cli.go b/cli/cli.go index f43eeca..0042b38 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -2,8 +2,10 @@ package cli import ( "encoding/json" + "errors" "fmt" "os" + "os/exec" "path/filepath" "github.com/coder/boundary/config" @@ -241,7 +243,19 @@ func BaseCommand(version string) *serpent.Command { } logger.Debug("Application config", "config", appConfigInJSON) - return run.Run(inv.Context(), logger, appConfig) + err = run.Run(inv.Context(), logger, appConfig) + + // If the child process exited with a non-zero code, exit + // with the same code directly. All cleanup (proxy, etc.) + // has already happened inside Run(). Exiting here ensures + // the correct code is propagated regardless of how the + // calling framework handles errors (standalone binary or + // embedded as a coder subcommand). + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + os.Exit(exitErr.ExitCode()) + } + return err }, } } diff --git a/landjail/child.go b/landjail/child.go index a88abf9..ad9c9dc 100644 --- a/landjail/child.go +++ b/landjail/child.go @@ -61,15 +61,12 @@ func RunChild(logger *slog.Logger, config config.AppConfig) error { // Run the command - this will block until it completes err = cmd.Run() if err != nil { - // Check if this is a normal exit with non-zero status code if exitError, ok := err.(*exec.ExitError); ok { - exitCode := exitError.ExitCode() - logger.Debug("Command exited with non-zero status", "exit_code", exitCode) - return fmt.Errorf("command exited with code %d", exitCode) + logger.Debug("Command exited with non-zero status", "exit_code", exitError.ExitCode()) + return fmt.Errorf("command exited with code %d: %w", exitError.ExitCode(), err) } - // This is an unexpected error logger.Error("Command execution failed", "error", err) - return fmt.Errorf("command execution failed: %v", err) + return fmt.Errorf("command execution failed: %w", err) } logger.Debug("Command completed successfully") diff --git a/landjail/manager.go b/landjail/manager.go index b38f54f..4167103 100644 --- a/landjail/manager.go +++ b/landjail/manager.go @@ -67,12 +67,12 @@ func (b *LandJail) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() + // childErr receives the result of RunChildProcess so we can + // propagate the child's exit code to our caller. + childErr := make(chan error, 1) go func() { defer cancel() - err := b.RunChildProcess(os.Args) - if err != nil { - b.logger.Error("Failed to run child process", "error", err) - } + childErr <- b.RunChildProcess(os.Args) }() // Setup signal handling BEFORE any setup @@ -89,7 +89,16 @@ func (b *LandJail) Run(ctx context.Context) error { b.logger.Info("Command completed, shutting down...") } - return nil + // Drain the child result if available. In the ctx.Done path the + // error is already buffered. In the signal path the child may still + // be running; return nil so deferred cleanup (iptables, proxy) can + // proceed before the process exits. + select { + case err := <-childErr: + return err + default: + return nil + } } func (b *LandJail) RunChildProcess(command []string) error { diff --git a/nsjail_manager/child.go b/nsjail_manager/child.go index d27c10a..41321ec 100644 --- a/nsjail_manager/child.go +++ b/nsjail_manager/child.go @@ -92,19 +92,12 @@ func RunChild(logger *slog.Logger, cfg config.AppConfig) error { } err = cmd.Run() if err != nil { - // Check if this is a normal exit with non-zero status code if exitError, ok := err.(*exec.ExitError); ok { - exitCode := exitError.ExitCode() - // Log at debug level for non-zero exits (normal behavior) - logger.Debug("Command exited with non-zero status", "exit_code", exitCode) - // Exit with the same code as the command - don't log as error - // This is normal behavior (commands can exit with any code) - os.Exit(exitCode) + logger.Debug("Command exited with non-zero status", "exit_code", exitError.ExitCode()) + return fmt.Errorf("command exited with code %d: %w", exitError.ExitCode(), err) } - // This is an unexpected error (not just a non-zero exit) - // Only log actual errors like "command not found" or "permission denied" logger.Error("Command execution failed", "error", err) - return err + return fmt.Errorf("command execution failed: %w", err) } // Command exited successfully diff --git a/nsjail_manager/manager.go b/nsjail_manager/manager.go index b0ddfda..348620d 100644 --- a/nsjail_manager/manager.go +++ b/nsjail_manager/manager.go @@ -71,9 +71,12 @@ func (b *NSJailManager) Run(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() + // childErr receives the result of RunChildProcess so we can + // propagate the child's exit code to our caller. + childErr := make(chan error, 1) go func() { defer cancel() - b.RunChildProcess(os.Args) + childErr <- b.RunChildProcess(os.Args) }() // Setup signal handling BEFORE any setup @@ -90,23 +93,32 @@ func (b *NSJailManager) Run(ctx context.Context) error { b.logger.Info("Command completed, shutting down...") } - return nil + // Drain the child result if available. In the ctx.Done path the + // error is already buffered. In the signal path the child may still + // be running; return nil so deferred cleanup (iptables, proxy) can + // proceed before the process exits. + select { + case err := <-childErr: + return err + default: + return nil + } } -func (b *NSJailManager) RunChildProcess(command []string) { +func (b *NSJailManager) RunChildProcess(command []string) error { cmd := b.jailer.Command(command) b.logger.Debug("Executing command in boundary", "command", strings.Join(os.Args, " ")) err := cmd.Start() if err != nil { b.logger.Error("Command failed to start", "error", err) - return + return err } err = b.jailer.ConfigureHostNsCommunication(cmd.Process.Pid) if err != nil { b.logger.Error("configuration after command execution failed", "error", err) - return + return err } b.logger.Debug("waiting on a child process to finish") @@ -121,9 +133,10 @@ func (b *NSJailManager) RunChildProcess(command []string) { // This is an unexpected error (not just a non-zero exit) b.logger.Error("Command execution failed", "error", err) } - return + return err } b.logger.Debug("Command completed successfully") + return nil } func (b *NSJailManager) setupHostAndStartProxy() error {