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?
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?
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.
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.
An improved approach based on what Eryk Sun suggested in their comment.
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:
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.
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 ;-)