Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fullscreen inhibit gets stuck due to a race condition #285

Closed
gentoo-root opened this issue Aug 10, 2023 · 5 comments · Fixed by #286
Closed

Fullscreen inhibit gets stuck due to a race condition #285

gentoo-root opened this issue Aug 10, 2023 · 5 comments · Fixed by #286
Labels

Comments

@gentoo-root
Copy link

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.

@stuarthayhurst
Copy link
Collaborator

stuarthayhurst commented Aug 10, 2023

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.

@pakaoraki
Copy link
Collaborator

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.

@pakaoraki pakaoraki added the bug label Aug 11, 2023
@stuarthayhurst
Copy link
Collaborator

stuarthayhurst commented Aug 11, 2023

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...

@pakaoraki
Copy link
Collaborator

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.

@stuarthayhurst
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants