Skip to content

sysupgrade: restore what an abort stopped, and mark the camera's return - #2419

Merged
widgetii merged 2 commits into
masterfrom
sysupgrade-abort-cleanup
Sep 14, 2026
Merged

widgetii merged 2 commits into
masterfrom
sysupgrade-abort-cleanup

Conversation

@widgetii

Copy link
Copy Markdown
Member

Problem

Two loose ends from #2417, both of them a camera left in a state nobody can see from outside.

check_sdcard kept everything it had been given. It runs after create_lock and free_resources, so a recovery file on the card exited straight out of it: the lock stayed in /tmp and every later run refused with "Another sysupgrade process is already running!" until a reboot cleared it; syslogd, klogd, ntpd and crond stayed stopped; and majestic stayed gutted by the SIGQUIT free_resources sends. Since #2417 it also left a collector holding "logging stops here until this camera returns" for a camera that was never touched. Nothing is written at that point, so die() already does the right thing — no-reboot branch, services restarted, lock dropped.

Nothing ever said the camera came back. free_resources tells a collector the log stops "until this camera returns"; the return was only implicit, whenever the camera next happened to say something. A reader could not tell a camera that came back in ninety seconds from one that is still gone, and had nothing saying which image it came back on.

Affects every board — both files are in the shared overlay (ci-matrix: 99/99).

Hardware tested on

hi3516av300 (hi3516av300-imx415, imx415, nightly-20260913-910e724), forwarding to a remote rsyslog collector. Both halves exercised on that board: the abort path with all three stoplist files on the card, and the boot marker across two reboots.

Evidence

1. The abort. All three stoplist files on the card, kernel path deliberately nonexistent so nothing could be written even if the check were skipped.

Before:

=== RUN THE INSTALLED (pre-fix) SCRIPT ===
Cannot upgrade! Recovery file /mnt/mmcblk0p1/autoupdate-kernel.img found on the mounted SD card!
Please remove the card from the slot and restart sysupgrade.

=== AFTER (pre-fix) ===
majestic: up
syslogd: DOWN
klogd: DOWN
crond: DOWN
ntpd: DOWN
lock: PRESENT (LEAKED)

=== and what the next run now says ===
Another sysupgrade process is already running!

majestic: up is the trap — pidof finds it because SIGQUIT is majestic's "release the SDK and keep serving" signal. In that state it has no video:

=== majestic process ===
2272
=== can it still produce a frame? (snapshot) ===
http=200 bytes=0
=== tail of majestic log ===
12:26:21 DEBUG <majestic> [sdk] stop_sdk@7386: Stop sdk Ok!

After:

=== RUN (expect abort at the recovery file) ===
- checking for /mnt/mmcblk0p1/autoupdate-kernel.img
Cannot upgrade! Recovery file /mnt/mmcblk0p1/autoupdate-kernel.img found on the mounted SD card. Remove the card from the slot and restart sysupgrade. Aborting.

Restarting the services stopped for the upgrade
sysupgrade: still here on nightly-20260913-910e724: this camera is not rebooting after all

=== AFTER ===
majestic: up
syslogd: up
klogd: up
crond: up
ntpd: up
lock: absent (cleaned up)

That run also confirmed #2417's retraction on hardware for the first time — it had only test coverage until now. Both halves reached the collector:

2026-09-14T13:26:21+01:00 10.216.128.39 warning sysupgrade  preparing to flash (kernel=1 rootfs=0 overlay-wipe=0) on nightly-20260913-910e724; logging stops here until this camera returns
2026-09-14T13:26:24+01:00 10.216.128.39 warning sysupgrade  still here on nightly-20260913-910e724: this camera is not rebooting after all

2. The boot marker. The first version used the two-second settle failsafe-rescue uses, and that is wrong here — it went out one second too early and was dropped, because forwarding is UDP and nothing retries it:

Before:

camera, locally:
  Sep 14 12:28:10 hi3516av300-imx415 user.warn boot: this camera is up on nightly-20260913-910e724

collector:
  (no match)

