Note: If you don’t care enough about my blabbering and you’re just looking for the code, you can find it on Github. And who can blame you? I blabber a lot =P
This is the second part of the Adventures with my Pinephone - A Matter of Time series. Part 1 may be found at Adventures with my Pinephone - A Matter of Time (Part 1)
The Problem
In our quest to enable the PostmarketOS Pinephone’s ability to wake itself from suspend to ring a timer or an alarm, we ended previously with the realisation that our two options weren’t going to work. The options were:
- Grant
CAP_WAKE_ALARM
to the Pinephone user during login, thereby having it be inherited by the PowerDevil process- NO: because Alpine does not ship
pam_cap.so
- NO: because Alpine does not ship
- Setting the
CAP_WAKE_ALARM
capability on theorg_kde_powerdevil
binary itself.- NO: because
CAP_WAKE_ALARM
“breaks dbus” and Alpine actually explicitly removes the capability before shipping KDE.
- NO: because
But why would DBus be broken with extra permissions granted to the binary? This is quite an interesting mystery which I hope to solve someday.
Nonetheless, it seems like our journey is heading towards a disappointing end - this issue seems to be something that should be sorted out between the distro and the KDE project.
That said, if all I wanted out of a project was to beg/coerce/whine/induce somebody else to do the work of implementing features and resolving bugs for me, I would just use an iPhone. My beautiful Linux phone has all the compiler tools. Plus the relevant components’ source code is readily available - so there’s no reasonable excuse on my part for not solving it myself!
Designing my Solution
If I were to implement a solution of my own, here are some goals that would be important to me:
- Lightweight:
- The solution should be extremely lightweight, since the Pinephone is not a particularly capable device and it runs on a battery
- Simple Interface:
- Part of the problem with the also (somewhat) simple
rtcwake
application is that it puts the system to sleep when setting a wakeup time! I want my solution to be “pluggable” and usable for more than just my own purposes. - As such, my solution should do only one thing - wake the system up if it is asleep. Nothing more.
- Part of the problem with the also (somewhat) simple
- Avoid root privs:
- As far as possible, avoid the need for root privileges. Ideally, the program should be invokable entirely as a low-privilege user.
Implementing the Waker
On first glance, the Waker does not seem particularly tricky. In fact, we already have
the trickiest part (creating a CLOCK_REALTIME_ALARM
) solved in our proof of concept
in the last post.
All that remains is to follow the manpage’s guidance and set an itimerspec
containing
our desired wake time. Nicely, timerfd
supports the TFD_TIMER_ABSTIME
parameter which
allows you to pass in an absolute timestamp when calling timerfd_settime
, instead of
having to calculate the time difference on your own.
...
#include <stdlib.h>
#include <time.h>
...
int main(int argc, char** argv) {
...
struct itimerspec its;
struct itimerspec new_its;
long int newtime = atol(argv[1]);
.it_value.tv_sec = newtime;
new_its.it_value.tv_nsec = 0;
new_its.it_interval.tv_sec = 0;
new_its.it_interval.tv_nsec = 0;
new_its
int res;
= timerfd_settime(timer, TFD_TIMER_ABSTIME, &new_its, &its);
res ...
}
We pull in stdlib.h
for the atol
function to convert our first argument to a long int. We also pull
in time.h
for the itimerspec
struct. We then set the new itimerspec
struct’s seconds value to our
desired time, and set the interval (used for repeating timers, not for us) to 0.
Finally, we call timerfd_settime
on the timer
we initially created, with the TFD_TIMER_ABSTIME
property.
Very nice. Now, we’ll create an event loop to read the timerfd
timer, and wait for it to return.
Because we’re doing this just for the side effect of the timerfd
waking the system up from sleep,
we don’t really have to care about what it returns. I picked epoll
for this task.
#include <sys/epoll.h>
...
int main(int argc, char** argv) {
...
int epoller;
struct epoll_event epollEvent;
struct epoll_event newEvents;
= epoll_create1(0);
epoller .events = EPOLLIN;
epollEvent.data.fd = timer;
epollEvent(epoller, EPOLL_CTL_ADD, timer, &epollEvent);
epoll_ctl
("Entering epoll event loop\n");
printf
while (1) {
int numEvents = epoll_wait(epoller, &newEvents, 1, -1);
if (numEvents > 0) {
("Picked up an epoll event\n");
printfbreak;
}
}
("Exiting\n");
printf(epoller);
close(timer);
close
}
After we pick up an event, we break out of the event loop and close the two fd
s.
Notice that I set epoll_wait
’s timeout to -1
: this allows our waker program to block perpetually
until an event is returned from timer
- thereby saving us as much CPU processing power
as possible on our relatively resource-starved Pinephone.
So now our waker program will take in a Unix timestamp in argv
, set up a timer, and block
until the timer fires. Simple!
Implementing the Sync-er
With rtcwaker
now capable of waking the Pinephone up at any arbitrary timestamp, we now
need some way of programmatically figuring out the timestamps that correspond to the alarms
and timers we set in KClock.
Nicely enough, KClock exposes a number of fantastic DBus methods that will provide us just the very timestamps we need (for Alarms, at least). Execute this most convenient command and see for yourself:
$ qdbus org.kde.kclockd
/
/Alarms
/Alarms/[SOME_HEXADECIMAL_VALUE1]
/Alarms/[SOME_HEXADECIMAL_VALUE2]
...
/MainApplication
/Settings
/Timers
/Timers/[SOME_HEXADECIMAL_VALUE...]
...
/org
/org/kde
/org/kde/kclockd
The /Alarms/ca98bf604945408184657b498f8ac673
and /Timers/8132e4badc0543599cb9ba01c5c1bb73
(example
values) are actually UUID strings with their -
characters removed, and they correspond to unique timers
and alarms set in the KClock app.
Particularly interesting for us is the getNextAlarm
Method in /Alarms
:
$ qdbus org.kde.kclockd /Alarms getNextAlarm
1619378700
It looks like getNextAlarm
actually returns the very information we need - a timestamp for the next
upcoming alarm for KClock to ring. Awesome!
In pursuit of being lightweight, I wanted my program to run compiled binary code that fetched these values from
DBus through these Methods. If it could be done in the same binary, we’d add a little “fetch_next_kclock_alarm_from_dbus
”
function into rtcwaker
, and have rtcwaker
call it and pass its value in timerfd_settime
. This would all
be done in one binary.
To make sure that I was on the right track, I pulled in qmake
and created this sample QDBus test program:
#include <iostream>
#include <QtCore/QDebug>
#include <QtDBus/QtDBus>
int main() {
// Try to connect to session bus
if (!QDBusConnection::sessionBus().isConnected()) {
std::cout << "Cannot connect to the session bus" << std::endl;
(1);
exit}
QDBusConnection bus = QDBusConnection::sessionBus();
QDBusInterface interface("org.kde.kclockd", "/Alarms", "org.kde.kclock.AlarmModel");
QDBusReply<qulonglong> reply = interface.call("getNextAlarm");
qDebug() << reply;
return 0;
}
It connects to the session bus, opens an interface to org.kde.kclockd
’s /Alarms
path, and
invokes getNextAlarm
and prints the response out through QDebug
. Basically, it performs exactly what
we did with the qdbus
program above.
$ qmake
$ make
$ ./dbustest
1619379700
Very nice! This is precisely what we needed, and our program is shaping up well indeed. All that
remains is to modify rtcwaker.c
to reconcile any C/C++ syntax differences, and convert the qulonglong
into the long int that itimerspec
requires.
But wait.
This is all very exciting and promising. Still, before we go ahead, let’s just make sure
that the program will absolutely run in the conditions we want it to run - namely, with
the CAP_WAKE_ALARM
capability assigned.
$ sudo setcap cap_wake_alarm+ep /home/user/dbustest/dbustest
$ getcap -v /home/user/dbustest/dbustest
/home/user/dbustest/dbustest cap_wake_alarm=ep
$ ./dbustest
Cannot connect to the session bus
…
…
“Cannot connect to the session bus”, it said.
…
WHATEVER.
I’ll do it in the shell.
Implementing the Sync-er (In the Shell)
After my intended binary failed me due to some dbus breakage (if anybody knows why this happens in pmOS, please do reach out), I figured the next best approach would be to pass the next alarm timer in through the shell. This was definitely not my first choice, as I anticipated the syncing script would have to be called relatively frequently, and a shell script is interpreted at runtime.
Nonetheless, I figured that the performance cost would be tolerable at this point in time. I set about
spinning up a quick shell script in the busybox ash
shell to perform this task. This was a pretty
fun experience, as I had to unlearn a number of “bashisms” that I’d gotten used to over the years. Without
array types, I had to consider using ugly variables and loops to handle a number of conditions.
But eventually I had a simple script
that would fetch the dbus output of getNextAlarm
, and start rtcwaker
in the background with it.
It was time for a test.
I set an alarm in KClock for around 10 minutes later (since I know PowerDevil suspends the Pinephone after around 5 minutes).
I unplugged my power cable to put the phone into the Battery profile.
I started the script, and the logs suggested that it had correctly picked up the next alarm time and passed it
to rtcwaker
. I checked ps -a
and rtcwaker was running. Good good.
I locked the Pinephone’s screen and left it on my desk. After 5 minutes, I tapped the screen twice. The phone did not respond. Good - this meant that the phone was suspended.
I waited for a touch longer…
BEEP BEEP, BEEP BEEP
IT WORKS!!
BEEP BEEP, BEEP BEEP
The wonderful Pinephone wakes on KDE Alarms!!! AWESOME!!!
And I didn’t need to sudo
anything throughout this process at all!
Adding More Features
With that proof of concept working, I quickly added in support for KClock timers as well as Alarms. The
timer interface is not as ideal as the Alarms one, but it does expose the elapsed
and length
properties
through DBus. Calculating the next timestamp would therefore just be a matter of computing
= NOW - elapsed + length NEXT_TIMESTAMP
And, a matter of adding a loop over the running timers in order to get the earliest expiring timer to pass to
rtcwaker
. After that, we compare the earliest expiring timer timestamp to getNextAlarm
’s output
and pick the earlier of the two.
And finally, I created an OpenRC init script and set it to run at the default
runlevel.
$ sudo rc-update add rtcsyncwake default
And now I get to use KClock Alarms and Timers, and my Pinephone will always wake the phone from suspend when it’s time for it to ring. And all this without sudo-ing or SUID-ing or su-ing.
Time for a Conclusion
We started with wake-mobile
’s GTK-based application with a root-owned SUID binary that sets timers in systemd,
and we ended with a user-owned binary that relies on Linux to handle our relationship with the real-time clock.
This was quite a journey!
I don’t write application code for a living, so do pardon the quality of my code. But if you’re on a Pinephone and you think
this program could be useful for you, I’ve left instructions on getting it running in this Github repo.
It would be great if my rtcwaker
program eventually proves useful to somebody else as well.
Ah, open-source is simply wonderful.