Skip to content

Commit

Permalink
feat: use squashfuse ready notifier if available
Browse files Browse the repository at this point in the history
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] vasi/squashfuse#49

Co-Developed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Ariel Miculas <amiculas@cisco.com>
  • Loading branch information
ariel-miculas committed Oct 23, 2023
1 parent 565b032 commit 24d726b
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 11 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
123 changes: 113 additions & 10 deletions pkg/squashfs/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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")
Expand All @@ -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, "/")

Expand All @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 24d726b

Please sign in to comment.