Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/shell.go
1645 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"os"
11
"os/exec"
12
"runtime"
13
"strconv"
14
"strings"
15
16
"al.essio.dev/pkg/shellescape"
17
"github.com/coreos/go-semver/semver"
18
"github.com/lima-vm/sshocker/pkg/ssh"
19
"github.com/mattn/go-isatty"
20
"github.com/sirupsen/logrus"
21
"github.com/spf13/cobra"
22
23
"github.com/lima-vm/lima/v2/pkg/envutil"
24
"github.com/lima-vm/lima/v2/pkg/instance"
25
"github.com/lima-vm/lima/v2/pkg/ioutilx"
26
"github.com/lima-vm/lima/v2/pkg/limatype"
27
networks "github.com/lima-vm/lima/v2/pkg/networks/reconcile"
28
"github.com/lima-vm/lima/v2/pkg/sshutil"
29
"github.com/lima-vm/lima/v2/pkg/store"
30
)
31
32
const shellHelp = `Execute shell in Lima
33
34
lima command is provided as an alias for limactl shell $LIMA_INSTANCE. $LIMA_INSTANCE defaults to "` + DefaultInstanceName + `".
35
36
By default, the first 'ssh' executable found in the host's PATH is used to connect to the Lima instance.
37
A custom ssh alias can be used instead by setting the $` + sshutil.EnvShellSSH + ` environment variable.
38
39
Environment Variables:
40
--preserve-env: Propagates host environment variables to the guest instance.
41
Use LIMA_SHELLENV_ALLOW to specify which variables to allow.
42
Use LIMA_SHELLENV_BLOCK to specify which variables to block (extends default blocklist with +).
43
44
Hint: try --debug to show the detailed logs, if it seems hanging (mostly due to some SSH issue).
45
`
46
47
func newShellCommand() *cobra.Command {
48
shellCmd := &cobra.Command{
49
Use: "shell [flags] INSTANCE [COMMAND...]",
50
Short: "Execute shell in Lima",
51
Long: shellHelp,
52
Args: WrapArgsError(cobra.MinimumNArgs(1)),
53
RunE: shellAction,
54
ValidArgsFunction: shellBashComplete,
55
SilenceErrors: true,
56
GroupID: basicCommand,
57
}
58
59
shellCmd.Flags().SetInterspersed(false)
60
61
shellCmd.Flags().String("shell", "", "Shell interpreter, e.g. /bin/bash")
62
shellCmd.Flags().String("workdir", "", "Working directory")
63
shellCmd.Flags().Bool("reconnect", false, "Reconnect to the SSH session")
64
shellCmd.Flags().Bool("preserve-env", false, "Propagate environment variables to the shell")
65
return shellCmd
66
}
67
68
func shellAction(cmd *cobra.Command, args []string) error {
69
ctx := cmd.Context()
70
// simulate the behavior of double dash
71
newArg := []string{}
72
if len(args) >= 2 && args[1] == "--" {
73
newArg = append(newArg, args[:1]...)
74
newArg = append(newArg, args[2:]...)
75
args = newArg
76
}
77
instName := args[0]
78
79
if len(args) >= 2 {
80
switch args[1] {
81
case "create", "start", "delete", "shell":
82
// `lima start` (alias of `limactl $LIMA_INSTANCE start`) is probably a typo of `limactl start`
83
logrus.Warnf("Perhaps you meant `limactl %s`?", strings.Join(args[1:], " "))
84
}
85
}
86
87
inst, err := store.Inspect(ctx, instName)
88
if err != nil {
89
if errors.Is(err, os.ErrNotExist) {
90
return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
91
}
92
return err
93
}
94
if inst.Status == limatype.StatusStopped {
95
startNow, err := askWhetherToStart()
96
if err != nil {
97
return err
98
}
99
100
if !startNow {
101
return nil
102
}
103
104
err = networks.Reconcile(ctx, inst.Name)
105
if err != nil {
106
return err
107
}
108
109
err = instance.Start(ctx, inst, "", false, false)
110
if err != nil {
111
return err
112
}
113
114
inst, err = store.Inspect(ctx, instName)
115
if err != nil {
116
return err
117
}
118
}
119
120
restart, err := cmd.Flags().GetBool("reconnect")
121
if err != nil {
122
return err
123
}
124
if restart {
125
logrus.Infof("Exiting ssh session for the instance %q", instName)
126
127
sshConfig := &ssh.SSHConfig{
128
ConfigFile: inst.SSHConfigFile,
129
Persist: false,
130
AdditionalArgs: []string{},
131
}
132
133
if err := ssh.ExitMaster(inst.Hostname, inst.SSHLocalPort, sshConfig); err != nil {
134
return err
135
}
136
}
137
138
// When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error.
139
//
140
// changeDirCmd := "cd workDir || exit 1" if workDir != ""
141
// := "cd hostCurrentDir || cd hostHomeDir" if workDir == ""
142
var changeDirCmd string
143
workDir, err := cmd.Flags().GetString("workdir")
144
if err != nil {
145
return err
146
}
147
if workDir != "" {
148
changeDirCmd = fmt.Sprintf("cd %s || exit 1", shellescape.Quote(workDir))
149
// FIXME: check whether y.Mounts contains the home, not just len > 0
150
} else if len(inst.Config.Mounts) > 0 || inst.VMType == limatype.WSL2 {
151
hostCurrentDir, err := os.Getwd()
152
if err == nil && runtime.GOOS == "windows" {
153
hostCurrentDir, err = mountDirFromWindowsDir(ctx, inst, hostCurrentDir)
154
}
155
if err == nil {
156
changeDirCmd = fmt.Sprintf("cd %s", shellescape.Quote(hostCurrentDir))
157
} else {
158
changeDirCmd = "false"
159
logrus.WithError(err).Warn("failed to get the current directory")
160
}
161
hostHomeDir, err := os.UserHomeDir()
162
if err == nil && runtime.GOOS == "windows" {
163
hostHomeDir, err = mountDirFromWindowsDir(ctx, inst, hostHomeDir)
164
}
165
if err == nil {
166
changeDirCmd = fmt.Sprintf("%s || cd %s", changeDirCmd, shellescape.Quote(hostHomeDir))
167
} else {
168
logrus.WithError(err).Warn("failed to get the home directory")
169
}
170
} else {
171
logrus.Debug("the host home does not seem mounted, so the guest shell will have a different cwd")
172
}
173
174
if changeDirCmd == "" {
175
changeDirCmd = "false"
176
}
177
logrus.Debugf("changeDirCmd=%q", changeDirCmd)
178
179
shell, err := cmd.Flags().GetString("shell")
180
if err != nil {
181
return err
182
}
183
if shell == "" {
184
shell = `"$SHELL"`
185
} else {
186
shell = shellescape.Quote(shell)
187
}
188
// Handle environment variable propagation
189
var envPrefix string
190
preserveEnv, err := cmd.Flags().GetBool("preserve-env")
191
if err != nil {
192
return err
193
}
194
if preserveEnv {
195
filteredEnv := envutil.FilterEnvironment()
196
if len(filteredEnv) > 0 {
197
envPrefix = "env "
198
for _, envVar := range filteredEnv {
199
envPrefix += shellescape.Quote(envVar) + " "
200
}
201
}
202
}
203
204
script := fmt.Sprintf("%s ; exec %s%s --login", changeDirCmd, envPrefix, shell)
205
if len(args) > 1 {
206
quotedArgs := make([]string, len(args[1:]))
207
parsingEnv := true
208
for i, arg := range args[1:] {
209
if parsingEnv && isEnv(arg) {
210
quotedArgs[i] = quoteEnv(arg)
211
} else {
212
parsingEnv = false
213
quotedArgs[i] = shellescape.Quote(arg)
214
}
215
}
216
script += fmt.Sprintf(
217
" -c %s",
218
shellescape.Quote(strings.Join(quotedArgs, " ")),
219
)
220
}
221
222
sshExe, err := sshutil.NewSSHExe()
223
if err != nil {
224
return err
225
}
226
227
sshOpts, err := sshutil.SSHOpts(
228
ctx,
229
sshExe,
230
inst.Dir,
231
*inst.Config.User.Name,
232
*inst.Config.SSH.LoadDotSSHPubKeys,
233
*inst.Config.SSH.ForwardAgent,
234
*inst.Config.SSH.ForwardX11,
235
*inst.Config.SSH.ForwardX11Trusted)
236
if err != nil {
237
return err
238
}
239
sshArgs := append([]string{}, sshExe.Args...)
240
sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
241
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
242
// required for showing the shell prompt: https://stackoverflow.com/a/626574
243
sshArgs = append(sshArgs, "-t")
244
}
245
if _, present := os.LookupEnv("COLORTERM"); present {
246
// SendEnv config is cumulative, with already existing options in ssh_config
247
sshArgs = append(sshArgs, "-o", "SendEnv=COLORTERM")
248
}
249
logLevel := "ERROR"
250
// For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to
251
// avoid the "Shared connection to 127.0.0.1 closed." message with -t.
252
olderSSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.9.0"))
253
if olderSSH {
254
logLevel = "QUIET"
255
}
256
sshArgs = append(sshArgs, []string{
257
"-o", fmt.Sprintf("LogLevel=%s", logLevel),
258
"-p", strconv.Itoa(inst.SSHLocalPort),
259
inst.SSHAddress,
260
"--",
261
script,
262
}...)
263
sshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)
264
sshCmd.Stdin = os.Stdin
265
sshCmd.Stdout = os.Stdout
266
sshCmd.Stderr = os.Stderr
267
logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args)
268
269
// TODO: use syscall.Exec directly (results in losing tty?)
270
return sshCmd.Run()
271
}
272
273
func mountDirFromWindowsDir(ctx context.Context, inst *limatype.Instance, dir string) (string, error) {
274
if inst.VMType == limatype.WSL2 {
275
distroName := "lima-" + inst.Name
276
return ioutilx.WindowsSubsystemPathForLinux(ctx, dir, distroName)
277
}
278
return ioutilx.WindowsSubsystemPath(ctx, dir)
279
}
280
281
func shellBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
282
return bashCompleteInstanceNames(cmd)
283
}
284
285
func isEnv(arg string) bool {
286
return len(strings.Split(arg, "=")) > 1
287
}
288
289
func quoteEnv(arg string) string {
290
env := strings.SplitN(arg, "=", 2)
291
env[1] = shellescape.Quote(env[1])
292
return strings.Join(env, "=")
293
}
294
295