Merge pull request #5494 from MichaelEischer/fix-background-handling

Refactor terminal background handling
This commit is contained in:
Michael Eischer
2025-09-13 22:48:11 +02:00
committed by GitHub
32 changed files with 226 additions and 218 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
@@ -33,13 +32,11 @@ 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"
"golang.org/x/term"
)
// ErrNoRepository is used to report if opening a repository failed due
@@ -199,46 +196,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 termstatus.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.
//
// 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 = stdoutTerminalWidth(); 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...)
@@ -333,52 +290,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.
@@ -399,11 +310,11 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin
err error
)
if stdinIsTerminal() {
password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt)
if terminal.StdinIsTerminal() {
password, err = terminal.ReadPassword(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 +338,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 +401,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 +431,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 +455,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 +474,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 +485,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)
}

View File

@@ -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
@@ -67,10 +68,9 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter
}
func printProgress(status string, final bool) {
canUpdateStatus := terminal.StdoutCanUpdateStatus()
canUpdateStatus := stdoutCanUpdateStatus()
w := stdoutTerminalWidth()
w := terminal.StdoutWidth()
if w > 0 {
if w < 3 {
status = termstatus.Truncate(status, w)
@@ -82,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"
@@ -96,18 +91,23 @@ 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"))
}
}
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 {

View File

@@ -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()

View File

@@ -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=./<command> to override", cmd.Path)

View File

@@ -1,6 +1,6 @@
//go:build unix
package termstatus
package terminal
import "github.com/restic/restic/internal/debug"
@@ -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
}

View File

@@ -1,6 +1,6 @@
//go:build unix
package termstatus
package terminal
import (
"os"

View File

@@ -1,4 +1,4 @@
package termstatus
package terminal
// IsProcessBackground reports whether the current process is running in the
// background. Not implemented for this platform.

View File

@@ -1,4 +1,4 @@
package util
package terminal
import (
"os"

View File

@@ -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())

View File

@@ -1,6 +1,6 @@
//go:build unix
package util
package terminal
import (
"os"
@@ -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

View File

@@ -1,4 +1,4 @@
package util
package terminal
import (
"os/exec"

View File

@@ -1,8 +1,8 @@
package termstatus
package terminal
import "golang.org/x/sys/unix"
func Getpgrp() int {
func getpgrp() int {
pid, _ := unix.Getpgrp()
return pid
}

View File

@@ -0,0 +1,7 @@
//go:build unix && !solaris
package terminal
import "golang.org/x/sys/unix"
func getpgrp() int { return unix.Getpgrp() }

View File

@@ -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
}

View File

@@ -0,0 +1,33 @@
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 StdoutWidth() int {
return Width(os.Stdout.Fd())
}
func Width(fd uintptr) int {
w, _, err := term.GetSize(int(fd))
if err != nil {
return 0
}
return w
}

View File

@@ -1,8 +1,8 @@
package termstatus
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

View File

@@ -1,9 +1,9 @@
//go:build unix && !linux
package termstatus
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)
}

View File

@@ -1,8 +1,8 @@
package termstatus
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)

View File

@@ -1,9 +1,9 @@
//go:build unix && !aix
package termstatus
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)
}

View File

@@ -0,0 +1,39 @@
package terminal
import (
"bytes"
"fmt"
"io"
"os"
)
const (
// PosixControlMoveCursorHome moves cursor to the first column
PosixControlMoveCursorHome = "\r"
// 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
// cursor position to the first column.
func PosixClearCurrentLine(wr io.Writer, _ uintptr) {
// clear current line
_, err := wr.Write([]byte(PosixControlMoveCursorHome + PosixControlClearLine))
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}
// 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)...)
_, err := wr.Write(data)
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}

View File

@@ -1,7 +1,7 @@
//go:build !windows
// +build !windows
package termstatus
package terminal
import (
"io"
@@ -10,15 +10,15 @@ 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
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
// MoveCursorUp moves the cursor to the line n lines above the current one.
func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) {
return PosixMoveCursorUp
}
// CanUpdateStatus returns true if status lines can be printed, the process

View File

@@ -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")

View File

@@ -1,4 +1,4 @@
package termstatus
package terminal
import (
"syscall"

View File

@@ -1,7 +0,0 @@
//go:build unix && !solaris
package termstatus
import "golang.org/x/sys/unix"
func Getpgrp() int { return unix.Getpgrp() }

View File

@@ -10,8 +10,9 @@ import (
"strings"
"unicode"
"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
@@ -67,12 +68,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
@@ -101,14 +102,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,13 +141,13 @@ func (t *Terminal) run(ctx context.Context) {
}
case stat := <-t.status:
if IsProcessBackground(t.fd) {
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)
}
}
@@ -319,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
}

View File

@@ -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"})

View File

@@ -1,36 +0,0 @@
package termstatus
import (
"bytes"
"fmt"
"io"
"os"
)
const (
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) {
// clear current line
_, err := wr.Write([]byte(posixControlMoveCursorHome + posixControlClearLine))
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}
// 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)...)
_, err := wr.Write(data)
if err != nil {
fmt.Fprintf(os.Stderr, "write failed: %v\n", err)
return
}
}