Skip to content

cli: ask majestic to apply a setting, instead of only writing it down - #2366

Merged
openipc-ai merged 2 commits into
masterfrom
cli-apply-live
Sep 5, 2026
Merged

cli: ask majestic to apply a setting, instead of only writing it down#2366
openipc-ai merged 2 commits into
masterfrom
cli-apply-live

Conversation

@widgetii

@widgetii widgetii commented Sep 5, 2026

Copy link
Copy Markdown
Member

Problem

cli -s .video0.fps 20 edits /etc/majestic.yaml, prints nothing and exits 0 — and the
camera carries on streaming at the old rate. Nothing in the tool says a reload is needed,
and the wiki is not consistent about it either: of the pages that change a setting, ten say
nothing about restarting, four say restart, and one says reboot.

Writing the file also cannot fail, so a malformed setting path is stored rather than
reported. The tree ships one:
builder/devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:15-19,25
says cli -s .video0.bitrate: 4000, and the trailing colon is why six of that device's
settings have never applied. Nobody noticed, because there was nothing to notice.

Shows on every board — general/overlay/usr/sbin/extutils is in the shared overlay.

What this does

cli -s / cli -d still write the file, and then ask majestic to reload if it is running.
signal_hup_cb already parses before tearing anything down, diffs the file against the tree
in force, prices the difference on the CFG_RELOAD_* ladder — a live key goes straight at
the SDK, a service key restarts only that service, and only a pipeline key rebuilds — and
debounces repeat signals inside 3s into one reload. So most settings now apply without the
stream noticing. This is the pattern wifibroadcast:82 and :212 already use by hand;
it is only being made the default.

The signal is conditional on majestic already running, which is what keeps provisioning
untouched: customizer.sh seeds a device at S30 and majestic does not start until S95, so
those writes take exactly the path they always did, silently. Reads and an explicit -i/-o
are left alone, so sensor_cli and the wifibroadcast cli applet are unaffected.

Plus a key-shape check that refuses a malformed path before the write, CMD from ${0##*/}
(cut -d / -f 4 needs a path of exactly four fields, so ./cli fell through to the silent
default), and removal of the dead api) applet — nothing creates /usr/sbin/api, and its
--data-urlencode "$@" only ever bound $1.

Why not route the writes through the HTTP API

That was the first design, and it was measured on a lab camera before being abandoned.

Of the fourteen keys wifibroadcast's video_settings seeds, six answer 404 from
/api/v1/setoutgoing.wfb, records.notime, fpv.noiseLevel, fpv.enabled,
isp.exposure, video0.noiseLevel. They are read by majestic but never declared in its
schema. .outgoing.wfb gates the wfb relay, so this would have broken video on every
wifibroadcast camera. The file path is forward- and backward-compatible across a key rename;
the API path is not, and wifibroadcast:183-188 says so in a comment.

It would also have had to guess whether majestic was up, and S98wifibroadcast and
S98datalink both run after S95majestic, where the honest answer is "starting".

Hardware tested on

hi3516ev200 (lab camera, OpenIPC 2.6.04.16, majestic master+a805052). The new extutils was
copied over the shipped one, exercised, and the camera restored afterwards — extutils and
majestic.yaml md5s both back to their originals.

Evidence

Before — the write lands in the file, the running camera ignores it, and a malformed path is
stored without complaint:

# cli -g .video0.bitrate
4096
# cli -s .video0.bitrate: 4000
# echo $?
0

After:

# cli -g .video0.fps
20
# cli -g .video0.bitrate
4096

--- the movols typo: refused, and the file is untouched ---
# md5sum /etc/majestic.yaml
981bfbaced4772c70259baa0844b9558  /etc/majestic.yaml
# cli -s .video0.bitrate: 4000
cli: '.video0.bitrate:' is not a setting path, nothing written
# echo $?
1
# md5sum /etc/majestic.yaml
981bfbaced4772c70259baa0844b9558  /etc/majestic.yaml

