Skip to content

Commit

Permalink
Support for inotify in mounted directories
Browse files Browse the repository at this point in the history
Signed-off-by: Balaji Vijayakumar <kuttibalaji.v6@gmail.com>
  • Loading branch information
balajiv113 committed Nov 23, 2023
1 parent f9ea34c commit 5359c05
Show file tree
Hide file tree
Showing 20 changed files with 235 additions and 12 deletions.
2 changes: 2 additions & 0 deletions cmd/limactl/editflags/editflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func registerEdit(cmd *cobra.Command, commentPrefix string) {
})

flags.Bool("mount-writable", false, commentPrefix+"make all mounts writable")
flags.Bool("mount-inotify", false, commentPrefix+"enable inotify for mounts")

flags.StringSlice("network", nil, commentPrefix+"additional networks, e.g., \"vzNAT\" or \"lima:shared\" to assign vmnet IP")
_ = cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
Expand Down Expand Up @@ -154,6 +155,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
false,
},
{"mount-type", d(".mountType = %q"), false, false},
{"mount-inotify", d(".mountInotify = %s"), false, true},
{"mount-writable", d(".mounts[].writable = %s"), false, false},
{
"network",
Expand Down
5 changes: 5 additions & 0 deletions examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ mounts:
# 🟢 Builtin default: "reverse-sshfs" (for QEMU), "virtiofs" (for vz)
mountType: null

# Enable inotify support for mounted directories (EXPERIMENTAL)
# 🟢 Builtin default: Disabled by default
mountInotify: null


# Lima disks to attach to the instance. The disks will be accessible from inside the
# instance, labeled by name. (e.g. if the disk is named "data", it will be labeled
# "lima-data" inside the instance). The disk will be mounted inside the instance at
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/nxadm/tail v1.4.11
github.com/opencontainers/go-digest v1.0.0
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/rjeczalik/notify v0.9.3
github.com/sethvargo/go-password v0.2.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down Expand Up @@ -305,6 +307,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
5 changes: 5 additions & 0 deletions pkg/guestagent/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ type Event struct {
LocalPortsRemoved []IPPort `json:"localPortsRemoved,omitempty"`
Errors []string `json:"errors,omitempty"`
}

type InotifyEvent struct {
Location string `json:"location,omitempty"`
Time time.Time `json:"time,omitempty"`
}
23 changes: 21 additions & 2 deletions pkg/guestagent/api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package client
// Apache License 2.0

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -18,12 +19,13 @@ type GuestAgentClient interface {
HTTPClient() *http.Client
Info(context.Context) (*api.Info, error)
Events(context.Context, func(api.Event)) error
Inotify(context.Context, api.InotifyEvent) error
}

// NewGuestAgentClient creates a client.
// remote is a path to the UNIX socket, without unix:// prefix or a remote hostname/IP address.
func NewGuestAgentClient(conn net.Conn) (GuestAgentClient, error) {
hc, err := httpclientutil.NewHTTPClientWithConn(conn)
func NewGuestAgentClient(dialFn func(ctx context.Context) (net.Conn, error)) (GuestAgentClient, error) {
hc, err := httpclientutil.NewHTTPClientWithDialFn(dialFn)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -81,3 +83,20 @@ func (c *client) Events(ctx context.Context, onEvent func(api.Event)) error {
onEvent(ev)
}
}

func (c *client) Inotify(ctx context.Context, event api.InotifyEvent) error {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
err := encoder.Encode(&event)
if err != nil {
return err
}

u := fmt.Sprintf("http://%s/%s/inotify", c.dummyHost, c.version)
resp, err := httpclientutil.Post(ctx, c.HTTPClient(), u, buffer)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
20 changes: 20 additions & 0 deletions pkg/guestagent/api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,28 @@ func (b *Backend) GetEvents(w http.ResponseWriter, r *http.Request) {
}
}

// PostInotify is the handler for POST /v{N}/inotify.
func (b *Backend) PostInotify(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
_, cancel := context.WithCancel(ctx)
defer cancel()

inotifyEvent := api.InotifyEvent{}
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&inotifyEvent); err != nil {
logrus.Warn(err)
return
}
go b.Agent.HandleInotify(inotifyEvent)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(""))
}

