DEV Community

Cover image for /proc, pkexec, and 678 commits
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

/proc, pkexec, and 678 commits

Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback.

Day 1 of Qt on Linux. I installed Stacer, opened the source, and found a few things worth stealing.

Every Qt tutorial builds a calculator or a todo list.

Those teach you the API.

They don't teach you how an app is shaped, where the OS code goes, where the root code goes, what holds twelve screens together.

So I went looking for a real app to read.

I found Stacer, a Linux system optimizer, installed it, poked every button, then opened the source.

It's abandoned, which is why it's good

Last commit August 2023, README says no further releases. 678 commits and then silence.

Which is perfect for reading.

Nothing moves under you, there's no "actually we refactored that last week," and at ~50 headers, 48 sources and 26 .ui forms you can read all of it in an evening or two.

A finished project sits still long enough to dissect.

The decision everything else hangs off

I read the build files first.

The top level CMakeLists.txt gives away the architecture:

add_subdirectory(stacer-core)
add_subdirectory(stacer)
Enter fullscreen mode Exit fullscreen mode

Then from stacer-core/CMakeLists.txt:

find_package(Qt5 COMPONENTS Core Network REQUIRED)
add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_srcs})
target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network)
Enter fullscreen mode Exit fullscreen mode

stacer-core is a static lib with no GUI components linked at all.

No Widgets, no Charts, nothing that can draw a pixel.

It's the whole "talk to the OS" layer, and it would happily compile into a headless CLI.

The app links it plus Widgets, Charts, Svg and Concurrent on top.

Most codebases claim a separation like this and then someone sneaks a QMessageBox into the data layer. Stacer can't. The linker would slap it.

The core splits three ways by verb:

  • Info/ reads system state
  • Tools/ changes system state
  • Utils/ is the primitives both are built on

Read, write, plumbing. I never had to grep for where anything lived, which in an unfamiliar codebase is a small miracle.

Every arrow points down.

/proc and roll

How does a system monitor monitor the system? I assumed a library, or a daemon, something official looking.

It reads text files.

#define PROC_CPUINFO "/proc/cpuinfo"
#define PROC_LOADAVG "/proc/loadavg"
#define PROC_STAT    "/proc/stat"
Enter fullscreen mode Exit fullscreen mode

Core count: read /proc/cpuinfo, filter ^processor, count the lines.

Load average: read /proc/loadavg, split on whitespace, take the first three.

Clock speed: filter ^cpu MHz, split on the colon.

The CPU section of a system monitor is a regex over a text file. And procfs is how everything on Linux exposes itself, so once you notice you can't stop.

The bit that broke my brain

So CPU usage should be the same, right? Find the line, take the number.

There is no number.

/proc/stat gives you cumulative tick counters since boot — ticks spent in user mode, system, idle, iowait, ever.

They only go up.

CPU usage isn't a value you can read.

It only exists as a relationship between two samples: read the counters, wait, read again, and the shape of the change is the answer.

From cpu_info.cpp:

int CpuInfo::getCpuPercent(const QList<double> &cpuTimes, const int &processor) const
{
    const int N = getCpuCoreCount()+1;

    static QVector<double> l_idles(N);
    static QVector<double> l_totals(N);

    int utilisation = 0;

    if (cpuTimes.count() > 0) {
        double idle = cpuTimes.at(3) + cpuTimes.at(4); // idle + iowait
        double total = 0.0;
        for (const double &t : cpuTimes) total += t;

        double idle_delta  = idle  - l_idles[processor];
        double total_delta = total - l_totals[processor];

        if (total_delta)
            utilisation = 100 * ((total_delta - idle_delta) / total_delta);

        l_idles[processor] = idle;
        l_totals[processor] = total;
    }
    // ...clamped to 0..100
    return utilisation;
}
Enter fullscreen mode Exit fullscreen mode

Those static locals. The function remembers: it stashes the previous idle and total per processor, diffs against them, overwrites them for next time.

Which makes an innocent looking getter stateful.

Call it twice in a row from two places and the second caller gets garbage, because the first one already consumed the delta.

The first call after launch is meaningless by definition.

Not a dunk — it's the correct algorithm, it's what top does, and the comment above it pastes in the /proc/stat column meanings for the next reader.

But it's a landmine, and it explains something further up the stack.

The refresh loop is refreshingly dumb. From dashboard_page.cpp:

connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateCpuBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateMemoryBar);
connect(mTimer, &QTimer::timeout, this, &DashboardPage::updateNetworkBar);

QTimer *timerDisk = new QTimer(this);
connect(timerDisk, &QTimer::timeout, this, &DashboardPage::updateDiskBar);
timerDisk->start(5 * 1000);

mTimer->start(1 * 1000);
Enter fullscreen mode Exit fullscreen mode

One timer at 1s fanned out to three slots, another at 5s for disk because disk changes slowly and checking it is expensive.

Nothing pushes; the UI pulls on a clock. That's the whole update architecture.

The smartest thing in here

Stacer kills processes, purges caches, toggles systemd services, uninstalls packages, edits /etc/hosts.

All of it needs root. Stacer does not run as root.

From command_util.cpp:

QString CommandUtil::sudoExec(const QString &cmd, QStringList args, QByteArray data)
{
    args.push_front(cmd);

    QString result("");

    try {
        result = CommandUtil::exec("pkexec", args, data);
    } catch (QString &ex) {
        qCritical() << ex;
    }

    return result;
}
Enter fullscreen mode Exit fullscreen mode

