1

How can I get a list of all mount points for physical drives only? I see there is a similar answer on here but this lists all mount points including network shares.

How can I get a listing of all drives on Windows using golang?

kostix
  • 51,517
  • 14
  • 93
  • 176
Jack bladk
  • 13
  • 2
  • Use the stock package `syscall` to call appropriate Win32 API function. What is your particular problem with that? – kostix Jul 04 '20 at 17:05
  • I'm new to Go so I'm not sure how to do it. I want to set it as a variable in a filepath.WalkFunc to list all files/folders in each physical drive (but not network drives) – Jack bladk Jul 04 '20 at 17:52
  • 1
    DOS drive names (i.e. "[A-Z]:") are usually aliases for volume devices, on which the default filesystem mountpoint is the root path. For example, "//?/C:" is a volume device (but just "C:" is a drive-relative path), and "C:/" is the filesystem mountpoint. Windows also has bind mountpoints, which are volume mountpoints if they target the root path of the "Volume{GUID}" device name. The mountpoint manager tracks all of this, and you can enumerate the volume names it knows about via `FindFirstVolumeW` / `FindNextVolumeW` and get the mountpoints for each via `GetVolumePathNamesForVolumeNameW`. – Eryk Sun Jul 05 '20 at 01:04

2 Answers2

1

OK, I've decided to dust off my Win32 API programming skills and prepare a solution.

A solution based on the lame approach from the thread you referred to is as follows:

package main

import (
    "errors"
    "fmt"
    "log"
    "syscall"
    "unsafe"
)

var (
    kernel32 = syscall.NewLazyDLL("kernel32.dll")

    getDriveTypeWProc = kernel32.NewProc("GetDriveTypeW")
)

func getDriveType(rootPathName []uint16) (int, error) {
    rc, _, _ := getDriveTypeWProc.Call(
        uintptr(unsafe.Pointer(&rootPathName[0])),
    )

    dt := int(rc)

    if dt == driveUnknown || dt == driveNoRootDir {
        return -1, driveTypeErrors[dt]
    }

    return dt, nil
}

var (
    errUnknownDriveType = errors.New("unknown drive type")
    errNoRootDir        = errors.New("invalid root drive path")

    driveTypeErrors = [...]error{
        0: errUnknownDriveType,
        1: errNoRootDir,
    }
)

const (
    driveUnknown = iota
    driveNoRootDir

    driveRemovable
    driveFixed
    driveRemote
    driveCDROM
    driveRamdisk
)

func getFixedDOSDrives() ([]string, error) {
    var drive = [4]uint16{
        1: ':',
        2: '\\',
    }

    var drives []string

    for c := 'A'; c <= 'Z'; c++ {
        drive[0] = uint16(c)
        dt, err := getDriveType(drive[:])

        if err != nil {
            if err == errNoRootDir {
                continue
            }
            return nil, fmt.Errorf("error getting type of: %s: %s",
                syscall.UTF16ToString(drive[:]), err)
        }

        if dt != driveFixed {
            continue
        }

        drives = append(drives, syscall.UTF16ToString(drive[:]))
    }

    return drives, nil
}

func main() {
    drives, err := getFixedDOSDrives()
    if err != nil {
        log.Fatal(err)
    }
    for _, drive := range drives {
        log.Println(drive)
    }
}

Running on by box (under Wine 4.0) I get:

tmp$ GOOS=windows go build drvs.go 
tmp$ wine64 ./drvs.exe
0009:fixme:process:SetProcessPriorityBoost (0xffffffffffffffff,1): stub
2020/07/06 21:06:02 C:\
2020/07/06 21:06:02 D:\
2020/07/06 21:06:02 X:\
2020/07/06 21:06:02 Z:\

(All drives are mapped using winecfg.)


The problems with this approach are:

  • It performs 26 system calls even if the number of DOS drives present in the system is way smaller than the number of ASCII capital letters.

  • On today's Windows systems a drive may be mapped under a regular directory — much like on POSIX systems, and so it will not have a DOS drive letter at all.

    Eryk Sun hinted precisely at what should be done about this.
    I will try to come back with a proper (although more complicated) solution which takes those considerations into account.


Here is a gist with the code.

GetDriveTypeW docs.

Hope this will get you interested in how Win32 API works and how to work with it from Go. Try looking at the sources of the syscall package in your Go installation — coupled with the MSDN docs, this should be revealing.

