Skip to content

Commit 318cf67

Browse files
committed
rdctl: refactor rdctl shell
This moves the bulk of the code into a separate package so that it can be reused elsewhere. It returns the command without executing so that it can be further customized by the caller. Signed-off-by: Mark Yen <[email protected]>
1 parent 866466d commit 318cf67

File tree

2 files changed

+167
-150
lines changed

2 files changed

+167
-150
lines changed

src/go/rdctl/cmd/shell.go

Lines changed: 2 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,11 @@ limitations under the License.
1717
package cmd
1818

1919
import (
20-
"bytes"
21-
"context"
22-
"fmt"
2320
"os"
24-
"os/exec"
25-
"path/filepath"
26-
"regexp"
27-
"runtime"
28-
"slices"
29-
"strings"
3021

31-
"github.com/sirupsen/logrus"
3222
"github.com/spf13/cobra"
33-
"golang.org/x/text/encoding/unicode"
3423

35-
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories"
36-
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lima"
37-
p "github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths"
38-
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils"
39-
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl"
24+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/shell"
4025
)
4126

4227
// shellCmd represents the shell command
@@ -69,145 +54,12 @@ func init() {
6954
func doShellCommand(cmd *cobra.Command, args []string) error {
7055
cmd.SilenceUsage = true
7156

72-
commandName, err := directories.GetLimactlPath()
57+
shellCommand, err := shell.SpawnCommand(cmd.Context(), args...)
7358
if err != nil {
7459
return err
7560
}
76-
77-
if runtime.GOOS == "windows" {
78-
distroNames := []string{wsl.DistributionName}
79-
found := false
80-
81-
if _, err = os.Stat(commandName); err == nil {
82-
// If limactl is available, try the lima distribution first.
83-
distroNames = append([]string{lima.InstanceFullName}, distroNames...)
84-
}
85-
86-
for _, distroName := range distroNames {
87-
err = assertWSLIsRunning(cmd.Context(), distroName)
88-
if err == nil {
89-
commandName = "wsl"
90-
args = append([]string{
91-
"--distribution", distroName,
92-
"--exec", "/usr/local/bin/wsl-exec",
93-
}, args...)
94-
found = true
95-
break
96-
}
97-
}
98-
99-
if !found {
100-
// We did not find a running distribution that we can use.
101-
// No further output wanted, so just exit with the desired status.
102-
fmt.Fprintf(os.Stderr, "%s", err)
103-
os.Exit(1)
104-
}
105-
} else {
106-
paths, err := p.GetPaths()
107-
if err != nil {
108-
return err
109-
}
110-
if err := directories.SetupLimaHome(paths.AppHome); err != nil {
111-
return err
112-
}
113-
if err := setupPathEnvVar(paths); err != nil {
114-
return err
115-
}
116-
if !checkLimaIsRunning(cmd.Context(), commandName) {
117-
// No further output wanted, so just exit with the desired status.
118-
os.Exit(1)
119-
}
120-
args = append([]string{"shell", lima.InstanceName}, args...)
121-
}
122-
shellCommand := exec.CommandContext(cmd.Context(), commandName, args...)
12361
shellCommand.Stdin = os.Stdin
12462
shellCommand.Stdout = os.Stdout
12563
shellCommand.Stderr = os.Stderr
12664
return shellCommand.Run()
12765
}
128-
129-
// Set up the PATH environment variable for limactl.
130-
func setupPathEnvVar(paths *p.Paths) error {
131-
if runtime.GOOS != "windows" {
132-
// This is only needed on Windows.
133-
return nil
134-
}
135-
msysDir := filepath.Join(utils.GetParentDir(paths.Resources, 2), "msys")
136-
pathList := filepath.SplitList(os.Getenv("PATH"))
137-
if slices.Contains(pathList, msysDir) {
138-
return nil
139-
}
140-
pathList = append([]string{msysDir}, pathList...)
141-
return os.Setenv("PATH", strings.Join(pathList, string(os.PathListSeparator)))
142-
}
143-
144-
const restartDirective = "Either run 'rdctl start' or start the Rancher Desktop application first"
145-
146-
func checkLimaIsRunning(ctx context.Context, commandName string) bool {
147-
var stdout bytes.Buffer
148-
var stderr bytes.Buffer
149-
150-
//nolint:gosec // The command name is auto-detected, and the instance name is constant.
151-
cmd := exec.CommandContext(ctx, commandName, "ls", lima.InstanceName, "--format", "{{.Status}}")
152-
cmd.Stdout = &stdout
153-
cmd.Stderr = &stderr
154-
if err := cmd.Run(); err != nil {
155-
logrus.Errorf("Failed to run %q: %s\n", cmd, err)
156-
return false
157-
}
158-
limaState := strings.TrimRight(stdout.String(), "\n")
159-
// We can do an equals check here because we should only have received the status for VM 0
160-
if limaState == "Running" {
161-
return true
162-
}
163-
if limaState != "" {
164-
fmt.Fprintf(os.Stderr,
165-
"The Rancher Desktop VM needs to be in state \"Running\" in order to execute 'rdctl shell', but it is currently in state %q.\n%s.\n", limaState, restartDirective)
166-
return false
167-
}
168-
errorMsg := stderr.String()
169-
if strings.Contains(errorMsg, fmt.Sprintf("No instance matching %s found.", lima.InstanceName)) {
170-
logrus.Errorf("The Rancher Desktop VM needs to be created.\n%s.\n", restartDirective)
171-
} else if errorMsg != "" {
172-
fmt.Fprintln(os.Stderr, errorMsg)
173-
} else {
174-
fmt.Fprintln(os.Stderr, "Underlying limactl check failed with no output.")
175-
}
176-
return false
177-
}
178-
179-
// Check that WSL is running the given distribution; if not, an error will be
180-
// returned with a message suitable for printing to the user.
181-
func assertWSLIsRunning(ctx context.Context, distroName string) error {
182-
// Ignore error messages; none are expected here
183-
rawOutput, err := exec.CommandContext(ctx, "wsl", "--list", "--verbose").CombinedOutput()
184-
if err != nil {
185-
return fmt.Errorf("failed to run `wsl --list --verbose: %w", err)
186-
}
187-
decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
188-
output, err := decoder.Bytes(rawOutput)
189-
if err != nil {
190-
return fmt.Errorf("failed to read WSL output ([% q]...); error: %w", rawOutput[:12], err)
191-
}
192-
isListed := false
193-
targetState := ""
194-
for _, line := range regexp.MustCompile(`\r?\n`).Split(string(output), -1) {
195-
fields := regexp.MustCompile(`\s+`).Split(strings.TrimLeft(line, " \t"), -1)
196-
if fields[0] == "*" {
197-
fields = fields[1:]
198-
}
199-
if len(fields) >= 2 && fields[0] == distroName {
200-
isListed = true
201-
targetState = fields[1]
202-
break
203-
}
204-
}
205-
const desiredState = "Running"
206-
if targetState == desiredState {
207-
return nil
208-
}
209-
if !isListed {
210-
return fmt.Errorf("the Rancher Desktop WSL distribution needs to be running in order to execute 'rdctl shell', but it currently is not.\n%s", restartDirective)
211-
}
212-
return fmt.Errorf("the Rancher Desktop WSL distribution needs to be in state %q in order to execute 'rdctl shell', but it is currently in state %q.\n%s", desiredState, targetState, restartDirective)
213-
}

src/go/rdctl/pkg/shell/shell.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package shell
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"regexp"
11+
"runtime"
12+
"slices"
13+
"strings"
14+
15+
"github.com/sirupsen/logrus"
16+
"golang.org/x/text/encoding/unicode"
17+
18+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories"
19+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lima"
20+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths"
21+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils"
22+
"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl"
23+
)
24+
25+
// Spawn a command that, when run, will be executed in the VM with the given
26+
// arguments.
27+
func SpawnCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
28+
commandName, err := directories.GetLimactlPath()
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
if runtime.GOOS == "windows" {
34+
distroNames := []string{wsl.DistributionName}
35+
found := false
36+
37+
if _, err = os.Stat(commandName); err == nil {
38+
// If limactl is available, try the lima distribution first.
39+
distroNames = append([]string{lima.InstanceFullName}, distroNames...)
40+
}
41+
42+
for _, distroName := range distroNames {
43+
err = assertWSLIsRunning(ctx, distroName)
44+
if err == nil {
45+
commandName = "wsl"
46+
args = append([]string{
47+
"--distribution", distroName,
48+
"--exec", "/usr/local/bin/wsl-exec",
49+
}, args...)
50+
found = true
51+
break
52+
}
53+
}
54+
55+
if !found {
56+
// We did not find a running distribution that we can use.
57+
// No further output wanted, so just exit with the desired status.
58+
fmt.Fprintf(os.Stderr, "%s", err)
59+
os.Exit(1)
60+
}
61+
} else {
62+
p, err := paths.GetPaths()
63+
if err != nil {
64+
return nil, err
65+
}
66+
if err := directories.SetupLimaHome(p.AppHome); err != nil {
67+
return nil, err
68+
}
69+
if err := setupPathEnvVar(p); err != nil {
70+
return nil, err
71+
}
72+
if !checkLimaIsRunning(ctx, commandName) {
73+
// No further output wanted, so just exit with the desired status.
74+
os.Exit(1)
75+
}
76+
args = append([]string{"shell", lima.InstanceName}, args...)
77+
}
78+
return exec.CommandContext(ctx, commandName, args...), nil
79+
}
80+
81+
// Set up the PATH environment variable for limactl.
82+
func setupPathEnvVar(p *paths.Paths) error {
83+
if runtime.GOOS != "windows" {
84+
// This is only needed on Windows.
85+
return nil
86+
}
87+
msysDir := filepath.Join(utils.GetParentDir(p.Resources, 2), "msys")
88+
pathList := filepath.SplitList(os.Getenv("PATH"))
89+
if slices.Contains(pathList, msysDir) {
90+
return nil
91+
}
92+
pathList = append([]string{msysDir}, pathList...)
93+
return os.Setenv("PATH", strings.Join(pathList, string(os.PathListSeparator)))
94+
}
95+
96+
const restartDirective = "Either run 'rdctl start' or start the Rancher Desktop application first"
97+
98+
func checkLimaIsRunning(ctx context.Context, commandName string) bool {
99+
var stdout bytes.Buffer
100+
var stderr bytes.Buffer
101+
102+
//nolint:gosec // The command name is auto-detected, and the instance name is constant.
103+
cmd := exec.CommandContext(ctx, commandName, "ls", lima.InstanceName, "--format", "{{.Status}}")
104+
cmd.Stdout = &stdout
105+
cmd.Stderr = &stderr
106+
if err := cmd.Run(); err != nil {
107+
logrus.Errorf("Failed to run %q: %s\n", cmd, err)
108+
return false
109+
}
110+
limaState := strings.TrimRight(stdout.String(), "\n")
111+
// We can do an equals check here because we should only have received the status for VM 0
112+
if limaState == "Running" {
113+
return true
114+
}
115+
if limaState != "" {
116+
fmt.Fprintf(os.Stderr,
117+
"The Rancher Desktop VM needs to be in state \"Running\" in order to execute 'rdctl shell', but it is currently in state %q.\n%s.\n", limaState, restartDirective)
118+
return false
119+
}
120+
errorMsg := stderr.String()
121+
if strings.Contains(errorMsg, fmt.Sprintf("No instance matching %s found.", lima.InstanceName)) {
122+
logrus.Errorf("The Rancher Desktop VM needs to be created.\n%s.\n", restartDirective)
123+
} else if errorMsg != "" {
124+
fmt.Fprintln(os.Stderr, errorMsg)
125+
} else {
126+
fmt.Fprintln(os.Stderr, "Underlying limactl check failed with no output.")
127+
}
128+
return false
129+
}
130+
131+
// Check that WSL is running the given distribution; if not, an error will be
132+
// returned with a message suitable for printing to the user.
133+
func assertWSLIsRunning(ctx context.Context, distroName string) error {
134+
// Ignore error messages; none are expected here
135+
rawOutput, err := exec.CommandContext(ctx, "wsl", "--list", "--verbose").CombinedOutput()
136+
if err != nil {
137+
return fmt.Errorf("failed to run `wsl --list --verbose: %w", err)
138+
}
139+
decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
140+
output, err := decoder.Bytes(rawOutput)
141+
if err != nil {
142+
return fmt.Errorf("failed to read WSL output ([% q]...); error: %w", rawOutput[:12], err)
143+
}
144+
isListed := false
145+
targetState := ""
146+
for _, line := range regexp.MustCompile(`\r?\n`).Split(string(output), -1) {
147+
fields := regexp.MustCompile(`\s+`).Split(strings.TrimLeft(line, " \t"), -1)
148+
if fields[0] == "*" {
149+
fields = fields[1:]
150+
}
151+
if len(fields) >= 2 && fields[0] == distroName {
152+
isListed = true
153+
targetState = fields[1]
154+
break
155+
}
156+
}
157+
const desiredState = "Running"
158+
if targetState == desiredState {
159+
return nil
160+
}
161+
if !isListed {
162+
return fmt.Errorf("the Rancher Desktop WSL distribution needs to be running in order to execute 'rdctl shell', but it currently is not.\n%s", restartDirective)
163+
}
164+
return fmt.Errorf("the Rancher Desktop WSL distribution needs to be in state %q in order to execute 'rdctl shell', but it is currently in state %q.\n%s", desiredState, targetState, restartDirective)
165+
}

0 commit comments

Comments
 (0)