From 1602593a24517ed955bedc08980258e854cc6a10 Mon Sep 17 00:00:00 2001 From: chluknight1224 Date: Sun, 14 Jun 2026 19:45:55 +0800 Subject: [PATCH] fix: restore terminal after Ctrl+C on Windows auth login --- cmd/auth/auth.go | 26 ++++++++++++++++++++++ cmd/auth/auth_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/cmd/auth/auth.go b/cmd/auth/auth.go index 25653d7..c0ee5d9 100644 --- a/cmd/auth/auth.go +++ b/cmd/auth/auth.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "os" + "os/signal" + "runtime" "strings" "github.com/spf13/cobra" @@ -87,6 +89,30 @@ func readPassword(in io.Reader, reader *bufio.Reader) ([]byte, error) { if file, ok := in.(*os.File); ok { fd := int(file.Fd()) if term.IsTerminal(fd) { + // On Windows, ExitProcess kills the process without running deferred + // cleanup, leaving the console in raw mode after Ctrl+C. We save the + // original terminal state and register a SIGINT handler to restore it + // before exit. Linux/macOS terminals recover automatically. + if runtime.GOOS == "windows" { + if oldState, err := term.GetState(fd); err == nil { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + done := make(chan struct{}) + go func() { + select { + case <-sigCh: + term.Restore(fd, oldState) + os.Exit(1) + case <-done: + return + } + }() + password, err := term.ReadPassword(fd) + close(done) + signal.Stop(sigCh) + return password, err + } + } return term.ReadPassword(fd) } } diff --git a/cmd/auth/auth_test.go b/cmd/auth/auth_test.go index d09602b..2b9d831 100644 --- a/cmd/auth/auth_test.go +++ b/cmd/auth/auth_test.go @@ -1,9 +1,11 @@ package auth import ( + "bufio" "errors" "os" "path/filepath" + "strings" "testing" "github.com/spf13/cobra" @@ -233,6 +235,55 @@ func TestLoginWithPasswordNoTerminal(t *testing.T) { } } +func TestReadPasswordFromPipe(t *testing.T) { + r, w, _ := os.Pipe() + go func() { + w.Write([]byte("password123\n")) + w.Close() + }() + + password, err := readPassword(r, bufio.NewReader(r)) + if err != nil { + t.Fatalf("readPassword from pipe: %v", err) + } + if string(password) != "password123" { + t.Fatalf("password = %q, want password123", string(password)) + } +} + +func TestReadPasswordFromNonFileReader(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("secret\n")) + password, err := readPassword(strings.NewReader(""), reader) + if err != nil { + t.Fatalf("readPassword from non-file: %v", err) + } + if string(password) != "secret" { + t.Fatalf("password = %q, want secret", string(password)) + } +} + +func TestReadPasswordCRLF(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("pass\r\n")) + password, err := readPassword(strings.NewReader(""), reader) + if err != nil { + t.Fatalf("readPassword CRLF: %v", err) + } + if string(password) != "pass" { + t.Fatalf("password = %q, want pass", string(password)) + } +} + +func TestReadPasswordEmpty(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("\n")) + password, err := readPassword(strings.NewReader(""), reader) + if err != nil { + t.Fatalf("readPassword empty: %v", err) + } + if string(password) != "" { + t.Fatalf("password = %q, want empty", string(password)) + } +} + func findSub(cmd *cobra.Command, name string) *cobra.Command { for _, sub := range cmd.Commands() { if sub.Use == name { -- 2.34.1