DeonMarais64 / PlutoPlusSDR-FW

PlutoPlus SDR Firmware binaries and build scripts
9 stars 4 forks source link

[Feature Request] Run user script/services on startup #1

Closed longview closed 1 year ago

longview commented 1 year ago

First of all thank you for taking on the burden of updating this! Really appreciated.

It would be super nice to have a way to execute a user script on startup, stored in the jffs2 partition. This way simple things could be replaced at run-time without rebuilding the entire rootfs image. (Also, I tried a rebuild according the instructions in the other repo you have and something broke so had to DFU recovery)

Perhaps one way to do it would be to have /etc/rcS also call a folder in /mnt/jffs2/etc/init.d? That way users could add or remove service control scripts as needed, but simply calling a shell script would also be fine and maybe easier for non-experts to set up.

My use case is kicking off a very simple script that reads the receiver frequency via the sysfs and controls an external filter-bank/antenna switcher over bit-bashed SPI. Working quite well, but for now I have to scp and run them manually on every reboot.

DeonMarais64 commented 1 year ago

You should be able to follow this method to do what you need to. If you are doing this for the plutoplus, then you will need to use the attached its file, the one linked to in the repo is that for PlutoSDR. plutoplus.zip BTW, I found that the process works best using root privileges on the machine you use to do the firmware modifications on.

You can extract, modify (eg, add required scripts and executable binaries) and then re-pack the rootfs. You will need to go through the process every time you need to do a change. AFAIK there are no "user" nv settings other than those described here . I suspect that it would be possible to add a fw_setenv type variable to allow limited startup options without the need to reload firmware, but I have not looked into that as yet.

I have used an init.d script (placed rootfs/etc/init.d/) to auto-run an application at startup. Something like this should serve as a rough guide.

longview commented 1 year ago

Thanks for getting back to me so soon!

I definitely made some mistakes the first time around, and didn't catch the different .its file, I also didn't run the cpio extract as root, so some files were probably missing when it was rebuilt.

But I got my build working now. And by modifying rcS to also search /mnt/jffs2/etc/init.d I can do what I want. /mnt/jffs2 is a small non-volatile but writable partition so I'm surprised ADI didn't add this by default.

DeonMarais64 commented 1 year ago

I am pleased you came right, would you kindly post or send me the modified rcS, I will then rebuild the current and make sure it is included in any future .frm's.

longview commented 1 year ago

I'll just post what I added, it's a duplicate of what's already there but searches a different path:

# append to /etc/init.d/rcS to execute user services after main init
for i in /mnt/jffs2/etc/init.d/S??* ;do

     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
        *.sh)
            # Source shell script for speed.
            (
                trap - INT QUIT TSTP
                set start
                . $i
            )
            ;;
        *)
            # No sh extension, so fork subprocess.
            $i start
            ;;
    esac
done

This doesn't seem to cause any issues with booting if that additional init.d folder is not present.

I guess some users may not have the partition, looking at the ADI wiki: https://wiki.analog.com/university/tools/pluto/users/customizing#enabling_persistent_ssh_keys the built in command device_format_jffs2 can be used to activate it.

Also for anyone reading this in the future I'll note that the files in /mnt/jffs2/etc/init.d have to be executable and follow the Snn... naming convention, but they do not actually have to be formatted like service control scripts (it's just better to do so).

So for example I added an additional "service" to fix up the network configuration when using a static IP to e.g. make ntpd work and use my local ntp servers:

echo "nameserver 192.168.1.1" > /etc/resolv.conf
route add default gw 192.168.1.1 eth0
#echo "server <user ntp server>">>/etc/ntp.conf
#/etc/init.d/S49ntp restart

So that's a simple way of just executing a few commands on startup.

longview commented 1 year ago

An additional thought I had is that the jffs2 partition is not cleared out even when doing a DFU update.

There is therefore a risk that adding a broken init script there could brick the device, and even a DFU update wouldn't restore it (if the DFU'd image also checks for the jffs inits).

