From 24d726bb65f11071de2b2d3434e98a43b8f8e45d Mon Sep 17 00:00:00 2001 From: Ariel Miculas Date: Thu, 19 Oct 2023 14:18:32 +0300 Subject: [PATCH] feat: use squashfuse ready notifier if available When using squasfuse, check whether it supports -o notifypipe. If it does, use it instead our manual checking of the mountpoint inode. The notification mechanism is a better alternative to the existing polling approach. If it's not available, then use the old mechanism. This feature is supported starting from squasfuse version 0.5.0, see [1] for details. [1] https://github.com/vasi/squashfuse/issues/49 Co-Developed-by: Serge Hallyn Signed-off-by: Ariel Miculas --- go.mod | 2 +- pkg/squashfs/squashfs.go | 123 +++++++++++++++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index d27da772..078596f0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module stackerbuild.io/stacker go 1.20 require ( + github.com/Masterminds/semver/v3 v3.2.1 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/apex/log v1.9.0 github.com/apparentlymart/go-shquot v0.0.1 @@ -45,7 +46,6 @@ require ( github.com/DataDog/zstd v1.4.8 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect diff --git a/pkg/squashfs/squashfs.go b/pkg/squashfs/squashfs.go index d08ddd0c..a9b4d840 100644 --- a/pkg/squashfs/squashfs.go +++ b/pkg/squashfs/squashfs.go @@ -15,6 +15,7 @@ import ( "syscall" "time" + "github.com/Masterminds/semver/v3" "github.com/pkg/errors" "golang.org/x/sys/unix" "stackerbuild.io/stacker/pkg/log" @@ -43,6 +44,15 @@ type ExcludePaths struct { include []string } +type squashFuseInfoStruct struct { + Path string + Version string + SupportsNotfiy bool +} + +var once sync.Once +var squashFuseInfo = squashFuseInfoStruct{"", "", false} + func NewExcludePaths() *ExcludePaths { return &ExcludePaths{ exclude: map[string]bool{}, @@ -198,11 +208,44 @@ func isMountedAtDir(src, dest string) (bool, error) { return false, nil } -func findSquashfusePath() string { +func findSquashFuseInfo() { + var sqfsPath string if p := which("squashfuse_ll"); p != "" { - return p + sqfsPath = p + } else { + sqfsPath = which("squashfuse") } - return which("squashfuse") + if sqfsPath == "" { + return + } + version, supportsNotfiy := sqfuseSupportsMountNotification(sqfsPath) + squashFuseInfo = squashFuseInfoStruct{sqfsPath, version, supportsNotfiy} +} + +// sqfuseSupportsMountNotification - returns true if squashfuse supports mount +// notification, false otherwise +// sqfuse is the path to the squashfuse binary +func sqfuseSupportsMountNotification(sqfuse string) (string, bool) { + cmd := exec.Command(sqfuse) + + // `squashfuse` always returns an error... so we ignore it. + out, _ := cmd.CombinedOutput() + + firstLine := strings.Split(string(out[:]), "\n")[0] + version := strings.Split(firstLine, " ")[1] + v, err := semver.NewVersion(version) + if err != nil { + return version, false + } + // squashfuse notify mechanism was merged in 0.5.0 + constraint, err := semver.NewConstraint(">= 0.5.0") + if err != nil { + return version, false + } + if constraint.Check(v) { + return version, true + } + return version, false } var squashNotFound = errors.Errorf("squashfuse program not found") @@ -211,12 +254,27 @@ var squashNotFound = errors.Errorf("squashfuse program not found") // return a pointer to the squashfuse cmd. // The caller of the this is responsible for the process created. func squashFuse(squashFile, extractDir string) (*exec.Cmd, error) { - sqfuse := findSquashfusePath() var cmd *exec.Cmd - if sqfuse == "" { + + once.Do(findSquashFuseInfo) + if squashFuseInfo.Path == "" { return cmd, squashNotFound } + notifyOpts := "" + notifyPath := "" + if squashFuseInfo.SupportsNotfiy { + sockdir, err := os.MkdirTemp("", "sock") + if err != nil { + return cmd, err + } + notifyPath = filepath.Join(sockdir, "notifypipe") + if err := syscall.Mkfifo(notifyPath, 0640); err != nil { + return cmd, err + } + notifyOpts = "notify_pipe=" + notifyPath + } + // given extractDir of path/to/some/dir[/], log to path/to/some/.dir-squashfs.log extractDir = strings.TrimSuffix(extractDir, "/") @@ -240,12 +298,16 @@ func squashFuse(squashFile, extractDir string) (*exec.Cmd, error) { // It would be nice to only enable debug (or maybe to only log to file at all) // if 'stacker --debug', but we do not have access to that info here. // to debug squashfuse, use "allow_other,debug" - cmd = exec.Command(sqfuse, "-f", "-o", "allow_other,debug", squashFile, extractDir) + optionArgs := "allow_other,debug" + if notifyOpts != "" { + optionArgs += "," + notifyOpts + } + cmd = exec.Command(squashFuseInfo.Path, "-f", "-o", optionArgs, squashFile, extractDir) cmd.Stdin = nil cmd.Stdout = cmdOut cmd.Stderr = cmdOut cmdOut.Write([]byte(fmt.Sprintf("# %s\n", strings.Join(cmd.Args, " ")))) - log.Debugf("Extracting %s -> %s with %s [%s]", squashFile, extractDir, sqfuse, logf) + log.Debugf("Extracting %s -> %s with %s [%s]", squashFile, extractDir, squashFuseInfo.Path, logf) err = cmd.Start() if err != nil { return cmd, err @@ -260,18 +322,58 @@ func squashFuse(squashFile, extractDir string) (*exec.Cmd, error) { // c. a timeout (timeLimit) was hit startTime := time.Now() timeLimit := 30 * time.Second + alarmCh := make(chan struct{}) go func() { cmd.Wait() + close(alarmCh) }() + if squashFuseInfo.SupportsNotfiy { + notifyCh := make(chan byte) + log.Infof("%s supports notify pipe, watching %q", squashFuseInfo.Path, notifyPath) + go func() { + f, err := os.Open(notifyPath) + if err != nil { + return + } + defer f.Close() + b1 := make([]byte, 1) + for { + n1, err := f.Read(b1) + if err != nil { + return + } + if err == nil && n1 >= 1 { + break + } + } + notifyCh <- b1[0] + }() + if err != nil { + return cmd, errors.Wrapf(err, "Failed reading %q", notifyPath) + } + + select { + case <-alarmCh: + cmd.Process.Kill() + return cmd, errors.Wrapf(err, "Gave up on squashFuse mount of %s with %s after %s", squashFile, squashFuseInfo.Path, timeLimit) + case ret := <-notifyCh: + if ret == 's' { + return cmd, nil + } else { + return cmd, errors.Errorf("squashfuse returned an error, check %s", logf) + } + } + } + log.Infof("%s does not support notify pipe", squashFuseInfo.Path) for count := 0; !fileChanged(fiPre, extractDir); count++ { if cmd.ProcessState != nil { // process exited, the Wait() call in the goroutine above // caused ProcessState to be populated. - return cmd, errors.Errorf("squashFuse mount of %s with %s exited unexpectedly with %d", squashFile, sqfuse, cmd.ProcessState.ExitCode()) + return cmd, errors.Errorf("squashFuse mount of %s with %s exited unexpectedly with %d", squashFile, squashFuseInfo.Path, cmd.ProcessState.ExitCode()) } if time.Since(startTime) > timeLimit { cmd.Process.Kill() - return cmd, errors.Wrapf(err, "Gave up on squashFuse mount of %s with %s after %s", squashFile, sqfuse, timeLimit) + return cmd, errors.Wrapf(err, "Gave up on squashFuse mount of %s with %s after %s", squashFile, squashFuseInfo.Path, timeLimit) } if count%10 == 1 { log.Debugf("%s is not yet mounted...(%s)", extractDir, time.Since(startTime)) @@ -448,7 +550,8 @@ func (k *SquashFuseExtractor) Name() string { } func (k *SquashFuseExtractor) IsAvailable() error { - if findSquashfusePath() == "" { + once.Do(findSquashFuseInfo) + if squashFuseInfo.Path == "" { return errors.Errorf("no 'squashfuse' in PATH") } return nil