first line the collector actually received that boot:
  2026-09-14T13:28:11.069819+01:00 10.216.128.39 info majestic  Cipher engine: key ladder slot 1 is programmed

It now sends one ICMP packet at the collector first, which resolves the ARP entry the datagram needs instead of guessing how long that takes. Two reboots, two deliveries:

After:

2026-09-14T13:33:52.496669+01:00 10.216.128.39 warning boot  this camera is up on nightly-20260913-910e724
2026-09-14T13:34:49.708876+01:00 10.216.128.39 warning boot  this camera is up on nightly-20260913-910e724

Costs the boot nothing — start returns in 0.00s in every case, the waiting is in a subshell, and with forwarding off no subshell is spawned at all:

forwarding OFF -> new helper processes: 0
forwarding ON  -> new helper processes: 1  (exits on its own)
/etc/init.d/S41bootmsg stop -> rc=0, 0.00s

610 bytes on the image once comments are stripped (3998 raw).

3. Tests. test_sysupgrade.sh gains four behaviour checks and four drift checks; the mount/umount stubs are now stateful so check_sdcard's unmount loop can be driven at all. The two assertions that encode the abort fix were confirmed to fail against the pre-fix script, and the two unrelated ones stayed green:

$ SRC=<pre-fix sysupgrade> bash .github/scripts/test_sysupgrade.sh
ok   a recovery file on the card aborts before anything is written
ok   a clean card is unmounted and the upgrade proceeds
2 check(s) failed.        # "...takes its lock file with it" and "...restarts the services"

Full local gate set green: test_sysupgrade, test_shell_parse (STRICT, 145 scripts incl. the new one), test_strip_shell_comments (STRICT), test_load_hisilicon, test_check_mac, test_automount, ci-matrix --self-test.

Scope

  • No kernel patches under general/package/all-patches/linux/ (those go to OpenIPC/linux)
  • No files specific to a single retail camera model (those go to OpenIPC/builder)
  • No probing or bring-up tooling (that goes to OpenIPC/ipctool)
  • Nothing under general/overlay/ or in a shared load_<vendor> script hardcodes a value specific to my board
  • Package sources come from an OpenIPC repository, and any version bump keeps at least the specificity of the pin it replaces (a new package should pin a full 40-character SHA)
  • No LD_PRELOAD, and no binaries that cannot be rebuilt from source
  • New code is selected by a defconfig, so CI actually builds it

Two loose ends from #2417, both of them a camera left in a state nobody can
see from outside.

check_sdcard kept everything it had been given. It runs after create_lock and
free_resources, and a recovery file on the card exited straight out of it, so
the lock stayed in /tmp and every later run answered "Another sysupgrade
process is already running!" until a reboot cleared it; syslogd, klogd, ntpd
and crond stayed stopped; and majestic stayed gutted by the SIGQUIT
free_resources sends. Since #2417 it also left a collector holding "logging
stops here until this camera returns" for a camera that was never touched.
Nothing is written at that point, so die() already does the right thing --
no-reboot branch, services restarted, lock dropped. Use it.

Measured on an hi3516av300 (nightly-20260913-910e724), all three stoplist
files on the card, kernel path deliberately nonexistent:

  pre-fix                      with this change
  syslogd/klogd/ntpd/crond     all four running
    all DOWN
  lock leaked                  lock cleaned up
  next run refuses with        run proceeds
    "Another sysupgrade
     process is already
     running!"

majestic reports "up" in both columns, and that is the trap: pidof finds it
because SIGQUIT is majestic's "release the SDK and keep serving" signal. In
the pre-fix state a snapshot returns HTTP 200 with 0 bytes and the log says
"stop_sdk: Stop sdk Ok!". The camera looks alive and has no video.

The second half is the other end of that sentence. free_resources tells a
collector the log stops "until this camera returns"; nothing ever said it
had. S41bootmsg logs "this camera is up on <BUILD_ID>" once the network can
carry it. The build id is the payload: it is the only word from the camera
itself that the flash took, or that the bootloader fell back to what it had.

