rr-/screeninfo

Python 3.8.x - sometimes fails

Berserker66 opened this issue · 19 comments

monitors = screeninfo.get_monitors()
print(monitors)

This will, for some reason, in Python 3.8.1 on windows 10 64Bit, sometimes fail. Not always. The way it fails is, that it returns an empty list.

Running the same code results in varying results, even in console:

image

It works fine on Python 3.7.6

Same thing here with Python 3.8.2 on Windows 10.
get_monitors() sporadically returns an empty list, sometimes multiple times in succession. No exceptions what so ever.

>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                
 >>> get_monitors()                                                                                                                
[]                                                                                                                               
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                               
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 
>>> get_monitors()                                                                                                                
[Monitor(x=0, y=0, width=3200, height=1800, width_mm=310, height_mm=174, name='\\\\.\\DISPLAY1')]                                 >>> 
get_monitors()                                                                                                                
[]                                                                                                                                
>>> get_monitors()                                                                                                                
[]                                                                                                                                

Small updates:

  • On my Manjaro machine with Python 3.8.1 get_monitors never fails.

  • I digged a bit into this on my Windows machine and found that with the code change:

    enum_res = ctypes.windll.user32.EnumDisplayMonitors(
        dc_full, None, MonitorEnumProc(callback), 0
    )
    print("enum_res", enum_res)
    print(monitors)

enum_res is 0 when the list is empty so it seems that it is the EnumDisplayMonitors method on Windows that fails. Given that it fails randomly I don't know if we can do anything to it in the scope of screeninfo? I also don't know how to troubleshoot it why it fails since it doesn't throw any errors as is.

The problem is python 3.8 specific though, so something there must have changed and broke it.

rr- commented

When EnumDisplayMonitors returns a zero value, it means it failed. Could you please try to call WinAPI's GetLastError function and see what error code it returns? (Unfortunately I don't have any code snippets for this) Thanks

I installed pypiwin32 to get easier access to GetLastError and by calling it right after EnumDisplayMonitors:

enum_res = ctypes.windll.user32.EnumDisplayMonitors(
        dc_full, None, MonitorEnumProc(callback), 0
    )
print(GetLastError())

it returns '6' regardless of whether the EnumDisplayMonitors call failed or succeeded.

Edit: Now that I thought it through it should keep printing the last error if the call succeeds..
Edit2: And the error code is something else (126) if the first call in a fresh python instance succeeds.

As per here: https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
the code '6' is ERROR_INVALID_HANDLE. I tested printing the dc_full variable during enumeration and it is a negative integer when the call fails and positive when the enumeration succeeds.

rr- commented

Out of curiosity, what if you wrap the body of the callback function with a try: … except Exception as ex: print(ex)? Maybe it has an unhandled exception and that's what stops the enumeration?

Tested with the try catch in the callback and it doesn't catch any exceptions.

I tried replacing the dc_full GetDC call with just a "0" (dc_full = 0) and then the enumeration goes through but it does not get the physical sizes in that case. It seems like that the GetDC method randomly returns an invalid handle.

rr- commented

Does changing GetDC(None) to GetDC(0) change anything?
Also, I'm not sure, but it's possible we should be calling ReleaseDC.

If not, our best shot for at least a partial fix I guess is to protect against invalid device contexts by falling back to 0 and not reporting the physical sizes in such cases.

Changing GetDC(None) to GetDC(0) didn't seem to change anything. Neither did adding a call to ReleaseDC.

Alternatively maybe we could query for a device context until we get a valid one and then continue to enumeration? The failure rate is not excessively high so this would mean a few extra calls at most.

rr- commented

Sounds good, but I'm afraid in pathological cases it could cause the application to hang, so I'd avoid coding a dumb busy loop and add some safety guard (such as if more than 1 s elapsed or we tried more than 100 times, give up)

10 tries should be plenty. But it would be nice if that's not necessary at all. Might be some problem with Python 3.8's ctypes? They made changes to the C api calling.

Yeah seems like it would be a regression in the GetDC implementation in ctypes or something related. Do you know where we could look for if this is a known issue there or where we should report it?

rr- commented

Unfortunately I don't.

I wrote a safety that stops trying after 100 queries since they seem to be very fast but I don't quite know what the behavior should be in the running out of retries case. I haven't tested whether dc_full = 0 produces correct resolutions and positions with multiple monitors since I've tested this on my laptop.

I myself am depending on the physical size data to reliably either be present or not but it's not designed for it to come and go. Would be nice if I wouldn't need to implement further workaround down the stream.

Finally had access to a Windows machine with 2 monitors to test my changes from here hhannine@2d37a82

Compared to 11/1000 calls failed of the current release my patch does succeed every time. The fall back does not report physical sizes but resolution and x,y positions seems to be OK.

rr- commented

Merged the fix by @hhannine ; thanks. Would you like me to release the fix now?

@rr-
We're looking at a potential update push next week, the only blocker left for moving to 3.8 in the list is this issue.
(medium size software for German doctor's offices)

rr- commented

Thanks for the reply. Released as 0.6.2. Let me know if you still encounter the problem, we'll create a hotfix if needed.