From 6ff7cd90507657caa12a5c46ff797bf2a72a077f Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 11:58:02 +0200 Subject: [PATCH 01/12] backend/util: extract background handling code --- internal/backend/rclone/backend.go | 4 ++-- internal/backend/sftp/sftp.go | 3 ++- internal/{backend/util => terminal}/foreground.go | 2 +- internal/{backend/util => terminal}/foreground_test.go | 6 +++--- internal/{backend/util => terminal}/foreground_unix.go | 2 +- internal/{backend/util => terminal}/foreground_windows.go | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) rename internal/{backend/util => terminal}/foreground.go (97%) rename internal/{backend/util => terminal}/foreground_test.go (84%) rename internal/{backend/util => terminal}/foreground_unix.go (99%) rename internal/{backend/util => terminal}/foreground_windows.go (96%) diff --git a/internal/backend/rclone/backend.go b/internal/backend/rclone/backend.go index 74b2148ce..5ee330a69 100644 --- a/internal/backend/rclone/backend.go +++ b/internal/backend/rclone/backend.go @@ -21,9 +21,9 @@ import ( "github.com/restic/restic/internal/backend/limiter" "github.com/restic/restic/internal/backend/location" "github.com/restic/restic/internal/backend/rest" - "github.com/restic/restic/internal/backend/util" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/terminal" "golang.org/x/net/http2" ) @@ -82,7 +82,7 @@ func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan stru cmd.Stdin = r cmd.Stdout = w - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) // close rclone side of pipes errR := r.Close() errW := w.Close() diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index c735b9b3a..78757a249 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -21,6 +21,7 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/feature" + "github.com/restic/restic/internal/terminal" "github.com/cenkalti/backoff/v4" "github.com/pkg/sftp" @@ -84,7 +85,7 @@ func startClient(cfg Config) (*SFTP, error) { return nil, errors.Wrap(err, "cmd.StdoutPipe") } - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) if err != nil { if errors.Is(err, exec.ErrDot) { return nil, errors.Errorf("cannot implicitly run relative executable %v found in current directory, use -o sftp.command=./ to override", cmd.Path) diff --git a/internal/backend/util/foreground.go b/internal/terminal/foreground.go similarity index 97% rename from internal/backend/util/foreground.go rename to internal/terminal/foreground.go index 477fc8900..e3c1ff0b5 100644 --- a/internal/backend/util/foreground.go +++ b/internal/terminal/foreground.go @@ -1,4 +1,4 @@ -package util +package terminal import ( "os" diff --git a/internal/backend/util/foreground_test.go b/internal/terminal/foreground_test.go similarity index 84% rename from internal/backend/util/foreground_test.go rename to internal/terminal/foreground_test.go index c26861a6c..c8392ab88 100644 --- a/internal/backend/util/foreground_test.go +++ b/internal/terminal/foreground_test.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package util_test +package terminal_test import ( "bufio" @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/restic/restic/internal/backend/util" + "github.com/restic/restic/internal/terminal" rtest "github.com/restic/restic/internal/test" ) @@ -22,7 +22,7 @@ func TestForeground(t *testing.T) { stdout, err := cmd.StdoutPipe() rtest.OK(t, err) - bg, err := util.StartForeground(cmd) + bg, err := terminal.StartForeground(cmd) rtest.OK(t, err) defer func() { rtest.OK(t, cmd.Wait()) diff --git a/internal/backend/util/foreground_unix.go b/internal/terminal/foreground_unix.go similarity index 99% rename from internal/backend/util/foreground_unix.go rename to internal/terminal/foreground_unix.go index 3952885f8..340f80791 100644 --- a/internal/backend/util/foreground_unix.go +++ b/internal/terminal/foreground_unix.go @@ -1,6 +1,6 @@ //go:build unix -package util +package terminal import ( "os" diff --git a/internal/backend/util/foreground_windows.go b/internal/terminal/foreground_windows.go similarity index 96% rename from internal/backend/util/foreground_windows.go rename to internal/terminal/foreground_windows.go index f9b753c35..8c541fe47 100644 --- a/internal/backend/util/foreground_windows.go +++ b/internal/terminal/foreground_windows.go @@ -1,4 +1,4 @@ -package util +package terminal import ( "os/exec" From 48cbbf9651ab5a7ebbaa0c9f6d13a74209ea756d Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 12:15:27 +0200 Subject: [PATCH 02/12] ui/termstatus: extract background handling code --- internal/{ui/termstatus => terminal}/background_unix.go | 2 +- .../{ui/termstatus => terminal}/background_unix_test.go | 2 +- .../{ui/termstatus => terminal}/background_windows.go | 2 +- internal/terminal/foreground_unix.go | 9 ++++----- internal/{ui/termstatus => terminal}/getpgrp_solaris.go | 2 +- internal/{ui/termstatus => terminal}/getpgrp_unix.go | 2 +- internal/{ui/termstatus => terminal}/tcgetpgrp_linux.go | 2 +- internal/{ui/termstatus => terminal}/tcgetpgrp_unix.go | 2 +- internal/{ui/termstatus => terminal}/tcsetpgrp_aix.go | 2 +- internal/{ui/termstatus => terminal}/tcsetpgrp_unix.go | 2 +- internal/ui/termstatus/status.go | 8 +++++--- 11 files changed, 18 insertions(+), 17 deletions(-) rename internal/{ui/termstatus => terminal}/background_unix.go (96%) rename internal/{ui/termstatus => terminal}/background_unix_test.go (94%) rename internal/{ui/termstatus => terminal}/background_windows.go (90%) rename internal/{ui/termstatus => terminal}/getpgrp_solaris.go (83%) rename internal/{ui/termstatus => terminal}/getpgrp_unix.go (84%) rename internal/{ui/termstatus => terminal}/tcgetpgrp_linux.go (95%) rename internal/{ui/termstatus => terminal}/tcgetpgrp_unix.go (88%) rename internal/{ui/termstatus => terminal}/tcsetpgrp_aix.go (93%) rename internal/{ui/termstatus => terminal}/tcsetpgrp_unix.go (89%) diff --git a/internal/ui/termstatus/background_unix.go b/internal/terminal/background_unix.go similarity index 96% rename from internal/ui/termstatus/background_unix.go rename to internal/terminal/background_unix.go index 666e42dd1..316466ce9 100644 --- a/internal/ui/termstatus/background_unix.go +++ b/internal/terminal/background_unix.go @@ -1,6 +1,6 @@ //go:build unix -package termstatus +package terminal import "github.com/restic/restic/internal/debug" diff --git a/internal/ui/termstatus/background_unix_test.go b/internal/terminal/background_unix_test.go similarity index 94% rename from internal/ui/termstatus/background_unix_test.go rename to internal/terminal/background_unix_test.go index 301e780de..189e8c7d4 100644 --- a/internal/ui/termstatus/background_unix_test.go +++ b/internal/terminal/background_unix_test.go @@ -1,6 +1,6 @@ //go:build unix -package termstatus +package terminal import ( "os" diff --git a/internal/ui/termstatus/background_windows.go b/internal/terminal/background_windows.go similarity index 90% rename from internal/ui/termstatus/background_windows.go rename to internal/terminal/background_windows.go index b210509e7..9b370d074 100644 --- a/internal/ui/termstatus/background_windows.go +++ b/internal/terminal/background_windows.go @@ -1,4 +1,4 @@ -package termstatus +package terminal // IsProcessBackground reports whether the current process is running in the // background. Not implemented for this platform. diff --git a/internal/terminal/foreground_unix.go b/internal/terminal/foreground_unix.go index 340f80791..a3fb4d086 100644 --- a/internal/terminal/foreground_unix.go +++ b/internal/terminal/foreground_unix.go @@ -9,7 +9,6 @@ import ( "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/ui/termstatus" "golang.org/x/sys/unix" ) @@ -29,13 +28,13 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // only move child process to foreground if restic is in the foreground - prev, err := termstatus.Tcgetpgrp(int(tty.Fd())) + prev, err := Tcgetpgrp(int(tty.Fd())) if err != nil { _ = tty.Close() return nil, err } - self := termstatus.Getpgrp() + self := Getpgrp() if prev != self { debug.Log("restic is not controlling the tty; err = %v", err) if err := tty.Close(); err != nil { @@ -56,7 +55,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // move the command's process group into the foreground - err = termstatus.Tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) + err = Tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) if err != nil { _ = tty.Close() return nil, err @@ -67,7 +66,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { signal.Reset(unix.SIGTTOU) // reset the foreground process group - err = termstatus.Tcsetpgrp(int(tty.Fd()), prev) + err = Tcsetpgrp(int(tty.Fd()), prev) if err != nil { _ = tty.Close() return err diff --git a/internal/ui/termstatus/getpgrp_solaris.go b/internal/terminal/getpgrp_solaris.go similarity index 83% rename from internal/ui/termstatus/getpgrp_solaris.go rename to internal/terminal/getpgrp_solaris.go index 1df46d93c..e326079c3 100644 --- a/internal/ui/termstatus/getpgrp_solaris.go +++ b/internal/terminal/getpgrp_solaris.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/getpgrp_unix.go b/internal/terminal/getpgrp_unix.go similarity index 84% rename from internal/ui/termstatus/getpgrp_unix.go rename to internal/terminal/getpgrp_unix.go index f5a7b213f..759c28513 100644 --- a/internal/ui/termstatus/getpgrp_unix.go +++ b/internal/terminal/getpgrp_unix.go @@ -1,6 +1,6 @@ //go:build unix && !solaris -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/tcgetpgrp_linux.go b/internal/terminal/tcgetpgrp_linux.go similarity index 95% rename from internal/ui/termstatus/tcgetpgrp_linux.go rename to internal/terminal/tcgetpgrp_linux.go index 51a9fd2da..bc7e18342 100644 --- a/internal/ui/termstatus/tcgetpgrp_linux.go +++ b/internal/terminal/tcgetpgrp_linux.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/tcgetpgrp_unix.go b/internal/terminal/tcgetpgrp_unix.go similarity index 88% rename from internal/ui/termstatus/tcgetpgrp_unix.go rename to internal/terminal/tcgetpgrp_unix.go index 1359b5e46..6fd7de0a5 100644 --- a/internal/ui/termstatus/tcgetpgrp_unix.go +++ b/internal/terminal/tcgetpgrp_unix.go @@ -1,6 +1,6 @@ //go:build unix && !linux -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/tcsetpgrp_aix.go b/internal/terminal/tcsetpgrp_aix.go similarity index 93% rename from internal/ui/termstatus/tcsetpgrp_aix.go rename to internal/terminal/tcsetpgrp_aix.go index 8f2a5cab1..671b03a7a 100644 --- a/internal/ui/termstatus/tcsetpgrp_aix.go +++ b/internal/terminal/tcsetpgrp_aix.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/tcsetpgrp_unix.go b/internal/terminal/tcsetpgrp_unix.go similarity index 89% rename from internal/ui/termstatus/tcsetpgrp_unix.go rename to internal/terminal/tcsetpgrp_unix.go index 7762ee448..524f5d05a 100644 --- a/internal/ui/termstatus/tcsetpgrp_unix.go +++ b/internal/terminal/tcsetpgrp_unix.go @@ -1,6 +1,6 @@ //go:build unix && !aix -package termstatus +package terminal import "golang.org/x/sys/unix" diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index d3674ac09..8d6413949 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -12,6 +12,8 @@ import ( "golang.org/x/term" "golang.org/x/text/width" + + "github.com/restic/restic/internal/terminal" ) // Terminal is used to write messages and display status lines which can be @@ -101,14 +103,14 @@ func (t *Terminal) run(ctx context.Context) { for { select { case <-ctx.Done(): - if !IsProcessBackground(t.fd) { + if !terminal.IsProcessBackground(t.fd) { t.writeStatus([]string{}) } return case msg := <-t.msg: - if IsProcessBackground(t.fd) { + if terminal.IsProcessBackground(t.fd) { // ignore all messages, do nothing, we are in the background process group continue } @@ -140,7 +142,7 @@ func (t *Terminal) run(ctx context.Context) { } case stat := <-t.status: - if IsProcessBackground(t.fd) { + if terminal.IsProcessBackground(t.fd) { // ignore all messages, do nothing, we are in the background process group continue } From 0ab38faa2ec95058962f6b0ca48e67d0c0a29f1a Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 12:19:11 +0200 Subject: [PATCH 03/12] termstatus: track current status also in background Without this, restic could temporarily print an outdated status when moving back into the foreground. --- internal/ui/termstatus/status.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 8d6413949..1e105b65f 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -142,13 +142,13 @@ func (t *Terminal) run(ctx context.Context) { } case stat := <-t.status: + status = append(status[:0], stat.lines...) + if terminal.IsProcessBackground(t.fd) { // ignore all messages, do nothing, we are in the background process group continue } - status = status[:0] - status = append(status, stat.lines...) t.writeStatus(status) } } From 93ccc548c8f8d79c94f797938415b6b47f85409a Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 13:49:26 +0200 Subject: [PATCH 04/12] termstatus: move cursor handling to terminal package --- cmd/restic/global.go | 4 ++-- .../termstatus => terminal}/terminal_posix.go | 18 +++++++++--------- .../termstatus => terminal}/terminal_unix.go | 10 +++++----- .../terminal_windows.go | 10 +++++----- .../terminal_windows_test.go | 2 +- internal/ui/termstatus/status.go | 6 +++--- internal/ui/termstatus/status_test.go | 11 ++++++----- 7 files changed, 31 insertions(+), 30 deletions(-) rename internal/{ui/termstatus => terminal}/terminal_posix.go (54%) rename internal/{ui/termstatus => terminal}/terminal_unix.go (77%) rename internal/{ui/termstatus => terminal}/terminal_windows.go (94%) rename internal/{ui/termstatus => terminal}/terminal_windows_test.go (97%) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 6e58a0d73..56a3f5f18 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -33,8 +33,8 @@ import ( "github.com/restic/restic/internal/options" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/textfile" - "github.com/restic/restic/internal/ui/termstatus" "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" @@ -210,7 +210,7 @@ func stdoutIsTerminal() bool { } func stdoutCanUpdateStatus() bool { - return termstatus.CanUpdateStatus(os.Stdout.Fd()) + return terminal.CanUpdateStatus(os.Stdout.Fd()) } func stdoutTerminalWidth() int { diff --git a/internal/ui/termstatus/terminal_posix.go b/internal/terminal/terminal_posix.go similarity index 54% rename from internal/ui/termstatus/terminal_posix.go rename to internal/terminal/terminal_posix.go index ca5468f45..cd9820f10 100644 --- a/internal/ui/termstatus/terminal_posix.go +++ b/internal/terminal/terminal_posix.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import ( "bytes" @@ -8,16 +8,16 @@ import ( ) const ( - posixControlMoveCursorHome = "\r" - posixControlMoveCursorUp = "\x1b[1A" - posixControlClearLine = "\x1b[2K" + PosixControlMoveCursorHome = "\r" + PosixControlMoveCursorUp = "\x1b[1A" + PosixControlClearLine = "\x1b[2K" ) // posixClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func posixClearCurrentLine(wr io.Writer, _ uintptr) { +func PosixClearCurrentLine(wr io.Writer, _ uintptr) { // clear current line - _, err := wr.Write([]byte(posixControlMoveCursorHome + posixControlClearLine)) + _, err := wr.Write([]byte(PosixControlMoveCursorHome + PosixControlClearLine)) if err != nil { fmt.Fprintf(os.Stderr, "write failed: %v\n", err) return @@ -25,9 +25,9 @@ func posixClearCurrentLine(wr io.Writer, _ uintptr) { } // posixMoveCursorUp moves the cursor to the line n lines above the current one. -func posixMoveCursorUp(wr io.Writer, _ uintptr, n int) { - data := []byte(posixControlMoveCursorHome) - data = append(data, bytes.Repeat([]byte(posixControlMoveCursorUp), n)...) +func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) { + data := []byte(PosixControlMoveCursorHome) + data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...) _, err := wr.Write(data) if err != nil { fmt.Fprintf(os.Stderr, "write failed: %v\n", err) diff --git a/internal/ui/termstatus/terminal_unix.go b/internal/terminal/terminal_unix.go similarity index 77% rename from internal/ui/termstatus/terminal_unix.go rename to internal/terminal/terminal_unix.go index e112be233..8893d21e9 100644 --- a/internal/ui/termstatus/terminal_unix.go +++ b/internal/terminal/terminal_unix.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package termstatus +package terminal import ( "io" @@ -12,13 +12,13 @@ import ( // clearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func clearCurrentLine(_ uintptr) func(io.Writer, uintptr) { - return posixClearCurrentLine +func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) { + return PosixClearCurrentLine } // moveCursorUp moves the cursor to the line n lines above the current one. -func moveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { - return posixMoveCursorUp +func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { + return PosixMoveCursorUp } // CanUpdateStatus returns true if status lines can be printed, the process diff --git a/internal/ui/termstatus/terminal_windows.go b/internal/terminal/terminal_windows.go similarity index 94% rename from internal/ui/termstatus/terminal_windows.go rename to internal/terminal/terminal_windows.go index 3603f16a3..d68d0197e 100644 --- a/internal/ui/termstatus/terminal_windows.go +++ b/internal/terminal/terminal_windows.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package termstatus +package terminal import ( "io" @@ -15,25 +15,25 @@ import ( // clearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. -func clearCurrentLine(fd uintptr) func(io.Writer, uintptr) { +func ClearCurrentLine(fd uintptr) func(io.Writer, uintptr) { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsClearCurrentLine } // assume we're running in mintty/cygwin - return posixClearCurrentLine + return PosixClearCurrentLine } // moveCursorUp moves the cursor to the line n lines above the current one. -func moveCursorUp(fd uintptr) func(io.Writer, uintptr, int) { +func MoveCursorUp(fd uintptr) func(io.Writer, uintptr, int) { // easy case, the terminal is cmd or psh, without redirection if isWindowsTerminal(fd) { return windowsMoveCursorUp } // assume we're running in mintty/cygwin - return posixMoveCursorUp + return PosixMoveCursorUp } var kernel32 = syscall.NewLazyDLL("kernel32.dll") diff --git a/internal/ui/termstatus/terminal_windows_test.go b/internal/terminal/terminal_windows_test.go similarity index 97% rename from internal/ui/termstatus/terminal_windows_test.go rename to internal/terminal/terminal_windows_test.go index e6eb39dd5..a6b853bae 100644 --- a/internal/ui/termstatus/terminal_windows_test.go +++ b/internal/terminal/terminal_windows_test.go @@ -1,4 +1,4 @@ -package termstatus +package terminal import ( "syscall" diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 1e105b65f..e4d9f19bf 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -69,12 +69,12 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } - if d, ok := wr.(fder); ok && CanUpdateStatus(d.Fd()) { + if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.Fd()) { // only use the fancy status code when we're running on a real terminal. t.canUpdateStatus = true t.fd = d.Fd() - t.clearCurrentLine = clearCurrentLine(t.fd) - t.moveCursorUp = moveCursorUp(t.fd) + t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) + t.moveCursorUp = terminal.MoveCursorUp(t.fd) } return t diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index b4e9be5b0..8e5414686 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -8,6 +8,7 @@ import ( "strconv" "testing" + "github.com/restic/restic/internal/terminal" rtest "github.com/restic/restic/internal/test" ) @@ -17,16 +18,16 @@ func TestSetStatus(t *testing.T) { term.canUpdateStatus = true term.fd = ^uintptr(0) - term.clearCurrentLine = posixClearCurrentLine - term.moveCursorUp = posixMoveCursorUp + term.clearCurrentLine = terminal.PosixClearCurrentLine + term.moveCursorUp = terminal.PosixMoveCursorUp ctx, cancel := context.WithCancel(context.Background()) go term.Run(ctx) const ( - cl = posixControlClearLine - home = posixControlMoveCursorHome - up = posixControlMoveCursorUp + cl = terminal.PosixControlClearLine + home = terminal.PosixControlMoveCursorHome + up = terminal.PosixControlMoveCursorUp ) term.SetStatus([]string{"first"}) From 0b0dd07f150035138305a561246e25f4923fdda4 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 14:19:29 +0200 Subject: [PATCH 05/12] consolidate checks whether stdin/stdout is terminal --- cmd/restic/cmd_dump.go | 3 ++- cmd/restic/cmd_generate.go | 3 ++- cmd/restic/cmd_restore.go | 3 ++- cmd/restic/global.go | 40 +++++++---------------------- cmd/restic/progress.go | 11 ++++---- internal/terminal/stdio.go | 29 +++++++++++++++++++++ internal/terminal/terminal_posix.go | 4 +-- internal/terminal/terminal_unix.go | 4 +-- 8 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 internal/terminal/stdio.go diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 978b64616..921309101 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -11,6 +11,7 @@ import ( "github.com/restic/restic/internal/dump" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/terminal" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -199,7 +200,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } func checkStdoutArchive() error { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { return fmt.Errorf("stdout is the terminal, please redirect output") } return nil diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 5ced5fe01..3c5ddffd5 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -6,6 +6,7 @@ import ( "time" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/terminal" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/pflag" @@ -71,7 +72,7 @@ func writeManpages(root *cobra.Command, dir string) error { } func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("writing %s completion file to %v\n", shell, filename) } var outWriter io.Writer diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index bb32333c9..f0c84c628 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -10,6 +10,7 @@ import ( "github.com/restic/restic/internal/filter" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restorer" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" restoreui "github.com/restic/restic/internal/ui/restore" "github.com/restic/restic/internal/ui/termstatus" @@ -260,7 +261,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, } var count int t0 := time.Now() - bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && stdoutIsTerminal(), 0, "files verified", term) + bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && terminal.StdoutIsTerminal(), 0, "files verified", term) count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar) if err != nil { return err diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 56a3f5f18..e603673df 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -199,28 +199,6 @@ func collectBackends() *location.Registry { return backends } -func stdinIsTerminal() bool { - return term.IsTerminal(int(os.Stdin.Fd())) -} - -func stdoutIsTerminal() bool { - // mintty on windows can use pipes which behave like a posix terminal, - // but which are not a terminal handle - return term.IsTerminal(int(os.Stdout.Fd())) || stdoutCanUpdateStatus() -} - -func stdoutCanUpdateStatus() bool { - return terminal.CanUpdateStatus(os.Stdout.Fd()) -} - -func stdoutTerminalWidth() int { - w, _, err := term.GetSize(int(os.Stdout.Fd())) - if err != nil { - return 0 - } - return w -} - // ClearLine creates a platform dependent string to clear the current // line, so it can be overwritten. // @@ -232,7 +210,7 @@ func clearLine(w int) string { // ANSI sequences are not supported on Windows cmd shell. if w <= 0 { - if w = stdoutTerminalWidth(); w <= 0 { + if w = terminal.StdoutTerminalWidth(); w <= 0 { return "" } } @@ -399,11 +377,11 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin err error ) - if stdinIsTerminal() { + if terminal.StdinIsTerminal() { password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt) } else { password, err = readPassword(os.Stdin) - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("reading repository password from stdin\n") } } @@ -427,7 +405,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt if err != nil { return "", err } - if stdinIsTerminal() { + if terminal.StdinIsTerminal() { pw2, err := ReadPassword(ctx, gopts, prompt2) if err != nil { return "", err @@ -490,7 +468,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi } passwordTriesLeft := 1 - if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { + if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword { passwordTriesLeft = 3 } @@ -520,7 +498,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi return nil, errors.Fatalf("%s", err) } - if stdoutIsTerminal() && !opts.JSON { + if terminal.StdoutIsTerminal() && !opts.JSON { id := s.Config().ID if len(id) > 8 { id = id[:8] @@ -544,7 +522,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi return s, nil } - if c.Created && !opts.JSON && stdoutIsTerminal() { + if c.Created && !opts.JSON && terminal.StdoutIsTerminal() { Verbosef("created new cache in %v\n", c.Base) } @@ -563,7 +541,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi // cleanup old cache dirs if instructed to do so if opts.CleanupCache { - if stdoutIsTerminal() && !opts.JSON { + if terminal.StdoutIsTerminal() && !opts.JSON { Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base) } for _, item := range oldCacheDirs { @@ -574,7 +552,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi } } } else { - if stdoutIsTerminal() { + if terminal.StdoutIsTerminal() { Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n", len(oldCacheDirs), c.Base) } diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 6f1626772..5af108944 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/termstatus" @@ -23,7 +24,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration { fps = 60 } interval = time.Duration(float64(time.Second) / fps) - } else if !json && !stdoutCanUpdateStatus() || !show { + } else if !json && !terminal.StdoutCanUpdateStatus() || !show { interval = 0 } return interval @@ -68,9 +69,9 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter func printProgress(status string, final bool) { - canUpdateStatus := stdoutCanUpdateStatus() + canUpdateStatus := terminal.StdoutCanUpdateStatus() - w := stdoutTerminalWidth() + w := terminal.StdoutTerminalWidth() if w > 0 { if w < 3 { status = termstatus.Truncate(status, w) @@ -103,11 +104,11 @@ func printProgress(status string, final bool) { } func newIndexProgress(quiet bool, json bool) *progress.Counter { - return newProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded") + return newProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded") } func newIndexTerminalProgress(quiet bool, json bool, term *termstatus.Terminal) *progress.Counter { - return newTerminalProgressMax(!quiet && !json && stdoutIsTerminal(), 0, "index files loaded", term) + return newTerminalProgressMax(!quiet && !json && terminal.StdoutIsTerminal(), 0, "index files loaded", term) } type terminalProgressPrinter struct { diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go new file mode 100644 index 000000000..4b11731c9 --- /dev/null +++ b/internal/terminal/stdio.go @@ -0,0 +1,29 @@ +package terminal + +import ( + "os" + + "golang.org/x/term" +) + +func StdinIsTerminal() bool { + return term.IsTerminal(int(os.Stdin.Fd())) +} + +func StdoutIsTerminal() bool { + // mintty on windows can use pipes which behave like a posix terminal, + // but which are not a terminal handle + return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus() +} + +func StdoutCanUpdateStatus() bool { + return CanUpdateStatus(os.Stdout.Fd()) +} + +func StdoutTerminalWidth() int { + w, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil { + return 0 + } + return w +} diff --git a/internal/terminal/terminal_posix.go b/internal/terminal/terminal_posix.go index cd9820f10..b279b0728 100644 --- a/internal/terminal/terminal_posix.go +++ b/internal/terminal/terminal_posix.go @@ -13,7 +13,7 @@ const ( PosixControlClearLine = "\x1b[2K" ) -// posixClearCurrentLine removes all characters from the current line and resets the +// PosixClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. func PosixClearCurrentLine(wr io.Writer, _ uintptr) { // clear current line @@ -24,7 +24,7 @@ func PosixClearCurrentLine(wr io.Writer, _ uintptr) { } } -// posixMoveCursorUp moves the cursor to the line n lines above the current one. +// PosixMoveCursorUp moves the cursor to the line n lines above the current one. func PosixMoveCursorUp(wr io.Writer, _ uintptr, n int) { data := []byte(PosixControlMoveCursorHome) data = append(data, bytes.Repeat([]byte(PosixControlMoveCursorUp), n)...) diff --git a/internal/terminal/terminal_unix.go b/internal/terminal/terminal_unix.go index 8893d21e9..65e353d9f 100644 --- a/internal/terminal/terminal_unix.go +++ b/internal/terminal/terminal_unix.go @@ -10,13 +10,13 @@ import ( "golang.org/x/term" ) -// clearCurrentLine removes all characters from the current line and resets the +// ClearCurrentLine removes all characters from the current line and resets the // cursor position to the first column. func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) { return PosixClearCurrentLine } -// moveCursorUp moves the cursor to the line n lines above the current one. +// MoveCursorUp moves the cursor to the line n lines above the current one. func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) { return PosixMoveCursorUp } From 43b5166de8267c4f8631b96917be230e41e75cf7 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 14:26:42 +0200 Subject: [PATCH 06/12] terminal: cleanup determining width --- cmd/restic/global.go | 2 +- cmd/restic/progress.go | 2 +- internal/terminal/stdio.go | 8 ++++++-- internal/ui/termstatus/status.go | 6 ++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index e603673df..18dd97d68 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -210,7 +210,7 @@ func clearLine(w int) string { // ANSI sequences are not supported on Windows cmd shell. if w <= 0 { - if w = terminal.StdoutTerminalWidth(); w <= 0 { + if w = terminal.StdoutWidth(); w <= 0 { return "" } } diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 5af108944..b0d80a896 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -71,7 +71,7 @@ func printProgress(status string, final bool) { canUpdateStatus := terminal.StdoutCanUpdateStatus() - w := terminal.StdoutTerminalWidth() + w := terminal.StdoutWidth() if w > 0 { if w < 3 { status = termstatus.Truncate(status, w) diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go index 4b11731c9..70e465ccb 100644 --- a/internal/terminal/stdio.go +++ b/internal/terminal/stdio.go @@ -20,8 +20,12 @@ func StdoutCanUpdateStatus() bool { return CanUpdateStatus(os.Stdout.Fd()) } -func StdoutTerminalWidth() int { - w, _, err := term.GetSize(int(os.Stdout.Fd())) +func StdoutWidth() int { + return Width(os.Stdout.Fd()) +} + +func Width(fd uintptr) int { + w, _, err := term.GetSize(int(fd)) if err != nil { return 0 } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index e4d9f19bf..71be8ec4e 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -10,7 +10,6 @@ import ( "strings" "unicode" - "golang.org/x/term" "golang.org/x/text/width" "github.com/restic/restic/internal/terminal" @@ -321,9 +320,8 @@ func (t *Terminal) SetStatus(lines []string) { // only truncate interactive status output var width int if t.canUpdateStatus { - var err error - width, _, err = term.GetSize(int(t.fd)) - if err != nil || width <= 0 { + width = terminal.Width(t.fd) + if width <= 0 { // use 80 columns by default width = 80 } From d10bd1d3216b56ce3cf4c9cbc8c0edb3f9b2b64b Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 14:30:04 +0200 Subject: [PATCH 07/12] terminal: move reading password from terminal here --- cmd/restic/global.go | 50 +------------------------------ internal/terminal/password.go | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 internal/terminal/password.go diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 18dd97d68..e5aa4e2b2 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -38,8 +38,6 @@ import ( "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" - - "golang.org/x/term" ) // ErrNoRepository is used to report if opening a repository failed due @@ -311,52 +309,6 @@ func readPassword(in io.Reader) (password string, err error) { return sc.Text(), errors.WithStack(sc.Err()) } -// readPasswordTerminal reads the password from the given reader which must be a -// tty. Prompt is printed on the writer out before attempting to read the -// password. If the context is canceled, the function leaks the password reading -// goroutine. -func readPasswordTerminal(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { - fd := int(out.Fd()) - state, err := term.GetState(fd) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) - return "", err - } - - done := make(chan struct{}) - var buf []byte - - go func() { - defer close(done) - _, err = fmt.Fprint(out, prompt) - if err != nil { - return - } - buf, err = term.ReadPassword(int(in.Fd())) - if err != nil { - return - } - _, err = fmt.Fprintln(out) - }() - - select { - case <-ctx.Done(): - err := term.Restore(fd, state) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) - } - return "", ctx.Err() - case <-done: - // clean shutdown, nothing to do - } - - if err != nil { - return "", errors.Wrap(err, "ReadPassword") - } - - return string(buf), nil -} - // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. @@ -378,7 +330,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin ) if terminal.StdinIsTerminal() { - password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt) + password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { password, err = readPassword(os.Stdin) if terminal.StdoutIsTerminal() { diff --git a/internal/terminal/password.go b/internal/terminal/password.go new file mode 100644 index 000000000..6d1b6c912 --- /dev/null +++ b/internal/terminal/password.go @@ -0,0 +1,55 @@ +package terminal + +import ( + "context" + "fmt" + "os" + + "golang.org/x/term" +) + +// ReadPassword reads the password from the given reader which must be a +// tty. Prompt is printed on the writer out before attempting to read the +// password. If the context is canceled, the function leaks the password reading +// goroutine. +func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { + fd := int(out.Fd()) + state, err := term.GetState(fd) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) + return "", err + } + + done := make(chan struct{}) + var buf []byte + + go func() { + defer close(done) + _, err = fmt.Fprint(out, prompt) + if err != nil { + return + } + buf, err = term.ReadPassword(int(in.Fd())) + if err != nil { + return + } + _, err = fmt.Fprintln(out) + }() + + select { + case <-ctx.Done(): + err := term.Restore(fd, state) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) + } + return "", ctx.Err() + case <-done: + // clean shutdown, nothing to do + } + + if err != nil { + return "", fmt.Errorf("ReadPassword: %w", err) + } + + return string(buf), nil +} From 529baf50f872d14f70e3d272951e74583da6386e Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 14:46:34 +0200 Subject: [PATCH 08/12] simplify message printing when restic receives signal --- cmd/restic/cleanup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/restic/cleanup.go b/cmd/restic/cleanup.go index 90ea93b92..45dc43b5c 100644 --- a/cmd/restic/cleanup.go +++ b/cmd/restic/cleanup.go @@ -23,7 +23,7 @@ func createGlobalContext() context.Context { func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) { s := <-c debug.Log("signal %v received, cleaning up", s) - Warnf("%ssignal %v received, cleaning up\n", clearLine(0), s) + Warnf("\rsignal %v received, cleaning up \n", s) if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" { _, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n") From e7890d7b811d2c9378a81f8e4714b1759d023892 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sun, 7 Sep 2025 14:47:31 +0200 Subject: [PATCH 09/12] use standard line clearing in printProgress --- cmd/restic/global.go | 19 ------------------- cmd/restic/progress.go | 15 +++++++-------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index e5aa4e2b2..df9461272 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strconv" "strings" "time" @@ -197,24 +196,6 @@ func collectBackends() *location.Registry { return backends } -// ClearLine creates a platform dependent string to clear the current -// line, so it can be overwritten. -// -// w should be the terminal width, or 0 to let clearLine figure it out. -func clearLine(w int) string { - if runtime.GOOS != "windows" { - return "\x1b[2K" - } - - // ANSI sequences are not supported on Windows cmd shell. - if w <= 0 { - if w = terminal.StdoutWidth(); w <= 0 { - return "" - } - } - return strings.Repeat(" ", w-1) + "\r" -} - // Printf writes the message to the configured stdout stream. func Printf(format string, args ...interface{}) { _, err := fmt.Fprintf(globalOptions.stdout, format, args...) diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index b0d80a896..b2abd61d2 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -68,7 +68,6 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter } func printProgress(status string, final bool) { - canUpdateStatus := terminal.StdoutCanUpdateStatus() w := terminal.StdoutWidth() @@ -83,12 +82,7 @@ func printProgress(status string, final bool) { } } - var carriageControl, cl string - - if canUpdateStatus { - cl = clearLine(w) - } - + var carriageControl string if !(strings.HasSuffix(status, "\r") || strings.HasSuffix(status, "\n")) { if canUpdateStatus { carriageControl = "\r" @@ -97,7 +91,12 @@ func printProgress(status string, final bool) { } } - _, _ = os.Stdout.Write([]byte(cl + status + carriageControl)) + if canUpdateStatus { + clearCurrentLine := terminal.ClearCurrentLine(os.Stdout.Fd()) + clearCurrentLine(os.Stdout, os.Stdout.Fd()) + } + + _, _ = os.Stdout.Write([]byte(status + carriageControl)) if final { _, _ = os.Stdout.Write([]byte("\n")) } From b71b77fa77daccbd7f0cf73ffc4fcd70282877f7 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Mon, 8 Sep 2025 10:55:58 +0200 Subject: [PATCH 10/12] terminal: unexport tcgetpgrp, tcsetpgrp and getpgrp --- internal/terminal/background_unix.go | 4 ++-- internal/terminal/foreground_unix.go | 8 ++++---- internal/terminal/getpgrp_solaris.go | 2 +- internal/terminal/getpgrp_unix.go | 2 +- internal/terminal/tcgetpgrp_linux.go | 2 +- internal/terminal/tcgetpgrp_unix.go | 2 +- internal/terminal/tcsetpgrp_aix.go | 2 +- internal/terminal/tcsetpgrp_unix.go | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/terminal/background_unix.go b/internal/terminal/background_unix.go index 316466ce9..9564c7cde 100644 --- a/internal/terminal/background_unix.go +++ b/internal/terminal/background_unix.go @@ -16,9 +16,9 @@ func IsProcessBackground(fd uintptr) bool { } func isProcessBackground(fd int) (bg bool, err error) { - pgid, err := Tcgetpgrp(fd) + pgid, err := tcgetpgrp(fd) if err != nil { return false, err } - return pgid != Getpgrp(), nil + return pgid != getpgrp(), nil } diff --git a/internal/terminal/foreground_unix.go b/internal/terminal/foreground_unix.go index a3fb4d086..a4f15eccf 100644 --- a/internal/terminal/foreground_unix.go +++ b/internal/terminal/foreground_unix.go @@ -28,13 +28,13 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // only move child process to foreground if restic is in the foreground - prev, err := Tcgetpgrp(int(tty.Fd())) + prev, err := tcgetpgrp(int(tty.Fd())) if err != nil { _ = tty.Close() return nil, err } - self := Getpgrp() + self := getpgrp() if prev != self { debug.Log("restic is not controlling the tty; err = %v", err) if err := tty.Close(); err != nil { @@ -55,7 +55,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { } // move the command's process group into the foreground - err = Tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) + err = tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) if err != nil { _ = tty.Close() return nil, err @@ -66,7 +66,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) { signal.Reset(unix.SIGTTOU) // reset the foreground process group - err = Tcsetpgrp(int(tty.Fd()), prev) + err = tcsetpgrp(int(tty.Fd()), prev) if err != nil { _ = tty.Close() return err diff --git a/internal/terminal/getpgrp_solaris.go b/internal/terminal/getpgrp_solaris.go index e326079c3..71e02e7bf 100644 --- a/internal/terminal/getpgrp_solaris.go +++ b/internal/terminal/getpgrp_solaris.go @@ -2,7 +2,7 @@ package terminal import "golang.org/x/sys/unix" -func Getpgrp() int { +func getpgrp() int { pid, _ := unix.Getpgrp() return pid } diff --git a/internal/terminal/getpgrp_unix.go b/internal/terminal/getpgrp_unix.go index 759c28513..c9fe1470f 100644 --- a/internal/terminal/getpgrp_unix.go +++ b/internal/terminal/getpgrp_unix.go @@ -4,4 +4,4 @@ package terminal import "golang.org/x/sys/unix" -func Getpgrp() int { return unix.Getpgrp() } +func getpgrp() int { return unix.Getpgrp() } diff --git a/internal/terminal/tcgetpgrp_linux.go b/internal/terminal/tcgetpgrp_linux.go index bc7e18342..b51796ace 100644 --- a/internal/terminal/tcgetpgrp_linux.go +++ b/internal/terminal/tcgetpgrp_linux.go @@ -2,7 +2,7 @@ package terminal import "golang.org/x/sys/unix" -func Tcgetpgrp(ttyfd int) (int, error) { +func tcgetpgrp(ttyfd int) (int, error) { // We need to use IoctlGetUint32 here, because pid_t is 32-bit even on // 64-bit Linux. IoctlGetInt doesn't work on big-endian platforms: // https://github.com/golang/go/issues/45585 diff --git a/internal/terminal/tcgetpgrp_unix.go b/internal/terminal/tcgetpgrp_unix.go index 6fd7de0a5..c24bc513f 100644 --- a/internal/terminal/tcgetpgrp_unix.go +++ b/internal/terminal/tcgetpgrp_unix.go @@ -4,6 +4,6 @@ package terminal import "golang.org/x/sys/unix" -func Tcgetpgrp(ttyfd int) (int, error) { +func tcgetpgrp(ttyfd int) (int, error) { return unix.IoctlGetInt(ttyfd, unix.TIOCGPGRP) } diff --git a/internal/terminal/tcsetpgrp_aix.go b/internal/terminal/tcsetpgrp_aix.go index 671b03a7a..f8e8b2972 100644 --- a/internal/terminal/tcsetpgrp_aix.go +++ b/internal/terminal/tcsetpgrp_aix.go @@ -2,7 +2,7 @@ package terminal import "golang.org/x/sys/unix" -func Tcsetpgrp(fd int, pid int) error { +func tcsetpgrp(fd int, pid int) error { // The second argument to IoctlSetPointerInt has type int on AIX, // but the constant overflows 64-bit int, hence the two-step cast. req := uint(unix.TIOCSPGRP) diff --git a/internal/terminal/tcsetpgrp_unix.go b/internal/terminal/tcsetpgrp_unix.go index 524f5d05a..4a2c1a12e 100644 --- a/internal/terminal/tcsetpgrp_unix.go +++ b/internal/terminal/tcsetpgrp_unix.go @@ -4,6 +4,6 @@ package terminal import "golang.org/x/sys/unix" -func Tcsetpgrp(fd int, pid int) error { +func tcsetpgrp(fd int, pid int) error { return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pid) } From 424316e0168b6b67f5feae7ba93bdbe9def1f800 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Mon, 8 Sep 2025 10:57:19 +0200 Subject: [PATCH 11/12] extend background handling changelog --- changelog/unreleased/issue-5354 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog/unreleased/issue-5354 b/changelog/unreleased/issue-5354 index 1fbda9077..d69d9fd2b 100644 --- a/changelog/unreleased/issue-5354 +++ b/changelog/unreleased/issue-5354 @@ -10,3 +10,5 @@ This has been fixed. https://github.com/restic/restic/issues/5354 https://github.com/restic/restic/pull/5358 +https://github.com/restic/restic/pull/5493 +https://github.com/restic/restic/pull/5494 From d757e399928926b16fc176ed65ccd18c02bdaadd Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Sat, 13 Sep 2025 22:22:53 +0200 Subject: [PATCH 12/12] make linter happy --- internal/terminal/terminal_posix.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/terminal/terminal_posix.go b/internal/terminal/terminal_posix.go index b279b0728..e8a5abc59 100644 --- a/internal/terminal/terminal_posix.go +++ b/internal/terminal/terminal_posix.go @@ -8,9 +8,12 @@ import ( ) const ( + // PosixControlMoveCursorHome moves cursor to the first column PosixControlMoveCursorHome = "\r" - PosixControlMoveCursorUp = "\x1b[1A" - PosixControlClearLine = "\x1b[2K" + // PosixControlMoveCursorUp moves cursor up one line + PosixControlMoveCursorUp = "\x1b[1A" + // PosixControlClearLine clears the current line + PosixControlClearLine = "\x1b[2K" ) // PosixClearCurrentLine removes all characters from the current line and resets the