Skip to content

Conversation

fohrloop
Copy link
Owner

@fohrloop fohrloop commented Sep 25, 2024

Closes: #404 #64

Add support for unix systems with GTK

  • The solution supports both, GTK3 and GTK4, and adds keep.running and keep.presenting modes to all platforms with the GTK graphical toolkit. Many desktop environments are based on GTK. A few examples are GNOME, Xfce, Cinnamon, LXDE, MATE, Unity, Budgie and Pantheon.
  • The requirements for this method to work are (1) GTK installed on the system (this a bit vague, but if you're running a DE which is based on GTK, that's most likely enough) 2) The python bindings for it; the PyGObject (gi) python library installed either on the current python environment or the system python site packages.

Details

  • Use the Gtk.Application.inhibit() from the GObject Instrospection python package (import name: gi, package name: PyGObject) for inhibiting sleep/idle. This is the PyGObject interface to the gtk_application_inhibit() function.
  • Add new server script which can be started with any python interpreter. The idea is that this is called with the system interpreter since that is expected to have the gi module for accessing GTK functions from python. User is not required to have installed the PyGObject in their current (virtual) environment. It's just faster to use (300ms vs 5ms) if gi is available in the current python environment. Most people probably won't care if their long running script takes a 300ms more, and not having to install PyGObject is a nice thing as the installation will require compilation step(s), so it's a bit trickier and slower to install than a pure python package.
  • New concept: Inhibit module. This is a python module (a .py file), which should contain one class with the name Inhibitor, which should comply with the new Inhibitor protocol: There must be a start(self, *args) and stop(self) methods. The *args are positional arguments given to the server and passed to the inhibitor.

TODO before merge

  • Add logic to get the system python interpreter path
  • Add logic for creating a random socket
  • Create wakepy.Method for this
  • Pass down the IDLE vs SUSPEND argument from the Method
  • Add tests
  • Update documentation
  • Check that the solution is not considerably slower than the other altenatives (like: D-Bus based methods) -- Assessed here: It's like 200-300ms for the inhibit when using system python. Not too bad.
  • Check is the wakelock released if the main process crashes -- Assessed here
  • Make it possible to control the printing / logging in the inhibitor server by using args
  • Unix sockets vs other possible alternatives (comparison) -- Assessed here
  • Implement subprocess.PIPE based solution (replace the one using unix sockets)
  • Would it be possible to use Method classes (and not introduce "inhibitor modules")? Or could "inhibitor modules" replace Method classes? -- Part of Using system python in a subprocess to enter wakepy Modes #484

TODO Later

  • Check how to make gtk_application_inhibit usable in environments using PyInstaller (__file__, import_module, ..)
  • Could D-Bus based method also be used with system python?
  • Test gtk_application_inhibit on Xfce, Cinnamon, LXDE, MATE, Unity, Budgie and Pantheon

@fohrloop
Copy link
Owner Author

fohrloop commented Sep 26, 2024

I have now a working implementation which still requires some refactoring + tests, but here are some timings. It can run in two modes:

  • If gi (PyGObject) is installed to the current python environment, that is used.
  • If gi is not installed to current python environment, but is available to system python, that is used.

Timings

Here are some timings for doing one inhibit -> uninhibit cycle:

Code used for benchmarking
import time 
import logging 
logging.basicConfig(level=logging.DEBUG)

from wakepy import keep

t0 = time.time()
with keep.presenting(methods=['gtk_application_inhibit']) as m:
    ...
t1 = time.time()
print(t1-t0)
  • Using local gi module: 198ms first run, 3.8ms second run. (for reference: org.gnome.SessionManager method took around 5ms)
  • Using the subprocess + system python: 200-300ms each run
  • out of this, about 150 ms is used for importing Gtk; this line:
    from gi.repository import Gio, Gtk.
  • Therefore, inhibiting with the system python in a subprocess adds about 100-150ms

Using local gi module:

# first run
DEBUG:wakepy.core.mode:'WAKEPY_FAKE_SUCCESS' not set.
DEBUG:wakepy.pyinhibitor.inhibitors:Inhibitor module 'wakepy.methods.gtk.inhibitor' loaded to local python environment
DEBUG:wakepy.methods.gtk.inhibitor:Registering Gtk.Application with id  io.readthedocs.wakepy.inhibitor1
DEBUG:wakepy.methods.gtk.inhibitor:Registered Gtk.Application with id  io.readthedocs.wakepy.inhibitor1
0.1985936164855957

# second run
DEBUG:wakepy.core.mode:'WAKEPY_FAKE_SUCCESS' not set.
DEBUG:wakepy.pyinhibitor.inhibitors:Inhibitor module 'wakepy.methods.gtk.inhibitor' loaded to local python environment
DEBUG:wakepy.methods.gtk.inhibitor:Registering Gtk.Application with id  io.readthedocs.wakepy.inhibitor2
DEBUG:wakepy.methods.gtk.inhibitor:Registered Gtk.Application with id  io.readthedocs.wakepy.inhibitor2
0.0038368701934814453

