From dfb50e8a31e9a93b31181113d7b44b657cf27168 Mon Sep 17 00:00:00 2001 From: Rutik Thakre <58112334+Rutik7066@users.noreply.github.com> Date: Sat, 8 Feb 2025 01:32:39 +0530 Subject: [PATCH] feat: agent support for windows (#1741) Signed-off-by: ANS-UXI Signed-off-by: Rutik7066 Co-authored-by: ANS-UXI --- .gitignore | 7 ++- go.mod | 3 +- go.sum | 6 ++- pkg/agent/agent.go | 32 +++++++++--- pkg/agent/ssh/server.go | 52 +++---------------- pkg/agent/ssh/server_unix.go | 75 +++++++++++++++++++++++++++ pkg/agent/ssh/server_win.go | 54 +++++++++++++++++++ pkg/agent/toolbox/fs/get_file_info.go | 22 -------- pkg/agent/toolbox/fs/info_unix.go | 36 +++++++++++++ pkg/agent/toolbox/fs/info_win.go | 65 +++++++++++++++++++++++ pkg/agent/toolbox/fs/list_files.go | 2 +- pkg/cmd/agent/agent.go | 2 - pkg/cmd/agent/agent_windows.go | 19 ------- pkg/ide/vscode.go | 19 ++++--- 14 files changed, 288 insertions(+), 106 deletions(-) create mode 100644 pkg/agent/ssh/server_unix.go create mode 100644 pkg/agent/ssh/server_win.go create mode 100644 pkg/agent/toolbox/fs/info_unix.go create mode 100644 pkg/agent/toolbox/fs/info_win.go delete mode 100644 pkg/cmd/agent/agent_windows.go diff --git a/.gitignore b/.gitignore index 45f06a2977..0662e3c053 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ DEV_WORKSPACES ./daytona .DS_Store -tmp \ No newline at end of file +tmp + + +*.exe + +main \ No newline at end of file diff --git a/go.mod b/go.mod index d11b0690cc..a7680a7f87 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ replace github.com/samber/lo => github.com/samber/lo v1.39.0 require ( code.gitea.io/sdk/gitea v0.17.1 gitee.com/openeuler/go-gitee v0.0.0-20220530104019-3af895bc380c + github.com/UserExistsError/conpty v0.1.4 github.com/antihax/optional v1.0.0 github.com/aws/aws-sdk-go-v2/config v1.27.26 github.com/aws/aws-sdk-go-v2/service/iam v1.34.3 @@ -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.33.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 0d786db7d1..84bd317929 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.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +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= @@ -1876,8 +1878,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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..0d2edc0f86 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -10,7 +10,7 @@ import ( "net/url" "os" "os/exec" - "syscall" + "runtime" "time" "github.com/daytonaio/daytona/cmd/daytona/config" @@ -18,6 +18,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,12 +114,31 @@ 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() - if err != nil { - log.Error(err) + user := a.Workspace.User + directory := a.Config.WorkspaceDir + if runtime.GOOS == "windows" { + takeownCmd := exec.Command("takeown", "/F", directory, "/R", "/D", "Y") + err := takeownCmd.Run() + if err != nil { + log.Errorf("Failed to take ownership: %v", err) + } + + icaclsCmd := exec.Command("icacls", directory, "/grant", fmt.Sprintf("%s:F", user), "/T") + err = icaclsCmd.Run() + if err != nil { + log.Errorf("Failed to grant permissions: %v", err) + } + } else { + chownCmd := exec.Command("sudo", "chown", "-R", fmt.Sprintf("%s:%s", user, user), directory) + err = chownCmd.Run() + if err != nil { + log.Error(err) + } } } } diff --git a/pkg/agent/ssh/server.go b/pkg/agent/ssh/server.go index 9d464ae4d1..e706ef0a34 100644 --- a/pkg/agent/ssh/server.go +++ b/pkg/agent/ssh/server.go @@ -8,15 +8,11 @@ import ( "io" "os" "os/exec" - "syscall" - "unsafe" - "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 +100,17 @@ 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) + + f, err := Start(cmd) if err != nil { - log.Errorf("Unable to start command: %v", err) + log.Errorf("Unable to start PTY: %v", err) return } + defer f.Close() 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 +125,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 +174,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 +195,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..03b677edc6 --- /dev/null +++ b/pkg/agent/ssh/server_unix.go @@ -0,0 +1,75 @@ +//go:build !windows + +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package ssh + +import ( + "fmt" + "os" + "os/exec" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + log "github.com/sirupsen/logrus" +) + +func Start(cmd interface{}) (*os.File, error) { + if command, ok := cmd.(*exec.Cmd); ok { + f, err := pty.Start(command) + if err != nil { + return nil, fmt.Errorf("Unable to start PTY: %v", err) + } + return f, nil + } + return nil, fmt.Errorf("Unable to start PTY") +} + +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..01a697e428 --- /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 ( + "fmt" + "os" + "os/exec" + "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, error) { + if shell, ok := cmd.(*exec.Cmd); ok { + f, err := conpty.Start(`c:\windows\system32\cmd.exe`, conpty.ConPtyEnv(shell.Env), conpty.ConPtyWorkDir(shell.Dir)) + if err != nil { + return nil, fmt.Errorf("Unable to start ConPTY: %v", err) + } + return f, nil + } + return nil, fmt.Errorf("Unable to start ConPTY") +} + +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") - }, -} diff --git a/pkg/ide/vscode.go b/pkg/ide/vscode.go index 44462ed9e9..76c624907f 100644 --- a/pkg/ide/vscode.go +++ b/pkg/ide/vscode.go @@ -50,19 +50,24 @@ func OpenVSCode(activeProfile config.Profile, workspaceId, repoName string, work } func setupVSCodeCustomizations(workspaceHostname string, workspaceProviderMetadata string, tool devcontainer.Tool, codeServerPath string, settingsPath string, lockFileName string) error { + var metadata map[string]interface{} + if err := json.Unmarshal([]byte(workspaceProviderMetadata), &metadata); err != nil { + return err + } + // Check if customizations are already set up - err := exec.Command("ssh", workspaceHostname, "test", "-f", fmt.Sprintf("$HOME/%s-%s", lockFileName, string(tool))).Run() + lockFileNamePath := fmt.Sprintf("$HOME/%s-%s", lockFileName, string(tool)) + if metadata["remote-os"] == "windows" { + lockFileNamePath = fmt.Sprintf("$HOME\\%s-%s", lockFileName, string(tool)) + } + + err := exec.Command("ssh", workspaceHostname, "test", "-f", lockFileNamePath).Run() if err == nil { return nil } fmt.Println("Setting up IDE customizations...") - var metadata map[string]interface{} - if err := json.Unmarshal([]byte(workspaceProviderMetadata), &metadata); err != nil { - return err - } - if devcontainerMetadata, ok := metadata["devcontainer.metadata"]; ok { var configs []devcontainer.Configuration if err := json.Unmarshal([]byte(devcontainerMetadata.(string)), &configs); err != nil { @@ -129,7 +134,7 @@ func setupVSCodeCustomizations(workspaceHostname string, workspaceProviderMetada } // Create lock file to indicate that customizations are set up - err = exec.Command("ssh", workspaceHostname, "touch", fmt.Sprintf("$HOME/%s-%s", lockFileName, string(tool))).Run() + err = exec.Command("ssh", workspaceHostname, "touch", lockFileNamePath).Run() if err != nil { return err }