Kalmat / PyWinCtl

Cross-Platform module to get info on and control windows on screen
Other
191 stars 20 forks source link

getWindowsAt errors randomly on Linux with PyQt6 #18

Closed Avasam closed 2 years ago

Avasam commented 2 years ago

Here's my stack trace. Simply calling pywinctl.getWindowsAt(x, y) results in an error most of the time. Seemingly randomly.
Ubuntu 22.04
Possibly caused by the fact I generate a PyQt widget for the screen-point selection. I wouldn't be surprised if it tries to create a LinuxWindow with it and fails.

Traceback (most recent call last):

  File "src/AutoSplit.py", line 175, in <lambda>
    self.select_window_button.clicked.connect(lambda: select_window(self))

  File "/home/avasam/Documents/Projects/Auto-Split/src/region_selection.py", line 130, in select_window
    hwnd, window_text = __get_window_from_point(x, y)

  File "/home/avasam/Documents/Projects/Auto-Split/src/region_selection.py", line 181, in __get_window_from_point
    windows = pywinctl.getWindowsAt(x, y)

  File "/usr/local/lib/python3.10/dist-packages/pywinctl/_pywinctl_linux.py", line 252, in getWindowsAt
    for win in getAllWindows():

  File "/usr/local/lib/python3.10/dist-packages/pywinctl/_pywinctl_linux.py", line 117, in getAllWindows
    return [LinuxWindow(window) for window in windows]

  File "/usr/local/lib/python3.10/dist-packages/pywinctl/_pywinctl_linux.py", line 117, in <listcomp>
    return [LinuxWindow(window) for window in windows]

  File "/usr/local/lib/python3.10/dist-packages/pywinctl/_pywinctl_linux.py", line 381, in __init__
    self._parent = self._hWnd.query_tree().parent

  File "/home/avasam/.local/lib/python3.10/site-packages/Xlib/xobject/drawable.py", line 413, in query_tree
    return request.QueryTree(display = self.display,

  File "/home/avasam/.local/lib/python3.10/site-packages/Xlib/protocol/rq.py", line 1481, in __init__
    self.reply()

  File "/home/avasam/.local/lib/python3.10/site-packages/Xlib/protocol/rq.py", line 1501, in reply
    raise self._error

Xlib.error.BadWindow: <class 'Xlib.error.BadWindow'>: code = 3, resource_id = <class 'Xlib.xobject.resource.Resource'>(0x03e0004b), sequence_number = 263, major_opcode = 15, minor_opcode = 0
Avasam commented 2 years ago

I think the issue is that I had a window disappear at the same time. The following fixed the issue and properly only listed existing non-problematic windows:

def getAllWindows():
    """
    Get the list of Window objects for all visible windows
    :return: list of Window objects
    """
    windows = EWMH.getClientList()
    def remove_bad_windows(windows):
        for window in windows:
            try:
                yield LinuxWindow(window)
            except Xlib.error.XResourceError:
                pass
    return [window for window in remove_bad_windows(windows)]

Edit: added the full getAllWindows function.

Kalmat commented 2 years ago

Hi! Thank you so much for your comments and help!

Sorry not to properly understand your issue. Is it solved now? Just curious, what does it mean "a window disappear"? Can you explain a little further or, even better, paste an example on how you create that window/widget? I think I could use your piece of code to solve the problem, right? But, since it's Linux specific, I would need to test/develop in other platforms too!

Thanks again.

Avasam commented 2 years ago

Hi! Sorry I'll try to be a bit more clear. Especially now that I have a better understanding of what's happening.

A bit of context: I ask my users to select a window by clicking on it. The way I achieve that using PyQt is to create a new Widget, place it above all windows fit it to screen size, (it's also transparent gray as a visual overlay indicator) then detect clicks on that widgets (so I know the X/Y position). Once the user has clicked, I record the click position and close the widget.* (<-- keep this in mind) I then ask PyWinCtl to tell me which windows intersect that position using getWindowsAt. But LinuxWindow (which is called by getWindowsAt) errors out.

The issue on the code side: The error happens because LinuxWindow tries to manipulate an invalid hWnd. Which in turn happens because EWMH.getClientList() in getAllWindows returned some invalid windows. There seem to be a wrong assumption in the code that getAllWindows will always return valid windows. I think that assumption is fine if returning a single Window (as I could just try-except it), but it's problematic when I expect a list, one of the elements are invalid, and I end up not being able to access the rest of the list.

How I think I'm triggering the issue: I believe that in my specific case, I get an invalid window because it's picked up by EWMH.getClientList as my Widget window is closing. I'd need to do a bit more tests to show whether that's actually the trigger here. I'll also try to get you a minimal reproduction. But in any case, it shows that it's possible in certain scenarios for EWMH.getClientList to return unusable/invalid windows.

Possible fixes:

  1. Remove windows that error out from getAllWindows before returning the list (this what my snippet of code above does)
  2. Include the windows but mark them as "invalid" somehow (either through typing or with a property). And let the user decide how to deal with them.
  3. idk, I'm sure there's more solutions I haven't thought of on the spot.

Once again, I don't think getAllWindows should raise an error because one item in the list is invalid, when the user may not even have control over this (the offending window may not even be part of the user's application/python script!).

Kalmat commented 2 years ago

Thanks a lot for your time and this careful and so well explained issue. This kind of discussion really helps me out to improve the module's approach from a real development perspective (not what I somehow "imagine" it should be, I mean).

The general answer is yes, getAllWindows() and all other methods can return "bad windows" (e.g., as you pointed out, a window closing right after it has been included in the return list). Some cases can be detected within getAllWindows(), but not all of them. So, de decission was to return everything, and let the developer to decide what to do with it and how to control it (note there is a property named isAlive, amongst others, which may help to detect the window status before doing anything with it). Anyway this doesn't mean it has to crash, of course!

Another fix that I was debating with myself is to wrap some parts within a try -- except block. This would avoid crashes like this, but would "hide" other relevant, conceptual errors. Since PyWinCtl module is still very experimental, I preferred to let it crash, so these errors would help to detect and improve "bad assumptions" I could've made.

In this case, I think the best option is to use your piece of code to make a last "filter", returning only "good windows", whilst avoiding to crash. I will apply this solution to all other methods which return Window() objects and all other platforms. Thanks a lot for this.

Besides, if you have any Xlib knowledge... do you know if there is any way or anything I can try to send a window above the wallpaper but behind desktop icons in GNOME??? This problem is turning me crazy (I managed to make it work in sendBehind() method for Mint/Cinnamon and Raspbian/LXDE, but found no way to do it in GNOME!)

Thanks a lot again!

Avasam commented 2 years ago

Given my use case, I wouldn't mind either solutions.

Unfortunately I'm still very new to developing on Linux (and MacOS). I am learning a lot as I am porting https://github.com/Avasam/Auto-Split/tree/linux , which deals with recording specific windows or screens, window positions and sizes, listening to user inputs, and sending keystrokes.

But if I find anything, I'll gladly let you know. I'd like to abstract away the Window system for all 3 platforms in my project and this is exactly what PyWinCtl already does. So any improvements I can bring to fully switch to an existing library instead of re-inventing the wheel, I'll do so.

Kalmat commented 2 years ago

Wow! Really interesting module! It seems quite complex too, an impressive point to start learning from!

I am out these days, but I hope that in a few days I will upload a new version of PyWinCtl including the improvements you suggested to avoid crashes. I will let you know so you can keep working with it. So happy to hear it's useful!

I really appreciate your comments and any issue, comment or suggestion you may have in the future.

Keep pushing, mate!!!