Gated on SYSLOG_REMOTE, so a camera that forwards nothing sources a file it
does not have and exits -- 610 bytes once comments are stripped.
Backgrounded, so the boot pays nothing: the script returns in 0.00s and the
waiting happens in a subshell. That matters because it can wait fifteen
seconds for a lease, and blocking would put that in front of majestic.

What it waits FOR is the part worth reading. A two-second settle after the
address -- failsafe-rescue's -- is wrong here: the marker went out at
13:28:10 and the first line the collector received that boot was majestic's
at 13:28:11. It missed by one second, and forwarding is UDP with nothing to
retry it, so it was gone. It now sends one ICMP packet at the collector
first, resolving the ARP entry the datagram needs instead of guessing how
long that takes. Two reboots, two deliveries:

  2026-09-14T13:33:52+01:00 10.216.128.39 warning boot  this camera is up on nightly-20260913-910e724
  2026-09-14T13:34:49+01:00 10.216.128.39 warning boot  this camera is up on nightly-20260913-910e724

test_sysupgrade.sh gains four behaviour checks and four drift checks, and the
mount/umount stubs are now stateful so check_sdcard's unmount loop can be
driven at all. The two assertions that encode the abort fix were confirmed to
fail against the pre-fix script; the other two stay green, so they are
pinning the fix and not the weather.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Clean up aborted sysupgrades and announce camera return

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Route SD-card recovery conflicts through cleanup, restoring services and releasing the upgrade
 lock.
• Emit asynchronous, build-aware boot markers to configured remote syslog collectors.
• Add stateful SD-mount stubs and regression checks for cleanup and marker invariants.
Diagram

sequenceDiagram
    participant U as sysupgrade
    participant S as SD Card
    participant R as Camera Services
    participant B as Boot Init
    participant N as Network
    participant C as Log Collector
    U->>R: Stop for upgrade
    U->>C: Announce log pause
    U->>S: Check recovery files
    alt Recovery image found
        S-->>U: Block upgrade
        U->>R: Restore services
        U->>U: Release lock
        U->>C: Retract log pause
    else Upgrade reboots
        B->>N: Wait for address
        N-->>B: Address ready
        B->>C: Prime network path
        B->>C: Announce build online
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a fixed boot delay
  • ➕ Simpler boot-marker implementation
  • ➕ Avoids sending an ICMP probe
  • ➖ Cannot reliably account for DHCP, routing, and ARP timing
  • ➖ May still lose the UDP marker or unnecessarily delay delivery
2. Persist an upgrade-in-progress flag
  • ➕ Could emit return markers only after upgrades
  • ➕ Would distinguish upgrade reboots from unrelated boots
  • ➖ Requires state that survives flashing and reboot
  • ➖ Conflicts with overlay-wipe upgrades and introduces stale-state cleanup risks
3. Use reliable remote logging transport
  • ➕ TCP-based delivery could avoid dropped boot markers
  • ➕ Removes dependence on best-effort UDP timing
  • ➖ Requires broader syslog and collector configuration changes
  • ➖ Does not address existing deployments using BusyBox UDP forwarding

Recommendation: The PR's approach is the best fit for the existing system: reusing die() centralizes established pre-flash cleanup, while a forwarding-gated background boot script avoids blocking startup. Probing the collector before logging is more reliable than a fixed delay and requires no persistent state or remote logging migration.

Files changed (3) +191 / -5

Enhancement (1) +78 / -0
S41bootmsgAnnounce camera return to remote log collectors +78/-0

Announce camera return to remote log collectors

• Adds an init script that asynchronously waits for a global IPv4 address, primes the collector path with bounded ICMP, and emits a warning containing the active build ID. It runs only when valid remote syslog forwarding is configured, avoiding cost for local-only logging.

general/overlay/etc/init.d/S41bootmsg

Bug fix (1) +12 / -3
sysupgradeClean up state when SD-card recovery files abort upgrades +12/-3

Clean up state when SD-card recovery files abort upgrades

• Routes recovery-image conflicts through die() instead of exiting directly. This restores stopped services, restarts Majestic, retracts the remote logging-pause message, and removes the owned upgrade lock before returning failure.

general/overlay/usr/sbin/sysupgrade

