Skip to main content
added source to cater to commentwriters
Source Link

And yes, IT DOES WORK PERFECTLY (even if you're in "dos" on the "D:" drive - it still ejects fine!).

It is better to use the above original source of this answers, instead of my copy below (because copies don't get updated or have maintenance etc applied to them, like github does)... but here is all is:-

  1. Create a WSL "bash" script: /usr/local/bin/ejectusb
#!/bin/bash

cmd.exe /c start python3 C:\\windows\\ejectusb.py

-or-

  1. Create a DOS .bat file: C:\windows\ejectusb.bat
python3 C:\windows\ejectusb.py
  1. Create the ejection code: C:\windows\ejectusb.py
#!/usr/bin/python3

import string
import ctypes
from ctypes import wintypes  # Using ctypes.wintypes in the code below does not seem to work

# Ignore windows error popups. Fixes the whole "Can't open drive X" when user has an SD card reader.
ctypes.windll.kernel32.SetErrorMode(1) #type: ignore

# WinAPI Constants that we need
# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values.
DRIVE_REMOVABLE = 2 # [CodeStyle: Windows Enum value]

GENERIC_READ = 2147483648 # [CodeStyle: Windows Enum value]
GENERIC_WRITE = 1073741824 # [CodeStyle: Windows Enum value]

FILE_SHARE_READ = 1 # [CodeStyle: Windows Enum value]
FILE_SHARE_WRITE = 2 # [CodeStyle: Windows Enum value]

IOCTL_STORAGE_EJECT_MEDIA = 2967560 # [CodeStyle: Windows Enum value]

OPEN_EXISTING = 3 # [CodeStyle: Windows Enum value]

# Setup the DeviceIoControl function arguments and return type.
# See ctypes documentation for details on how to call C functions from python, and why this is important.
ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
    wintypes.HANDLE,                    # _In_          HANDLE hDevice
    wintypes.DWORD,                     # _In_          DWORD dwIoControlCode
    wintypes.LPVOID,                    # _In_opt_      LPVOID lpInBuffer
    wintypes.DWORD,                     # _In_          DWORD nInBufferSize
    wintypes.LPVOID,                    # _Out_opt_     LPVOID lpOutBuffer
    wintypes.DWORD,                     # _In_          DWORD nOutBufferSize
    ctypes.POINTER(wintypes.DWORD),     # _Out_opt_     LPDWORD lpBytesReturned
    wintypes.LPVOID                     # _Inout_opt_   LPOVERLAPPED lpOverlapped
]
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore



def checkRemovableDrives():
    drives = {}

    # The currently available disk drives, e.g.: bitmask = ...1100 <-- ...DCBA
    bitmask = ctypes.windll.kernel32.GetLogicalDrives()
    # Since we are ignoring drives A and B, the bitmask has has to shift twice to the right
    bitmask >>= 2
    # Check possible drive letters, from C to Z
    # Note: using ascii_uppercase because we do not want this to change with locale!
    # Skip A and B, since those drives are typically reserved for floppy disks.
    # Those drives can theoretically be reassigned but it's safer to not check them for removable drives.
    # Windows will also behave weirdly even with some of its internal functions if you do this (e.g. search indexing doesn't search it).
    # Users that have removable drives in A or B will just have to save to file and select the drive there.
    for letter in string.ascii_uppercase[2:]:
        drive = "{0}:/".format(letter)

        # Do we really want to skip A and B?
        # GetDriveTypeA explicitly wants a byte array of type ascii. It will accept a string, but this wont work
        if bitmask & 1 and ctypes.windll.kernel32.GetDriveTypeA(drive.encode("ascii")) == DRIVE_REMOVABLE:
            volume_name = ""
            name_buffer = ctypes.create_unicode_buffer(1024)
            filesystem_buffer = ctypes.create_unicode_buffer(1024)
            error = ctypes.windll.kernel32.GetVolumeInformationW(ctypes.c_wchar_p(drive), name_buffer, ctypes.sizeof(name_buffer), None, None, None, filesystem_buffer, ctypes.sizeof(filesystem_buffer))

            if error != 0:
                volume_name = name_buffer.value

            if not volume_name:
                volume_name = "Removable Drive"

            # Certain readers will report themselves as a volume even when there is no card inserted, but will show an
            # "No volume in drive" warning when trying to call GetDiskFreeSpace. However, they will not report a valid
            # filesystem, so we can filter on that. In addition, this excludes other things with filesystems Windows
            # does not support.
            if filesystem_buffer.value == "":
                continue

            # Check for the free space. Some card readers show up as a drive with 0 space free when there is no card inserted.
            free_bytes = ctypes.c_longlong(0)
            if ctypes.windll.kernel32.GetDiskFreeSpaceExA(drive.encode("ascii"), ctypes.byref(free_bytes), None, None) == 0:
                continue

            if free_bytes.value < 1:
                continue

            drives[drive] = "{0} ({1}:)".format(volume_name, letter)
        bitmask >>= 1

    return drives

def performEjectDevice(device):
    # Magic WinAPI stuff
    # First, open a handle to the Device
    #handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device.getId()[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
    handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
    #handle = ctypes.windll.kernel32.CreateFileA("E:/".encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )

    if handle == -1:
        # ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
        # So we use this to raise the error to our caller.
        raise ctypes.WinError()

    # The DeviceIoControl requires a bytes_returned pointer to be a valid pointer.
    # So create a ctypes DWORD to reference. (Without this pointer the DeviceIoControl function will crash with an access violation after doing its job.
    bytes_returned = wintypes.DWORD(0)

    error = None

    # Then, try and tell it to eject
    return_code = ctypes.windll.kernel32.DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, None, 0, None, 0, ctypes.pointer(bytes_returned), None)
    # DeviceIoControl with IOCTL_STORAGE_EJECT_MEDIA return 0 on error.
    if return_code == 0:
        # ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
        # So we use this to raise the error to our caller.
        error = ctypes.WinError()
        # Do not raise an error here yet, so we can properly close the handle.

    # Finally, close the handle
    ctypes.windll.kernel32.CloseHandle(handle)

    # If an error happened in the DeviceIoControl, raise it now.
    if error:
        raise error

    # Return success
    return True


