Python 3.8.x - sometimes fails
Berserker66 opened this issue · 19 comments
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.
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.
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.
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.
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?
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-
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)
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.