Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,26 @@ func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

if err := command.Main(ctx, os.Args); err != nil {
// intCh receives only SIGINT so we know whether the termination was
// caused by Ctrl-C (as opposed to SIGTERM or a command error). It
// must be registered *before* command.Main is called so the signal is
// captured even if the command returns very quickly.
intCh := make(chan os.Signal, 1)
signal.Notify(intCh, os.Interrupt)
defer signal.Stop(intCh)

err := command.Main(ctx, os.Args)

// Check non-blocking: if the channel already has a value, SIGINT arrived
// while the command was running.
select {
case <-intCh:
// SIGINT was the cause; exit 130 per POSIX convention (128 + 2).
os.Exit(130)
default:
}

if err != nil {
os.Exit(1)
}
}
67 changes: 67 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"os"
"os/exec"
"syscall"
"testing"
"time"
)

// TestSIGINTExitCode verifies that sending SIGINT to s5cmd results in
// exit code 130 (the POSIX standard: 128 + signal number 2).
//
// The test builds the binary, starts it with a long-running command,
// sends SIGINT after a short delay, and checks the exit code.
func TestSIGINTExitCode(t *testing.T) {
if testing.Short() {
t.Skip("skipping subprocess test in short mode")
}

// Build the binary into a temp dir.
tmpDir := t.TempDir()
bin := tmpDir + "/s5cmd"
if err := exec.Command("go", "build", "-o", bin, ".").Run(); err != nil {
t.Fatalf("failed to build s5cmd: %v", err)
}

// Use --no-sign-request so credential lookup is skipped entirely, then
// point at a black-hole address. The binary will block on the TCP dial
// for as long as we need. Setting AWS_DEFAULT_REGION avoids the
// region-discovery HTTP call so the binary goes straight to dialling
// the black-hole endpoint.
cmd := exec.Command(bin,
"--no-sign-request",
"--endpoint-url", "http://10.255.255.1:19999", // routable but unreachable
"--retry-count", "10",
"ls", "s3://bucket/",
)
cmd.Env = append(os.Environ(), "AWS_DEFAULT_REGION=us-east-1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
t.Fatalf("failed to start s5cmd: %v", err)
}

// Give the process time to start and block on the network dial.
// Use 3 s to remain reliable even under heavy test-suite parallelism.
time.Sleep(3 * time.Second)

// Send SIGINT.
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
t.Fatalf("failed to send SIGINT: %v", err)
}

err := cmd.Wait()
exitErr, ok := err.(*exec.ExitError)
if !ok {
t.Fatalf("expected ExitError after SIGINT, got: %T %v", err, err)
}

got := exitErr.ExitCode()
t.Logf("process state: %v", exitErr.ProcessState)
if got != 130 {
t.Errorf("expected exit code 130 after SIGINT, got %d", got)
}
}
Loading