Skip to content

Commit 21058b0

Browse files
committed
Support for inotify in mounted directories
Signed-off-by: Balaji Vijayakumar <kuttibalaji.v6@gmail.com>
1 parent c4986e7 commit 21058b0

File tree

11 files changed

+200
-14
lines changed

11 files changed

+200
-14
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ jobs:
216216
fetch-depth: 1
217217
- uses: actions/setup-go@v4
218218
with:
219-
go-version: 1.20.x
219+
go-version: 1.21.x
220220
- uses: actions/cache@v3
221221
with:
222222
path: ~/.cache/lima/download
@@ -260,7 +260,7 @@ jobs:
260260
fetch-depth: 1
261261
- uses: actions/setup-go@v4
262262
with:
263-
go-version: 1.20.x
263+
go-version: 1.21.x
264264
- uses: actions/cache@v3
265265
with:
266266
path: ~/.cache/lima/download

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/lima-vm/lima
22

3-
go 1.20
3+
4+
go 1.21
45

56
require (
67
github.com/AlecAivazis/survey/v2 v2.3.7
@@ -33,6 +34,7 @@ require (
3334
github.com/nxadm/tail v1.4.11
3435
github.com/opencontainers/go-digest v1.0.0
3536
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
37+
github.com/rjeczalik/notify v0.9.3
3638
github.com/sethvargo/go-password v0.2.0
3739
github.com/sirupsen/logrus v1.9.3
3840
github.com/spf13/cobra v1.7.0

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
220220
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
221221
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
222222
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
223+
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
224+
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
223225
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
224226
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
225227
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -302,6 +304,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
302304
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
303305
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
304306
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
307+
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
305308
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
306309
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
307310
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
@@ -34,3 +34,8 @@ type Event struct {
3434
LocalPortsRemoved []IPPort `json:"localPortsRemoved,omitempty"`
3535
Errors []string `json:"errors,omitempty"`
3636
}
37+
38+
type InotifyEvent struct {
39+
Location string `json:"location,omitempty"`
40+
Time time.Time `json:"time,omitempty"`
41+
}

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: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -568,13 +568,19 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
568568
guestSocketAddr = fmt.Sprintf("0.0.0.0:%d", a.vSockPort)
569569
}
570570

571+
go func() {
572+
err := a.startInotify(ctx, guestSocketAddr, localUnix, remoteUnix)
573+
if err != nil {
574+
logrus.WithError(err).Warn("failed to start inotify")
575+
}
576+
}()
577+
571578
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-
}
579+
client, err := a.createClient(ctx, guestSocketAddr, localUnix, remoteUnix)
580+
if err != nil && !errors.Is(err, context.Canceled) {
581+
logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly")
576582
}
577-
if err := a.processGuestAgentEvents(ctx, guestSocketAddr, a.guestAgentProto, a.instName); err != nil {
583+
if err := a.processGuestAgentEvents(ctx, client); err != nil {
578584
if !errors.Is(err, context.Canceled) {
579585
logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly")
580586
}
@@ -587,6 +593,20 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
587593
}
588594
}
589595

596+
func (a *HostAgent) createClient(ctx context.Context, guestSocketAddr string, localUnix string, remoteUnix string) (guestagentclient.GuestAgentClient, error) {
597+
if !isGuestAgentSocketAccessible(ctx, guestSocketAddr, a.guestAgentProto, a.instName) {
598+
if a.guestAgentProto != guestagentclient.VSOCK {
599+
_ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false)
600+
}
601+
}
602+
603+
client, err := guestagentclient.NewGuestAgentClient(localUnix, a.guestAgentProto, a.instName)
604+
if err != nil {
605+
return nil, err
606+
}
607+
return client, nil
608+
}
609+
590610
func isGuestAgentSocketAccessible(ctx context.Context, localUnix string, proto guestagentclient.Proto, instanceName string) bool {
591611
client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName)
592612
if err != nil {
@@ -596,12 +616,7 @@ func isGuestAgentSocketAccessible(ctx context.Context, localUnix string, proto g
596616
return err == nil
597617
}
598618

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
603-
}
604-
619+
func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client guestagentclient.GuestAgentClient) error {
605620
info, err := client.Info(ctx)
606621
if err != nil {
607622
return err

pkg/hostagent/inotify.go

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

0 commit comments

Comments
 (0)