kostix
  • 51,517
  • 14
  • 93
  • 176
  • Thank you kindly, this works and I will look at the source for syscall. I was also looking at the gopsutil library to archive this and will report back if I'm successful. – Jack bladk Jul 06 '20 at 19:48
1

An improved approach based on what Eryk Sun suggested in their comment.

The code

package main

import (
    "errors"
    "log"
    "strings"
    "syscall"
    "unsafe"
)

func main() {
    mounts, err := getFixedDriveMounts()
    if err != nil {
        log.Fatal(err)
    }
    for _, m := range mounts {
        log.Println("volume:", m.volume,
            "mounts:", strings.Join(m.mounts, ", "))
    }
}

var (
    kernel32 = syscall.NewLazyDLL("kernel32.dll")

    findFirstVolumeWProc = kernel32.NewProc("FindFirstVolumeW")
    findNextVolumeWProc  = kernel32.NewProc("FindNextVolumeW")
    findVolumeCloseProc  = kernel32.NewProc("FindVolumeClose")

    getVolumePathNamesForVolumeNameWProc = kernel32.NewProc("GetVolumePathNamesForVolumeNameW")

    getDriveTypeWProc = kernel32.NewProc("GetDriveTypeW")
)

const guidBufLen = syscall.MAX_PATH + 1

func findFirstVolume() (uintptr, []uint16, error) {
    const invalidHandleValue = ^uintptr(0)

    guid := make([]uint16, guidBufLen)

    handle, _, err := findFirstVolumeWProc.Call(
        uintptr(unsafe.Pointer(&guid[0])),
        uintptr(guidBufLen*2),
    )

    if handle == invalidHandleValue {
        return invalidHandleValue, nil, err
    }

    return handle, guid, nil
}

func findNextVolume(handle uintptr) ([]uint16, bool, error) {
    const noMoreFiles = 18

    guid := make([]uint16, guidBufLen)

    rc, _, err := findNextVolumeWProc.Call(
        handle,
        uintptr(unsafe.Pointer(&guid[0])),
        uintptr(guidBufLen*2),
    )

    if rc == 1 {
        return guid, true, nil
    }

    if err.(syscall.Errno) == noMoreFiles {
        return nil, false, nil
    }
    return nil, false, err
}

func findVolumeClose(handle uintptr) error {
    ok, _, err := findVolumeCloseProc.Call(handle)
    if ok == 0 {
        return err
    }

    return nil
}

func getVolumePathNamesForVolumeName(volName []uint16) ([][]uint16, error) {
    const (
        errorMoreData = 234
        NUL           = 0x0000
    )

    var (
        pathNamesLen uint32
        pathNames    []uint16
    )

    pathNamesLen = 2
    for {
        pathNames = make([]uint16, pathNamesLen)
        pathNamesLen *= 2

        rc, _, err := getVolumePathNamesForVolumeNameWProc.Call(
            uintptr(unsafe.Pointer(&volName[0])),
            uintptr(unsafe.Pointer(&pathNames[0])),
            uintptr(pathNamesLen),
            uintptr(unsafe.Pointer(&pathNamesLen)),
        )

        if rc == 0 {
            if err.(syscall.Errno) == errorMoreData {
                continue
            }

            return nil, err
        }

        pathNames = pathNames[:pathNamesLen]
        break
    }

    var out [][]uint16
    i := 0
    for j, c := range pathNames {
        if c == NUL && i < j {
            out = append(out, pathNames[i:j+1])
            i = j + 1
        }
    }
    return out, nil
}

func getDriveType(rootPathName []uint16) (int, error) {
    rc, _, _ := getDriveTypeWProc.Call(
        uintptr(unsafe.Pointer(&rootPathName[0])),
    )

    dt := int(rc)

    if dt == driveUnknown || dt == driveNoRootDir {
        return -1, driveTypeErrors[dt]
    }

    return dt, nil
}

var (
    errUnknownDriveType = errors.New("unknown drive type")
    errNoRootDir        = errors.New("invalid root drive path")

    driveTypeErrors = [...]error{
        0: errUnknownDriveType,
        1: errNoRootDir,
    }
)

const (
    driveUnknown = iota
    driveNoRootDir

    driveRemovable
    driveFixed
    driveRemote
    driveCDROM
    driveRamdisk

    driveLastKnownType = driveRamdisk
)

type fixedDriveVolume struct {
    volName          string
    mountedPathnames []string
}

type fixedVolumeMounts struct {
    volume string
    mounts []string
}