Using the subprocess:

DEBUG:wakepy.core.mode:'WAKEPY_FAKE_SUCCESS' not set.
DEBUG:wakepy.pyinhibitor.inhibitors:Inhibitor module "wakepy.methods.gtk.inhibitor" not found in the current python environment. Trying to use "/usr/bin/python" instead.
DEBUG:wakepy.pyinhibitor.inhibitors:Response from pyinhibit server: INHIBIT_OK
Received request: QUIT
DEBUG:wakepy.pyinhibitor.inhibitors:Response from pyinhibit server: UNINHIBIT_OK
0.22979283332824707

@fohrloop fohrloop force-pushed the issue-404-using-gtk_application_inhibit branch from 5ce3042 to 8c264f2 Compare September 27, 2024 07:43
@fohrloop
Copy link
Owner Author

I'm leaving this one open. I'm working on other projects and I don't have this top of my priority list. Coming back to this later, whenever I have spare time for a new release :)

@fohrloop fohrloop force-pushed the issue-404-using-gtk_application_inhibit branch 3 times, most recently from 1be37dd to d1eec70 Compare July 4, 2025 16:46
@fohrloop
Copy link
Owner Author

fohrloop commented Jul 9, 2025

Is the wakelock released if the main process crashes?

There are two ways to crash a python process (1) With Exceptions, where normal context manager clean up takes place and __exit__() is called and (2) with no possibility for the cleanup. For example, having a Segfault.

TLDR:

  • if the script in the with block has a normal Exception, the uninhibit is called before the exception is raised (this releases the wakelock)
  • if the script in the with block does segfault, the inhibit server subprocess gets killed (the stop() does not get called but the process is killed. This releases the wakelock in most cases. Exception would be a inhibit method which would for example alter system-wide settings in start())

Test 1: Normal Exceptions

Using this script (foo.py) for testing the new "gtk_application_inhibit" method:

import logging
import time

logging.basicConfig(level=logging.DEBUG)

from wakepy import keep

t0 = time.time()
with keep.presenting(methods=["gtk_application_inhibit"]) as m:
    time.sleep(100)


time.sleep(10)
t1 = time.time()
print(t1 - t0)

Running ps to check the processes. This is what I got when running the ps without any errors in foo.py:

❯ ps ux | grep -E "[w]akepy|[f]oo\.py"
fohrloop   20660  8.0  0.0 247732 17776 pts/0    S+   19:09   0:00 python foo.py
fohrloop   20662 13.9  0.2 1158200 84780 pts/0   Sl+  19:09   0:00 /usr/bin/python3 /home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py /tmp/wakepy/wakepy-pyinhibit-subprocess-4b1b1f77-26be-4831-8f03-5d2666c42804.socket /home/fohrloop/code/wakepy/src/wakepy/methods/gtk/inhibitor.py

And this is what I get when I place 1/0 error in the foo.py:

import logging
import time

logging.basicConfig(level=logging.DEBUG)

from wakepy import keep

t0 = time.time()
with keep.presenting(methods=["gtk_application_inhibit"]) as m:
    time.sleep(40)
    1 / 0

time.sleep(10)
t1 = time.time()
print(t1 - t0)

the ps output:

❯ ps ux | grep -E "[w]akepy|[f]oo\.py"
fohrloop   21068  1.1  0.0 247732 17784 pts/0    S+   19:11   0:00 python foo.py
fohrloop   21075  1.9  0.2 1158200 84780 pts/0   Sl+  19:11   0:00 /usr/bin/python3 /home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py /tmp/wakepy/wakepy-pyinhibit-subprocess-fc87e90e-85ca-4911-a429-bc2b2ace6ffc.socket /home/fohrloop/code/wakepy/src/wakepy/methods/gtk/inhibitor.py

~ 
❯ ps ux | grep -E "[w]akepy|[f]oo\.py"

~ 

The output in the terminal starting the foo.py:

DEBUG:wakepy.pyinhibitor.inhibitors:ImportError while importing the Inhibitor module "wakepy.methods.gtk.inhibitor":
    No module named 'gi'
    Trying to use "/usr/bin/python3" instead.
DEBUG:wakepy.pyinhibitor.inhibitors:Response from pyinhibitor server: INHIBIT_OK
Received request: QUIT
DEBUG:wakepy.pyinhibitor.inhibitors:Response from pyinhibitor server: UNINHIBIT_OK
Traceback (most recent call last):
  File "/home/fohrloop/code/wakepy/foo.py", line 11, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

wakepy on  issue-404-using-gtk_application_inhibit [?⇕] via 🐍 v3.12.6 (.venv) took 41s 

In other words, if the script in the with block has an error, the uninhibit is called before the exception is raised. This is done through the Mode.__exit__() method, which is part of the context manager protocol.

Test 2: Segfault

It is also possible to crash a python process in a way which does not call the Mode.__exit__() properly. Here is an example:

# foo.py

import ctypes
import logging
import time