func AddRoutes(r *mux.Router, b *Backend) {
v1 := r.PathPrefix("/v1").Subrouter()
v1.Path("/info").Methods("GET").HandlerFunc(b.GetInfo)
v1.Path("/events").Methods("GET").HandlerFunc(b.GetEvents)
v1.Path("/inotify").Methods("POST").HandlerFunc(b.PostInotify)
}
1 change: 1 addition & 0 deletions pkg/guestagent/guestagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ type Agent interface {
Info(ctx context.Context) (*api.Info, error)
Events(ctx context.Context, ch chan api.Event)
LocalPorts(ctx context.Context) ([]api.IPPort, error)
HandleInotify(event api.InotifyEvent)
}
11 changes: 11 additions & 0 deletions pkg/guestagent/guestagent_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package guestagent
import (
"context"
"errors"
"os"
"reflect"
"sync"
"syscall"
Expand Down Expand Up @@ -333,3 +334,13 @@ func (a *agent) fixSystemTimeSkew() {
ticker.Stop()
}
}

func (a *agent) HandleInotify(event api.InotifyEvent) {
location := event.Location
if _, err := os.Stat(location); err == nil {
err := os.Chtimes(location, event.Time.Local(), event.Time.Local())
if err != nil {
logrus.Errorf("error in inotify handle. Event: %s, Error: %s", event, err)
}
}
}
19 changes: 12 additions & 7 deletions pkg/hostagent/hostagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,16 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {
}
return errors.Join(errs...)
})

go func() {
if a.y.MountInotify != nil && *a.y.MountInotify {
err := a.startInotify(ctx)
if err != nil {
logrus.WithError(err).Warn("failed to start inotify", err)
}
}
}()

