About a month ago I started a project to create a Perl solution that would help me manage my Unix desktop in a unified way. You can read more about it here. This blog post will focus on discussing the implementation, not the tool itself. Treat it as a success story with some behind-the-scenes info.
Initial idea
My Unix setup is pretty unconventional. I don't use a full-blown desktop environment like Gnome or KDE, instead I have my own fork of suckless dwm which a pretty straightforward C program to manage windows under X. Since dwm is a window manager and not a desktop environment, I needed something that would make up for its deficiencies. For years, that something was a collection of random scripts and hacks.
Lately, after I switched from FreeBSD to Linux for my daily driver, these hacks became cumbersome to deal with. Not only they had their set of limitations, but also it was hard to remember which part controls what, and under which operating system.
One silly example of such limitation is how I view time and date in my environment. Normally I don't show it anywhere, but when I click the status line that normally lets me monitor battery status and volume level, it changes to show time and date instead for a couple of seconds. Without a central program that can be communicated with, there was no way to control for how long the time will be visible. One program was printing standard status line in a loop with sleep 10
, another program was printing a status line with time in it on click. The time for which the time line was visible was effectively random.
To solve this, I wanted to create a simple event loop program that would just stay on and monitor system resources, let me change some parameters, and be communicated with through a Unix socket file. I called this program PCRD, (Parameter|Perl) Control and Reporting Daemon.
First implementation
I wanted PCRD to be light on dependencies and target rather old perl, so that it can be installed on system perl and be available system-wide. I chose IO::Async as the event loop implementation. The whole program was executed as my regular user during the start of the X server (in .xinitrc
).
Even though IO::Async has much more verbose interface than Mojolicious I was already familiar with, it has all the tools needed to create a functional event loop program. I feel that using a web framework as a base for a system daemon would be rather silly. Also, I really like the interface of Future, though I still can't get myself to use async/await - I feel like it is too magical and hides too many details.
# example of Futures usage in PCRD
return Future->wrap($self->owner->$method($self, $arg))->then(
sub {
my ($result) = @_;
foreach my $hook (@{$self->execute_hooks}) {
$hook->($action, $arg, $result);
}
return $result;
}
);
I only wrote a couple modules for power management, performance monitoring, controlling sound volume, display brightness and reporting the most basic system information like time and date. Modules are chosen based on current kernel reported by $^O
, and for now only Linux implementations exist. I also added a simple plugin system, so that I can make the program do anything I want without bloating the base module. I wrote status line generators as a plugin modules.
# high-level plugin code that builds a status line from subparts
sub set_build_default_line
{
my ($self, $feature, $value) = @_;
my @futures = (
$self->battery_status($feature),
$self->sound_status($feature),
$self->memory_status($feature),
$self->cpu_status($feature),
);
return Future->wait_all(@futures)->then(
sub {
$self->{next_build} = time + $feature->config->{interval};
$self->set_dwm_line(map { $_->get } @futures);
return 1;
}
);
}
I made the program extremely configurable, mostly since I was not sure whether the location from which I am gathering data is constant or if it will vary on different machines / kernel versions. Furthermore, I added a description to most of configuration values, and wrote an utility which shows all of them at once together with descriptions, handy for debugging.
# glob file pattern
Power.ac.pattern=/sys/class/power_supply/AC*/online
Power.all_features=1
# glob file pattern
Power.capacity.pattern=/sys/class/power_supply/BAT*/capacity
# glob file pattern
Power.charging.pattern=/sys/class/power_supply/BAT*/status
# glob file pattern
Power.charging_threshold.start_pattern=/sys/class/power_supply/BAT*/charge_start_threshold
# glob file pattern
Power.charging_threshold.stop_pattern=/sys/class/power_supply/BAT*/charge_stop_threshold
Power.enabled=1
# time window (in minutes) which will be used for the calculation
Power.life.measurement_window=5
# glob file pattern
Power.life.pattern=/sys/class/power_supply/BAT*/energy_now
It also does its best to check whether the current configuration is correct, by checking whether the Linux files specified in configuration exist, are readable and/or writable, and if commands run without an error. If a problem is detected, a helpful message is printed and the program refuses to run.
To communicate with the daemon, I wrote a very simple client program, also based on IO::Async, which connects to the server and runs a single query, printing the result.
$ pcrctl Power capacity # current battery capacity
✅ 85
$ pcrctl _ cpu_scaling # guess a module if a feature name is unique
✅ powersave
$ pcrctl _ ac # connected to AC
✅ true
$ pcrctl _ life # currently charging, so remaining battery life is incalculable
❎ result is not available
This all worked well and within a week I had a functional program that replaced most of my hacks.
The problem with running as non-root
To make the program work correctly, I had to add write permissions to a bunch of system files. Even though it only meant changing their group to wheel
and adding a group write permission, the number of these files grew quickly and it started to be problematic, since most new features required adding more permissions.
I also wanted the program to control more behaviors of the system, most notably the suspending. In Slackware I am currently using, suspending was controlled by a program called elogind
, which had no way to temporarily disable suspend on closing the laptop lid. I would have to change the configuration and restart the daemon every time, which was not acceptable solution.
Of course a program running as UID 1000 should not control this kind of basic system behavior, so I knew it is time to start coding the program in a way that would allow it to be ran as root.
Mite
I am not good at managing applications written with plain bless OO, but Moose helps me a lot in wrapping my head around non-trivial OO systems. At this point the whole system was hacked with plain hash access, so it needed to be upgraded to something more maintainable. Since I wanted minimal dependencies I did not choose Moo this time, I decided to give Mite a try. It promises to provide Moose-style OO to an app with no additional runtime dependencies, at the cost of a compilation step.
Mite is not yet perfect, I found and reported a couple of problems with it, but overall it works really well. Thanks to this, PCRD has just one direct runtime dependency, which is IO::Async.
PCRD as root
The problem with running this particular daemon as root is that it needs to run some commands as the user who started the X session. For example pulseaudio can only be run as the proper user. Most X programs (like xrandr) should be run with the proper user to get all the right configuration values, and are easier to run that way since they can access environmental variables for info about the display.
To work around this problem, I decided to split the daemon into two parts. The main daemon works as regular system daemon started by root. Later, when the X session is started, a special kind of client connects to it and runs continously in the background - I called it a user agent. The sole purpose of an agent is to run some daemon commands on behalf of the user. This way a daemon ran as root can request certain userspace programs to be called. The result of calling the programs is captured and sent back to the daemon.
$ pcrd user-agent & # ran in .xinitrc
This of course introduced an additional complexity - now some features need to wait for the agent to connect until they can be used. That means a feature must be uncallable until the agent appears, but it must also initialize and de-initialize itself multiple times.
This whole plan turned out to be a good idea since it made PCRD even more powerful. After I developed this solution and made it stable, I wrote a plugin which handles every ACPI signal on my machine. I could now remove handling scripts from acpid, disable everything elogind does, remove all bindings of sound / brightness keys from xbindkeys and glue it all with the plugin code. It sends internal signals within PCRD to call certain features depending on acpi signal received. All I needed is this snipped in /etc/acpi/events/default
:
event=.*
action=/usr/local/bin/pcrctl Acpi signal "%e"
I could also now remove the special permissions from all the files which needed to be writable before. It makes the system more secure, since the user can not write anything into the files, only the values preconfigured in the daemon. Of course, the unix socket file created by the server still needs to be writable by user in order to communicate with the daemon.
Perl for the win
The program is really stable thus far and does only take a fraction of CPU time a full desktop environment would take. It allowed my environment to stay lean, trustworthy and hackable - which was the whole point.
Perl is an extremely flexible language that allows you to write a lot of different stuff, even system daemon - as demonstrated above. I will now be able enjoy my program for however long I want, since I can count on Perl to respect my time and not break backward compatibility unnecessarily.
Comments? Suggestions? Send to [email protected]
Published on 2025-06-16