Files
lg_update_blocker/README.md
T
Rene KievitsandClaude Opus 5 bf2daac146
Build / build (push) Successful in 48s
ci: publish releases to Gitea instead of GitHub
The workflow was written for github.com: actions/upload-artifact failed on
the Gitea runner, softprops/action-gh-release only talks to the GitHub API,
and gen-manifest.js hardcoded github.com URLs.

- drop the artifact upload (release assets are the deliverable)
- upload release assets with tools/release-gitea.js, a dependency-free
  Gitea API client; re-running a tag replaces the attachments
- gen-manifest.js takes --server/PUBLIC_SERVER_URL and knows that Gitea
  spells the latest-asset URL /releases/download/latest/<file>
- PUBLIC_SERVER_URL is set explicitly because the runner only sees the
  instance's internal LAN address

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:41:03 +02:00

157 lines
6.7 KiB
Markdown

<p align="center">
<img src="assets/icon400.png" width="120" alt="LG Update Blocker">
</p>
# LG Update Blocker
A webOS homebrew app that gets rid of the *"a new software version is
available"* popup LG shows on every boot — without touching the firmware
itself, and fully reversible.
Built for a rooted **LG CX (webOS 5)**, but nothing in it is model specific: it
only ever probes for things and acts on what it actually finds on your TV.
![screenshot](docs/screenshot.png)
## Why the popup keeps coming back
The Homebrew Channel's own *Block system updates* toggle only writes four
hostnames into `/etc/hosts` at boot. That is not enough in practice:
* it misses several update hostnames (`nsu`, `*-ssl`, the CDN aliases),
* it does nothing about a firmware image the TV **already downloaded** — a
staged update keeps prompting even when the update servers are unreachable,
* it leaves the auto-update settings and the update services alone.
This app covers all of that, and re-applies everything on each boot (webOS
resets `/`, `/tmp` and the mount namespace on every start).
## Protection layers
| Layer | What it does | Default |
| --- | --- | --- |
| **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 |
| **Delete staged firmware** | Wipes the update image staged in `/mnt/lg/cmn_data/swupdate` | 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 |
| **Stop update services at boot** | Stops the update-related upstart jobs discovered in `/etc/init` (advanced) | off |
Every layer is undone by **Remove protection**, which also restores the
settings from the backup it made.
## Requirements
* A rooted webOS TV with the [Homebrew Channel](https://github.com/webosbrew/webos-homebrew-channel)
installed and its root status showing **ok**.
* The app's own service needs root. On first launch it asks the Homebrew
Channel to elevate it (`elevateService`, falling back to running
`elevate-service` through `exec`), then restarts itself. The header badge
turns green when it worked.
## Install
### Through the Homebrew Channel (repository)
Add the repository once — *Homebrew Channel → Settings → Add repository*:
```text
https://git.crylia.de/Crylia/lg_update_blocker/releases/download/latest/apps.json
```
The app then shows up in the Homebrew Channel list, and every new release that
is tagged is picked up automatically — Gitea resolves `.../download/latest/...`
to the newest release.
### Manually
Download the `.ipk` from the release and:
```sh
ares-install com.rkievits.lgupdateblocker_1.0.0_all.ipk
ares-launch com.rkievits.lgupdateblocker
```
## Using it
Navigate with ▲▼, select with OK, leave with BACK.
1. Check the layers you want (the defaults are a good start).
2. **Apply protection** — the log pane shows exactly what was changed.
3. Reboot and confirm the popup is gone.
Other buttons: **Purge staged update** (wipe a downloaded image right now),
**Remove protection** (undo everything), **Refresh status**, and
**Diagnostics** (dumps OS info, mounts, discovered settings, the tail of
`/etc/hosts` and the boot-hook log — handy for a bug report).
## What it touches
| Path | Purpose |
| --- | --- |
| `/etc/hosts` | Blocked hostnames, between `# >>> lg-update-blocker >>>` markers. Bind-mounted from `/tmp/lgupdateblocker-hosts` because `/` is read-only |
| `/var/lib/webosbrew/init.d/lgupdateblocker` | Boot hook, run by the Homebrew Channel startup script; re-applies the enabled layers |
| `/var/lib/webosbrew/lgupdateblocker/` | `config.json`, `hosts.txt`, `settings-backup.json`, `boot.log` |
| `/mnt/lg/cmn_data/swupdate` | Staged firmware, emptied (and optionally locked) |
Nothing is written to system partitions, and no LG binaries are patched or
replaced.
## Troubleshooting
* **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 which jobs were stopped.
* **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.
* **Forced *app* updates** (the *"You must update this app to launch it"*
dialog) are a different mechanism — that is what
[LG App Update Blocker](https://github.com/dr0dr1dr2dr3/lgappupdateblocker)
is for. This app deliberately leaves the `lgtvsdp.com` hosts alone, since
blocking those also breaks the store and several other services.
## Building
No bundler, no runtime dependencies — `tools/build.js` copies `app/` and
`service/` into `build/` and substitutes the app id and version.
```sh
npm install # only the ares CLI
npm run dist # build + package + manifest, output in dist/
npm run deploy # ares-install the resulting ipk
```
## Releasing
[.github/workflows/release.yml](.github/workflows/release.yml) builds on every
push (Gitea Actions reads `.github/workflows/` too). Tag a commit `v<version>`
(matching `version` in `package.json`) and the workflow additionally publishes a
release containing:
* `com.rkievits.lgupdateblocker_<version>_all.ipk`
* `com.rkievits.lgupdateblocker.manifest.json` — Homebrew Channel manifest,
with the absolute ipk URL, its sha256 and size
* `apps.json` — the one-package repository index used by the repository URL above
* `description.html` — the long description shown in the Homebrew Channel
Uploading is done by [tools/release-gitea.js](tools/release-gitea.js) against
the Gitea API (`softprops/action-gh-release` only speaks to github.com). It is
idempotent: re-running a tag reuses the release and replaces the attachments.
The URLs inside the manifest come from `PUBLIC_SERVER_URL` in the workflow, not
from `GITHUB_SERVER_URL` — the runner is handed the instance's internal LAN
address, which the TV cannot reach. Change that one variable if the instance
moves. Generating for github.com instead works too: `--server` (or
`PUBLIC_SERVER_URL`) switches the "latest release asset" URL to GitHub's shape.
The app id lives in exactly one place: the `name` field of `package.json`.
Change it there and everything (appinfo, service name, Luna URIs, manifest)
follows.
## License
MIT