mirror of
https://github.com/restic/restic.git
synced 2025-12-11 18:47:50 +00:00
Merge pull request #5494 from MichaelEischer/fix-background-handling
Refactor terminal background handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//go:build unix
|
||||
|
||||
package termstatus
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package termstatus
|
||||
package terminal
|
||||
|
||||
// IsProcessBackground reports whether the current process is running in the
|
||||
// background. Not implemented for this platform.
|
||||
@@ -1,4 +1,4 @@
|
||||
package util
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package util
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
@@ -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
|
||||
}
|
||||
7
internal/terminal/getpgrp_unix.go
Normal file
7
internal/terminal/getpgrp_unix.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build unix && !solaris
|
||||
|
||||
package terminal
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func getpgrp() int { return unix.Getpgrp() }
|
||||
55
internal/terminal/password.go
Normal file
55
internal/terminal/password.go
Normal 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
|
||||
}
|
||||
33
internal/terminal/stdio.go
Normal file
33
internal/terminal/stdio.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
39
internal/terminal/terminal_posix.go
Normal file
39
internal/terminal/terminal_posix.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -1,4 +1,4 @@
|
||||
package termstatus
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build unix && !solaris
|
||||
|
||||
package termstatus
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func Getpgrp() int { return unix.Getpgrp() }
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user