diff --git a/pkg/squashfs/squashfs.go b/pkg/squashfs/squashfs.go index d08ddd0c..4dfcd414 100644 --- a/pkg/squashfs/squashfs.go +++ b/pkg/squashfs/squashfs.go @@ -19,6 +19,7 @@ import ( "golang.org/x/sys/unix" "stackerbuild.io/stacker/pkg/log" "stackerbuild.io/stacker/pkg/mount" + "github.com/Masterminds/semver/v3" ) var checkZstdSupported sync.Once @@ -205,6 +206,32 @@ func findSquashfusePath() string { return which("squashfuse") } +// sqfuseSupportsMountNotification - returns true if squashfuse supports mount +// notification, false otherwise +// sqfuse is the path to the squashfuse binary +func sqfuseSupportsMountNotification(sqfuse string) (bool) { + cmd := exec.Command(sqfuse) + + // `squashfuse` always returns an error... so we ignore it. + out, _ := cmd.CombinedOutput() + + first_line := strings.Split(string(out[:]), "\n")[0]; + version := strings.Split(first_line, " ")[1]; + v, err := semver.NewVersion(version) + if err != nil { + return false + } + // squashfuse notify mechanism was merged in 0.5.0 + constraint, err := semver.NewConstraint(">= 0.5.0"); + if err != nil { + return false + } + if constraint.Check(v) { + return true + } + return false +} + var squashNotFound = errors.Errorf("squashfuse program not found") // squashFuse - mount squashFile to extractDir @@ -217,6 +244,22 @@ func squashFuse(squashFile, extractDir string) (*exec.Cmd, error) { return cmd, squashNotFound } + sqNotify := sqfuseSupportsMountNotification(sqfuse) + + notifyOpts := "" + notifyPath := "" + if sqNotify { + 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,7 +283,11 @@ 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(sqfuse, "-f", "-o", optionArgs, squashFile, extractDir) cmd.Stdin = nil cmd.Stdout = cmdOut cmd.Stderr = cmdOut @@ -260,9 +307,49 @@ 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 sqNotify { + notifyCh := make(chan byte) + log.Infof("%s supports notify pipe, watching %q", sqfuse, 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, sqfuse, timeLimit) + case ret := <-notifyCh: + if ret == 's' { + return cmd, nil + } else { + return cmd, fmt.Errorf("squashfuse returned an error, check %s", logf) + } + } + } + log.Infof("%s does not support notify pipe", sqfuse) for count := 0; !fileChanged(fiPre, extractDir); count++ { if cmd.ProcessState != nil { // process exited, the Wait() call in the goroutine above