func getFixedDriveMounts() ([]fixedVolumeMounts, error) {
    var out []fixedVolumeMounts

    err := enumVolumes(func(guid []uint16) error {
        mounts, err := maybeGetFixedVolumeMounts(guid)
        if err != nil {
            return err
        }
        if len(mounts) > 0 {
            out = append(out, fixedVolumeMounts{
                volume: syscall.UTF16ToString(guid),
                mounts: LPSTRsToStrings(mounts),
            })
        }
        return nil
    })

    if err != nil {
        return nil, err
    }

    return out, nil
}

func enumVolumes(handleVolume func(guid []uint16) error) error {
    handle, guid, err := findFirstVolume()
    if err != nil {
        return err
    }
    defer func() {
        err = findVolumeClose(handle)
    }()

    if err := handleVolume(guid); err != nil {
        return err
    }

    for {
        guid, more, err := findNextVolume(handle)
        if err != nil {
            return err
        }

        if !more {
            break
        }

        if err := handleVolume(guid); err != nil {
            return err
        }
    }

    return nil
}

func maybeGetFixedVolumeMounts(guid []uint16) ([][]uint16, error) {
    paths, err := getVolumePathNamesForVolumeName(guid)
    if err != nil {
        return nil, err
    }

    if len(paths) == 0 {
        return nil, nil
    }

    var lastErr error
    for _, path := range paths {
        dt, err := getDriveType(path)
        if err == nil {
            if dt == driveFixed {
                return paths, nil
            }
            return nil, nil
        }
        lastErr = err
    }

    return nil, lastErr
}

func LPSTRsToStrings(in [][]uint16) []string {
    if len(in) == 0 {
        return nil
    }

    out := make([]string, len(in))
    for i, s := range in {
        out[i] = syscall.UTF16ToString(s)
    }

    return out
}

(Here is a gist with this code.)

Under Wine 4.0 with 4 drives (configured using `winecfg), I have:

tmp$ GOOS=windows go build fvs.go
tmp$ wine64 ./fvs.exe 
0009:fixme:process:SetProcessPriorityBoost (0xffffffffffffffff,1): stub
2020/07/09 22:48:25 volume: \\?\Volume{00000000-0000-0000-0000-000000000043}\ mounts: C:\
2020/07/09 22:48:25 volume: \\?\Volume{00000000-0000-0000-0000-000000000044}\ mounts: D:\
2020/07/09 22:48:25 volume: \\?\Volume{00000000-0000-0000-0000-00000000005a}\ mounts: Z:\
2020/07/09 22:48:25 volume: \\?\Volume{169203c7-20c7-4ca6-aaec-19a806b9b81e}\ mounts: X:\

The code rolls like this:

  1. Enumerates all the volumes (not DOS "drives") in the system.
  2. For each volume, it queries the list of pathnames on which that volume is mounted, if any.
  3. For each such pathname in turn, it tries to get its type — to see whether it's fixed or not.

The upside

The upside of this approach is, as stated in my other answer, this code should be able to detect non-drive mounts — that is, voluments mounted as directories, not DOS deives.

The downsides

  • The code is more complicated.
  • The it actually produces a two-level structure (a tree of depth 2) as a result: a list of fixed volumes, each holding a list of its mounts.
    The only tangible problem with this is that it may be harder to process but you're free to forge it so that it returns a flat list of mounts.

Possible problems

Just now I see two, and I unfortunately have no easily accessible machine running Windows aside to check.

The first problem is that I would expect a call to kernel32!GetDriveTypeW to work on volume names — those \\?\Volume{169203c7-20c7-4ca6-aaec-19a806b9b81e}\-style things returned by volume enumeration API calls, but it doesn't — always returning the DRIVE_UNKNOWN error code.
So, in my code I do not even attempt to call it on a volume name, and go directly to querying the volume's mounts via the kernel32!GetVolumePathNamesForVolumeNameW and then attempt to get the drive type on them.

I have no idea why it works like this. May be — just may be — it's a bug in Wine 4.0 which I used for testing, but it's unlikely.

Another problem is that I have no idea how to create a new-style non-drive volume mount under Wine, and honestly I have no time to find out. It hence may turn out that kernel32!GetDriveTypeW also fails for directory mounts (and works only for "DOS drive" mounts).
MSDN is silent on these matters, so I just don't know how it's supposed to behave. If I were you I'd probably do some testing on a Windows box ;-)

kostix
  • 51,517
  • 14
  • 93
  • 176
  • @eryk-sun, do you may be have any insight on the `GetDriveTypeW` issues I discuss at the bottom of this question? – kostix Jul 09 '20 at 20:14