获取固定驱动器列表

Get list of fixed drives

我如何才能获得一个包含所有物理驱动器挂载点的列表?我看到这里有一个类似的答案,但它列出了所有挂载点,包括网络共享。

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

好的,我决定重温我的 Win32 API 编程技巧并准备一个解决方案。

基于 the thread you referred to 的蹩脚方法的解决方案如下:

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)
    }
}

运行 按框(在 Wine 4.0 下)我得到:

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:\

(所有驱动器都使用 winecfg 映射。)


这种方法的问题是:

  • 即使系统中存在的 DOS 驱动器数量远小于 ASCII 大写字母的数量,它也会执行 26 个系统调用。

  • 在今天的 Windows 系统上,驱动器可能映射到常规目录下 — 与 POSIX 系统非常相似,因此它根本没有 DOS 驱动器号.

    Eryk Sun 准确地暗示了应该怎么做。
    我会尝试提供一个适当的(虽然更复杂)解决方案,将这些考虑因素考虑在内。


这里是 gist 代码。

GetDriveTypeW docs.

希望这会让您对 Win32 API 的工作原理以及如何在 Go 中使用它感兴趣。尝试在您的 Go 安装中查看 syscall 包的源代码——结合 MSDN 文档,这应该是有启发性的。

基于 Eryk Sun 在他们的评论中建议的改进方法。

代码

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
}

(这里是 gist 代码。)

在具有 4 个驱动器的 Wine 4.0 下(使用 `winecfg 配置),我有:

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:\

代码是这样滚动的:

  1. 枚举系统中的所有(不是 DOS“驱动器”)。
  2. 对于每个卷,它查询安装该卷的路径名列表(如果有)。
  3. 对于每个这样的路径名,它依次尝试获取其类型 — 看它是否固定。

优势

这种方法的好处是,正如我在其他回答中所述,此代码 应该 能够检测到非驱动器挂载 — 即作为目录挂载的卷,而不是DOS deives.

缺点

  • 代码比较复杂
  • 结果它实际上产生了一个两级结构(深度为 2 的树):一个固定卷的列表,每个卷都有一个其安装列表。
    唯一明显的问题是它可能更难处理,但你可以自由地伪造它,以便它 returns 一个简单的坐骑列表。

可能出现的问题

刚才我看到两个,可惜我没有方便的机器运行 Windows 放在一边检查。

第一个问题是,我希望对 kernel32!GetDriveTypeW 的调用能够处理卷名——那些由卷枚举 API 调用返回的 \?\Volume{169203c7-20c7-4ca6-aaec-19a806b9b81e}\ 风格的东西,但它没有't — 始终返回 DRIVE_UNKNOWN 错误代码。
因此,在我的代码中,我什至没有尝试在卷名上调用它,而是直接通过 kernel32!GetVolumePathNamesForVolumeNameW 查询卷的安装,然后尝试获取 上的驱动器类型。

我不知道为什么会这样。可能 - 只是可能 - 这是我用于测试的 Wine 4.0 中的一个错误,但不太可能。

另一个问题是我不知道如何在Wine 下创建一个新型的非驱动卷挂载,老实说我没有时间去了解。因此 可能 结果是 kernel32!GetDriveTypeW 也无法用于目录挂载(并且仅适用于“DOS 驱动器”挂载)。
MSDN 对这些问题保持沉默,所以我只是不知道它应该如何表现。如果我是你,我可能会在 Windows 盒子上做一些测试 ;-)