Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/watch.go
2608 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
"encoding/json"
9
"fmt"
10
"io"
11
"os"
12
"path/filepath"
13
"strings"
14
"sync"
15
"time"
16
17
"github.com/rjeczalik/notify"
18
"github.com/sirupsen/logrus"
19
"github.com/spf13/cobra"
20
21
"github.com/lima-vm/lima/v2/pkg/hostagent/events"
22
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
23
"github.com/lima-vm/lima/v2/pkg/store"
24
)
25
26
func newWatchCommand() *cobra.Command {
27
watchCommand := &cobra.Command{
28
Use: "watch [INSTANCE]...",
29
Short: "Watch events from instances",
30
Long: `Watch events from Lima instances.
31
32
Events include status changes (starting, running, stopping), port forwarding
33
events, and other instance lifecycle events.
34
35
If no instance is specified, events from all instances are watched,
36
including newly created instances.
37
38
The command will continue watching until interrupted (Ctrl+C).`,
39
Example: ` # Watch events from all instances:
40
$ limactl watch
41
42
# Watch events from a specific instance:
43
$ limactl watch default
44
45
# Include historical events:
46
$ limactl watch --history default
47
48
# Show verbose output (host agent logs, etc.):
49
$ limactl watch --verbose
50
51
# Watch events in JSON format (for scripting):
52
$ limactl watch --json default`,
53
Args: WrapArgsError(cobra.ArbitraryArgs),
54
RunE: watchAction,
55
ValidArgsFunction: watchBashComplete,
56
GroupID: advancedCommand,
57
}
58
watchCommand.Flags().Bool("json", false, "Output events as newline-delimited JSON")
59
watchCommand.Flags().Bool("history", false, "Include historical events from before watch started")
60
watchCommand.Flags().Bool("verbose", false, "Show verbose output")
61
return watchCommand
62
}
63
64
type watchEvent struct {
65
Instance string `json:"instance"`
66
Event events.Event `json:"event"`
67
}
68
69
type eventWatcher struct {
70
ctx context.Context
71
begin time.Time
72
propagateStderr bool
73
eventCh chan watchEvent
74
watching sync.Map
75
}
76
77
func (w *eventWatcher) startInstance(instName string) {
78
if _, loaded := w.watching.LoadOrStore(instName, true); loaded {
79
return
80
}
81
82
inst, err := store.Inspect(w.ctx, instName)
83
if err != nil {
84
logrus.WithError(err).Warnf("Failed to inspect instance %q", instName)
85
w.watching.Delete(instName)
86
return
87
}
88
89
haStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog)
90
haStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog)
91
92
go w.watchInstance(instName, haStdoutPath, haStderrPath)
93
}
94
95
func (w *eventWatcher) watchInstance(instName, haStdoutPath, haStderrPath string) {
96
err := events.Watch(w.ctx, haStdoutPath, haStderrPath, w.begin, w.propagateStderr, func(ev events.Event) bool {
97
select {
98
case w.eventCh <- watchEvent{Instance: instName, Event: ev}:
99
case <-w.ctx.Done():
100
return true
101
}
102
return false
103
})
104
if err != nil && w.ctx.Err() == nil {
105
logrus.WithError(err).Warnf("Watcher for instance %q stopped", instName)
106
}
107
}
108
109
func watchAction(cmd *cobra.Command, args []string) error {
110
ctx := cmd.Context()
111
112
jsonFormat, err := cmd.Flags().GetBool("json")
113
if err != nil {
114
return err
115
}
116
history, err := cmd.Flags().GetBool("history")
117
if err != nil {
118
return err
119
}
120
verbose, err := cmd.Flags().GetBool("verbose")
121
if err != nil {
122
return err
123
}
124
125
if !verbose {
126
logrus.SetLevel(logrus.WarnLevel)
127
}
128
129
var begin time.Time
130
if !history {
131
begin = time.Now()
132
}
133
134
stdout := cmd.OutOrStdout()
135
stderr := cmd.ErrOrStderr()
136
watchAll := len(args) == 0
137
138
var instNames []string
139
if watchAll {
140
instNames, err = store.Instances()
141
if err != nil {
142
return err
143
}
144
if len(instNames) == 0 {
145
printStatus(stderr, "No instances found")
146
}
147
} else {
148
instNames = args
149
}
150
151
newInstanceCh := make(chan string, 16)
152
w := &eventWatcher{
153
ctx: ctx,
154
begin: begin,
155
propagateStderr: verbose,
156
eventCh: make(chan watchEvent, 64),
157
}
158
159
for _, instName := range instNames {
160
w.startInstance(instName)
161
}
162
163
if watchAll {
164
go watchLimaDir(ctx, newInstanceCh)
165
}
166
167
printStatus(stderr, "Watching for events...")
168
169
for {
170
select {
171
case <-ctx.Done():
172
return nil
173
case instName := <-newInstanceCh:
174
printStatus(stderr, "New instance detected: "+instName)
175
w.startInstance(instName)
176
case ev := <-w.eventCh:
177
if jsonFormat {
178
j, err := json.Marshal(ev)
179
if err != nil {
180
fmt.Fprintf(stderr, "error marshaling event: %v\n", err)
181
continue
182
}
183
fmt.Fprintln(stdout, string(j))
184
} else {
185
printHumanReadableEvent(stdout, ev.Instance, ev.Event)
186
}
187
}
188
}
189
}
190
191
func watchLimaDir(ctx context.Context, newInstanceCh chan<- string) {
192
limaDir := store.Directory()
193
if limaDir == "" {
194
logrus.Warn("Could not determine lima directory")
195
return
196
}
197
198
fsEvents := make(chan notify.EventInfo, 128)
199
if err := notify.Watch(limaDir, fsEvents, notify.Create); err != nil {
200
logrus.WithError(err).Warn("Failed to watch lima directory for new instances")
201
return
202
}
203
defer notify.Stop(fsEvents)
204
205
for {
206
select {
207
case <-ctx.Done():
208
return
209
case ev := <-fsEvents:
210
name := filepath.Base(ev.Path())
211
if !isValidInstanceName(name) {
212
continue
213
}
214
if !isInstanceDir(ev.Path()) {
215
continue
216
}
217
select {
218
case newInstanceCh <- name:
219
case <-ctx.Done():
220
return
221
}
222
}
223
}
224
}
225
226
func isValidInstanceName(name string) bool {
227
return !strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "_")
228
}
229
230
func isInstanceDir(path string) bool {
231
info, err := os.Stat(path)
232
if err != nil || !info.IsDir() {
233
return false
234
}
235
yamlPath := filepath.Join(path, filenames.LimaYAML)
236
_, err = os.Stat(yamlPath)
237
return err == nil
238
}
239
240
func printStatus(out io.Writer, msg string) {
241
fmt.Fprintf(out, "%s %s\n", time.Now().Format("2006-01-02 15:04:05"), msg)
242
}
243
244
func printHumanReadableEvent(out io.Writer, instName string, ev events.Event) {
245
timestamp := ev.Time.Format("2006-01-02 15:04:05")
246
247
printEvent := func(msg string) {
248
fmt.Fprintf(out, "%s %s | %s\n", timestamp, instName, msg)
249
}
250
251
if ev.Status.Running {
252
if ev.Status.Degraded {
253
printEvent("running (degraded)")
254
} else {
255
printEvent("running")
256
}
257
}
258
if ev.Status.Exiting {
259
printEvent("exiting")
260
}
261
if ev.Status.SSHLocalPort != 0 {
262
printEvent(fmt.Sprintf("ssh available on port %d", ev.Status.SSHLocalPort))
263
}
264
for _, e := range ev.Status.Errors {
265
printEvent(fmt.Sprintf("error: %s", e))
266
}
267
if ev.Status.CloudInitProgress != nil {
268
if ev.Status.CloudInitProgress.Completed {
269
printEvent("cloud-init completed")
270
} else if ev.Status.CloudInitProgress.LogLine != "" {
271
printEvent(fmt.Sprintf("cloud-init: %s", ev.Status.CloudInitProgress.LogLine))
272
}
273
}
274
if ev.Status.PortForward != nil {
275
pf := ev.Status.PortForward
276
switch pf.Type {
277
case events.PortForwardEventForwarding:
278
printEvent(fmt.Sprintf("forwarding %s %s to %s", pf.Protocol, pf.GuestAddr, pf.HostAddr))
279
case events.PortForwardEventNotForwarding:
280
printEvent(fmt.Sprintf("not forwarding %s %s", pf.Protocol, pf.GuestAddr))
281
case events.PortForwardEventStopping:
282
printEvent(fmt.Sprintf("stopping forwarding %s %s", pf.Protocol, pf.GuestAddr))
283
case events.PortForwardEventFailed:
284
printEvent(fmt.Sprintf("failed to forward %s %s: %s", pf.Protocol, pf.GuestAddr, pf.Error))
285
}
286
}
287
if ev.Status.Vsock != nil {
288
vs := ev.Status.Vsock
289
switch vs.Type {
290
case events.VsockEventStarted:
291
printEvent(fmt.Sprintf("started vsock forwarder: %s -> vsock:%d", vs.HostAddr, vs.VsockPort))
292
case events.VsockEventSkipped:
293
printEvent(fmt.Sprintf("skipped vsock forwarder: %s", vs.Reason))
294
case events.VsockEventFailed:
295
printEvent(fmt.Sprintf("failed to start vsock forwarder: %s", vs.Reason))
296
}
297
}
298
}
299
300
func watchBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
301
return bashCompleteInstanceNames(cmd)
302
}
303
304