carlos / platform Public

instances import: its own stop signals, without SIGPIPE (Plan 6 Task 6 fix round 2)

carlos db's set catches SIGPIPE, which a write to a reset connection raises,
so reusing it let a connection reset mid-upload, mid-poll or mid-verify
cancel the whole import. The import now uses os.Interrupt, SIGTERM and
SIGHUP; the db helper is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b7b33e738d81a28ab40a6a4062b22400734e5c0b parent 0fc9502
2 files changed, +27 −3
  • instances_import.go +12 −3
  • instances_import_test.go +15 −0

CI passed — run details

diff --git a/instances_import.go b/instances_import.go
index aa2bba149..f5573189d 100644
--- a/instances_import.go
+++ b/instances_import.go
@@ -12,9 +12,11 @@ import (
"net/http"
"net/url"
"os"
+ "os/signal"
"slices"
"strconv"
"strings"
+ "syscall"
"time"
)
@@ -50,6 +52,14 @@ type importLive struct{ lines []string }
func (e importLive) Error() string { return strings.Join(e.lines, "\n") }
+// importStopSignals end an import early, through its live exit: Ctrl-C, a
+// supervisor's SIGTERM, and a closed terminal (SIGHUP). Not SIGPIPE, unlike
+// carlos db's set: a write to a reset connection (the upload, a poll, the
+// verify) raises it, and that must stay the request's own error rather than
+// cancel the whole import. syscall.SIGHUP and SIGTERM exist on every
+// platform the CLI builds for, as carlos db's set relies on.
+var importStopSignals = []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGHUP}
+
// importJob is one `carlos instances import`.
type importJob struct {
acct apiAcct
@@ -98,9 +108,8 @@ func runInstancesImport(args []string) {
}
// Ctrl-C, SIGTERM or a closed terminal cancels the import's context
// rather than killing the process, so an import left live still shows
- // its token and the commands to finish it (M17). The signals are
- // carlos db's.
- ctx, stop := dbSignalContext()
+ // its token and the commands to finish it (M17).
+ ctx, stop := signal.NotifyContext(context.Background(), importStopSignals...)
defer stop()
err := sess.runImport(ctx, stdout, importJob{acct: acct, app: app2, host: strings.TrimSpace(*host), file: *file,
replaces: strings.TrimSpace(*replaces), environment: strings.TrimSpace(*environment), verify: strings.TrimSpace(*verify), message: message})
diff --git a/instances_import_test.go b/instances_import_test.go
index 2800ce51b..8ecb61bd5 100644
--- a/instances_import_test.go
+++ b/instances_import_test.go
@@ -15,6 +15,7 @@ import (
"strings"
"sync"
"sync/atomic"
+ "syscall"
"testing"
"time"
)
@@ -863,3 +864,17 @@ func TestImportVerifyTarget(t *testing.T) {
t.Fatal("the request would have been made")
}
}
+
+// The import's own signal set: Ctrl-C, SIGTERM and SIGHUP cancel it, and
+// SIGPIPE never does, since a reset connection raises it mid-upload.
+func TestImportStopSignalsLeaveOutSIGPIPE(t *testing.T) {
+ want := map[os.Signal]bool{os.Interrupt: true, syscall.SIGTERM: true, syscall.SIGHUP: true}
+ if len(importStopSignals) != len(want) {
+ t.Fatalf("importStopSignals = %v", importStopSignals)
+ }
+ for _, sig := range importStopSignals {
+ if sig == syscall.SIGPIPE || !want[sig] {
+ t.Fatalf("importStopSignals = %v, want exactly Interrupt, SIGTERM and SIGHUP", importStopSignals)
+ }
+ }
+}