jdSystemMonitor - A New System Monitor for Linux
Today, I am excited to announce my new program that I have been working on for the past few months: jdSystemMonitor. For years, I have missed having a good GUI software to manage processes on Linux. With some time on my hands and a desire for a project, I decided to create one. Thus, I began developing a program to manage processes.
These types of programs are referred to as "System Monitors" on Linux and are similar to the Task Manager on Windows. True to my creative nature, I named the new program jdSystemMonitor, with "jd" standing for JakobDev.
Since system monitors typically provide more information than just process management, I included additional features. Currently, the following tabs are available in jdSystemMonitor:
Systeminfo
This tab provides general information about your system. It takes it information from os-release.
Processes
This tab provides detailed information about all running processes.
Performance
This tab provides some statistics about your system.
Services
This tab allows you to manage SystemD services. It is only visible if SystemD is in use on your system.
Running flatpaks
This tab lists all currently running Flatpak applications. It is only visible if Flatpak is installed on your system.
Autostart
This tab shows all programs configured to start automatically.
Mounts
This tab displays all mount points on your system.
Installed packages
This tab lists all installed packages. Native packages are displayed if PackageKit is installed, and Flatpaks are shown if Flatpak is installed.
Users
This tab shows all users currently on the system.
System configuration
This tab displays all Sysctl values.
Screenshots
You can find Screenshots here. Unfortunately I can't embed them here due to the bot protection of Codeberg.
Installing
jdSystemMonitor is currently available in the AUR. It will also soon be available on Flathub. If you are reading this later, you can find it under this link. Additionally, you can build from source if you have the necessary skills.
Technical Design
This is just a explanation on a few design decisions and thoughts that I made during development. They might be interesting for you, if you are also a developer.
General
It was clear from the beginning that jdSystemMonitor should be split into 2 parts: A GUI and a Daemon. The Daemon should contain all the Logic. A Daemon can be granted root permissions if needed while a graphical Program can't run under a different User. This split has also advantages for Flatpak: A Daemon can be run outside the Flatpak Sandbox and have access to the full System while the GUI is running inside the Sandbox.
The Daemon
It was evident from the beginning that the daemon should be written in Go. Go is one of my favorite programming languages due to its excellent syntax, good error handling (though I know this can be controversial), and overall design. Additionally, Go is quite fast, which is crucial for a system monitor that needs to update its data (e.g., all processes) every few seconds.
Another significant advantage is that Go binaries are completely static, meaning they do not depend on the host OS. For context, if you run a binary outside a Flatpak, it will use the libraries from the host system rather than those bundled with the Flatpak. While there are workarounds (e.g. flatpak-host-launch), it is preferable to avoid such issues altogether. I still use flatpak-host-launch to start the daemon for convenience.
The daemon must run outside the Flatpak sandbox due to its limitations; for example, it cannot access the /proc
filesystem or run as root.
The GUI
The GUI presented a more interesting challenge. As previously mentioned, it requires good performance since the data is updated every few seconds. Even with caching (which I implement), the GUI must loop through many processes to update the data. Therefore, I could not use Python for the GUI, as its performance issues are well-known.
While Go would have been a natural choice since the daemon is already written in it, there is no suitable GUI library for Go. As far as I know, only Fyne is a usable GUI library, but it currently does not support building a binary that works with both X11 and Wayland. To support both, I would need to either run the GUI with XWayland in a Wayland session or compile two binaries (one for X11 and one for Wayland) and write a launch script to select the correct binary based on the environment. Neither option was appealing to me. Additionally, Fyne does not integrate well with the system theme, and its tree widget lacks multiple columns, which is a feature I require.
Ultimately, I decided to use C++ with Qt Widgets. C++ is not my favorite programming language; it can be challenging to write and easy to make mistakes. Memory leaks are also a concern. I had previously only written small tools in C++, so I was (and still am) a novice. However, I have considerable experience with Qt Widgets from Python, making this my best option. Moreover, Qt is a comprehensive toolkit, not just a GUI library, which abstracts away some complexities. A significant advantage is that I chose to keep the logic in the daemon, meaning the GUI primarily retrieves and displays data from the daemon, with some caching involved in certain areas. This approach minimizes the amount of logic I need to write in C++.
Note: There is a Go binding for Qt but it is still in development. There are also a few other discontinued Go bindings. So I will keep a eye on that and maybe use it if a write another small tool (which I do a lot) but for this Project (which is bigger) I wanted to use something stable.
The Communication Between Daemon and GUI
The communication between the daemon and the GUI is handled through D-Bus.
The daemon is started from the GUI and uses page.codeberg.JakobDev.jdSystemMonitor.Daemon.I<random number>
as its bus name.
The random number ensures that each instance of the GUI can have its own instance of the daemon.
The Build System
As mentioned earlier, I am a novice in C++. I have been using CMake in my existing projects, but they are not very large, so I cannot claim to have much experience with it.
For this project, I decided to try Meson, as I had heard good things about it. Meson handled everything well except for Qt translations.
To provide some context: The Qt translation system has two file types: .ts
and .qm
. .ts
files are XML files containing all the text to be translated, comments for translators, and the location in the source code. They are solely for translation purposes. You can create them from the source code using a tool called lupdate
and use Qt Linguist or Weblate for the translations.
.qm
files are compiled versions of .ts
files that Qt uses to load translations. You can compile a .ts
file into a .qm
file using a tool called lrelease
. You can also embed .qm
files directly into the executable using the Qt resource system for easier loading.
This process is typically handled by the build system. In CMake, it requires just five lines:
file(GLOB TS_FILES "translations/*.ts")
qt6_add_translations(target
TS_FILE_BASE src
TS_FILES ${TS_FILES}
LUPDATE_OPTIONS "-no-obsolete"
)
In Meson, it is more complex. You can compile and embed .ts
files using Meson, but this requires you to have a list of all translations in a .qrc file.
I use Weblate for translations. When a new language is added, Weblate can create the corresponding .ts
file but cannot update the .qrc file.
I want Weblate to work seamlessly without manual intervention, so I needed something that could pick up all translations in a directory.
Another significant issue is lupdate
.
If you look at the Qt6 module for Meson, you will see a function called compile_translations, but there is no way to run lupdate
.
In the end, I had to write a custom script to meet my needs. I expect a build tool to handle tasks like managing translations without requiring a custom script.
Another minor issue is that the Qt tools are not located in the PATH
.
Meson knows where they are located but does not provide a function to retrieve their location. Therefore, I also had to include logic in my script to find them.
In conclusion, if you are developing something with C++ and Qt, Meson may not be the best choice.
Running as root
Since jdSystemMonitor is split between the GUI and the daemon, only the daemon requires root permissions. Granting root permissions to the daemon sounds straightforward but presents several challenges.
pkexec
I decided to use pkexec, which is part of polkit, to execute the daemon as root. pkexec allows you to run a command as root. You can compare it to sudo, but it features a graphical password input instead of a console one, making it ideal for use in GUI applications. Polkit is preinstalled on almost every Linux desktop distribution (I am not aware of any that do not use it), so we can assume it is available. This is especially important if you use jdSystemMonitor as a Flatpak, as pkexec needs to be available on the host outside the Flatpak. Flatpak packages cannot depend on host packages, so we need to use something that is already present. If pkexec is not found, jdSystemMonitor will display an error message.
Connecting to the session bus
There are two main buses in D-Bus: the session bus and the system bus. The daemon needs access to both. The session bus is for everything related to the current user, while the system bus is for system-wide services and is shared among all users. This is necessary for communicating with services like SystemD on both the session and system buses. Unfortunately, only processes belonging to the user can connect to the session bus.
This is where xdg-dbus-proxy comes into play. As the name suggests, it acts as a proxy for D-Bus. It is used by Flatpak to provide sandboxed access to D-Bus, so it should be available on any system that has Flatpak installed. jdSystemMonitor will still check if it is installed and display an error if it is not.
xdg-dbus-proxy can be used without sandboxing. You can start xdg-dbus-proxy for the session bus like this:
xdg-dbus-proxy xdg-dbus-proxy $DBUS_SESSION_BUS_ADDRESS /tmp/proxy
Now you can connect to unix:path=/tmp/proxy
.
xdg-dbus-proxy does not check the user that connects, allowing us to start xdg-dbus-proxy as a user process and connect to the proxy as root.
Which Bus to Use for the Daemon
Now comes the next question: On which bus should the daemon run? A natural choice would be the system bus.
However, it is not possible to simply request a name on the system bus; you need a configuration file in /etc/dbus-1/system.d
or /usr/share/dbus-1/system.d
to do that.
Some services on the system bus are run not by root but by specific users with limited permissions.
For example, org.freedesktop.PolicyKit1
belongs to a service from the user polkitd on my system (Arch Linux). Therefore, it makes sense to have that configuration file for those cases.
What does not make sense to me is that root also needs a configuration file, as root has the permission to create one. Thus, I would need to create a temporary configuration file in a system directory to own a name on the session bus, which is not something I want to do.
Another option would be to use dbus-daemon to create a new custom bus. This is a bit more complex, so I decided not to pursue that option for now.
The third choice is to simply use the session bus, which is already utilized when not running as root. I decided to stick with that for now, as it is the easiest solution.
Authentication
Since it is possible to run the daemon as root, authentication is necessary to ensure that requests come from the GUI. Otherwise, any other program could use the daemon to, for example, kill a system process, which poses a security risk. Any request on the session bus can be monitored by anyone who has access to it, so we cannot use a token here. D-Bus provides information about who calls a function (the unique connection name). We can use this to our advantage.
When starting the daemon, the GUI passes its unique connection name to the daemon as a command-line argument. Only calls from this unique connection name will be allowed.