Skip to content

Commit a36e2fd

Browse files
committed
Support for inotify in mounted directories
Signed-off-by: Balaji Vijayakumar <[email protected]>
1 parent ce3b98a commit a36e2fd

File tree

11 files changed

+228
-38
lines changed

11 files changed

+228
-38
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ jobs:
226226
fetch-depth: 1
227227
- uses: actions/setup-go@v4
228228
with:
229-
go-version: 1.20.x
229+
go-version: 1.21.x
230230
- uses: actions/cache@v3
231231
with:
232232
path: ~/.cache/lima/download
@@ -270,7 +270,7 @@ jobs:
270270
fetch-depth: 1
271271
- uses: actions/setup-go@v4
272272
with:
273-
go-version: 1.20.x
273+
go-version: 1.21.x
274274
- uses: actions/cache@v3
275275
with:
276276
path: ~/.cache/lima/download

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/nxadm/tail v1.4.11
3434
github.com/opencontainers/go-digest v1.0.0
3535
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
36+
github.com/rjeczalik/notify v0.9.3
3637
github.com/sethvargo/go-password v0.2.0
3738
github.com/sirupsen/logrus v1.9.3
3839
github.com/spf13/cobra v1.7.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
222222
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
223223
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
224224
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
225+
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
226+
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
225227
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
226228
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
227229
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -306,6 +308,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
306308
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
307309
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
308310
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
311+
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
309312
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
310313
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
311314
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

pkg/guestagent/api/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ type Event struct {
3232
LocalPortsRemoved []IPPort `json:"localPortsRemoved,omitempty"`
3333
Errors []string `json:"errors,omitempty"`
3434
}
35+
36+
type InotifyEvent struct {
37+
Location string `json:"location,omitempty"`
38+
Time time.Time `json:"time,omitempty"`
39+
}

pkg/guestagent/api/client/client.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package client
44
// Apache License 2.0
55

66
import (
7+
"bytes"
78
"context"
89
"encoding/json"
910
"fmt"
@@ -19,6 +20,7 @@ type GuestAgentClient interface {
1920
HTTPClient() *http.Client
2021
Info(context.Context) (*api.Info, error)
2122
Events(context.Context, func(api.Event)) error
23+
Inotify(context.Context, api.InotifyEvent) error
2224
}
2325

2426
type Proto = string
@@ -108,3 +110,20 @@ func (c *client) Events(ctx context.Context, onEvent func(api.Event)) error {
108110
onEvent(ev)
109111
}
110112
}
113+
114+
func (c *client) Inotify(ctx context.Context, event api.InotifyEvent) error {
115+
buffer := &bytes.Buffer{}
116+
encoder := json.NewEncoder(buffer)
117+
err := encoder.Encode(&event)
118+
if err != nil {
119+
return err
120+
}
121+
122+
u := fmt.Sprintf("http://%s/%s/inotify", c.dummyHost, c.version)
123+
resp, err := httpclientutil.Post(ctx, c.HTTPClient(), u, buffer)
124+
if err != nil {
125+
return err
126+
}
127+
defer resp.Body.Close()
128+
return nil
129+
}

pkg/guestagent/api/server/server.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,33 @@ func (b *Backend) GetEvents(w http.ResponseWriter, r *http.Request) {
7676
}
7777
}
7878

79+
// PostInotify is the handler for POST /v{N}/inotify.
80+
func (b *Backend) PostInotify(w http.ResponseWriter, r *http.Request) {
81+
ctx := r.Context()
82+
_, cancel := context.WithCancel(ctx)
83+
defer cancel()
84+
85+
inotifyEvent := api.InotifyEvent{}
86+
decoder := json.NewDecoder(r.Body)
87+
if err := decoder.Decode(&inotifyEvent); err != nil {
88+
logrus.Warn(err)
89+
return
90+
}
91+
go b.Agent.HandleInotify(inotifyEvent)
92+
93+
flusher, ok := w.(http.Flusher)
94+
if !ok {
95+
panic("http.ResponseWriter has to implement http.Flusher")
96+
}
97+
98+
w.Header().Set("Content-Type", "application/x-ndjson")
99+
w.WriteHeader(http.StatusOK)
100+
flusher.Flush()
101+
}
102+
79103
func AddRoutes(r *mux.Router, b *Backend) {
80104
v1 := r.PathPrefix("/v1").Subrouter()
81105
v1.Path("/info").Methods("GET").HandlerFunc(b.GetInfo)
82106
v1.Path("/events").Methods("GET").HandlerFunc(b.GetEvents)
107+
v1.Path("/inotify").Methods("POST").HandlerFunc(b.PostInotify)
83108
}

