backend,termstatus: Unify foreground/background detection

PR #5358 reintroduced a version of the TIOCGPGRP ioctl call that works
on all Unix platforms except Linux, due to a bug/inconsistency in
x/sys/unix. This commit fixes that by introducing termstatus.Tcgetpgrp.

It also introduces termstatus.Getpgrp and termstatus.Tcsetpgrp to deal
with the different signature of unix.Getpgrp in Solaris vs. all other
Unix platforms and an int-overflowing constant on AIX, so that some
AIX/Solaris-specific code can be removed elsewhere and
foreground/background detection is done the same everywhere except on
Windows.
This commit is contained in:
greatroar
2025-09-07 12:57:01 +02:00
parent 42f690dbab
commit 1ed93bd54d
12 changed files with 89 additions and 76 deletions

View File

@@ -1,28 +0,0 @@
//go:build aix || solaris
// +build aix solaris
package util
import (
"os/exec"
"syscall"
"github.com/restic/restic/internal/errors"
)
func startForeground(cmd *exec.Cmd) (bg func() error, err error) {
// run the command in its own process group so that SIGINT
// is not sent to it.
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
// start the process
err = cmd.Start()
if err != nil {
return nil, errors.Wrap(err, "cmd.Start")
}
bg = func() error { return nil }
return bg, nil
}

View File

@@ -1,5 +1,4 @@
//go:build !aix && !solaris && !windows //go:build unix
// +build !aix,!solaris,!windows
package util package util
@@ -10,20 +9,11 @@ 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"
) )
func tcgetpgrp(fd int) (int, error) {
return unix.IoctlGetInt(fd, unix.TIOCGPGRP)
}
func tcsetpgrp(fd int, pid int) error {
// IoctlSetPointerInt silently casts to int32 internally,
// so this assumes pid fits in 31 bits.
return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pid)
}
func startForeground(cmd *exec.Cmd) (bg func() error, err error) { func startForeground(cmd *exec.Cmd) (bg func() error, err error) {
// run the command in its own process group // run the command in its own process group
// this ensures that sending ctrl-c to restic will not immediately stop the backend process. // this ensures that sending ctrl-c to restic will not immediately stop the backend process.
@@ -39,15 +29,15 @@ 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 := tcgetpgrp(int(tty.Fd())) prev, err := termstatus.Tcgetpgrp(int(tty.Fd()))
if err != nil { if err != nil {
_ = tty.Close() _ = tty.Close()
return nil, err return nil, err
} }
self := unix.Getpgrp() self := termstatus.Getpgrp()
if prev != self { if prev != self {
debug.Log("restic is not controlling the tty") debug.Log("restic is not controlling the tty; err = %v", err)
if err := tty.Close(); err != nil { if err := tty.Close(); err != nil {
return nil, err return nil, err
} }
@@ -66,7 +56,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 = tcsetpgrp(int(tty.Fd()), cmd.Process.Pid) err = termstatus.Tcsetpgrp(int(tty.Fd()), cmd.Process.Pid)
if err != nil { if err != nil {
_ = tty.Close() _ = tty.Close()
return nil, err return nil, err
@@ -77,7 +67,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 = tcsetpgrp(int(tty.Fd()), prev) err = termstatus.Tcsetpgrp(int(tty.Fd()), prev)
if err != nil { if err != nil {
_ = tty.Close() _ = tty.Close()
return err return err

View File

@@ -1,27 +0,0 @@
package termstatus
import (
"github.com/restic/restic/internal/debug"
"golang.org/x/sys/unix"
)
// IsProcessBackground reports whether the current process is running in the
// background. fd must be a file descriptor for the terminal.
func IsProcessBackground(fd uintptr) bool {
bg, err := isProcessBackground(fd)
if err != nil {
debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error())
return false
}
return bg
}
func isProcessBackground(fd uintptr) (bool, 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
// https://github.com/golang/go/issues/60429
pid, err := unix.IoctlGetUint32(int(fd), unix.TIOCGPGRP)
return int(pid) != unix.Getpgrp(), err
}

View File

@@ -0,0 +1,24 @@
//go:build unix
package termstatus
import "github.com/restic/restic/internal/debug"
// IsProcessBackground reports whether the current process is running in the
// background. fd must be a file descriptor for the terminal.
func IsProcessBackground(fd uintptr) bool {
bg, err := isProcessBackground(int(fd))
if err != nil {
debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error())
return false
}
return bg
}
func isProcessBackground(fd int) (bg bool, err error) {
pgid, err := Tcgetpgrp(fd)
if err != nil {
return false, err
}
return pgid != Getpgrp(), nil
}

View File

@@ -1,3 +1,5 @@
//go:build unix
package termstatus package termstatus
import ( import (
@@ -13,7 +15,7 @@ func TestIsProcessBackground(t *testing.T) {
t.Skipf("can't open terminal: %v", err) t.Skipf("can't open terminal: %v", err)
} }
_, err = isProcessBackground(tty.Fd()) _, err = isProcessBackground(int(tty.Fd()))
rtest.OK(t, err) rtest.OK(t, err)
_ = tty.Close() _ = tty.Close()

View File

@@ -1,6 +1,3 @@
//go:build !linux
// +build !linux
package termstatus package termstatus
// IsProcessBackground reports whether the current process is running in the // IsProcessBackground reports whether the current process is running in the

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
package termstatus
import "golang.org/x/sys/unix"
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
// https://github.com/golang/go/issues/60429
pid, err := unix.IoctlGetUint32(ttyfd, unix.TIOCGPGRP)
return int(pid), err
}

View File

@@ -0,0 +1,9 @@
//go:build unix && !linux
package termstatus
import "golang.org/x/sys/unix"
func Tcgetpgrp(ttyfd int) (int, error) {
return unix.IoctlGetInt(ttyfd, unix.TIOCGPGRP)
}

View File

@@ -0,0 +1,10 @@
package termstatus
import "golang.org/x/sys/unix"
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)
return unix.IoctlSetPointerInt(fd, int(req), pid)
}

View File

@@ -0,0 +1,9 @@
//go:build unix && !aix
package termstatus
import "golang.org/x/sys/unix"
func Tcsetpgrp(fd int, pid int) error {
return unix.IoctlSetPointerInt(fd, unix.TIOCSPGRP, pid)
}