eonpatapon / gnome-shell-extension-caffeine

Disable screensaver and auto suspend

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Fullscreen inhibit gets stuck due to a race condition

gentoo-root opened this issue · comments

Steps to reproduce:

  1. Configure Caffeine to inhibit screen lock for fullscreen windows (the default configuration).
  2. Open and close a fullscreen window very frequently. For example, open a picture in Eog and double-click on it constantly to toggle fullscreen.
  3. Observe a lot of "fullscreen" inhibitors accumulate using the following script:
busctl --user -j call org.gnome.SessionManager /org/gnome/SessionManager org.gnome.SessionManager GetInhibitors | jq -r '.data[0][]' | while read -r obj; do
	busctl --user call org.gnome.SessionManager "$obj" org.gnome.SessionManager.Inhibitor GetAppId
done

Output:

s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"
s "fullscreen"

By adding debug prints, I found out that addInhibit('fullscreen') can be called multiple times in a row, followed by only one removeInhibit('fullscreen'). What I believe to be the root cause of the issue is that addInhibit() doesn't populate this._appInhibitedData right away, but only after the callback is called. Combined with a 2-second delay in toggleFullscreen(), this means that this piece of code may be scheduled multiple times over a 2-second window, and when it comes to executing it, this._appInhibitedData.has('fullscreen') will return false every time, addInhibit will be called more than once, and only then its callback will kick in and populate this._appInhibitedData, effectively overwriting old cookies and not allowing to remove the inhibit anymore.

Thanks for troubleshooting this, I'll try to reproduce this and patch it :)

EDIT: So thinking about this, there isn't really a lot that can be done for race conditions in JS, nearly any check we do might still slip through. Maybe we could give each inhibitor an ID as well, like 'fullscreen-1'. Then when 'fullscreen' is removed, it removes all instances of 'fullscreen-[any ID]'. That way a collision can't take place, and inhibitors can't be lost.

I think I managed a similar issue with apps:

            // Block App state signal
            appSys.block_signal_handler(this._appStateChangedSignalId);

           .....
            }

            // Add 200 ms delay before unblock state signal
            this._timeAppUnblock = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => {
                appSys.unblock_signal_handler(this._appStateChangedSignalId);
                this._timeAppUnblock = null;
                return GLib.SOURCE_REMOVE;
            });

When an app toggle caffeine, it block the signal event for 200 ms: this avoid adding multiple inhibitors. I reckon it can be done for fullscreen inhibitor ? I will check that.

Since we have no locking primitives in JS, it will probably still be possible to register duplicate entries. Still, it seems like a good approach, I don't see why it shouldn't work.

Only semi-related, but I noticed the Caffeine class is trying to do a lot at once. It might be worth creating a separate InhibitorManager class that allows us to register and unregister inhibitors without worrying about the details so much. It also means the Caffeine class is directly responsible for 1 less thing, hopefully making maintenance easier.

If you like the sound of that, I'll work on it soon :)

EDIT: Whoops sorry if that sounded negative, I accidentally deleted the line where I agreed with you...

After few test, I came up with a reset of the two second timer for each call of toggleFullscreen(). That prevent the creation of multiple timers and then inhibitors.

 GLib.Source.remove(this._timeFullscreen);
 this._timeFullscreen = null;

I can make a PR tonight if you like this solution, it's seams to work fine and fix the issue in my test.

Only semi-related, but I noticed the Caffeine class is trying to do a lot at once. It might be worth creating a separate InhibitorManager class that allows us to register and unregister inhibitors without worrying about the details so much. It also means the Caffeine class is directly responsible for 1 less thing, hopefully making maintenance easier.

I think it's a good point, it make sense to create a specific class to manage inhibitors. It's something that can be done in a second time maybe but I like this idea.

GLib.Source.remove(this._timeFullscreen);
this._timeFullscreen = null;

I can make a PR tonight if you like this solution, it's seams to work fine and fix the issue in my test.

Good to hear it working, nice and simple fix :D
If you submit a PR with that, make sure to add a check that this._timeFullscreen isn't null first, as otherwise we'll get warnings in the shell's logs, and possibly fail the extension review when it's uploaded.