diff --git a/go.mod b/go.mod index d52446c12a..8032023fa3 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/UserExistsError/conpty v0.1.4 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect @@ -332,7 +333,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/xanzy/go-gitlab v0.97.0 golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.29.0 golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect diff --git a/go.sum b/go.sum index 8dc494ab65..4426403582 100644 --- a/go.sum +++ b/go.sum @@ -626,6 +626,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to= +github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -1881,6 +1883,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 038d0b1097..dfd33c3d17 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -10,7 +10,6 @@ import ( "net/url" "os" "os/exec" - "syscall" "time" "github.com/daytonaio/daytona/cmd/daytona/config" @@ -18,6 +17,7 @@ import ( apiclient_util "github.com/daytonaio/daytona/internal/util/apiclient" "github.com/daytonaio/daytona/internal/util/apiclient/conversion" agent_config "github.com/daytonaio/daytona/pkg/agent/config" + "github.com/daytonaio/daytona/pkg/agent/toolbox/fs" "github.com/daytonaio/daytona/pkg/apiclient" "github.com/daytonaio/daytona/pkg/gitprovider" "github.com/daytonaio/daytona/pkg/models" @@ -113,7 +113,11 @@ func (a *Agent) startWorkspaceMode() error { log.Info("Repository already exists. Skipping clone...") } else { if stat, err := os.Stat(a.Config.WorkspaceDir); err == nil { - ownerUid := stat.Sys().(*syscall.Stat_t).Uid + ownerUid, err := fs.GetFileUid(stat) + if err != nil { + log.Error(err) + } + if ownerUid != uint32(os.Getuid()) { chownCmd := exec.Command("sudo", "chown", "-R", fmt.Sprintf("%s:%s", a.Workspace.User, a.Workspace.User), a.Config.WorkspaceDir) err = chownCmd.Run() diff --git a/pkg/agent/ssh/server.go b/pkg/agent/ssh/server.go index 9d464ae4d1..0c459df50e 100644 --- a/pkg/agent/ssh/server.go +++ b/pkg/agent/ssh/server.go @@ -8,15 +8,12 @@ import ( "io" "os" "os/exec" - "syscall" - "unsafe" + "runtime" - "github.com/creack/pty" "github.com/daytonaio/daytona/pkg/agent/ssh/config" "github.com/daytonaio/daytona/pkg/common" "github.com/gliderlabs/ssh" "github.com/pkg/sftp" - "golang.org/x/sys/unix" log "github.com/sirupsen/logrus" ) @@ -104,16 +101,22 @@ func (s *Server) handlePty(session ssh.Session, ptyReq ssh.Pty, winCh <-chan ssh cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = append(cmd.Env, fmt.Sprintf("SHELL=%s", shell)) - f, err := pty.Start(cmd) - if err != nil { - log.Errorf("Unable to start command: %v", err) - return - } + var f io.ReadWriteCloser + if runtime.GOOS == "windows" { + output := Start(shell) + if output != nil { + f = output + } + } else { + output := Start(cmd) + if output != nil { + f = output + } + } go func() { for win := range winCh { - syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), - uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(win.Height), uint16(win.Width), 0, 0}))) + SetPtySize(f, win) } }() go func() { @@ -128,7 +131,7 @@ func (s *Server) handleNonPty(session ssh.Session) { args = append([]string{"-c"}, session.RawCommand()) } - cmd := exec.Command("/bin/sh", args...) + cmd := exec.Command("sh", args...) cmd.Env = append(cmd.Env, os.Environ()...) @@ -177,7 +180,7 @@ func (s *Server) handleNonPty(session ssh.Session) { }() go func() { for sig := range sigs { - signal := s.osSignalFrom(sig) + signal := OsSignalFrom(sig) err := cmd.Process.Signal(signal) if err != nil { log.Warnf("Unable to send signal to process: %v", err) @@ -198,41 +201,6 @@ func (s *Server) handleNonPty(session ssh.Session) { } } -func (s *Server) osSignalFrom(sig ssh.Signal) os.Signal { - switch sig { - case ssh.SIGABRT: - return unix.SIGABRT - case ssh.SIGALRM: - return unix.SIGALRM - case ssh.SIGFPE: - return unix.SIGFPE - case ssh.SIGHUP: - return unix.SIGHUP - case ssh.SIGILL: - return unix.SIGILL - case ssh.SIGINT: - return unix.SIGINT - case ssh.SIGKILL: - return unix.SIGKILL - case ssh.SIGPIPE: - return unix.SIGPIPE - case ssh.SIGQUIT: - return unix.SIGQUIT - case ssh.SIGSEGV: - return unix.SIGSEGV - case ssh.SIGTERM: - return unix.SIGTERM - case ssh.SIGUSR1: - return unix.SIGUSR1 - case ssh.SIGUSR2: - return unix.SIGUSR2 - - // Unhandled, use sane fallback. - default: - return unix.SIGKILL - } -} - func (s *Server) sftpHandler(session ssh.Session) { debugStream := io.Discard serverOptions := []sftp.ServerOption{ diff --git a/pkg/agent/ssh/server_unix.go b/pkg/agent/ssh/server_unix.go new file mode 100644 index 0000000000..198cccfd4d --- /dev/null +++ b/pkg/agent/ssh/server_unix.go @@ -0,0 +1,74 @@ +//go:build !windows + +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package ssh + +import ( + "os" + "os/exec" + "syscall" + "unsafe" + + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +func Start(cmd interface{}) *os.File { + if command, ok := cmd.(*exec.Cmd); ok { + f, err := pty.Start(command) + if err != nil { + log.Errorf("Unable to start PTY: %v", err) + return nil + } + return f + } + return nil +} + +func SetPtySize(f interface{}, win ssh.Window) { + if file, ok := f.(*os.File); ok { + syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(win.Height), uint16(win.Width), 0, 0}))) + } else { + log.Errorf("Unable to resize PTY") + } +} + +func OsSignalFrom(sig ssh.Signal) os.Signal { + switch sig { + case ssh.SIGABRT: + return unix.SIGABRT + case ssh.SIGALRM: + return unix.SIGALRM + case ssh.SIGFPE: + return unix.SIGFPE + case ssh.SIGHUP: + return unix.SIGHUP + case ssh.SIGILL: + return unix.SIGILL + case ssh.SIGINT: + return unix.SIGINT + case ssh.SIGKILL: + return unix.SIGKILL + case ssh.SIGPIPE: + return unix.SIGPIPE + case ssh.SIGQUIT: + return unix.SIGQUIT + case ssh.SIGSEGV: + return unix.SIGSEGV + case ssh.SIGTERM: + return unix.SIGTERM + case ssh.SIGUSR1: + return unix.SIGUSR1 + case ssh.SIGUSR2: + return unix.SIGUSR2 + + // Unhandled, use sane fallback. + default: + return unix.SIGKILL + } +} diff --git a/pkg/agent/ssh/server_win.go b/pkg/agent/ssh/server_win.go new file mode 100644 index 0000000000..747957878d --- /dev/null +++ b/pkg/agent/ssh/server_win.go @@ -0,0 +1,54 @@ +//go:build windows + +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package ssh + +import ( + "os" + "syscall" + + "github.com/UserExistsError/conpty" + "github.com/gliderlabs/ssh" + log "github.com/sirupsen/logrus" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + setConsoleWindowInfo = kernel32.NewProc("SetConsoleWindowInfo") +) + +func Start(cmd interface{}) *conpty.ConPty { + if shell, ok := cmd.(string); ok { + f, err := conpty.Start(shell) + if err != nil { + log.Errorf("Unable to start ConPTY: %v", err) + return nil + } + return f + } + + return nil +} + +func SetPtySize(f interface{}, win ssh.Window) { + if cpty, ok := f.(*conpty.ConPty); ok { + cpty.Resize(win.Width, win.Height) + } else { + log.Errorf("Unable to resize ConPTY") + } +} + +func OsSignalFrom(sig ssh.Signal) os.Signal { + switch sig { + case ssh.SIGINT: + return os.Interrupt + case ssh.SIGTERM: + return os.Kill + case ssh.SIGKILL: + return os.Kill + default: + return os.Kill + } +} diff --git a/pkg/agent/toolbox/fs/get_file_info.go b/pkg/agent/toolbox/fs/get_file_info.go index aa6b96670b..4fbb23a674 100644 --- a/pkg/agent/toolbox/fs/get_file_info.go +++ b/pkg/agent/toolbox/fs/get_file_info.go @@ -5,10 +5,7 @@ package fs import ( "errors" - "fmt" "os" - "strconv" - "syscall" "github.com/gin-gonic/gin" ) @@ -32,22 +29,3 @@ func GetFileInfo(c *gin.Context) { c.JSON(200, info) } - -func getFileInfo(path string) (FileInfo, error) { - info, err := os.Stat(path) - if err != nil { - return FileInfo{}, err - } - - stat := info.Sys().(*syscall.Stat_t) - return FileInfo{ - Name: info.Name(), - Size: info.Size(), - Mode: info.Mode().String(), - ModTime: info.ModTime().String(), - IsDir: info.IsDir(), - Owner: strconv.FormatUint(uint64(stat.Uid), 10), - Group: strconv.FormatUint(uint64(stat.Gid), 10), - Permissions: fmt.Sprintf("%04o", info.Mode().Perm()), - }, nil -} diff --git a/pkg/agent/toolbox/fs/info_unix.go b/pkg/agent/toolbox/fs/info_unix.go new file mode 100644 index 0000000000..7ba7d9a5e6 --- /dev/null +++ b/pkg/agent/toolbox/fs/info_unix.go @@ -0,0 +1,36 @@ +//go:build !windows + +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package fs + +import ( + "fmt" + "os" + "strconv" + "syscall" +) + +func getFileInfo(path string) (*FileInfo, error) { + info, err := os.Stat(path) + if err != nil { + return &FileInfo{}, err + } + + stat := info.Sys().(*syscall.Stat_t) + return &FileInfo{ + Name: info.Name(), + Size: info.Size(), + Mode: info.Mode().String(), + ModTime: info.ModTime().String(), + IsDir: info.IsDir(), + Owner: strconv.FormatUint(uint64(stat.Uid), 10), + Group: strconv.FormatUint(uint64(stat.Gid), 10), + Permissions: fmt.Sprintf("%04o", info.Mode().Perm()), + }, nil +} + +func GetFileUid(stat os.FileInfo) (uint32, error) { + return stat.Sys().(*syscall.Stat_t).Uid, nil +} diff --git a/pkg/agent/toolbox/fs/info_win.go b/pkg/agent/toolbox/fs/info_win.go new file mode 100644 index 0000000000..b8b3c89d1b --- /dev/null +++ b/pkg/agent/toolbox/fs/info_win.go @@ -0,0 +1,65 @@ +//go:build windows + +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package fs + +import ( + "fmt" + "os" + "strconv" + + "golang.org/x/sys/windows" +) + +func getFileInfo(path string) (*FileInfo, error) { + info, err := os.Stat(path) + if err != nil { + return &FileInfo{}, err + } + + ownerSid, groupSid, err := getFileGidUid(path) + if err != nil { + return &FileInfo{}, err + } + + return &FileInfo{ + Name: info.Name(), + Size: info.Size(), + Mode: info.Mode().String(), + ModTime: info.ModTime().String(), + IsDir: info.IsDir(), + Owner: ownerSid, + Group: groupSid, + Permissions: fmt.Sprintf("%04o", info.Mode().Perm()), + }, nil +} + +func getFileGidUid(path string) (string, string, error) { + sd, err := windows.GetNamedSecurityInfo(path, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.GROUP_SECURITY_INFORMATION) + if err != nil { + return "", "", err + } + owner, _, err := sd.Owner() + if err != nil { + return "", "", err + } + group, _, err := sd.Group() + if err != nil { + return "", "", err + } + return owner.String(), group.String(), nil +} + +func GetFileUid(stat os.FileInfo) (uint32, error) { + uid, _, err := getFileGidUid(stat.Name()) + if err != nil { + return 0, err + } + uidInt, err := strconv.ParseUint(uid, 10, 32) + if err != nil { + return 0, err + } + return uint32(uidInt), nil +} diff --git a/pkg/agent/toolbox/fs/list_files.go b/pkg/agent/toolbox/fs/list_files.go index eea4078747..406852ec29 100644 --- a/pkg/agent/toolbox/fs/list_files.go +++ b/pkg/agent/toolbox/fs/list_files.go @@ -32,7 +32,7 @@ func ListFiles(c *gin.Context) { if err != nil { continue } - fileInfos = append(fileInfos, info) + fileInfos = append(fileInfos, *info) } c.JSON(200, fileInfos) diff --git a/pkg/cmd/agent/agent.go b/pkg/cmd/agent/agent.go index 6682cc9cd5..748d9352e9 100644 --- a/pkg/cmd/agent/agent.go +++ b/pkg/cmd/agent/agent.go @@ -1,5 +1,3 @@ -//go:build !windows - // Copyright 2024 Daytona Platforms Inc. // SPDX-License-Identifier: Apache-2.0 diff --git a/pkg/cmd/agent/agent_windows.go b/pkg/cmd/agent/agent_windows.go deleted file mode 100644 index 6bdf0cc62d..0000000000 --- a/pkg/cmd/agent/agent_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build windows - -// Copyright 2024 Daytona Platforms Inc. -// SPDX-License-Identifier: Apache-2.0 - -package agent - -import ( - "github.com/spf13/cobra" -) - -var AgentCmd = &cobra.Command{ - Use: "agent", - Short: "Start the agent process", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - panic("Not implemented") - }, -}