That's the function. Shove the command onto the front of the args, run pkexec instead.

polkit pops the auth dialog, tells the user which command wants elevation, and runs that one command as root.

So the app process is unprivileged for its entire life — a bug in the theming code can't rm -rf your home directory.

Privilege is granted per operation, at the moment of use, not once at launch for everything forever. And pkexec kill 1337 is a more honest dialog than "Stacer would like admin access, kthx."

Sixteen sudoExec call sites, sixteen places the user gets asked.

The alternative you see everywhere is a .desktop file that launches the whole GUI under sudo, so now your event loop and your stylesheet parser are root, forever, for the sake of one rm.

Tools/package_tool.cpp sits on top of this: same intent, dispatched to apt-get, dnf, yum, pacman or snap depending on the machine.

The UI just says "uninstall", and the packaging ecosystem stays on the other side of the wall.

Twelve screens

Each page is a directory under Pages/ with the Qt triple: .h, .cpp, .ui.

App news them all up at startup into a SlidingStackedWidget, a QStackedWidget subclass that animates transitions instead of hard cutting. Small thing, and most of why the app feels nicer than its feature list.

Best part: two pages don't always exist.

// APT SOURCE MANAGER
if (ToolManager::ins()->checkSourceRepository()) {
    aptSourceManagerPage = new APTSourceManagerPage(mSlidingStacked);
    mListPages.insert(7, aptSourceManagerPage);
    mListSidebarButtons.insert(7, ui->btnAptSourceManager);
} else {
    ui->btnAptSourceManager->hide();
}
Enter fullscreen mode Exit fullscreen mode

An APT source manager on Arch is nonsense, so it checks for APT config and doesn't build the page.

Same for Gnome settings, which checks DESKTOP_SESSION and the distro string.

One binary reshaping itself around the machine it woke up on, hiding the button rather than showing you something broken.

Theming, or: CSS variables, hand rolled

QSS looks like CSS and lacks most of what makes CSS bearable. Notably, no variables.

So Stacer built them. Each theme is a style.qss plus a values.ini, both baked into the Qt resource system, and AppManager::updateStylesheet does this:

QString appThemePath = QString(":/static/themes/%1/style").arg(mSettingManager->getThemeName());
mStyleValues = new QSettings(QString("%1/values.ini").arg(appThemePath), QSettings::IniFormat);

mStylesheetFileContent = FileUtil::readStringFromFile(QString("%1/style.qss").arg(appThemePath));

// set values example: @color01 => #fff
for (const QString &key : mStyleValues->allKeys()) {
    mStylesheetFileContent.replace(key, mStyleValues->value(key).toString());
}

qApp->setStyleSheet(mStylesheetFileContent);

emit SignalMapper::ins()->sigChangedAppTheme();
Enter fullscreen mode Exit fullscreen mode

Read the stylesheet, string replace every @color01 with #fff from the ini, apply globally.

A preprocessor in nine lines. Swapping themes is pointing at a different folder.

Two details worth stealing. Widgets opt into styling with setAccessibleName("danger") and the QSS selects on it — an accessibility field as a CSS class, which is either beautiful or slightly rude depending on your mood, but it's a known Qt move and it works.

And that emit SignalMapper::ins()->sigChangedAppTheme() at the end. SignalMapper is a global QObject that exists purely as a signal bus.

No logic, three signals:

signals:
    void sigChangedAppTheme();
    void sigUninstallStarted();
    void sigUninstallFinished();
Enter fullscreen mode Exit fullscreen mode

Widgets that paint themselves (the circle bars, the charts) can't get colors from a stylesheet, so they need to know when the theme changed and re-read.

Without the bus, AppManager needs a pointer to every widget that cares.

With it, it shouts into the void and whoever cares listens. Good signal to noise ratio, and I'll see myself out.

What I'm stealing

Split the OS layer into a target that can't link Widgets — the linker enforces the boundary better than my discipline does.

Split that layer by verb.

Never run the whole app as root; pkexec per command is four lines and it's the difference between a bug and a catastrophe.

Poll with QTimer, two rates for two costs, ship it.

QSS plus a token file gives you variables. A signal bus beats N pointers when a global thing changes and unknown widgets care. And know when a singleton is load bearing versus lazy, in here it's holding the delta math together, and it's also why nothing is testable.

If you want to trace one thing end to end, do the CPU percentage.

Timer fires in dashboard_page.cpp, through InfoManager::getCpuPercents, into CpuInfo::getCpuPercents, which reads /proc/stat as text, diffs it against static state from a second ago, and hands a number to a hand painted CircleBar.

Every layer of the app in one number, once a second, on a project nobody maintains anymore.

Next I want to build something with SlidingStackedWidget, and work out whether the .ui files are worth it or whether I should just write layouts by hand.

If you've got opinions, I'd love to hear them, because right now I have two and they're both wrong xD


AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.

git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.

Any feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.

⭐ Star it on GitHub:

GitHub logo HexmosTech / git-lrc

Free, Micro AI Code Reviews That Run on Git Commit




GenAI today is a race car without brakes. It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents silently break things: they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production.

git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff before it lands. 60-second setup. Completely free.

In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen

At a glance: 10 risk categories · 100+ failure patterns tracked · every commit…

Top comments (0)