pkg/guestagent/guestagent.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type Agent interface {
1010
Info(ctx context.Context) (*api.Info, error)
1111
Events(ctx context.Context, ch chan api.Event)
1212
LocalPorts(ctx context.Context) ([]api.IPPort, error)
13+
HandleInotify(event api.InotifyEvent)
1314
}

pkg/guestagent/guestagent_linux.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package guestagent
33
import (
44
"context"
55
"errors"
6+
"os"
67
"reflect"
78
"sync"
89
"syscall"
@@ -333,3 +334,13 @@ func (a *agent) fixSystemTimeSkew() {
333334
ticker.Stop()
334335
}
335336
}
337+
338+
func (a *agent) HandleInotify(event api.InotifyEvent) {
339+
location := event.Location
340+
if _, err := os.Stat(location); err == nil {
341+
err := os.Chtimes(location, event.Time.Local(), event.Time.Local())
342+
if err != nil {
343+
logrus.Errorf("error in inotify handle. Event: %s, Error: %s", event, err)
344+
}
345+
}
346+
}

pkg/hostagent/hostagent.go

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ type HostAgent struct {
5555
eventEncMu sync.Mutex
5656

5757
vSockPort int
58+
59+
clientMu sync.RWMutex
60+
client guestagentclient.GuestAgentClient
5861
}
5962

6063
type options struct {
@@ -542,39 +545,41 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
542545
}
543546
}
544547

545-
localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock)
546-
remoteUnix := "/run/lima-guestagent.sock"
548+
local, remote := a.localAndRemoteGuestAgentPaths()
547549

