Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/driver/wsl2/vm_windows.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package wsl2
5
6
import (
7
"context"
8
_ "embed"
9
"fmt"
10
"net"
11
"os"
12
"os/exec"
13
"path/filepath"
14
"regexp"
15
"strconv"
16
"strings"
17
18
"github.com/sirupsen/logrus"
19
20
"github.com/lima-vm/lima/v2/pkg/executil"
21
"github.com/lima-vm/lima/v2/pkg/limatype"
22
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
23
"github.com/lima-vm/lima/v2/pkg/textutil"
24
)
25
26
// startVM calls WSL to start a VM.
27
func startVM(ctx context.Context, distroName string) error {
28
out, err := executil.RunUTF16leCommand([]string{
29
"wsl.exe",
30
"--distribution",
31
distroName,
32
}, executil.WithContext(ctx))
33
if err != nil {
34
return fmt.Errorf("failed to run `wsl.exe --distribution %s`: %w (out=%q)",
35
distroName, err, out)
36
}
37
return nil
38
}
39
40
// initVM calls WSL to import a new VM specifically for Lima.
41
func initVM(ctx context.Context, instanceDir, distroName string) error {
42
baseDisk := filepath.Join(instanceDir, filenames.BaseDisk)
43
logrus.Infof("Importing distro from %q to %q", baseDisk, instanceDir)
44
out, err := executil.RunUTF16leCommand([]string{
45
"wsl.exe",
46
"--import",
47
distroName,
48
instanceDir,
49
baseDisk,
50
}, executil.WithContext(ctx))
51
if err != nil {
52
return fmt.Errorf("failed to run `wsl.exe --import %s %s %s`: %w (out=%q)",
53
distroName, instanceDir, baseDisk, err, out)
54
}
55
return nil
56
}
57
58
// stopVM calls WSL to stop a running VM.
59
func stopVM(ctx context.Context, distroName string) error {
60
out, err := executil.RunUTF16leCommand([]string{
61
"wsl.exe",
62
"--terminate",
63
distroName,
64
}, executil.WithContext(ctx))
65
if err != nil {
66
return fmt.Errorf("failed to run `wsl.exe --terminate %s`: %w (out=%q)",
67
distroName, err, out)
68
}
69
return nil
70
}
71
72
//go:embed lima-init.TEMPLATE
73
var limaBoot string
74
75
// provisionVM starts Lima's boot process inside an already imported VM.
76
func provisionVM(ctx context.Context, instanceDir, instanceName, distroName string, errCh chan<- error) error {
77
ciDataPath := filepath.Join(instanceDir, filenames.CIDataISODir)
78
m := map[string]string{
79
"CIDataPath": ciDataPath,
80
}
81
limaBootB, err := textutil.ExecuteTemplate(limaBoot, m)
82
if err != nil {
83
return fmt.Errorf("failed to construct wsl boot.sh script: %w", err)
84
}
85
limaBootFile, err := os.CreateTemp("", "lima-wsl2-boot-*.sh")
86
if err != nil {
87
return err
88
}
89
if _, err = limaBootFile.Write(limaBootB); err != nil {
90
limaBootFile.Close()
91
return err
92
}
93
limaBootFileWinPath := limaBootFile.Name()
94
if err = limaBootFile.Close(); err != nil {
95
return err
96
}
97
// path should be quoted and use \\ as separator
98
bootFileWSLPath := strconv.Quote(limaBootFileWinPath)
99
limaBootFilePathOnLinuxB, err := exec.CommandContext(
100
ctx,
101
"wsl.exe",
102
"-d",
103
distroName,
104
"bash",
105
"-c",
106
fmt.Sprintf("wslpath -u %s", bootFileWSLPath),
107
bootFileWSLPath,
108
).Output()
109
if err != nil {
110
os.RemoveAll(limaBootFileWinPath)
111
// this can return an error with an exit code, which causes it not to be logged
112
// because main.handleExitCoder() traps it, so wrap the error
113
return fmt.Errorf("failed to run wslpath command: %w", err)
114
}
115
limaBootFileLinuxPath := strings.TrimSpace(string(limaBootFilePathOnLinuxB))
116
go func() {
117
cmd := exec.CommandContext(
118
ctx,
119
"wsl.exe",
120
"-d",
121
distroName,
122
"bash",
123
"-c",
124
limaBootFileLinuxPath,
125
)
126
out, err := cmd.CombinedOutput()
127
os.RemoveAll(limaBootFileWinPath)
128
logrus.Debugf("%v: %q", cmd.Args, string(out))
129
if err != nil {
130
errCh <- fmt.Errorf(
131
"error running wslCommand that executes boot.sh (%v): %w, "+
132
"check /var/log/lima-init.log for more details (out=%q)", cmd.Args, err, string(out))
133
}
134
135
for {
136
<-ctx.Done()
137
logrus.Info("Context closed, stopping vm")
138
if status, err := getWslStatus(ctx, instanceName); err == nil &&
139
status == limatype.StatusRunning {
140
_ = stopVM(ctx, distroName)
141
}
142
}
143
}()
144
145
return err
146
}
147
148
// keepAlive runs a background process which in order to keep the WSL2 VM running in the background after launch.
149
func keepAlive(ctx context.Context, distroName string, errCh chan<- error) {
150
keepAliveCmd := exec.CommandContext(
151
ctx,
152
"wsl.exe",
153
"-d",
154
distroName,
155
"bash",
156
"-c",
157
"nohup sleep 2147483647d >/dev/null 2>&1",
158
)
159
160
go func() {
161
if err := keepAliveCmd.Run(); err != nil {
162
errCh <- fmt.Errorf(
163
"error running wsl keepAlive command: %w", err)
164
}
165
}()
166
}
167
168
// unregisterVM calls WSL to unregister a VM.
169
func unregisterVM(ctx context.Context, distroName string) error {
170
logrus.Info("Unregistering WSL2 VM")
171
out, err := executil.RunUTF16leCommand([]string{
172
"wsl.exe",
173
"--unregister",
174
distroName,
175
}, executil.WithContext(ctx))
176
if err != nil {
177
return fmt.Errorf("failed to run `wsl.exe --unregister %s`: %w (out=%q)",
178
distroName, err, out)
179
}
180
return nil
181
}
182
183
// GetWslStatus runs `wsl --list --verbose` and parses its output.
184
// There are several possible outputs, all listed with their whitespace preserved output below.
185
//
186
// (1) Expected output if at least one distro is installed:
187
// PS > wsl --list --verbose
188
//
189
// NAME STATE VERSION
190
//
191
// * Ubuntu Stopped 2
192
//
193
// (2) Expected output when no distros are installed, but WSL is configured properly:
194
// PS > wsl --list --verbose
195
// Windows Subsystem for Linux has no installed distributions.
196
//
197
// Use 'wsl.exe --list --online' to list available distributions
198
// and 'wsl.exe --install <Distro>' to install.
199
//
200
// Distributions can also be installed by visiting the Microsoft Store:
201
// https://aka.ms/wslstore
202
// Error code: Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND
203
//
204
// (3) Expected output when no distros are installed, and WSL2 has no kernel installed:
205
//
206
// PS > wsl --list --verbose
207
// Windows Subsystem for Linux has no installed distributions.
208
// Distributions can be installed by visiting the Microsoft Store:
209
// https://aka.ms/wslstore
210
func getWslStatus(ctx context.Context, instName string) (string, error) {
211
distroName := "lima-" + instName
212
out, err := executil.RunUTF16leCommand([]string{
213
"wsl.exe",
214
"--list",
215
"--verbose",
216
}, executil.WithContext(ctx))
217
if err != nil {
218
return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w (out=%q)", err, out)
219
}
220
221
if out == "" {
222
return limatype.StatusBroken, fmt.Errorf("failed to read instance state for instance %q, try running `wsl --list --verbose` to debug, err: %w", instName, err)
223
}
224
225
// Check for edge cases first
226
if strings.Contains(out, "Windows Subsystem for Linux has no installed distributions.") {
227
if strings.Contains(out, "Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND") {
228
return limatype.StatusBroken, fmt.Errorf(
229
"failed to read instance state for instance %q because no distro is installed,"+
230
"try running `wsl --install -d Ubuntu` and then re-running Lima", instName)
231
}
232
return limatype.StatusBroken, fmt.Errorf(
233
"failed to read instance state for instance %q because there is no WSL kernel installed,"+
234
"this usually happens when WSL was installed for another user, but never for your user."+
235
"Try running `wsl --install -d Ubuntu` and `wsl --update`, and then re-running Lima", instName)
236
}
237
238
var instState string
239
wslListColsRegex := regexp.MustCompile(`\s+`)
240
// wsl --list --verbose may have different headers depending on localization, just split by line
241
for rows := range strings.SplitSeq(strings.ReplaceAll(out, "\r\n", "\n"), "\n") {
242
cols := wslListColsRegex.Split(strings.TrimSpace(rows), -1)
243
nameIdx := 0
244
// '*' indicates default instance
245
if cols[0] == "*" {
246
nameIdx = 1
247
}
248
if cols[nameIdx] == distroName {
249
instState = cols[nameIdx+1]
250
break
251
}
252
}
253
254
if instState == "" {
255
return limatype.StatusUninitialized, nil
256
}
257
258
return instState, nil
259
}
260
261
// GetSSHAddress runs a hostname command to get the IP from inside of a wsl2 VM.
262
//
263
// Expected output (whitespace preserved, [] for optional):
264
// PS > wsl -d <distroName> bash -c hostname -I | cut -d' ' -f1
265
// 168.1.1.1 [10.0.0.1]
266
// But busybox hostname does not implement --all-ip-addresses:
267
// hostname: unrecognized option: I
268
func getSSHAddress(ctx context.Context, instName string) (string, error) {
269
distroName := "lima-" + instName
270
// Ubuntu
271
cmd := exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`)
272
out, err := cmd.CombinedOutput()
273
if err == nil {
274
return strings.TrimSpace(string(out)), nil
275
}
276
// Alpine
277
cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "sh", "-c", `ip route get 1 | awk '{gsub("^.*src ",""); print $1; exit}'`)
278
out, err = cmd.CombinedOutput()
279
if err == nil {
280
return strings.TrimSpace(string(out)), nil
281
}
282
// fallback
283
cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "hostname", "-i")
284
out, err = cmd.CombinedOutput()
285
if err == nil {
286
ip := net.ParseIP(strings.TrimSpace(string(out)))
287
// some distributions use "127.0.1.1" as the host IP, but we want something that we can route to here
288
if ip != nil && !ip.IsLoopback() {
289
return strings.TrimSpace(string(out)), nil
290
}
291
}
292
return "", fmt.Errorf("failed to get hostname for instance %q, err: %w (out=%q)", instName, err, string(out))
293
}
294
295