--- a real write now reaches the running streamer ---
# cli -s .video0.bitrate 2048
# echo $?
0
# grep -A6 '^video0:' /etc/majestic.yaml | grep bitrate
  bitrate: 2048
# curl -s http://127.0.0.1/api/v1/config.json | tr ',' '\n' | grep -o '"bitrate": *[0-9]*' | head -1
"bitrate": 2048
# pidof majestic        # same pid before and after: reloaded in place, not restarted
1055
# curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/image.jpg
200

--- boot path unchanged: majestic down, silent, and the && chain still fires ---
# /etc/init.d/S95majestic stop
# pidof majestic || echo "not running"
not running
# cli -s .video0.fps 18
# echo $?
0
# grep -A3 '^video0:' /etc/majestic.yaml | grep fps
  fps: 18
# cli -s .isp.blkCnt 1 && echo "&& chain still fires"
&& chain still fires

--- reads and an explicit -i are untouched ---
# printf 'sensor:\n  width: 1280\n' > /tmp/t.yaml
# cli -i /tmp/t.yaml -s .sensor.width 1920 ; cat /tmp/t.yaml
sensor:
  width: 1920

Off-camera gates:

$ STRICT=1 bash .github/scripts/test_shell_parse.sh
checked 134 shell script(s)
all parsed clean under busybox ash

$ STRICT=1 bash .github/scripts/test_strip_shell_comments.sh
ok   stripping is idempotent
ok   134 shipped scripts parse identically after stripping (106670 bytes saved)
All strip-shell-comments checks passed.

$ git diff --name-only origin/master | python3 .github/scripts/ci-matrix.py --stdin
ci-matrix: 99/99 boards (needs_build=True) --- general/overlay/usr/sbin/extutils affects every board

Follow-ups this turned up

  • zoom.sh: killall -10 majestic sends SIGUSR1, which parks a thread instead of reloading config #2365zoom.sh sends killall -10 majestic, which is SIGUSR1, caught by the bundled
    thread pool's thread_hold() and never released. Those nine calls should be deleted rather
    than corrected, since cli -s now asks for the reload itself.
  • The six undeclared keys above are worth declaring in majestic regardless of this PR.
  • builder's t40_lite_movols-mo-805p still has the six trailing colons; that is a fix for
    that repo, and this change is what makes them visible.

Scope

  • No kernel patches under general/package/all-patches/linux/
  • No files specific to a single retail camera model
  • No probing or bring-up tooling
  • Nothing under general/overlay/ or in a shared load_<vendor> script hardcodes a value specific to my board
  • No LD_PRELOAD, and no binaries that cannot be rebuilt from source
  • New code is selected by a defconfig, so CI actually builds it

`cli -s .video0.fps 20` edited /etc/majestic.yaml, printed nothing and exited
0, while the camera carried on streaming at the old rate. Whether a reload was
needed depended on the page you happened to read: of the wiki pages that change
a setting, ten say nothing, four say restart and one says reboot.

majestic already re-reads the file on SIGHUP, diffs it against the tree in
force and prices the difference on its own ladder -- a live key is pushed
straight at the SDK, a service key restarts only that service, and only a
pipeline key rebuilds -- so most settings now apply without the stream
noticing. Repeat signals inside 3s collapse into one reload, so a burst of
writes still reloads once. This is the pattern wifibroadcast already applies
by hand after its own writes; it is only being made the default.

The signal is conditional on majestic already running, which is what keeps
provisioning untouched: customizer.sh seeds a device at S30 and majestic does
not start until S95, so those writes take exactly the path they always did,
silently. Reads and an explicit -i/-o are left alone -- sensor_cli and the wfb
applet both address other files through them.

