Skip to content

Commit

Permalink
rdctl: factory-reset: Terminate extension processes
Browse files Browse the repository at this point in the history
This attempts to terminate extension process (as in, processes where the
executable are in the extension directory) when we run
`rdctl factory-reset`.  This is needed because we don't gracefully shut
down Rancher Desktop when we do a factory reset.

Signed-off-by: Mark Yen <mark.yen@suse.com>
  • Loading branch information
mook-as committed Oct 7, 2024
1 parent e28e9ba commit ad1d81b
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/go/rdctl/cmd/factoryReset.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Use the --remove-kubernetes-cache=BOOLEAN flag to also remove the cached Kuberne
if err != nil {
return fmt.Errorf("failed to get paths: %w", err)
}
return factoryreset.DeleteData(paths, removeKubernetesCache)
return factoryreset.DeleteData(cmd.Context(), paths, removeKubernetesCache)
},
}

Expand Down
8 changes: 7 additions & 1 deletion src/go/rdctl/pkg/factoryreset/delete_data_darwin.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package factoryreset

import (
"context"
"os"
"path/filepath"

"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process"
"github.com/sirupsen/logrus"
)

func DeleteData(appPaths paths.Paths, removeKubernetesCache bool) error {
func DeleteData(ctx context.Context, appPaths paths.Paths, removeKubernetesCache bool) error {
if err := autostart.EnsureAutostart(false); err != nil {
logrus.Errorf("Failed to remove autostart configuration: %s", err)
}

if err := process.TerminateProcessInDirectory(ctx, appPaths.ExtensionRoot); err != nil {
logrus.Errorf("Failed to stop extension processes, ignoring: %s", err)
}

pathList := []string{
appPaths.AltAppHome,
appPaths.Config,
Expand Down
8 changes: 7 additions & 1 deletion src/go/rdctl/pkg/factoryreset/delete_data_linux.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package factoryreset

import (
"context"
"os"
"path/filepath"

"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process"
"github.com/sirupsen/logrus"
)

func DeleteData(appPaths paths.Paths, removeKubernetesCache bool) error {
func DeleteData(ctx context.Context, appPaths paths.Paths, removeKubernetesCache bool) error {
if err := autostart.EnsureAutostart(false); err != nil {
logrus.Errorf("Failed to remove autostart configuration: %s", err)
}
Expand All @@ -19,6 +21,10 @@ func DeleteData(appPaths paths.Paths, removeKubernetesCache bool) error {
logrus.Errorf("Error getting home directory: %s", err)
}

if err := process.TerminateProcessInDirectory(ctx, appPaths.ExtensionRoot); err != nil {
logrus.Errorf("Failed to stop extension processes, ignoring: %s", err)
}

pathList := []string{
appPaths.AltAppHome,
appPaths.Config,
Expand Down
8 changes: 7 additions & 1 deletion src/go/rdctl/pkg/factoryreset/delete_data_windows.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package factoryreset

import (
"context"

"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process"
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl"
"github.com/sirupsen/logrus"
)

func DeleteData(paths paths.Paths, removeKubernetesCache bool) error {
func DeleteData(ctx context.Context, appPaths paths.Paths, removeKubernetesCache bool) error {
if err := autostart.EnsureAutostart(false); err != nil {
logrus.Errorf("Failed to remove autostart configuration: %s", err)
}
Expand All @@ -16,6 +19,9 @@ func DeleteData(paths paths.Paths, removeKubernetesCache bool) error {
logrus.Errorf("could not unregister WSL: %s", err)
return err
}
if err := process.TerminateProcessInDirectory(ctx, appPaths.ExtensionRoot); err != nil {
logrus.Errorf("Failed to stop extension processes, ignoring: %s", err)
}
if err := deleteWindowsData(!removeKubernetesCache, "rancher-desktop"); err != nil {
logrus.Errorf("could not delete data: %s", err)
return err
Expand Down
61 changes: 61 additions & 0 deletions src/go/rdctl/pkg/process/process_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package process

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"

"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

const (
CTL_KERN = "kern"
KERN_PROCARGS = 38
)

// TerminateProcessInDirectory terminates all processes where the executable
// resides within the given directory, as gracefully as possible.
func TerminateProcessInDirectory(ctx context.Context, directory string) error {
procs, err := unix.SysctlKinfoProcSlice("kern.proc.all")
if err != nil {
return fmt.Errorf("failed to list processes: %w", err)
}
for _, proc := range procs {
pid := int(proc.Proc.P_pid)
buf, err := unix.SysctlRaw(CTL_KERN, KERN_PROCARGS, pid)
if err != nil {
if !errors.Is(err, unix.EINVAL) {
logrus.Infof("Failed to get command line of pid %d: %s", pid, err)
}
continue
}
// The buffer starts with a null-terminated executable path, plus
// command line arguments and things.
index := slices.Index(buf, 0)
if index < 0 {
// If we have unexpected data, don't fall over.
continue
}
procPath := string(buf[:index])
relPath, err := filepath.Rel(directory, procPath)
if err != nil || strings.HasPrefix(relPath, "../") {
continue
}
process, err := os.FindProcess(pid)
if err != nil {
continue
}
err = process.Signal(unix.SIGTERM)
if err == nil {
logrus.Infof("Terminated process %d (%s)", pid, procPath)
} else if !errors.Is(err, unix.EINVAL) {
logrus.Infof("Ignoring failure to terminate pid %d (%s): %s", pid, procPath, err)
}
}
return nil
}
53 changes: 53 additions & 0 deletions src/go/rdctl/pkg/process/process_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package process

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

// TerminateProcessInDirectory terminates all processes where the executable
// resides within the given directory, as gracefully as possible.
func TerminateProcessInDirectory(ctx context.Context, directory string) error {
// Check /proc/<pid>/exe to see if they're the correct file.
pidfds, err := os.ReadDir("/proc")
if err != nil {
return fmt.Errorf("error listing processes: %w", err)
}
for _, pidfd := range pidfds {
if !pidfd.IsDir() {
continue
}
pid, err := strconv.Atoi(pidfd.Name())
if err != nil {
continue
}
procPath, err := os.Readlink(filepath.Join("/proc", pidfd.Name(), "exe"))
if err != nil {
continue
}
relPath, err := filepath.Rel(directory, procPath)
if err != nil || strings.HasPrefix(relPath, "../") {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
err = proc.Signal(unix.SIGTERM)
if err == nil {
logrus.Infof("Terminated process %d", pid)
} else if !errors.Is(err, unix.EINVAL) {
logrus.Infof("Ignoring failure to terminate pid %d: %s", pid, err)
}
}

return nil
}
81 changes: 81 additions & 0 deletions src/go/rdctl/pkg/process/process_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package process

import (
"context"
"fmt"
"path/filepath"
"strings"
"unsafe"

"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)

// TerminateProcessInDirectory terminates all processes where the executable
// resides within the given directory, as gracefully as possible.
func TerminateProcessInDirectory(ctx context.Context, directory string) error {
pids := make([]uint32, 4096)
// Try EnumProcesses until the number of pids returned is less than the
// buffer size.
for {
var bytesReturned uint32
err := windows.EnumProcesses(pids, &bytesReturned)
if err != nil || len(pids) < 1 {
return fmt.Errorf("failed to enumerate processes: %w", err)
}
pidsReturned := uintptr(bytesReturned) / unsafe.Sizeof(pids[0])
if pidsReturned < uintptr(len(pids)) {
// Remember to truncate the pids to only the valid set.
pids = pids[:pidsReturned]
break
}
pids = make([]uint32, len(pids)*2)
}

for _, pid := range pids {
// Do each iteration in a function so defer statements run faster.
err := (func() error {
hProc, err := windows.OpenProcess(
windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_TERMINATE,
false,
pid)
if err != nil {
logrus.Infof("Ignoring error opening process %d: %s", pid, err)
return nil
}
defer func() {
_ = windows.CloseHandle(hProc)
}()

nameBuf := make([]uint16, 1024)
for {
bufSize := uint32(len(nameBuf))
err = windows.QueryFullProcessImageName(hProc, 0, &nameBuf[0], &bufSize)
if err != nil {
return fmt.Errorf("error getting process %d executable: %w", pid, err)
}
if int(bufSize) < len(nameBuf) {
break
}
nameBuf = make([]uint16, len(nameBuf)*2)
}
executablePath := windows.UTF16ToString(nameBuf)

relPath, err := filepath.Rel(directory, executablePath)
if err != nil || strings.HasPrefix(relPath, "../") {
return nil
}

if err = windows.TerminateProcess(hProc, 0); err != nil {
return fmt.Errorf("failed to terminate pid %d (%s): %w", pid, executablePath, err)
}

return nil
})()
if err != nil {
logrus.Errorf("%s", err)
}
}

return nil
}

0 comments on commit ad1d81b

Please sign in to comment.