mirror of
https://github.com/restic/restic.git
synced 2025-12-14 02:57:04 +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/issues/5354
|
||||||
https://github.com/restic/restic/pull/5358
|
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) {
|
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) {
|
||||||
s := <-c
|
s := <-c
|
||||||
debug.Log("signal %v received, cleaning up", s)
|
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 != "" {
|
if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" {
|
||||||
_, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n")
|
_, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/dump"
|
"github.com/restic/restic/internal/dump"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -199,7 +200,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkStdoutArchive() error {
|
func checkStdoutArchive() error {
|
||||||
if stdoutIsTerminal() {
|
if terminal.StdoutIsTerminal() {
|
||||||
return fmt.Errorf("stdout is the terminal, please redirect output")
|
return fmt.Errorf("stdout is the terminal, please redirect output")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
"github.com/spf13/pflag"
|
"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) {
|
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)
|
Verbosef("writing %s completion file to %v\n", shell, filename)
|
||||||
}
|
}
|
||||||
var outWriter io.Writer
|
var outWriter io.Writer
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/filter"
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/restorer"
|
"github.com/restic/restic/internal/restorer"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
restoreui "github.com/restic/restic/internal/ui/restore"
|
restoreui "github.com/restic/restic/internal/ui/restore"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
@@ -260,7 +261,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
}
|
}
|
||||||
var count int
|
var count int
|
||||||
t0 := time.Now()
|
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)
|
count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -33,13 +32,11 @@ import (
|
|||||||
"github.com/restic/restic/internal/options"
|
"github.com/restic/restic/internal/options"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoRepository is used to report if opening a repository failed due
|
// ErrNoRepository is used to report if opening a repository failed due
|
||||||
@@ -199,46 +196,6 @@ func collectBackends() *location.Registry {
|
|||||||
return backends
|
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.
|
// Printf writes the message to the configured stdout stream.
|
||||||
func Printf(format string, args ...interface{}) {
|
func Printf(format string, args ...interface{}) {
|
||||||
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
|
_, 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())
|
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
|
// ReadPassword reads the password from a password file, the environment
|
||||||
// variable RESTIC_PASSWORD or prompts the user. If the context is canceled,
|
// variable RESTIC_PASSWORD or prompts the user. If the context is canceled,
|
||||||
// the function leaks the password reading goroutine.
|
// the function leaks the password reading goroutine.
|
||||||
@@ -399,11 +310,11 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if stdinIsTerminal() {
|
if terminal.StdinIsTerminal() {
|
||||||
password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt)
|
password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt)
|
||||||
} else {
|
} else {
|
||||||
password, err = readPassword(os.Stdin)
|
password, err = readPassword(os.Stdin)
|
||||||
if stdoutIsTerminal() {
|
if terminal.StdoutIsTerminal() {
|
||||||
Verbosef("reading repository password from stdin\n")
|
Verbosef("reading repository password from stdin\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,7 +338,7 @@ func ReadPasswordTwice(ctx context.Context, gopts GlobalOptions, prompt1, prompt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if stdinIsTerminal() {
|
if terminal.StdinIsTerminal() {
|
||||||
pw2, err := ReadPassword(ctx, gopts, prompt2)
|
pw2, err := ReadPassword(ctx, gopts, prompt2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -490,7 +401,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordTriesLeft := 1
|
passwordTriesLeft := 1
|
||||||
if stdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
|
if terminal.StdinIsTerminal() && opts.password == "" && !opts.InsecureNoPassword {
|
||||||
passwordTriesLeft = 3
|
passwordTriesLeft = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +431,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
|
|||||||
return nil, errors.Fatalf("%s", err)
|
return nil, errors.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stdoutIsTerminal() && !opts.JSON {
|
if terminal.StdoutIsTerminal() && !opts.JSON {
|
||||||
id := s.Config().ID
|
id := s.Config().ID
|
||||||
if len(id) > 8 {
|
if len(id) > 8 {
|
||||||
id = id[:8]
|
id = id[:8]
|
||||||
@@ -544,7 +455,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
|
|||||||
return s, nil
|
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)
|
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
|
// cleanup old cache dirs if instructed to do so
|
||||||
if opts.CleanupCache {
|
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)
|
Verbosef("removing %d old cache dirs from %v\n", len(oldCacheDirs), c.Base)
|
||||||
}
|
}
|
||||||
for _, item := range oldCacheDirs {
|
for _, item := range oldCacheDirs {
|
||||||
@@ -574,7 +485,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stdoutIsTerminal() {
|
if terminal.StdoutIsTerminal() {
|
||||||
Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n",
|
Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n",
|
||||||
len(oldCacheDirs), c.Base)
|
len(oldCacheDirs), c.Base)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
@@ -23,7 +24,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration {
|
|||||||
fps = 60
|
fps = 60
|
||||||
}
|
}
|
||||||
interval = time.Duration(float64(time.Second) / fps)
|
interval = time.Duration(float64(time.Second) / fps)
|
||||||
} else if !json && !stdoutCanUpdateStatus() || !show {
|
} else if !json && !terminal.StdoutCanUpdateStatus() || !show {
|
||||||
interval = 0
|
interval = 0
|
||||||
}
|
}
|
||||||
return interval
|
return interval
|
||||||
@@ -67,10 +68,9 @@ func newProgressMax(show bool, max uint64, description string) *progress.Counter
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printProgress(status string, final bool) {
|
func printProgress(status string, final bool) {
|
||||||
|
canUpdateStatus := terminal.StdoutCanUpdateStatus()
|
||||||
|
|
||||||
canUpdateStatus := stdoutCanUpdateStatus()
|
w := terminal.StdoutWidth()
|
||||||
|
|
||||||
w := stdoutTerminalWidth()
|
|
||||||
if w > 0 {
|
if w > 0 {
|
||||||
if w < 3 {
|
if w < 3 {
|
||||||
status = termstatus.Truncate(status, w)
|
status = termstatus.Truncate(status, w)
|
||||||
@@ -82,12 +82,7 @@ func printProgress(status string, final bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var carriageControl, cl string
|
var carriageControl string
|
||||||
|
|
||||||
if canUpdateStatus {
|
|
||||||
cl = clearLine(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(strings.HasSuffix(status, "\r") || strings.HasSuffix(status, "\n")) {
|
if !(strings.HasSuffix(status, "\r") || strings.HasSuffix(status, "\n")) {
|
||||||
if canUpdateStatus {
|
if canUpdateStatus {
|
||||||
carriageControl = "\r"
|
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 {
|
if final {
|
||||||
_, _ = os.Stdout.Write([]byte("\n"))
|
_, _ = os.Stdout.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIndexProgress(quiet bool, json bool) *progress.Counter {
|
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 {
|
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 {
|
type terminalProgressPrinter struct {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import (
|
|||||||
"github.com/restic/restic/internal/backend/limiter"
|
"github.com/restic/restic/internal/backend/limiter"
|
||||||
"github.com/restic/restic/internal/backend/location"
|
"github.com/restic/restic/internal/backend/location"
|
||||||
"github.com/restic/restic/internal/backend/rest"
|
"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/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan stru
|
|||||||
cmd.Stdin = r
|
cmd.Stdin = r
|
||||||
cmd.Stdout = w
|
cmd.Stdout = w
|
||||||
|
|
||||||
bg, err := util.StartForeground(cmd)
|
bg, err := terminal.StartForeground(cmd)
|
||||||
// close rclone side of pipes
|
// close rclone side of pipes
|
||||||
errR := r.Close()
|
errR := r.Close()
|
||||||
errW := w.Close()
|
errW := w.Close()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/feature"
|
"github.com/restic/restic/internal/feature"
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
@@ -84,7 +85,7 @@ func startClient(cfg Config) (*SFTP, error) {
|
|||||||
return nil, errors.Wrap(err, "cmd.StdoutPipe")
|
return nil, errors.Wrap(err, "cmd.StdoutPipe")
|
||||||
}
|
}
|
||||||
|
|
||||||
bg, err := util.StartForeground(cmd)
|
bg, err := terminal.StartForeground(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, exec.ErrDot) {
|
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)
|
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
|
//go:build unix
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import "github.com/restic/restic/internal/debug"
|
import "github.com/restic/restic/internal/debug"
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ func IsProcessBackground(fd uintptr) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isProcessBackground(fd int) (bg bool, err error) {
|
func isProcessBackground(fd int) (bg bool, err error) {
|
||||||
pgid, err := Tcgetpgrp(fd)
|
pgid, err := tcgetpgrp(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return pgid != Getpgrp(), nil
|
return pgid != getpgrp(), nil
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build unix
|
//go:build unix
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
// IsProcessBackground reports whether the current process is running in the
|
// IsProcessBackground reports whether the current process is running in the
|
||||||
// background. Not implemented for this platform.
|
// background. Not implemented for this platform.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package util
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package util_test
|
package terminal_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend/util"
|
"github.com/restic/restic/internal/terminal"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ func TestForeground(t *testing.T) {
|
|||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
bg, err := util.StartForeground(cmd)
|
bg, err := terminal.StartForeground(cmd)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
rtest.OK(t, cmd.Wait())
|
rtest.OK(t, cmd.Wait())
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build unix
|
//go:build unix
|
||||||
|
|
||||||
package util
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"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
|
// 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 {
|
if err != nil {
|
||||||
_ = tty.Close()
|
_ = tty.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
self := termstatus.Getpgrp()
|
self := getpgrp()
|
||||||
if prev != self {
|
if prev != self {
|
||||||
debug.Log("restic is not controlling the tty; err = %v", err)
|
debug.Log("restic is not controlling the tty; err = %v", err)
|
||||||
if err := tty.Close(); err != nil {
|
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
|
// 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 {
|
if err != nil {
|
||||||
_ = tty.Close()
|
_ = tty.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -67,7 +66,7 @@ func startForeground(cmd *exec.Cmd) (bg func() error, err error) {
|
|||||||
signal.Reset(unix.SIGTTOU)
|
signal.Reset(unix.SIGTTOU)
|
||||||
|
|
||||||
// reset the foreground process group
|
// reset the foreground process group
|
||||||
err = termstatus.Tcsetpgrp(int(tty.Fd()), prev)
|
err = tcsetpgrp(int(tty.Fd()), prev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tty.Close()
|
_ = tty.Close()
|
||||||
return err
|
return err
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package util
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
func Getpgrp() int {
|
func getpgrp() int {
|
||||||
pid, _ := unix.Getpgrp()
|
pid, _ := unix.Getpgrp()
|
||||||
return pid
|
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"
|
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
|
// 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:
|
// 64-bit Linux. IoctlGetInt doesn't work on big-endian platforms:
|
||||||
// https://github.com/golang/go/issues/45585
|
// https://github.com/golang/go/issues/45585
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
//go:build unix && !linux
|
//go:build unix && !linux
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
func Tcgetpgrp(ttyfd int) (int, error) {
|
func tcgetpgrp(ttyfd int) (int, error) {
|
||||||
return unix.IoctlGetInt(ttyfd, unix.TIOCGPGRP)
|
return unix.IoctlGetInt(ttyfd, unix.TIOCGPGRP)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
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,
|
// The second argument to IoctlSetPointerInt has type int on AIX,
|
||||||
// but the constant overflows 64-bit int, hence the two-step cast.
|
// but the constant overflows 64-bit int, hence the two-step cast.
|
||||||
req := uint(unix.TIOCSPGRP)
|
req := uint(unix.TIOCSPGRP)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
//go:build unix && !aix
|
//go:build unix && !aix
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
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)
|
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
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -10,15 +10,15 @@ import (
|
|||||||
"golang.org/x/term"
|
"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.
|
// cursor position to the first column.
|
||||||
func clearCurrentLine(_ uintptr) func(io.Writer, uintptr) {
|
func ClearCurrentLine(_ uintptr) func(io.Writer, uintptr) {
|
||||||
return posixClearCurrentLine
|
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) {
|
func MoveCursorUp(_ uintptr) func(io.Writer, uintptr, int) {
|
||||||
return posixMoveCursorUp
|
return PosixMoveCursorUp
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanUpdateStatus returns true if status lines can be printed, the process
|
// CanUpdateStatus returns true if status lines can be printed, the process
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -15,25 +15,25 @@ import (
|
|||||||
|
|
||||||
// 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.
|
// 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
|
// easy case, the terminal is cmd or psh, without redirection
|
||||||
if isWindowsTerminal(fd) {
|
if isWindowsTerminal(fd) {
|
||||||
return windowsClearCurrentLine
|
return windowsClearCurrentLine
|
||||||
}
|
}
|
||||||
|
|
||||||
// assume we're running in mintty/cygwin
|
// assume we're running in mintty/cygwin
|
||||||
return posixClearCurrentLine
|
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(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
|
// easy case, the terminal is cmd or psh, without redirection
|
||||||
if isWindowsTerminal(fd) {
|
if isWindowsTerminal(fd) {
|
||||||
return windowsMoveCursorUp
|
return windowsMoveCursorUp
|
||||||
}
|
}
|
||||||
|
|
||||||
// assume we're running in mintty/cygwin
|
// assume we're running in mintty/cygwin
|
||||||
return posixMoveCursorUp
|
return PosixMoveCursorUp
|
||||||
}
|
}
|
||||||
|
|
||||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package termstatus
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"syscall"
|
"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"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
"golang.org/x/text/width"
|
"golang.org/x/text/width"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Terminal is used to write messages and display status lines which can be
|
// 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
|
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.
|
// only use the fancy status code when we're running on a real terminal.
|
||||||
t.canUpdateStatus = true
|
t.canUpdateStatus = true
|
||||||
t.fd = d.Fd()
|
t.fd = d.Fd()
|
||||||
t.clearCurrentLine = clearCurrentLine(t.fd)
|
t.clearCurrentLine = terminal.ClearCurrentLine(t.fd)
|
||||||
t.moveCursorUp = moveCursorUp(t.fd)
|
t.moveCursorUp = terminal.MoveCursorUp(t.fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
return t
|
return t
|
||||||
@@ -101,14 +102,14 @@ func (t *Terminal) run(ctx context.Context) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
if !IsProcessBackground(t.fd) {
|
if !terminal.IsProcessBackground(t.fd) {
|
||||||
t.writeStatus([]string{})
|
t.writeStatus([]string{})
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case msg := <-t.msg:
|
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
|
// ignore all messages, do nothing, we are in the background process group
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -140,13 +141,13 @@ func (t *Terminal) run(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case stat := <-t.status:
|
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
|
// ignore all messages, do nothing, we are in the background process group
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
status = status[:0]
|
|
||||||
status = append(status, stat.lines...)
|
|
||||||
t.writeStatus(status)
|
t.writeStatus(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,9 +320,8 @@ func (t *Terminal) SetStatus(lines []string) {
|
|||||||
// only truncate interactive status output
|
// only truncate interactive status output
|
||||||
var width int
|
var width int
|
||||||
if t.canUpdateStatus {
|
if t.canUpdateStatus {
|
||||||
var err error
|
width = terminal.Width(t.fd)
|
||||||
width, _, err = term.GetSize(int(t.fd))
|
if width <= 0 {
|
||||||
if err != nil || width <= 0 {
|
|
||||||
// use 80 columns by default
|
// use 80 columns by default
|
||||||
width = 80
|
width = 80
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/terminal"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,16 +18,16 @@ func TestSetStatus(t *testing.T) {
|
|||||||
|
|
||||||
term.canUpdateStatus = true
|
term.canUpdateStatus = true
|
||||||
term.fd = ^uintptr(0)
|
term.fd = ^uintptr(0)
|
||||||
term.clearCurrentLine = posixClearCurrentLine
|
term.clearCurrentLine = terminal.PosixClearCurrentLine
|
||||||
term.moveCursorUp = posixMoveCursorUp
|
term.moveCursorUp = terminal.PosixMoveCursorUp
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go term.Run(ctx)
|
go term.Run(ctx)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cl = posixControlClearLine
|
cl = terminal.PosixControlClearLine
|
||||||
home = posixControlMoveCursorHome
|
home = terminal.PosixControlMoveCursorHome
|
||||||
up = posixControlMoveCursorUp
|
up = terminal.PosixControlMoveCursorUp
|
||||||
)
|
)
|
||||||
|
|
||||||
term.SetStatus([]string{"first"})
|
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