|
| 1 | +// SPDX-FileCopyrightText: Copyright The Lima Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +package main |
| 5 | + |
| 6 | +import ( |
| 7 | + "encoding/json" |
| 8 | + "fmt" |
| 9 | + "path/filepath" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/sirupsen/logrus" |
| 13 | + "github.com/spf13/cobra" |
| 14 | + |
| 15 | + "github.com/lima-vm/lima/v2/pkg/hostagent/events" |
| 16 | + "github.com/lima-vm/lima/v2/pkg/limatype" |
| 17 | + "github.com/lima-vm/lima/v2/pkg/limatype/filenames" |
| 18 | + "github.com/lima-vm/lima/v2/pkg/store" |
| 19 | +) |
| 20 | + |
| 21 | +func newWatchCommand() *cobra.Command { |
| 22 | + watchCommand := &cobra.Command{ |
| 23 | + Use: "watch [INSTANCE]...", |
| 24 | + Short: "Watch events from instances", |
| 25 | + Long: `Watch events from Lima instances. |
| 26 | +
|
| 27 | +Events include status changes (starting, running, stopping), port forwarding |
| 28 | +events, and other instance lifecycle events. |
| 29 | +
|
| 30 | +If no instance is specified, events from all running instances are watched. |
| 31 | +
|
| 32 | +The command will continue watching until interrupted (Ctrl+C).`, |
| 33 | + Example: ` # Watch events from all instances: |
| 34 | + $ limactl watch |
| 35 | +
|
| 36 | + # Watch events from a specific instance: |
| 37 | + $ limactl watch default |
| 38 | +
|
| 39 | + # Watch events in JSON format (for scripting): |
| 40 | + $ limactl watch --json default`, |
| 41 | + Args: WrapArgsError(cobra.ArbitraryArgs), |
| 42 | + RunE: watchAction, |
| 43 | + ValidArgsFunction: watchBashComplete, |
| 44 | + GroupID: advancedCommand, |
| 45 | + } |
| 46 | + watchCommand.Flags().Bool("json", false, "Output events as newline-delimited JSON") |
| 47 | + return watchCommand |
| 48 | +} |
| 49 | + |
| 50 | +// watchEvent wraps an event with its instance name for JSON output. |
| 51 | +type watchEvent struct { |
| 52 | + Instance string `json:"instance"` |
| 53 | + Event events.Event `json:"event"` |
| 54 | +} |
| 55 | + |
| 56 | +func watchAction(cmd *cobra.Command, args []string) error { |
| 57 | + ctx := cmd.Context() |
| 58 | + |
| 59 | + jsonFormat, err := cmd.Flags().GetBool("json") |
| 60 | + if err != nil { |
| 61 | + return err |
| 62 | + } |
| 63 | + |
| 64 | + // Determine which instances to watch |
| 65 | + var instNames []string |
| 66 | + if len(args) > 0 { |
| 67 | + instNames = args |
| 68 | + } else { |
| 69 | + // Watch all instances |
| 70 | + allInstances, err := store.Instances() |
| 71 | + if err != nil { |
| 72 | + return err |
| 73 | + } |
| 74 | + if len(allInstances) == 0 { |
| 75 | + logrus.Warn("No instances found.") |
| 76 | + return nil |
| 77 | + } |
| 78 | + instNames = allInstances |
| 79 | + } |
| 80 | + |
| 81 | + // Validate instances and collect their log paths |
| 82 | + type instanceInfo struct { |
| 83 | + name string |
| 84 | + haStdoutPath string |
| 85 | + haStderrPath string |
| 86 | + } |
| 87 | + var instances []instanceInfo |
| 88 | + |
| 89 | + for _, instName := range instNames { |
| 90 | + inst, err := store.Inspect(ctx, instName) |
| 91 | + if err != nil { |
| 92 | + return err |
| 93 | + } |
| 94 | + if inst.Status != limatype.StatusRunning { |
| 95 | + logrus.Warnf("Instance %q is not running (status: %s). Watching for events anyway...", instName, inst.Status) |
| 96 | + } |
| 97 | + instances = append(instances, instanceInfo{ |
| 98 | + name: instName, |
| 99 | + haStdoutPath: filepath.Join(inst.Dir, filenames.HostAgentStdoutLog), |
| 100 | + haStderrPath: filepath.Join(inst.Dir, filenames.HostAgentStderrLog), |
| 101 | + }) |
| 102 | + } |
| 103 | + |
| 104 | + // If only one instance, watch it directly |
| 105 | + if len(instances) == 1 { |
| 106 | + inst := instances[0] |
| 107 | + return events.Watch(ctx, inst.haStdoutPath, inst.haStderrPath, time.Now(), !jsonFormat, func(ev events.Event) bool { |
| 108 | + if jsonFormat { |
| 109 | + we := watchEvent{Instance: inst.name, Event: ev} |
| 110 | + j, err := json.Marshal(we) |
| 111 | + if err != nil { |
| 112 | + fmt.Fprintf(cmd.ErrOrStderr(), "error marshaling event: %v\n", err) |
| 113 | + return false |
| 114 | + } |
| 115 | + fmt.Fprintln(cmd.OutOrStdout(), string(j)) |
| 116 | + } else { |
| 117 | + printHumanReadableEvent(cmd, inst.name, ev) |
| 118 | + } |
| 119 | + return false |
| 120 | + }) |
| 121 | + } |
| 122 | + |
| 123 | + // Watch multiple instances concurrently |
| 124 | + type eventWithInstance struct { |
| 125 | + instance string |
| 126 | + event events.Event |
| 127 | + } |
| 128 | + eventCh := make(chan eventWithInstance) |
| 129 | + errCh := make(chan error, len(instances)) |
| 130 | + |
| 131 | + for _, inst := range instances { |
| 132 | + go func() { |
| 133 | + err := events.Watch(ctx, inst.haStdoutPath, inst.haStderrPath, time.Now(), !jsonFormat, func(ev events.Event) bool { |
| 134 | + select { |
| 135 | + case eventCh <- eventWithInstance{instance: inst.name, event: ev}: |
| 136 | + case <-ctx.Done(): |
| 137 | + return true |
| 138 | + } |
| 139 | + return false |
| 140 | + }) |
| 141 | + if err != nil { |
| 142 | + errCh <- fmt.Errorf("instance %s: %w", inst.name, err) |
| 143 | + } |
| 144 | + }() |
| 145 | + } |
| 146 | + |
| 147 | + // Process events from all instances |
| 148 | + for { |
| 149 | + select { |
| 150 | + case <-ctx.Done(): |
| 151 | + return nil |
| 152 | + case err := <-errCh: |
| 153 | + return err |
| 154 | + case ev := <-eventCh: |
| 155 | + if jsonFormat { |
| 156 | + we := watchEvent{Instance: ev.instance, Event: ev.event} |
| 157 | + j, err := json.Marshal(we) |
| 158 | + if err != nil { |
| 159 | + fmt.Fprintf(cmd.ErrOrStderr(), "error marshaling event: %v\n", err) |
| 160 | + continue |
| 161 | + } |
| 162 | + fmt.Fprintln(cmd.OutOrStdout(), string(j)) |
| 163 | + } else { |
| 164 | + printHumanReadableEvent(cmd, ev.instance, ev.event) |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +func printHumanReadableEvent(cmd *cobra.Command, instName string, ev events.Event) { |
| 171 | + timestamp := ev.Time.Format("2006-01-02 15:04:05") |
| 172 | + out := cmd.OutOrStdout() |
| 173 | + |
| 174 | + printEvent := func(msg string) { |
| 175 | + fmt.Fprintf(out, "%s %s | %s\n", timestamp, instName, msg) |
| 176 | + } |
| 177 | + |
| 178 | + // Status changes |
| 179 | + if ev.Status.Running { |
| 180 | + if ev.Status.Degraded { |
| 181 | + printEvent("running (degraded)") |
| 182 | + } else { |
| 183 | + printEvent("running") |
| 184 | + } |
| 185 | + } |
| 186 | + if ev.Status.Exiting { |
| 187 | + printEvent("exiting") |
| 188 | + } |
| 189 | + |
| 190 | + // SSH port |
| 191 | + if ev.Status.SSHLocalPort != 0 { |
| 192 | + printEvent(fmt.Sprintf("ssh available on port %d", ev.Status.SSHLocalPort)) |
| 193 | + } |
| 194 | + |
| 195 | + // Errors |
| 196 | + for _, e := range ev.Status.Errors { |
| 197 | + printEvent(fmt.Sprintf("error: %s", e)) |
| 198 | + } |
| 199 | + |
| 200 | + // Cloud-init progress |
| 201 | + if ev.Status.CloudInitProgress != nil { |
| 202 | + if ev.Status.CloudInitProgress.Completed { |
| 203 | + printEvent("cloud-init completed") |
| 204 | + } else if ev.Status.CloudInitProgress.LogLine != "" { |
| 205 | + printEvent(fmt.Sprintf("cloud-init: %s", ev.Status.CloudInitProgress.LogLine)) |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + // Port forwarding events |
| 210 | + if ev.Status.PortForward != nil { |
| 211 | + pf := ev.Status.PortForward |
| 212 | + switch pf.Type { |
| 213 | + case events.PortForwardEventForwarding: |
| 214 | + printEvent(fmt.Sprintf("forwarding %s %s to %s", pf.Protocol, pf.GuestAddr, pf.HostAddr)) |
| 215 | + case events.PortForwardEventNotForwarding: |
| 216 | + printEvent(fmt.Sprintf("not forwarding %s %s", pf.Protocol, pf.GuestAddr)) |
| 217 | + case events.PortForwardEventStopping: |
| 218 | + printEvent(fmt.Sprintf("stopping forwarding %s %s", pf.Protocol, pf.GuestAddr)) |
| 219 | + case events.PortForwardEventFailed: |
| 220 | + printEvent(fmt.Sprintf("failed to forward %s %s: %s", pf.Protocol, pf.GuestAddr, pf.Error)) |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + // Vsock events |
| 225 | + if ev.Status.Vsock != nil { |
| 226 | + vs := ev.Status.Vsock |
| 227 | + switch vs.Type { |
| 228 | + case events.VsockEventStarted: |
| 229 | + printEvent(fmt.Sprintf("started vsock forwarder: %s -> vsock:%d", vs.HostAddr, vs.VsockPort)) |
| 230 | + case events.VsockEventSkipped: |
| 231 | + printEvent(fmt.Sprintf("skipped vsock forwarder: %s", vs.Reason)) |
| 232 | + case events.VsockEventFailed: |
| 233 | + printEvent(fmt.Sprintf("failed to start vsock forwarder: %s", vs.Reason)) |
| 234 | + } |
| 235 | + } |
| 236 | +} |
| 237 | + |
| 238 | +func watchBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { |
| 239 | + return bashCompleteInstanceNames(cmd) |
| 240 | +} |
0 commit comments