for {
client, err := a.getOrCreateClient(ctx)
if err == nil {
Expand Down Expand Up @@ -590,13 +600,8 @@ func (a *HostAgent) getOrCreateClient(ctx context.Context) (guestagentclient.Gue
return a.client, err
}

func (a *HostAgent) createClient(ctx context.Context) (guestagentclient.GuestAgentClient, error) {
conn, err := a.driver.GuestAgentConn(ctx)
if err != nil {
return nil, err
}

return guestagentclient.NewGuestAgentClient(conn)
func (a *HostAgent) createClient(_ context.Context) (guestagentclient.GuestAgentClient, error) {
return guestagentclient.NewGuestAgentClient(a.driver.GuestAgentConn)
}

func (a *HostAgent) processGuestAgentEvents(ctx context.Context, client guestagentclient.GuestAgentClient) error {
Expand Down
84 changes: 84 additions & 0 deletions pkg/hostagent/inotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package hostagent

import (
"context"
"os"
"path"

guestagentapi "github.com/lima-vm/lima/pkg/guestagent/api"
"github.com/lima-vm/lima/pkg/localpathutil"
"github.com/rjeczalik/notify"
"github.com/sirupsen/logrus"
)

const CacheSize = 10000

var inotifyCache = make(map[string]string)

func (a *HostAgent) startInotify(ctx context.Context) error {
mountWatchCh := make(chan notify.EventInfo, 128)
err := a.setupWatchers(mountWatchCh)
if err != nil {
return err
}

for {
select {
case <-ctx.Done():
return nil
case watchEvent := <-mountWatchCh:
client, err := a.getOrCreateClient(ctx)
if err != nil {
logrus.Error("failed to create client for inotify", err)
}
stat, err := os.Stat(watchEvent.Path())
if err != nil {
continue
}

if filterEvents(watchEvent) {
continue
}

event := guestagentapi.InotifyEvent{Location: watchEvent.Path(), Time: stat.ModTime().UTC()}
err = client.Inotify(ctx, event)

if err != nil {
logrus.WithError(err).Warn("failed to send inotify", err)
}
}
}
}

func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {
for _, m := range a.y.Mounts {
if *m.Writable {
location, err := localpathutil.Expand(m.Location)
if err != nil {
return err
}
logrus.Infof("enable inotify for writable mount: %s", location)
err = notify.Watch(path.Join(location, "..."), events, notify.Create|notify.Write)
if err != nil {
return err
}
}
}
return nil
}

func filterEvents(event notify.EventInfo) bool {
eventPath := event.Path()
_, ok := inotifyCache[eventPath]
if ok {
// Ignore the duplicate inotify on mounted directories, so always remove a entry if already present
delete(inotifyCache, eventPath)
return true
}
inotifyCache[eventPath] = ""

if len(inotifyCache) >= CacheSize {
inotifyCache = make(map[string]string)
}
return false
}
22 changes: 19 additions & 3 deletions pkg/httpclientutil/httpclientutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ func Get(ctx context.Context, c *http.Client, url string) (*http.Response, error
return resp, nil
}

func Post(ctx context.Context, c *http.Client, url string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
if err != nil {
return nil, err
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
if err := Successful(resp); err != nil {
resp.Body.Close()
return nil, err
}
return resp, nil
}

func readAtMost(r io.Reader, maxBytes int) ([]byte, error) {
lr := &io.LimitedReader{
R: r,
Expand Down Expand Up @@ -85,13 +101,13 @@ func Successful(resp *http.Response) error {
return nil
}

// NewHTTPClientWithConn creates a client.
// NewHTTPClientWithDialFn creates a client.
// conn is a raw net.Conn instance.
func NewHTTPClientWithConn(conn net.Conn) (*http.Client, error) {
func NewHTTPClientWithDialFn(dialFn func(ctx context.Context) (net.Conn, error)) (*http.Client, error) {
hc := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return conn, nil
return dialFn(ctx)
},
},
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,16 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
}
}

if y.MountInotify == nil {
y.MountInotify = d.MountInotify
}
if o.MountInotify != nil {
y.MountInotify = o.MountInotify
}
if y.MountInotify == nil {
y.MountInotify = ptr.Of(false)
}

// Combine all mounts; highest priority entry determines writable status.
// Only works for exact matches; does not normalize case or resolve symlinks.
mounts := make([]Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))
Expand Down
5 changes: 5 additions & 0 deletions pkg/limayaml/defaults_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ func TestFillDefault(t *testing.T) {

expect.MountType = ptr.Of(NINEP)

expect.MountInotify = ptr.Of(false)

expect.Provision = y.Provision
expect.Provision[0].Mode = ProvisionModeSystem

Expand Down Expand Up @@ -390,6 +392,7 @@ func TestFillDefault(t *testing.T) {
"default": d.HostResolver.Hosts["default"],
}
expect.MountType = ptr.Of(VIRTIOFS)
expect.MountInotify = ptr.Of(false)
expect.CACertificates.RemoveDefaults = ptr.Of(true)
expect.CACertificates.Certs = []string{
"-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT\n-----END CERTIFICATE-----\n",
Expand Down Expand Up @@ -520,6 +523,7 @@ func TestFillDefault(t *testing.T) {
},
},
},
MountInotify: ptr.Of(true),
Provision: []Provision{
{
Script: "#!/bin/true",
Expand Down Expand Up @@ -596,6 +600,7 @@ func TestFillDefault(t *testing.T) {
expect.Mounts[0].Virtiofs.QueueSize = ptr.Of(2048)

expect.MountType = ptr.Of(NINEP)
expect.MountInotify = ptr.Of(true)

// o.Networks[1] is overriding the d.Networks[0].Lima entry for the "def0" interface
expect.Networks = append(append(d.Networks, y.Networks...), o.Networks[0])
Expand Down
1 change: 1 addition & 0 deletions pkg/limayaml/limayaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type LimaYAML struct {
AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty"`
Mounts []Mount `yaml:"mounts,omitempty" json:"mounts,omitempty"`
MountType *MountType `yaml:"mountType,omitempty" json:"mountType,omitempty"`
MountInotify *bool `yaml:"mountInotify,omitempty" json:"mountInotify,omitempty"`
SSH SSH `yaml:"ssh,omitempty" json:"ssh,omitempty"` // REQUIRED (FIXME)
Firmware Firmware `yaml:"firmware,omitempty" json:"firmware,omitempty"`
Audio Audio `yaml:"audio,omitempty" json:"audio,omitempty"`
Expand Down
3 changes: 3 additions & 0 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,7 @@ func warnExperimental(y LimaYAML) {
if y.Audio.Device != nil && *y.Audio.Device != "" {
logrus.Warn("`audio.device` is experimental")
}
if y.MountInotify != nil && *y.MountInotify {
logrus.Warn("`mountInotify` is experimental")
}
}
1 change: 1 addition & 0 deletions pkg/vz/vz_driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func (l *LimaVzDriver) Validate() error {
"Disk",
"Mounts",
"MountType",
"MountInotify",
"SSH",
"Firmware",
"Provision",
Expand Down
Loading

0 comments on commit 5359c05

Please sign in to comment.