548-
a.onClose = append(a.onClose, func() error {
549-
logrus.Debugf("Stop forwarding unix sockets")
550-
var errs []error
551-
for _, rule := range a.y.PortForwards {
552-
if rule.GuestSocket != "" {
553-
local := hostAddress(rule, guestagentapi.IPPort{})
554-
// using ctx.Background() because ctx has already been cancelled
555-
if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil {
556-
errs = append(errs, err)
550+
if a.guestAgentProto != guestagentclient.VSOCK {
551+
a.onClose = append(a.onClose, func() error {
552+
logrus.Debugf("Stop forwarding unix sockets")
553+
var errs []error
554+
for _, rule := range a.y.PortForwards {
555+
if rule.GuestSocket != "" {
556+
local := hostAddress(rule, guestagentapi.IPPort{})
557+
// using ctx.Background() because ctx has already been cancelled
558+
if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil {
559+
errs = append(errs, err)
560+
}
557561
}
558562
}
559-
}
560-
if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbCancel, false); err != nil {
561-
errs = append(errs, err)
562-
}
563-
return errors.Join(errs...)
564-
})
565-
566-
guestSocketAddr := localUnix
567-
if a.guestAgentProto == guestagentclient.VSOCK {
568-
guestSocketAddr = fmt.Sprintf("0.0.0.0:%d", a.vSockPort)
563+
if err := forwardSSH(context.Background(), a.sshConfig, a.sshLocalPort, local, remote, verbCancel, false); err != nil {
564+
errs = append(errs, err)
565+
}
566+
return errors.Join(errs...)
567+
})
569568
}
570569

570+
go func() {
571+
err := a.startInotify(ctx)
572+
if err != nil {
573+
logrus.WithError(err).Warn("failed to start inotify", err)
574+
}
575+
}()
576+
571577
for {
572-
if !isGuestAgentSocketAccessible(ctx, guestSocketAddr, a.guestAgentProto, a.instName) {
573-
if a.guestAgentProto != guestagentclient.VSOCK {
574-
_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false)
575-
}
578+
client, err := a.getOrCreateClient(ctx)
579+
if err != nil && !errors.Is(err, context.Canceled) {
580+
logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly")
576581
}
577-
if err := a.processGuestAgentEvents(ctx, guestSocketAddr, a.guestAgentProto, a.instName); err != nil {
582+
if err := a.processGuestAgentEvents(ctx, client); err != nil {
578583
if !errors.Is(err, context.Canceled) {
579584
logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly")
580585
}
@@ -587,21 +592,43 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
587592
}
588593
}
589594

590-
func isGuestAgentSocketAccessible(ctx context.Context, localUnix string, proto guestagentclient.Proto, instanceName string) bool {
591-
client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName)
592-
if err != nil {
593-
return false
595+
func (a *HostAgent) getOrCreateClient(ctx context.Context) (guestagentclient.GuestAgentClient, error) {
596+
a.clientMu.Lock()
597+
defer a.clientMu.Unlock()
598+
if a.client != nil && isGuestAgentSocketAccessible(ctx, a.client) {
599+
return a.client, nil
594600
}
595-
_, err = client.Info(ctx)
596-
return err == nil
601+
var err error
602+
a.client, err = a.createClient(ctx)
603+
return a.client, err
597604
}
598605

599-
func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix string, proto guestagentclient.Proto, instanceName string) error {
600-
client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName)
601-
if err != nil {
602-
return err
606+
func (a *HostAgent) createClient(ctx context.Context) (guestagentclient.GuestAgentClient, error) {
607+
local, remote := a.localAndRemoteGuestAgentPaths()
608+
if a.guestAgentProto != guestagentclient.VSOCK {
609+
_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, remote, verbForward, false)
603610
}
604611

612+
return guestagentclient.NewGuestAgentClient(local, a.guestAgentProto, a.instName)
613+
}
614+
615+
func (a *HostAgent) localAndRemoteGuestAgentPaths() (string, string) {
616+
localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock)
617+
remoteUnix := "/run/lima-guestagent.sock"
618+
619+
guestSocketAddr := localUnix
620+
if a.guestAgentProto == guestagentclient.VSOCK {
621+
guestSocketAddr = fmt.Sprintf("0.0.0.0:%d", a.vSockPort)
622+
}
623+
return guestSocketAddr, remoteUnix
624+
}
625+
626+
func isGuestAgentSocketAccessible(ctx context.Context, client guestagentclient.GuestAgentClient) bool {
627+
_, err := client.Info(ctx)
628+
return err == nil
629+
}
630+
631+
func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client guestagentclient.GuestAgentClient) error {
605632
info, err := client.Info(ctx)
606633
if err != nil {
607634
return err

pkg/hostagent/inotify.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package hostagent
2+
3+
import (
4+
"context"
5+
"os"
6+
"path"
7+
8+
guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api"
9+
"github.com/lima-vm/lima/pkg/localpathutil"
10+
"github.com/rjeczalik/notify"
11+
"github.com/sirupsen/logrus"
12+
)
13+
14+
const CacheSize = 10000
15+
16+
var inotifyCache = make(map[string]string)
17+
18+
func (a *HostAgent) startInotify(ctx context.Context) error {
19+
mountWatchCh := make(chan notify.EventInfo, 128)
20+
err := a.setupWatchers(mountWatchCh)
21+
if err != nil {
22+
return err
23+
}
24+
25+
for {
26+
select {
27+
case <-ctx.Done():
28+
return nil
29+
case watchEvent := <-mountWatchCh:
30+
client, err := a.getOrCreateClient(ctx)
31+
if err != nil {
32+
logrus.Error("failed to create client for inotify", err)
33+
}
34+
stat, err := os.Stat(watchEvent.Path())
35+
if err != nil {
36+
continue
37+
}
38+
39+
if filterEvents(watchEvent) {
40+
continue
41+
}
42+
43+
event := guestagentapi.InotifyEvent{Location: watchEvent.Path(), Time: stat.ModTime().UTC()}
44+
err = client.Inotify(ctx, event)
45+
if err != nil {
46+
logrus.WithError(err).Warn("failed to send inotify", err)
47+
}
48+
}
49+
}
50+
}
51+
52+
func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {
53+
for _, m := range a.y.Mounts {
54+
if *m.Writable {
55+
location, err := localpathutil.Expand(m.Location)
56+
if err != nil {
57+
return err
58+
}
59+
err = notify.Watch(path.Join(location, "..."), events, notify.Create|notify.Write)
60+
if err != nil {
61+
return err
62+
}
63+
}
64+
}
65+
return nil
66+
}
67+
68+
func filterEvents(event notify.EventInfo) bool {
69+
eventPath := event.Path()
70+
_, ok := inotifyCache[eventPath]
71+
if ok {
72+
// Ignore the duplicate inotify on mounted directories, so always remove a entry if already present
73+
delete(inotifyCache, eventPath)
74+
return true
75+
}
76+
inotifyCache[eventPath] = ""
77+
78+
if len(inotifyCache) >= CacheSize {
79+
inotifyCache = make(map[string]string)
80+
}
81+
return false
82+
}

pkg/httpclientutil/httpclientutil.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ func Get(ctx context.Context, c *http.Client, url string) (*http.Response, error
3131
return resp, nil
3232
}
3333

34+
func Post(ctx context.Context, c *http.Client, url string, body io.Reader) (*http.Response, error) {
35+
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
36+
if err != nil {
37+
return nil, err
38+
}
39+
resp, err := c.Do(req)
40+
if err != nil {
41+
return nil, err
42+
}
43+
if err := Successful(resp); err != nil {
44+
resp.Body.Close()
45+
return nil, err
46+
}
47+
return resp, nil
48+
}
49+
3450
func readAtMost(r io.Reader, maxBytes int) ([]byte, error) {
3551
lr := &io.LimitedReader{
3652
R: r,

0 commit comments

Comments
 (0)