Also refuse a malformed setting path rather than storing it. Writing the file
cannot fail, so `cli -s .video0.bitrate: 4000` recorded the trailing colon and
said nothing; that line is in t40_lite_movols-mo-805p's customizer.sh and is
why six of that device's settings have never applied.

Routing the writes through the HTTP API was the other candidate and was
measured on a lab camera first: of the fourteen keys wifibroadcast seeds,
six answer 404 -- including .outgoing.wfb, which gates the wfb relay -- because
they are read from the config but never declared in the schema. It would also
have had to guess whether majestic was up, and S98wifibroadcast and S98datalink
both run after S95majestic, where the answer is "starting".

CMD now comes from ${0##*/}: `cut -d / -f 4` needs a path of exactly four
fields, so ./cli fell through to the silent default. The api) applet goes --
nothing creates /usr/sbin/api, and --data-urlencode "$@" only ever bound $1.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Apply CLI configuration writes to running Majestic

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Reload running Majestic after successful default configuration writes.
• Reject malformed setting paths before they are persisted.
• Preserve provisioning, reads, and explicit input/output file behavior.
Diagram

graph TD
  A["CLI write"] --> B["Parse arguments"] --> C{"Path valid?"}
  C -->|No| D["Reject write"]
  C -->|Yes| E["Write YAML"] --> F{"Majestic running?"}
  F -->|Yes| G["SIGHUP reload"]
  F -->|No| H["Persist only"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Route writes through Majestic HTTP API
  • ➕ Provides daemon-side validation for declared settings
  • ➕ Applies supported settings directly without process signaling
  • ➖ Returns 404 for configuration keys consumed but absent from Majestic's schema
  • ➖ Reduces compatibility across key renames and Majestic versions
  • ➖ Introduces startup races when provisioning follows daemon launch
2. Restart Majestic after each write
  • ➕ Simple operational model
  • ➕ Guarantees the full configuration is reloaded
  • ➖ Interrupts active streams unnecessarily
  • ➖ Costs more than Majestic's existing granular reload ladder
  • ➖ Handles bursts of writes less efficiently

Recommendation: Keep the file-first, conditional-SIGHUP approach. It preserves compatibility with undeclared and renamed settings, leaves pre-start provisioning unchanged, and delegates reload scope and debouncing to Majestic's existing configuration engine.

Files changed (1) +81 / -6

Bug fix (1) +81 / -6
extutilsValidate CLI writes and reload running Majestic +81/-6

Validate CLI writes and reload running Majestic

• Determines the invoked applet from its basename, removes the unused API applet, and parses yaml-cli arguments to identify writes targeting Majestic's default configuration. It rejects malformed setting paths and sends SIGHUP after successful writes only when Majestic is running, while preserving reads, alternate files, and provisioning behavior.

general/overlay/usr/sbin/extutils

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

qodo-free-for-open-source-projects Bot commented Sep 5, 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. Reload can kill startup ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new pidof check treats process existence as readiness, but S95majestic backgrounds Majestic
and immediately allows boot to continue into S98 scripts that call cli -s. A SIGHUP arriving
before Majestic installs its handler takes the default terminating action, leaving the camera
without its streamer for the remainder of boot.
Code

general/overlay/usr/sbin/extutils[R87-88]

+		if [ "$rc" = 0 ] && [ -n "$wpath" ] && pidof majestic >/dev/null 2>&1; then
+			killall -1 majestic
Evidence
The init runner proceeds directly through lexical startup order, while S95 starts Majestic in the
background without waiting for readiness. S98wifibroadcast then reaches a series of Majestic
configuration writes, each of which now performs the added process check and SIGHUP, establishing
the startup signal race.

general/overlay/usr/sbin/extutils[87-90]
general/package/majestic/files/S95majestic[24-31]
general/overlay/etc/init.d/rcS[8-14]
general/package/wifibroadcast-ng/files/S98wifibroadcast[3-7]
general/package/wifibroadcast-ng/files/wifibroadcast[168-180]
general/package/wifibroadcast-ng/files/wifibroadcast[189-199]

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

## Issue description
`cli` sends SIGHUP as soon as `pidof majestic` succeeds. During boot, Majestic is launched asynchronously, so it may be visible before its SIGHUP handler is installed; signaling in that window can terminate it.
## Issue Context
`S95majestic` uses `start-stop-daemon -b` and returns immediately. `rcS` then starts S98 services, and wifibroadcast performs multiple `cli -s` writes during initialization. Ensure reloads are sent only after a reliable Majestic readiness indication, while preserving silent provisioning when Majestic is down.
## Fix Focus Areas
- general/overlay/usr/sbin/extutils[87-90]
- general/package/majestic/files/S95majestic[24-31]
- general/package/wifibroadcast-ng/files/wifibroadcast[168-199]

ⓘ 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 keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread general/overlay/usr/sbin/extutils Outdated
pidof answers that the process exists, which is not the same as being ready to
be signalled. S95majestic starts it with start-stop-daemon -b, so the call
returns at the fork and boot carries straight on into the S98 scripts --
wifibroadcast writes fourteen keys there -- while majestic is still working its
way down main() towards init_libevent(), which is where the SIGHUP handler is
installed. Until it gets there the default action applies, and for SIGHUP that
is to terminate. So a reload asked for in that window does not reload the
camera, it takes the streamer away for the rest of the boot.

/proc/<pid>/status answers the question exactly rather than by timing guess:
SigCgt is the caught-signal mask, SIGHUP is signal 1 and therefore its lowest
bit, so the mask ends in an odd hex digit precisely when the handler is in
place. Signal only those pids, and a write that lands early simply does not
reload -- which is what it did before this branch existed.

Measured on a hi3516ev200. The process is visible to pidof with SigCgt still
0000000000000000, and reaches 0000000000004007 once the handler is installed.
Firing 25 writes straight into that window: with this check majestic is still
alive afterwards and streaming; with an unconditional killall -1 in the same
window it is dead.

Worth noting the hazard is older than this branch -- wifibroadcast:82 and :212
already send killall -1 at S98, and the wiki has long told people to run
killall -HUP majestic by hand. Majestic ignoring SIGHUP until it can handle it
would fix the class for every sender; this fixes the one the tree controls.
@openipc-ai
openipc-ai enabled auto-merge (squash) September 5, 2026 11:10
@openipc-ai
openipc-ai merged commit d981bc1 into master Sep 5, 2026
118 checks passed
@openipc-ai
openipc-ai deleted the cli-apply-live branch September 5, 2026 12:17
widgetii added a commit to OpenIPC/wiki that referenced this pull request Sep 5, 2026
OpenIPC/firmware#2366 made `cli -s` write /etc/majestic.yaml and then ask
Majestic to reload, so the pages telling people to prefer the HTTP API because
`cli` does not apply anything are describing firmware that no longer exists.
`cli` and the API now do the same job, and each is better at something: `cli`
is the one that works before Majestic is running, which is the whole of first
boot, and it can write a key this build does not declare; the API validates,
and can be called from another machine.

Three claims were wrong rather than merely stale:

The SIGHUP row said a reload tears the pipeline down and builds it again. It
has not meant that for a while -- Majestic prices the difference and takes the
cheapest option that carries it, which is why setting a bitrate no longer costs
the stream. Rewritten to describe the ladder.

The plugin table attributed reloading to who sent the signal, "what the WebUI
and cli use to apply a change". Both produce either outcome depending on the
key, so the row now splits on what actually decides it: a rebuild reloads the
plugin, an in-place change does not.

And majestic-config.md now says why a saved file stays short. widgetii/majestic
PR 608 stopped Majestic writing its own defaults into majestic.yaml, so a value
you never set is absent rather than frozen at whatever the default was when you
last pressed Save -- which is what lets a corrected default reach a camera that
has already been configured.
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.

2 participants