Tests (1) +101 / -2
test_sysupgrade.shCover SD-card abort cleanup and boot-marker invariants +101/-2

Cover SD-card abort cleanup and boot-marker invariants

• Makes mount and unmount stubs stateful so SD-card removal loops can be exercised. Adds behavioral checks for clean aborts, lock removal, service restoration, and clean-card continuation, plus drift checks for boot-marker execution, severity, forwarding gates, and background operation.

.github/scripts/test_sysupgrade.sh

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Hostname collectors miss boot markers ✓ Resolved 🐞 Bug ≡ Correctness
Description
S41bootmsg resolves a configured hostname only in ping, while logger still sends through the
syslog daemon that cached its failed pre-network lookup. When SYSLOG_REMOTE uses the documented
hostname form, the marker is emitted within 18 seconds but the daemon does not retry DNS for 120
seconds, so the datagram is dropped along with other early forwarded messages.
Code

general/overlay/etc/init.d/S41bootmsg[R74-75]

+	logger -p user.warning -t boot \
+		"this camera is up on ${build:-an unidentified build}"
Evidence
The configuration explicitly supports host[:port] but documents that the daemon starts before
networking, drops early messages, and waits 120 seconds before retrying a failed DNS lookup. The new
script waits at most 15 seconds plus a three-second ping and then invokes local logger; resolving
the name in ping does not update the separate, already-running syslog daemon.

general/overlay/etc/default/syslogd[15-29]
general/overlay/etc/init.d/S01syslogd[49-56]
general/overlay/etc/init.d/rcS[19-25]
general/overlay/etc/init.d/S41bootmsg[47-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The boot marker resolves hostname collectors in a separate `ping` process, but the already-running syslog daemon retains the failed DNS lookup it made before networking started. Consequently, `logger` emits the marker before the daemon's 120-second DNS retry and hostname-based collectors do not receive it.
## Fix Focus Areas
- general/overlay/etc/init.d/S41bootmsg[65-75]
- general/overlay/etc/init.d/S01syslogd[49-56]
## Recommended Fix
After networking and DNS become available, make the active forwarding syslog daemon refresh a hostname destination before emitting the marker. Preserve local logging and its buffer while doing so, and add a behavioral test that starts with DNS unavailable, enables it, and verifies that a hostname-configured collector receives the boot marker.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread general/overlay/etc/init.d/S41bootmsg
Review found the half I had not tested. Every measurement behind the marker
was taken against a literal IP, and a name behaves differently: busybox
resolves the destination once, when syslogd starts -- S01, before the network
-- and retries only every 120 s. etc/default/syslogd has said so since #2415
and tells people to prefer an IP for exactly this reason. So on a name
everything sent in the first two minutes is dropped however ready the path
is, and this marker, at roughly eighteen seconds, was precisely the line that
silently never arrived.

Reproduced on an hi3516av300 pointed at a hostname collector. Local:

  Sep 14 12:49:55 hi3516av300-imx415 user.warn boot: this camera is up on nightly-20260913-910e724

Collector, same boot: nothing -- while 48 other lines from that window did get
through, once the daemon had re-resolved.

The marker now waits the window out on a name. Late and delivered beats
punctual and dropped, and the build id is no less true two minutes on. It is
free: the wait was already in a subshell, so nothing on the boot path is
waiting for it. Same board, same hostname collector, after:

  Sep 14 12:55:39 hi3516av300-imx415 user.warn boot: this camera is up on nightly-20260913-910e724
  2026-09-14T13:55:39.271358+01:00 10.216.128.39 warning boot  this camera is up on nightly-20260913-910e724

Deliberately not the other repair, which is to restart syslogd so it resolves
again. The buffer logread and the WebUI read is in RAM and dies with the
daemon, so that trades the whole local record of the boot -- the thing that
still exists when forwarding does not -- for one forwarded line. Two drift
checks now pin both halves of that: that the wait exists, and that it is not
done by restarting syslogd.
@widgetii
widgetii merged commit d71a47b into master Sep 14, 2026
221 of 224 checks passed
@widgetii
widgetii deleted the sysupgrade-abort-cleanup branch September 14, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant