Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build mountinfo on multiple platforms #3

Merged
merged 2 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !(linux || darwin || windows)

package mountinfo

import (
"fmt"
"runtime"

sglog "github.com/sourcegraph/log"
)

func discoverDeviceName(logger sglog.Logger, filePath string) (string, error) {
return "", fmt.Errorf("not implemented on %s", runtime.GOOS)
}
53 changes: 53 additions & 0 deletions device_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package mountinfo

import (
"fmt"
"os/exec"
"path/filepath"
"regexp"
"strings"

sglog "github.com/sourcegraph/log"
)

// discoverDeviceName returns the name of the block device that filePath is
// stored on.
func discoverDeviceName(logger sglog.Logger, filePath string) (string, error) {
// on macOS (darwin), use the `stat` and `diskutil` OS tools
//diskutil info $(stat -f '%Sd' <path>) | grep 'Part of Whole:' | awk '{print $NF}'

// macOS does support using the `unix.Stat_t` struct and `unix.Stat` function,
// but finding the device identifier name from the major + minor idendifiers proved difficult,
// so just use `stat` to print out the partition identifier name, and `diskutil`
// to find the disk identifier name from that

filePath, err := filepath.EvalSymlinks(filePath)
if err != nil {
return "", fmt.Errorf("unable to resolve %s: %w", filePath, err)
}
filePath, err = filepath.Abs(filePath)
if err != nil {
return "", fmt.Errorf("unable to resolve %s: %w", filePath, err)
}
stat, err := exec.Command("stat", "-f", "%Sd", filePath).Output()
if err != nil {
return "", fmt.Errorf("unable to stat %s: %w", filePath, err)
}

diskinfo, err := exec.Command("diskutil", "info", strings.TrimSpace(string(stat))).CombinedOutput()
if err != nil {
// log the output from `diskutil` instead of including it in the error message because it may be multiline
logger.Error(fmt.Sprintf("unable to get disk info on %s. Output is (%s)", string(stat), string(diskinfo)))
return "", fmt.Errorf("unable to get disk info on %s: %w", string(stat), err)
}

regex := regexp.MustCompile("Part of Whole:[ \t]+(?P<name>\\w+)")
match := regex.FindSubmatch(diskinfo)
if match == nil {
// log the output from `diskutil` instead of including it in the error message because it may be multiline
logger.Error(fmt.Sprintf("unable to find disk info in (%s)", string(diskinfo)))
return "", fmt.Errorf("unable to find disk info on %s: %w", string(stat), err)
}

return string(match[1]), nil
}
150 changes: 150 additions & 0 deletions device_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package mountinfo

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/moby/sys/mountinfo"
sglog "github.com/sourcegraph/log"
)

// defined as a variable so that it can be redefined by test routines
var findSysfsMountpoint = func() (mountpoint string, err error) {
fsinfo := func(info *mountinfo.Info) (skip, stop bool) {
if info.FSType == "sysfs" {
return false, true
}
return true, false
}
info, err := mountinfo.GetMounts(fsinfo)
if err == nil && len(info) == 0 {
err = errors.New("findSysfsMountpoint: no sysfs mountpoint found")
}
if err != nil {
return "", fmt.Errorf("findSysfsMountpoint: %w", err)
}
// the provided sysfs mountpoint could itself be a symlink, so we
// resolve it immediately so that future file path
// evaluations / massaging doesn't break
cleanedPath, err := filepath.EvalSymlinks(filepath.Clean(info[0].Mountpoint))
if err != nil {
return "", fmt.Errorf("findSysfsMountpoint: verifying sysfs mountpoint %q: failed to resolve symlink: %w", info[0].Mountpoint, err)
}
return cleanedPath, nil
}

func discoverSysfsDevicePath(sysfsMountPoint string, deviceNumber string) (string, error) {

// /sys/dev/block/<device_number> symlinks to /sys/devices/.../block/.../<deviceName>
symlink := filepath.Join(sysfsMountPoint, "dev", "block", deviceNumber)

devicePath, err := filepath.EvalSymlinks(symlink)
if err != nil {
return "", fmt.Errorf("discoverSysfsDevicePath: failed to evaluate sysfs symlink %q: %w", symlink, err)
}

devicePath, err = filepath.Abs(devicePath)
if err != nil {
return "", fmt.Errorf("discoverSysfsDevicePath: failed to massage device path %q to absolute path: %w", devicePath, err)
}

return devicePath, nil
}

func getDeviceBlockName(sysfsMountPoint, devicePath string) (string, error) {

// Check to see if devicePath points to a disk partition. If so, we need to find the parent
// device.

// massage the sysfs folder name to ensure that it always ends in a '/'
// so that strings.HasPrefix does what we expect when checking to see if
// we're still under the /sys sub-folder
sysFolderPrefix := strings.TrimSuffix(sysfsMountPoint, string(os.PathSeparator))
sysFolderPrefix = sysFolderPrefix + string(os.PathSeparator)

for {
if !strings.HasPrefix(devicePath, sysFolderPrefix) {
// ensure that we're still under the /sys/ sub-folder
return "", fmt.Errorf("getDeviceBlockName: device path %q isn't a subpath of %q", devicePath, sysFolderPrefix)
}

_, err := os.Stat(filepath.Join(devicePath, "partition"))
if errors.Is(err, os.ErrNotExist) {
break
}

parent := filepath.Dir(devicePath)
devicePath = parent
}

// If this device is a block device, its device path should have a symlink
// to the block subsystem.

subsystemPath, err := filepath.EvalSymlinks(filepath.Join(devicePath, "subsystem"))
if err != nil {
return "", fmt.Errorf("getDeviceBlockName: failed to discover subsystem that device (path %q) is part of: %w", devicePath, err)
}

if filepath.Base(subsystemPath) != "block" {
return "", fmt.Errorf("getDeviceBlockName: device (path %q) is not part of the block subsystem", devicePath)
}

return filepath.Base(filepath.Base(devicePath)), nil
}

// discoverDeviceName returns the name of the block device that filePath is
// stored on.
func discoverDeviceName(logger sglog.Logger, filePath string) (string, error) {
// Note: It's quite involved to implement the device discovery logic for
// every possible kind of storage device (e.x. logical volumes, NFS, etc.) See
// https://unix.stackexchange.com/a/11312 for more information.
//
// As a result, this logic will only work correctly for filePaths that are either:
// - stored directly on a block device
// - stored on a block device's partition
//
// For all other device types, this logic will either:
// - return an incorrect device name
// - return an error
//
// This logic was implemented from information gathered from the following sources (amongst others):
// - "The Linux Programming Interface" by Michael Kerrisk: Chapter 14
// - "Linux Kernel Development" by Robert Love: Chapters 13, 17
// - https://man7.org/linux/man-pages/man5/sysfs.5.html
// - https://en.wikipedia.org/wiki/Sysfs
// - https://unix.stackexchange.com/a/11312
// - https://www.kernel.org/doc/ols/2005/ols2005v1-pages-321-334.pdf

sysfsMountPoint, err := findSysfsMountpoint()
if err != nil {
return "", fmt.Errorf("finding sysfs mountpoint: %w", err)
}

deviceNumber, err := getDeviceNumber(filepath.Clean(filePath))
if err != nil {
return "", fmt.Errorf("discovering device number: %w", err)
}

logger.Debug(
"discovered device number",
sglog.String("deviceNumber", deviceNumber),
)

devicePath, err := discoverSysfsDevicePath(sysfsMountPoint, deviceNumber)
if err != nil {
return "", fmt.Errorf("discovering device path: %w", err)
}

logger.Debug("discovered device path",
sglog.String("devicePath", devicePath),
)

name, err := getDeviceBlockName(sysfsMountPoint, devicePath)
if err != nil {
return "", fmt.Errorf("failed resolving block device name: %w", err)
}
return name, nil
}
29 changes: 29 additions & 0 deletions device_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build !windows

// file to hold functions that work on both Linux and Unix operating systems

package mountinfo

import (
"fmt"

"golang.org/x/sys/unix"
)

// defined as a variable so that it can be redefined by test routines
var getDeviceNumber = func(filePath string) (string, error) {
// this is the only explicitely platform-dependent code being used: Stat_t and Stat.
// (requires a Unix/Linux OS to compile)
// Other code is implicitly dependent on Linux's sysfs, but will compile on other OSs
var stat unix.Stat_t
err := unix.Stat(filePath, &stat)
if err != nil {
return "", fmt.Errorf("getDeviceNumber: failed to stat %q: %w", filePath, err)
}

//nolint:unconvert // We need the unix.Major/Minor functions to perform the proper bit-shifts
major, minor := unix.Major(uint64(stat.Dev)), unix.Minor(uint64(stat.Dev))

// Represent the number in <major>:<minor> format.
return fmt.Sprintf("%d:%d", major, minor), nil
}
12 changes: 12 additions & 0 deletions device_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package mountinfo

import (
"fmt"
"runtime"

sglog "github.com/sourcegraph/log"
)

func discoverDeviceName(logger sglog.Logger, filePath string) (string, error) {
return "", fmt.Errorf("not implemented on %s", runtime.GOOS)
}
34 changes: 18 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,36 @@ go 1.19

require (
github.com/google/go-cmp v0.5.9
github.com/prometheus/client_golang v1.13.0
github.com/sourcegraph/log v0.0.0-20221006140640-7c567caa79cb
golang.org/x/sys v0.1.0
github.com/moby/sys/mountinfo v0.6.2
github.com/prometheus/client_golang v1.14.0
github.com/sourcegraph/log v0.0.0-20221206163500-7d93c6ad7037
golang.org/x/sys v0.3.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/errors v1.9.0 // indirect
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/getsentry/sentry-go v0.13.0 // indirect
github.com/getsentry/sentry-go v0.16.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.22.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
Loading