DFU update does seem to clear the firmware parameters, so a ideally it should check some firmware parameter before executing the jffs init scripts. That way a DFU recovery would be a simple way to disable the extra init scripts.

DeonMarais64 commented 1 year ago

Thank you, but should /etc/init.d/rcK not also be similarly modified ? Also, would it not be more consistent to rewrite the script's rcS and rcK so that the perspective Snn files are run in numerical nn order /etc/init.d/Snn vs /mnt/jffs2/etc/init.d/Snn with priority given to those in /etc/init.d ?

longview commented 1 year ago

rcK is not something I concerned myself with since nothing I run on the Pluto requires a clean stop, but it should probably be modified similarly yes.

I modified the above slightly to check for the firmware variable localinit:

LOCALINIT=`fw_printenv -n localinit`

if [ "$LOCALINIT" == "true" ]
then
    for i in /mnt/jffs2/etc/init.d/S??* ;do
     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
        *.sh)
            # Source shell script for speed.
            (
                trap - INT QUIT TSTP
                set start
                . $i
            )
            ;;
        *)
            # No sh extension, so fork subprocess.
            $i start
            ;;
    esac
    done
fi

Then it can be activated by running fw_setenv localinit true and disabled by e.g. setting fw_setenv localinit "" or setting it to any other value. This should be cleared by DFU updates but not normal ssh or USB mass storage updates, allowing for relatively simple recovery in case someone messes up.

Tested with values other than 'true' and by clearing the variable and both those cases don't execute the local scripts.

To run the new scripts in order we could simply copy /mnt/jffs/etc/init.d/* to /etc/init.d right at the start? This would allow full replacement of the init scripts as needed, though it would prioritize the users scripts since those could overwrite the firmware scripts.

I suspect it's probably not strictly necessary to have this level of control though, running the user inits last is likely to be what most people want anyway I think?

DeonMarais64 commented 1 year ago

I suspect it's probably not strictly necessary to have this level of control though, running the user inits last is likely to be what most people want anyway I think?

Agreed, anyone who wants something more complex than what is on offer could just modify the firmware themselves.

I think what you have suggested makes sense. Please check the modified scripts below, I changed the name of the variable to make it a little more descriptive (at least to me).

/etc/init.d/rcS

#!/bin/sh

# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do

     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
    *.sh)
        # Source shell script for speed.
        (
        trap - INT QUIT TSTP
        set start
        . $i
        )
        ;;
    *)
        # No sh extension, so fork subprocess.
        $i start
        ;;
    esac
done

INIT_LOCAL=`fw_printenv -n init_ext_jffs2`

if [ "$INIT_LOCAL" == "true" ]
then
    for i in /mnt/jffs2/etc/init.d/S??* ;do

         # Ignore dangling symlinks (if any).
         [ ! -f "$i" ] && continue

         case "$i" in
        *.sh)
            # Source shell script for speed.
            (
            trap - INT QUIT TSTP
            set start
            . $i
            )
            ;;
        *)
            # No sh extension, so fork subprocess.
            $i start
            ;;
        esac
    done
fi

/etc/init.d/rcK

#!/bin/sh

# Stop all init scripts in /etc/init.d
# executing them in reversed numerical order.
#
for i in $(ls -r /etc/init.d/S??*) ;do

     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
    *.sh)
        # Source shell script for speed.
        (
        trap - INT QUIT TSTP
        set stop
        . $i
        )
        ;;
    *)
        # No sh extension, so fork subprocess.
        $i stop
        ;;
    esac
done

KILL_LOCAL=`fw_printenv -n init_ext_jffs2`

if [ "$KILL_LOCAL" == "true" ]
then
    for i in $(ls -r /mnt/jffs2/etc/init.d/S??*) ;do

         # Ignore dangling symlinks (if any).
         [ ! -f "$i" ] && continue

         case "$i" in
        *.sh)
            # Source shell script for speed.
            (
            trap - INT QUIT TSTP
            set stop
            . $i
            )
            ;;
        *)
            # No sh extension, so fork subprocess.
            $i stop
            ;;
        esac
    done
fi