logging.basicConfig(level=logging.DEBUG)

from wakepy import keep

with keep.presenting(methods=["gtk_application_inhibit"]) as m:
    time.sleep(10)
    ctypes.string_at(0)  # This will cause a segmentation fault after 10 seconds


time.sleep(10)

The ps output shows that the subprocess is killed at the same time as the main process:

❯ ps ux | grep -E "[w]akepy|[f]oo\.py"
fohrloop   22089 13.0  0.0 247664 17784 pts/0    S+   19:19   0:00 python foo.py
fohrloop   22091 24.6  0.2 1158200 85028 pts/0   Sl+  19:19   0:00 /usr/bin/python3 /home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py /tmp/wakepy/wakepy-pyinhibit-subprocess-6b52fe8d-2c55-4b0b-ad66-6be803c91c77.socket /home/fohrloop/code/wakepy/src/wakepy/methods/gtk/inhibitor.py

~ 
❯ ps ux | grep -E "[w]akepy|[f]oo\.py"

~ 

this is because the BrokenPipeError crashes the inhibitor server process. See:

DEBUG:wakepy.core.registry:Registering Method <class 'wakepy.methods.windows.WindowsKeepPresenting'> (name: SetThreadExecutionState)
DEBUG:wakepy.core.mode:'WAKEPY_FAKE_SUCCESS' not set.
DEBUG:wakepy.pyinhibitor.inhibitors:ImportError while importing the Inhibitor module "wakepy.methods.gtk.inhibitor":
    No module named 'gi'
    Trying to use "/usr/bin/python3" instead.
DEBUG:wakepy.pyinhibitor.inhibitors:Response from pyinhibitor server: INHIBIT_OK
Received request: 
fish: Job 1, 'python foo.py' terminated by signal SIGSEGV (Address boundary error)

wakepy on  issue-404-using-gtk_application_inhibit [?⇕] via 🐍 v3.12.6 (.venv) took 11s 
❯ Traceback (most recent call last):
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 93, in _run
    self.send_message(client_socket, "UNINHIBIT_OK")
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 147, in send_message
    client_socket.sendall(message.encode())
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
BrokenPipeError: [Errno 32] Broken pipe

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 171, in <module>
    InhibitorServer().run(
    ~~~~~~~~~~~~~~~~~~~~~^
        socket_path=sys.argv[1], inhibitor_module=sys.argv[2], *sys.argv[3:]
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 69, in run
    self._run(server_socket, inhibitor_module, *inhibit_args)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 95, in _run
    self.send_message(client_socket, f"UNINHIBIT_ERROR:{error}")
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/fohrloop/code/wakepy/src/wakepy/pyinhibitor/inhibitor_server.py", line 147, in send_message
    client_socket.sendall(message.encode())
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
BrokenPipeError: [Errno 32] Broken pipe

@fohrloop fohrloop closed this Jul 10, 2025
@fohrloop fohrloop reopened this Jul 10, 2025
@fohrloop fohrloop force-pushed the issue-404-using-gtk_application_inhibit branch from d1eec70 to c954020 Compare July 17, 2025 15:42
@fohrloop
Copy link
Owner Author

License check

The PyGObject (gi) is licensed under LGPLv2.1+. The LGPL permits linking (and importing) LGPL licensed code to code with any license, so there's no problem. In fact, if I understand it correctly, wakepy is just a “work that uses the Library”, and that work, in isolation, is not subject to the LGPL. In other words, importing (LGPL licensed) gi and calling some of it's functions in (MIT licensed) wakepy is ok.

@fohrloop
Copy link
Owner Author

Unix sockets vs other alternatives

The alternatives for communicating between the main process (current env python) and sub-process (system python) are:

subprocess.PIPE:

  • Means: Use standard input/output
  • Should work just fine between python versions, as out can send simple text.
  • Con: One limitation is that it only works for subprocesses, but the inhibitor server would always be a subprocess, so that's not a concern.
  • Pro: Works on "any system", or at least Windows, Linux, macOS, BSDs, WSL

Unix sockets:

  • The implemented solution. Uses socket files, like /tmp/wakepy/wakepy-pyinhibit-subprocess-80e78385-2869-4494-909c-4ac354b204d8.socket
  • Pro: Would work even if the inhibitor server would not be a sub-process of the main python process. I don't know if there are any use cases for this. Theoretically, could allow quitting any wakepy inhibitor, inluding the ones in started with the wakepy CLI command, by sending a command to the socket (any python or non-python process). Not sure if that would be valuable.
  • Con: Only for Unix. Not problem with this Method, but the could some non-unix system use a solution like this?

Message Queues

  • Seems like an overkill to this problem. Skipping.

Communication through files:

  • I can't see any upsides on this compared to subprocess.PIPE / Unix sockets. Skipping.

Thoughts

The only viable solutions seem to be unix sockets and subprocess.PIPE. The subprocess.PIPE is a bit more intriguing as it would be cross-platform solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Method using gtk_application_inhibit()?

1 participant