mydrives=checkRemovableDrives()
if mydrives:
    #print(mydrives)
    for drive in mydrives:
        print("Ejecting drive {0} {1}".format(drive,mydrives[drive]))
        if performEjectDevice(drive):
            print("Success")
            exit(0)
        else:
            print("Failed")
            exit(0)
else:
    print("No removable drives")

Note that I have not installed "python" on my Windows-11, it's just magically there somehow (I do use WSL (Windows Services for Linux) and Fusion360, both of which include python)

And yes, IT DOES WORK PERFECTLY (even if you're in "dos" on the "D:" drive - it still ejects fine!).

It is better to use the above original source of this answers, instead of my copy below (because copies don't get updated or have maintenance etc applied to them, like github does)... but here is all is:-

  1. Create a WSL "bash" script: /usr/local/bin/ejectusb
#!/bin/bash

cmd.exe /c start python3 C:\\windows\\ejectusb.py

-or-

  1. Create a DOS .bat file: C:\windows\ejectusb.bat
python3 C:\windows\ejectusb.py
  1. Create the ejection code: C:\windows\ejectusb.py
#!/usr/bin/python3

import string
import ctypes
from ctypes import wintypes  # Using ctypes.wintypes in the code below does not seem to work

# Ignore windows error popups. Fixes the whole "Can't open drive X" when user has an SD card reader.
ctypes.windll.kernel32.SetErrorMode(1) #type: ignore

# WinAPI Constants that we need
# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values.
DRIVE_REMOVABLE = 2 # [CodeStyle: Windows Enum value]

GENERIC_READ = 2147483648 # [CodeStyle: Windows Enum value]
GENERIC_WRITE = 1073741824 # [CodeStyle: Windows Enum value]

FILE_SHARE_READ = 1 # [CodeStyle: Windows Enum value]
FILE_SHARE_WRITE = 2 # [CodeStyle: Windows Enum value]

IOCTL_STORAGE_EJECT_MEDIA = 2967560 # [CodeStyle: Windows Enum value]

OPEN_EXISTING = 3 # [CodeStyle: Windows Enum value]

# Setup the DeviceIoControl function arguments and return type.
# See ctypes documentation for details on how to call C functions from python, and why this is important.
ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore
    wintypes.HANDLE,                    # _In_          HANDLE hDevice
    wintypes.DWORD,                     # _In_          DWORD dwIoControlCode
    wintypes.LPVOID,                    # _In_opt_      LPVOID lpInBuffer
    wintypes.DWORD,                     # _In_          DWORD nInBufferSize
    wintypes.LPVOID,                    # _Out_opt_     LPVOID lpOutBuffer
    wintypes.DWORD,                     # _In_          DWORD nOutBufferSize
    ctypes.POINTER(wintypes.DWORD),     # _Out_opt_     LPDWORD lpBytesReturned
    wintypes.LPVOID                     # _Inout_opt_   LPOVERLAPPED lpOverlapped
]
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore



def checkRemovableDrives():
    drives = {}

    # The currently available disk drives, e.g.: bitmask = ...1100 <-- ...DCBA
    bitmask = ctypes.windll.kernel32.GetLogicalDrives()
    # Since we are ignoring drives A and B, the bitmask has has to shift twice to the right
    bitmask >>= 2
    # Check possible drive letters, from C to Z
    # Note: using ascii_uppercase because we do not want this to change with locale!
    # Skip A and B, since those drives are typically reserved for floppy disks.
    # Those drives can theoretically be reassigned but it's safer to not check them for removable drives.
    # Windows will also behave weirdly even with some of its internal functions if you do this (e.g. search indexing doesn't search it).
    # Users that have removable drives in A or B will just have to save to file and select the drive there.
    for letter in string.ascii_uppercase[2:]:
        drive = "{0}:/".format(letter)

        # Do we really want to skip A and B?
        # GetDriveTypeA explicitly wants a byte array of type ascii. It will accept a string, but this wont work
        if bitmask & 1 and ctypes.windll.kernel32.GetDriveTypeA(drive.encode("ascii")) == DRIVE_REMOVABLE:
            volume_name = ""
            name_buffer = ctypes.create_unicode_buffer(1024)
            filesystem_buffer = ctypes.create_unicode_buffer(1024)
            error = ctypes.windll.kernel32.GetVolumeInformationW(ctypes.c_wchar_p(drive), name_buffer, ctypes.sizeof(name_buffer), None, None, None, filesystem_buffer, ctypes.sizeof(filesystem_buffer))

            if error != 0:
                volume_name = name_buffer.value

            if not volume_name:
                volume_name = "Removable Drive"

            # Certain readers will report themselves as a volume even when there is no card inserted, but will show an
            # "No volume in drive" warning when trying to call GetDiskFreeSpace. However, they will not report a valid
            # filesystem, so we can filter on that. In addition, this excludes other things with filesystems Windows
            # does not support.
            if filesystem_buffer.value == "":
                continue

            # Check for the free space. Some card readers show up as a drive with 0 space free when there is no card inserted.
            free_bytes = ctypes.c_longlong(0)
            if ctypes.windll.kernel32.GetDiskFreeSpaceExA(drive.encode("ascii"), ctypes.byref(free_bytes), None, None) == 0:
                continue

            if free_bytes.value < 1:
                continue

            drives[drive] = "{0} ({1}:)".format(volume_name, letter)
        bitmask >>= 1

    return drives

def performEjectDevice(device):
    # Magic WinAPI stuff
    # First, open a handle to the Device
    #handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device.getId()[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
    handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
    #handle = ctypes.windll.kernel32.CreateFileA("E:/".encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )

    if handle == -1:
        # ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
        # So we use this to raise the error to our caller.
        raise ctypes.WinError()

    # The DeviceIoControl requires a bytes_returned pointer to be a valid pointer.
    # So create a ctypes DWORD to reference. (Without this pointer the DeviceIoControl function will crash with an access violation after doing its job.
    bytes_returned = wintypes.DWORD(0)

    error = None

    # Then, try and tell it to eject
    return_code = ctypes.windll.kernel32.DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, None, 0, None, 0, ctypes.pointer(bytes_returned), None)
    # DeviceIoControl with IOCTL_STORAGE_EJECT_MEDIA return 0 on error.
    if return_code == 0:
        # ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
        # So we use this to raise the error to our caller.
        error = ctypes.WinError()
        # Do not raise an error here yet, so we can properly close the handle.

    # Finally, close the handle
    ctypes.windll.kernel32.CloseHandle(handle)

    # If an error happened in the DeviceIoControl, raise it now.
    if error:
        raise error

    # Return success
    return True


mydrives=checkRemovableDrives()
if mydrives:
    #print(mydrives)
    for drive in mydrives:
        print("Ejecting drive {0} {1}".format(drive,mydrives[drive]))
        if performEjectDevice(drive):
            print("Success")
            exit(0)
        else:
            print("Failed")
            exit(0)
else:
    print("No removable drives")

Note that I have not installed "python" on my Windows-11, it's just magically there somehow (I do use WSL (Windows Services for Linux) and Fusion360, both of which include python)

Source Link

This option works in DOS and WSL, and ejects the device without asking any questions or popping open UI dialogues etc:-

https://gist.github.com/gitcnd/0fcc98e2dd2b18b844770666d95e8bf7