Skip to content

Instantly share code, notes, and snippets.

@gitcnd
Created December 4, 2022 12:33
Show Gist options
  • Select an option

  • Save gitcnd/0fcc98e2dd2b18b844770666d95e8bf7 to your computer and use it in GitHub Desktop.

Select an option

Save gitcnd/0fcc98e2dd2b18b844770666d95e8bf7 to your computer and use it in GitHub Desktop.

Revisions

  1. gitcnd created this gist Dec 4, 2022.
    149 changes: 149 additions & 0 deletions ejectusb.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,149 @@
    #!/usr/bin/python3

    # From DOS commandline, run this as follows:-
    # python3 ejectusb.py
    # -or- create C:\windows\ejectusb.bat to do the above in fewer characters.
    #
    # From WSL bash, run it as follows:-
    # cmd.exe /c start python3 C:\\windows\\ejectusb.py
    # -or- create /usr/local/bin/ejectusb to do the above in fewer characters.

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