~craftyguy/blog.craftyguy.net

0c48454a6e5278e57597ea29a304a3d34c57fc50 — Clayton Craft 8 months ago d70b269
New post: Alpine Linux adventures: running a thing on system shutdown
1 files changed, 83 insertions(+), 0 deletions(-)

A content/alpine_openrc_reboot.md
A content/alpine_openrc_reboot.md => content/alpine_openrc_reboot.md +83 -0
@@ 0,0 1,83 @@
+++
title = "Alpine Linux adventures: running a thing only on system shutdown"
date = 2021-03-13
[taxonomies]
tags = ["Alpine Linux", "postmarketOS", "kernel"]
+++
One interesting change from Purism recently was to enable something called "ship mode" on the Librem 5's USB charge controller chip. Ship mode causes the chip to cut all power to the rest of the phone, solving the problem of the phone completely draining its battery even when it is "off." This mode is enabled by setting some bits in certain registers on the chip, and should *only* be enabled on shutdown and *not* on system reboot. This is key, since their script enables ship mode with a delay... if it's enabled on reboot then power to the phone will be suddenly cut off when the phone is booting back up.

Purism's implementation for setting this up involves a script using `i2cset` to set these registers. This script is installed to `/usr/lib/systemd/system-shutdown/`, so systemd will call it on shutdown/reboot. The script knows whether or not the system is going down for a shutdown vs. reboot, because systemd will pass something like `poweroff` to the script on shutdown. Easy peasy lemon squeezy.

Porting this thing to postmarketOS has been anything but...

Being based on Alpine Linux, pmOS uses openrc to manage daemons. Openrc's `openrc-run` manpage states that there's an `RC_REBOOT` variable set in the environment when it runs scripts in the `shutdown` runlevel. Well, that's exciting, this should be easy too! And wrong I was. After observing that `RC_REBOOT` is *not* set on reboot, or shutdown, or at any time, I popped into the `#openrc` IRC channel for advice. The reason why this wasn't working was because I completely forgot that Alpine Linux uses busybox for init, and does not use `openrc-init`. `RC_REBOOT` is only set by `openrc-init`.

One idea was put forth: just run `openrc-shutdown --reboot` in the `inittab` on reboot, which should be straight forward to do since the `inittab` file allows for specifying a runlevel in the second column on each line.

After that didn't work at all, I came across this gem in [the busybox inittab example](https://git.busybox.net/busybox/tree/examples/inittab):

> \# <runlevels>: The runlevels field is completely ignored.

womp womp.

Next idea: replace busybox init with openrc-init! This was mostly straight forward to do, but required adding some getty setup in `/etc/init.d` since openrc does not use inittab for spawning tty. Somewhat unsurprisingly, this resulted in `RC_REBOOT` being set when my ship mode script ran in the `shutdown` runlevel. I didn't like this solution though, since I did not want the Librem 5 to be the only device in pmOS that required a different init, and replacing the init just for this 1 feature didn't seem worth any future maintenance cost associated with having a different init than the rest of Alpine Linux and pmOS.

Feeling somewhat stuck, I mostly gave up on the idea of using some incantation of busybox and/or openrc to solve this, and started wondering what exactly happens when the Linux kernel does a shutdown or reboot. Let's grep the Linux source!

Searching file names in the tree for 'reboot' yields a number of possibilities, many of them are architecture-specific (e.g. under 'x86' and so on), but one file in particular seemed like a good place to start: `kernel/reboot.c`
The shutdown routine, at least at this level, is pretty straight forward, and can be found under `kernel_power_off`:

```C
void kernel_power_off(void)
{
    kernel_shutdown_prepare(SYSTEM_POWER_OFF);
    if (pm_power_off_prepare)
        pm_power_off_prepare();
    migrate_to_reboot_cpu();
    syscore_shutdown();
    pr_emerg("Power down\n");
    kmsg_dump(KMSG_DUMP_SHUTDOWN);
    machine_power_off();
}
```

The important part here is the last line, `machine_power_off`, which sounds like some pointer to some machine-specific shutdown routine. Grepping for this results in a direct hit in `arch/arm64/kernel/process.c`. That's relevant since the Librem 5 is an arm64 platform.

`machine_power_off` in `arch/arm64/kernel/process.c` is really short and sweet:
```C
/*
* Power-off simply requires that the secondary CPUs stop performing any
* activity (executing tasks, handling interrupts). smp_send_stop()
* achieves this. When the system power is turned off, it will take all CPUs
* with it.
*/
void machine_power_off(void)
{
    local_irq_disable();
    smp_send_stop();
    if (pm_power_off)
        pm_power_off();
}
```

`pm_power_off` is a function pointer that can apparently be set by anything (drivers!) to have the "final say" in what actually powers off. `pm_power_off` is *not* called on reboot. Now we're getting somewhere!

New idea: modify the `bq25890-charger` driver to hook into `pm_power_off`, and set appropriate ship mode registers on shutdown. There are lots (and lots) of examples in the kernel tree of drivers using `pm_power_off` to do similar things, so it wasn't too bad coming up with a functional POC.

One problem I ran into early with this approach had to do with one condition where we do *not* want to enable ship mode: when shutting down *and* the device is charging. Since I was in the charge controller driver, it's trivial to determine the charging state (it's figured out elsewhere in the driver), so I naively added a very simple condition at the start of my `pm_power_off` function:
```C
/* Don't enable ship mode if charging */
if (bq25890_dev->state.online) {
    dev_info(bq25890_dev->dev, "Charging, not entering ship mode");
    return;
}
```
In theory, this is what we want... bail out early if charging so that ship mode is not enabled. The important part I was not understanding at the time was that the kernel seems to expect that `pm_power_off` will actually end with the system powered off. Returning here without doing that results in the system being in some on-but-kinda-off state where it's sucking power but there's nothing alive. Oops!
The "fix" was to save the previous value of `pm_power_off` when this driver loads, then call that when putting the device into ship mode is aborted. I'm sure there's a better way to do this.

In any case, I've compiled a patch and [submitted it to Purism's kernel fork](https://source.puri.sm/Librem5/linux-next/-/merge_requests/333). With this I'm able to, as far as I can tell, reliably put the device into ship mode by powering it off while not charging, and *not* put it into ship mode in all other cases.
I hope to have this patch, or something like it, upstreamed to the mainline kernel. At the very least, I now have a way to enable ship mode on the Librem 5 in postmarketOS.

To be continued...

Note: all kernel snippets are from 5.11.4.