1
0
mirror of https://github.com/lxc/incus.git synced 2026-02-05 09:46:19 +01:00

incus-agent/darwin: Implement interactive console

Signed-off-by: Benjamin Somers <benjamin.somers@imt-atlantique.fr>
This commit is contained in:
Benjamin Somers
2025-10-15 17:37:22 +00:00
parent 0fe66971e2
commit 105325bc58

View File

@@ -14,6 +14,7 @@ import (
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/shirou/gopsutil/v4/disk"
"golang.org/x/sys/unix"
@@ -22,6 +23,7 @@ import (
"github.com/lxc/incus/v6/internal/version"
"github.com/lxc/incus/v6/shared/api"
"github.com/lxc/incus/v6/shared/logger"
"github.com/lxc/incus/v6/shared/revert"
"github.com/lxc/incus/v6/shared/subprocess"
)
@@ -287,21 +289,167 @@ func osGetOSState() *api.InstanceStateOSInfo {
return osInfo
}
func osGetInteractiveConsole(s *execWs) (io.ReadWriteCloser, io.ReadWriteCloser, error) {
return nil, nil, errors.New("Only non-interactive exec sessions are currently supported on Darwin")
// openPty is is the same as linux.OpenPty for Darwin.
func openPty(uid, gid int64) (*os.File, *os.File, error) {
reverter := revert.New()
defer reverter.Fail()
fd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
ptx := os.NewFile(uintptr(fd), "")
reverter.Add(func() { _ = ptx.Close() })
// Unlock the ptx and pty.
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(ptx.Fd()), unix.TIOCPTYUNLK, 0)
if errno != 0 {
return nil, nil, unix.Errno(errno)
}
var ptyName [256]byte
_, _, errno = unix.Syscall(unix.SYS_IOCTL, uintptr(ptx.Fd()), unix.TIOCPTYGNAME, uintptr(unsafe.Pointer(&ptyName)))
if errno != 0 {
return nil, nil, unix.Errno(errno)
}
pty, err := os.OpenFile(parseBytes(ptyName[:]), unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
reverter.Add(func() { _ = pty.Close() })
// Configure both sides
for _, entry := range []*os.File{ptx, pty} {
// Get termios.
t, err := unix.IoctlGetTermios(int(entry.Fd()), unix.TIOCGETA)
if err != nil {
return nil, nil, err
}
// Set flags.
t.Cflag |= unix.IMAXBEL
t.Cflag |= unix.IUTF8
t.Cflag |= unix.BRKINT
t.Cflag |= unix.IXANY
t.Cflag |= unix.HUPCL
// Set termios.
err = unix.IoctlSetTermios(int(entry.Fd()), unix.TIOCSETA, t)
if err != nil {
return nil, nil, err
}
// Set the default window size.
sz := &unix.Winsize{
Col: 80,
Row: 25,
}
err = unix.IoctlSetWinsize(int(entry.Fd()), unix.TIOCSWINSZ, sz)
if err != nil {
return nil, nil, err
}
// Set CLOEXEC.
_, _, errno = unix.Syscall(unix.SYS_FCNTL, uintptr(entry.Fd()), unix.F_SETFD, unix.FD_CLOEXEC)
if errno != 0 {
return nil, nil, unix.Errno(errno)
}
}
// Fix the ownership of the pty side.
err = unix.Fchown(int(pty.Fd()), int(uid), int(gid))
if err != nil {
return nil, nil, err
}
reverter.Success()
return ptx, pty, nil
}
// setPtySize is the same as linux.SetPtySize for Darwin.
func setPtySize(fd int, width int, height int) (err error) {
var dimensions [4]uint16
dimensions[0] = uint16(height)
dimensions[1] = uint16(width)
_, _, errno := unix.Syscall6(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.TIOCSWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0)
if errno != 0 {
return errno
}
return nil
}
func osGetInteractiveConsole(s *execWs) (*os.File, *os.File, error) {
pty, tty, err := openPty(int64(s.uid), int64(s.gid))
if err != nil {
return nil, nil, err
}
if s.width > 0 && s.height > 0 {
_ = setPtySize(int(pty.Fd()), s.width, s.height)
}
return pty, tty, nil
}
func osPrepareExecCommand(s *execWs, cmd *exec.Cmd) {
if s.cwd == "" {
cmd.Dir = osBaseWorkingDirectory
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: s.uid,
Gid: s.gid,
},
// Creates a new session if the calling process is not a process group leader.
// The calling process is the leader of the new session, the process group leader of
// the new process group, and has no controlling terminal.
// This is important to allow remote shells to handle ctrl+c.
Setsid: true,
}
return
// Make the given terminal the controlling terminal of the calling process.
// The calling process must be a session leader and not have a controlling terminal already.
// This is important as allows ctrl+c to work as expected for non-shell programs.
if s.interactive {
cmd.SysProcAttr.Setctty = true
}
}
func osHandleExecControl(control api.InstanceExecControl, s *execWs, pty io.ReadWriteCloser, cmd *exec.Cmd, l logger.Logger) {
// Ignore control messages.
return
if control.Command == "window-resize" && s.interactive {
winchWidth, err := strconv.Atoi(control.Args["width"])
if err != nil {
l.Debug("Unable to extract window width", logger.Ctx{"err": err})
return
}
winchHeight, err := strconv.Atoi(control.Args["height"])
if err != nil {
l.Debug("Unable to extract window height", logger.Ctx{"err": err})
return
}
osFile, ok := pty.(*os.File)
if ok {
err = setPtySize(int(osFile.Fd()), winchWidth, winchHeight)
if err != nil {
l.Debug("Failed to set window size", logger.Ctx{"err": err, "width": winchWidth, "height": winchHeight})
return
}
}
} else if control.Command == "signal" {
err := unix.Kill(cmd.Process.Pid, unix.Signal(control.Signal))
if err != nil {
l.Debug("Failed forwarding signal", logger.Ctx{"err": err, "signal": control.Signal})
return
}
l.Info("Forwarded signal", logger.Ctx{"signal": control.Signal})
}
}
// osExitStatus is is the same as linux.ExitStatus for Darwin.
@@ -329,4 +477,33 @@ func osExitStatus(err error) (int, error) {
func osSetEnv(post *api.InstanceExecPost, env map[string]string) {
env["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
// If running as root, set some env variables.
if post.User == 0 {
// Set default value for HOME. Fix /root.
home, ok := env["HOME"]
if !ok || home == "/root" {
env["HOME"] = "/var/root"
}
// Set default value for USER.
_, ok = env["USER"]
if !ok {
env["USER"] = "root"
}
}
// Set default value for LANG.
_, ok := env["LANG"]
if !ok {
env["LANG"] = "C.UTF-8"
}
// Set the default working directory.
if post.Cwd == "" {
post.Cwd = env["HOME"]
if post.Cwd == "" {
post.Cwd = osBaseWorkingDirectory
}
}
}