Closing the popup after the fact left it on screen for the 15-20 seconds between the updater's version check and the Homebrew Channel running its init.d hooks. That race cannot be won, so remove it. /usr/sbin/update is launched on demand by ls-hubd via a manifest, and ls-hubd.conf lists ManifestsVolatileDirectories under /var - writable, persistent, and how webOS itself ships manifest updates. A manifest there with the same id and a higher version replaces the read-only one, so drop in a copy that keeps every role and permission file and only empties serviceFiles. Nothing on the bus can start the updater after that: no version check, no alert, nothing to dismiss. Callers get an immediate "Service does not exist" rather than a hang, and deleting the file undoes it. Verified on a CX (webOS 5, 04.60.65) across reboots: no update process, /tmp/var/log/update.log never created, no alert. Previously that log was 55 kB with two server checks and an _gAlertWindowId per boot. Revert puts the updater back on the bus. The manifest, its D-Bus service file and the updater binary are all discovered from ls-hubd.conf rather than hardcoded. The popup-closing layer stays as a fallback for when this one is off. Also tried and rejected, now documented in the README: update-related settings, masking update.service (no writable unit directory), and breaking its ping through /var/systemd/system/env/update.env - the ping does fail, but something else on the bus activates the updater anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ef8b6b96c4
commit
80e2d7e5fa
@@ -19,8 +19,7 @@ Blocking `snu.lge.com` in `/etc/hosts` is the obvious fix, and it is what the
|
||||
Homebrew Channel's own *Block system updates* toggle does. On a CX it does not
|
||||
stop the boot popup, and it took a session on the TV to work out why.
|
||||
|
||||
`/usr/sbin/update` is started by **systemd** (`update.service`, pulled in by
|
||||
`webos-mbd.target`) and runs its version check against
|
||||
The updater runs its version check against
|
||||
`https://snu.lge.com/CheckSWAutoUpdate.laf` roughly **20 seconds before** the
|
||||
Homebrew Channel gets as far as running its `init.d` hooks. So on every boot
|
||||
the check reaches a real LG server, gets `RESULT_CD 900 / Success` back, and
|
||||
@@ -32,29 +31,55 @@ the popup is already on screen by the time any hosts entry exists:
|
||||
00:21:11 lg-update-blocker: hosts entries added ← 19 seconds too late
|
||||
```
|
||||
|
||||
Losing that race is not fixable from userspace. Every directory systemd reads
|
||||
units from is a read-only overlay except `/run/systemd/system`, which is tmpfs
|
||||
and created after the updater has already gone. Nor is there a setting that
|
||||
gates the check — `automaticUpdate`, `support/softwareUpdateEnable`,
|
||||
`hotelMode/swUpdateEnable` and the `.UpdateIsInprogress` flag were each tested
|
||||
by restarting the daemon and counting the requests it made, and none of them
|
||||
stop it.
|
||||
That race cannot be won — nothing unprivileged runs earlier than those hooks —
|
||||
so the app removes it instead.
|
||||
|
||||
So this app **closes the popup** instead of trying to prevent it, and blocks
|
||||
the hostnames to stop the download and every later check in the session. Two
|
||||
things also turned out not to be true, and the app no longer pretends
|
||||
otherwise: the popup is not caused by a staged firmware image (the staging
|
||||
directory is empty at boot; the size the daemon reports is in-memory only), and
|
||||
it is not caused by an update *service* that can be stopped — `/etc/init` is
|
||||
dead upstart leftovers on a systemd TV, and `initctl stop update` silently
|
||||
"succeeds" while doing nothing.
|
||||
`/usr/sbin/update` is not a normal daemon you can stop. It is launched on
|
||||
demand by **ls-hubd**, which finds it through a *manifest*: a small JSON file
|
||||
listing the D-Bus service file that carries `Exec=/usr/sbin/update`. LG's own
|
||||
manifest is on a read-only partition, but `/etc/luna-service2/ls-hubd.conf`
|
||||
also lists `ManifestsVolatileDirectories` under `/var` — writable, persistent,
|
||||
and the mechanism webOS itself uses to ship manifest updates. A manifest there
|
||||
with the **same id and a higher version replaces** the read-only one.
|
||||
|
||||
So the app drops in a copy that keeps every role and permission file exactly as
|
||||
it was and only empties the `serviceFiles` list. That single change takes away
|
||||
ls-hubd's ability to launch the updater, and nothing on the bus can start it
|
||||
any more: no version check, no alert, nothing to dismiss. Callers get an
|
||||
immediate `Service does not exist` rather than hanging, and deleting the one
|
||||
file puts everything back.
|
||||
|
||||
Measured on a CX, before and after: `/tmp/var/log/update.log` used to be 55 kB
|
||||
with two server checks and an `_gAlertWindowId` per boot — now the file is never
|
||||
created at all.
|
||||
|
||||
### Things that do not work
|
||||
|
||||
Each of these was tried on the TV before settling on the above, and none of
|
||||
them stop the popup:
|
||||
|
||||
* **Settings.** `automaticUpdate`, `support/softwareUpdateEnable`,
|
||||
`hotelMode/swUpdateEnable` and the `.UpdateIsInprogress` flag were each tested
|
||||
by restarting the daemon and counting the requests it made. It checks anyway.
|
||||
* **Masking the systemd unit.** `update.service` does ping the updater at boot,
|
||||
but every directory systemd reads units from is a read-only overlay except
|
||||
`/run/systemd/system`, which is tmpfs and created too late.
|
||||
* **Breaking that ping.** `update.service` reads
|
||||
`EnvironmentFile=-/var/systemd/system/env/update.env`, which *is* writable, and
|
||||
pointing `LS_HUB_LOCAL_SOCKET_DIRECTORY` at a bogus path does make the ping
|
||||
fail. The updater still gets activated — something else on the bus asks for it.
|
||||
* **Stopping a service.** `/etc/init` is dead upstart leftovers on a systemd TV;
|
||||
`initctl stop update` silently "succeeds" while doing nothing.
|
||||
* **Deleting a staged image.** The popup is not caused by one — the staging
|
||||
directory is empty at boot, and the size the daemon reports is in-memory only.
|
||||
|
||||
## Protection layers
|
||||
|
||||
| Layer | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| **Stop the updater running** | Overrides LG's ls-hubd manifest with one that has no `serviceFiles`, so `com.webos.service.update` can never be launched. This is the layer that actually stops the popup | on |
|
||||
| **Block LG update servers** | Points `snu`/`su`/`nsu.lge.com`, their TLS variants, the `-dev` fallbacks and the CDN aliases at `127.0.0.1` in `/etc/hosts` | on |
|
||||
| **Close the update popup** | Closes the alert the updater raised during boot, then stays subscribed to `com.webos.notification` and closes any later one as it opens | on |
|
||||
| **Close the update popup** | Fallback for a popup that appears anyway: closes the alert the updater raised during boot, then stays subscribed to `com.webos.notification` and closes any later one as it opens | on |
|
||||
| **Delete staged firmware** | Wipes the update image staged in `/mnt/lg/cmn_data/swupdate` if the TV ever does download one | on |
|
||||
| **Lock the staging folder** | Bind-mounts an empty read-only directory over it, so nothing can be staged again | off |
|
||||
| **Turn off auto-update settings** | Reads every `com.webos.settingsservice` category, switches off each update-related key it finds, and remembers the original values. On a CX this includes `hotelMode/swUpdateEnable`, which also hides the *Software Update* menu until you revert | on |
|
||||
@@ -62,13 +87,24 @@ dead upstart leftovers on a systemd TV, and `initctl stop update` silently
|
||||
Every layer is undone by **Remove protection**, which also restores the
|
||||
settings from the backup it made.
|
||||
|
||||
Be honest about what this buys you: the popup is closed within a fraction of a
|
||||
second of the Homebrew Channel starting its hooks, but the updater raised it
|
||||
15–20 seconds earlier, so on a cold boot **you will see it appear and then
|
||||
vanish by itself**. It is gone before you can act on it, and you never have to
|
||||
dismiss it. The hook is installed as `00-lgupdateblocker` so `run-parts` runs
|
||||
it before the other homebrew hooks, which is as early as anything unprivileged
|
||||
can go. Measured on a CX: alert raised at `01:26:22`, closed at `01:26:37`.
|
||||
With *Stop the updater running* on, the popup does not appear at all — there is
|
||||
nothing to close, because the version check never happens. That layer lives on
|
||||
a persistent partition, so it also survives a reboot without help; the boot hook
|
||||
only puts it back if something (a firmware update, an app install that rewrote
|
||||
the volatile manifest directory) removed it.
|
||||
|
||||
If you turn that layer off and rely on *Close the update popup* alone, the
|
||||
popup is closed within a fraction of a second of the Homebrew Channel starting
|
||||
its hooks — but the updater raised it 15–20 seconds earlier, so on a cold boot
|
||||
**you will see it appear and then vanish by itself**. The hook is installed as
|
||||
`00-lgupdateblocker` so `run-parts` runs it before the other homebrew hooks,
|
||||
which is as early as anything unprivileged can go.
|
||||
|
||||
Expect the *Software Update* screen in Settings to stop working while this is
|
||||
on — it queries the service that is now off the bus, and will get an error
|
||||
instead of an answer. On a CX the *Turn off auto-update settings* layer already
|
||||
hides that menu anyway (`hotelMode/swUpdateEnable`). *Remove protection* brings
|
||||
both back.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -119,6 +155,7 @@ Other buttons: **Purge staged update** (wipe a downloaded image right now),
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `/var/luna-service2/manifests.d/update.json` | The manifest override that takes the updater off the bus. Same id and role files as LG's, higher version, empty `serviceFiles`. Written only if nothing else is there, and only removed again if it is still ours |
|
||||
| `/etc/hosts` | Blocked hostnames, between `# >>> lg-update-blocker >>>` markers. Bind-mounted from `/tmp/lgupdateblocker-hosts` because `/` is read-only |
|
||||
| `/var/lib/webosbrew/init.d/00-lgupdateblocker` | Boot hook, run by the Homebrew Channel startup script; re-applies the enabled layers. The `00-` prefix makes `run-parts` run it first |
|
||||
| `/var/lib/webosbrew/lgupdateblocker/` | `config.json`, `hosts.txt`, `settings-backup.json`, `boot.log`, `alert-watch.sh` |
|
||||
@@ -133,15 +170,18 @@ replaced.
|
||||
* **Badge says "no root"** — open the Homebrew Channel, verify *Root status* is
|
||||
`ok`, then relaunch this app. If the Homebrew Channel itself is unelevated,
|
||||
fix that first (its Settings screen has a button for it).
|
||||
* **Popup still appears after a reboot** — run *Diagnostics* and look at the
|
||||
boot-hook log at the bottom. It records, per boot, whether the hosts entries
|
||||
were added, what was purged, and the id of the popup it closed. If there is
|
||||
no `dismissed update popup …` line, check `updateLog` in the same dump for
|
||||
`_gAlertWindowId` — if that is missing too, your TV raises the alert by some
|
||||
other route and the diagnostics dump is what to open an issue with.
|
||||
* **The popup appears for ~15 seconds, then disappears on its own** — expected, see
|
||||
[Why blocking the update servers is not enough](#why-blocking-the-update-servers-is-not-enough).
|
||||
Nothing running after the Homebrew Channel can beat the updater to it.
|
||||
* **Popup still appears after a reboot** — run *Diagnostics* and check
|
||||
`updater` in the status block. `blocked` should be `true` and `running`
|
||||
`false`; if `found` is `false`, ls-hubd on your firmware locates the updater
|
||||
some other way and the diagnostics dump is what to open an issue with. If it
|
||||
says `blocked: true` and the popup still shows up, `updateLog` in the same
|
||||
dump tells you whether the updater ran at all — on a working setup
|
||||
`/tmp/var/log/update.log` does not exist.
|
||||
* **The popup appears for ~15 seconds, then disappears on its own** — that is
|
||||
the *Close the update popup* fallback doing its job because the updater is
|
||||
still running. Make sure *Stop the updater running* is ticked and applied.
|
||||
* **Software Update in Settings shows an error** — expected while the updater
|
||||
layer is on; see [Protection layers](#protection-layers).
|
||||
* **Belt and braces** — a hosts file only helps if the TV uses DNS. Blocking
|
||||
`snu.lge.com`, `su.lge.com` and `nsu.lge.com` on your router or Pi-hole as
|
||||
well is the one measure that also survives a factory reset.
|
||||
|
||||
Reference in New Issue
Block a user