wakepy
wakepy copied to clipboard
Method using gtk_application_inhibit()
Aims to close: #404
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
gimodule for accessing GTK functions from python. User is not required to have installed the in their current (virtual) environment. It's just faster to use (300ms vs 5ms) ifgiis 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 astart(self, *args)andstop(self)methods. The*argsare positional arguments given to the server and passed to the inhibitor.
TODO
- [ ] Add logic to get the system python interpreter path
- [x] Add logic for creating a random socket
- [x] 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)
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
giis 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
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 :)
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
withblock has a normal Exception, the uninhibit is called before the exception is raised (this releases the wakelock) - if the script in the
withblock 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
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.
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.