feat: stop the updater being launched at all
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
6fa8ad62f6
@@ -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.
|
||||
|
||||
+14
-1
@@ -13,6 +13,11 @@
|
||||
'/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service';
|
||||
|
||||
var TOGGLES = [
|
||||
{
|
||||
key: 'blockUpdater',
|
||||
title: 'Stop the updater running',
|
||||
desc: 'Takes /usr/sbin/update off the bus, so no version check happens and no popup appears'
|
||||
},
|
||||
{
|
||||
key: 'blockHosts',
|
||||
title: 'Block LG update servers',
|
||||
@@ -21,7 +26,7 @@
|
||||
{
|
||||
key: 'dismissPopup',
|
||||
title: 'Close the update popup',
|
||||
desc: 'The TV asks LG about updates ~20s before homebrew starts, so the popup is closed instead'
|
||||
desc: 'Fallback: closes a popup that appears anyway, e.g. with the updater left running'
|
||||
},
|
||||
{
|
||||
key: 'purgeCache',
|
||||
@@ -49,6 +54,7 @@
|
||||
];
|
||||
|
||||
var config = {
|
||||
blockUpdater: true,
|
||||
blockHosts: true,
|
||||
dismissPopup: true,
|
||||
purgeCache: true,
|
||||
@@ -224,6 +230,13 @@
|
||||
});
|
||||
|
||||
row('Service user', status.root ? 'root' : 'uid ' + status.uid + ' (not elevated)');
|
||||
var updater = status.updater || {};
|
||||
if (!updater.found) {
|
||||
row('Update service', 'not present on this TV');
|
||||
} else {
|
||||
row('Update service', (updater.blocked ? 'disabled' : 'launchable') +
|
||||
(updater.running ? ', running now' : ', not running'));
|
||||
}
|
||||
row('Hosts entries', status.hosts.blockedDomains + ' / ' + status.hosts.totalDomains +
|
||||
(status.hosts.bindMounted ? ' (bind-mounted)' : ''));
|
||||
row('Staging folders', stagedDirs ? stagedDirs + ' found' : 'none on this TV');
|
||||
|
||||
+16
-7
@@ -4,19 +4,24 @@ that webOS shows on every boot, without touching the firmware itself.</p>
|
||||
<p>Blocking the update servers alone does not do it: the updater asks LG about
|
||||
new firmware around 20 seconds before the Homebrew Channel gets to run its boot
|
||||
hooks, so on a CX the check succeeds every time and the popup is already up.
|
||||
Nothing running that late can win the race, so this app closes the popup as
|
||||
well as blocking the servers.</p>
|
||||
Nothing running that late can win that race, so this app removes it instead -
|
||||
it takes the updater off the bus entirely, and the check never happens.</p>
|
||||
|
||||
<p>It applies up to five reversible layers and re-applies them on every boot
|
||||
<p>It applies up to six reversible layers and re-applies them on every boot
|
||||
through a Homebrew Channel startup hook:</p>
|
||||
|
||||
<ul>
|
||||
<li><b>Stop the updater running</b> - webOS launches /usr/sbin/update on
|
||||
demand from an ls-hubd manifest. This drops a higher-version manifest into
|
||||
the writable directory ls-hubd already scans, identical to LG's except
|
||||
that it lists no service files, so the updater can no longer be launched:
|
||||
no version check, no popup, nothing to dismiss.</li>
|
||||
<li><b>Block LG update servers</b> - points snu/su/nsu.lge.com and their TLS
|
||||
and CDN aliases at 127.0.0.1 in /etc/hosts, which stops the download and
|
||||
every later check in the session.</li>
|
||||
<li><b>Close the update popup</b> - closes the alert raised during boot, then
|
||||
stays subscribed to the notification manager and closes any later one as
|
||||
it opens.</li>
|
||||
<li><b>Close the update popup</b> - fallback for a popup that appears anyway:
|
||||
closes the alert raised during boot, then stays subscribed to the
|
||||
notification manager and closes any later one as it opens.</li>
|
||||
<li><b>Delete staged firmware</b> - wipes /mnt/lg/cmn_data/swupdate should the
|
||||
TV ever manage to download an image.</li>
|
||||
<li><b>Lock the staging folder</b> - bind-mounts an empty read-only directory
|
||||
@@ -27,4 +32,8 @@ through a Homebrew Channel startup hook:</p>
|
||||
</ul>
|
||||
|
||||
<p>Requires root and the Homebrew Channel: the app asks the Homebrew Channel to
|
||||
elevate its own service on first launch. "Remove protection" undoes everything.</p>
|
||||
elevate its own service on first launch. "Remove protection" undoes everything,
|
||||
including putting the updater back on the bus.</p>
|
||||
|
||||
<p>While the updater layer is on, expect the Software Update screen in Settings
|
||||
to show an error - it queries the service that is now gone.</p>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.rkievits.lgupdateblocker",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"title": "LG Update Blocker",
|
||||
"description": "Blocks the recurring LG webOS firmware update popup on rooted TVs",
|
||||
"private": true,
|
||||
|
||||
+269
-24
@@ -3,29 +3,44 @@
|
||||
*
|
||||
* Everything this service does is reversible and is applied in layers:
|
||||
*
|
||||
* 1. hosts - point LG's firmware update servers at 127.0.0.1
|
||||
* 2. dismiss - close the "software update available" alert at boot
|
||||
* 3. purge - delete the firmware image the TV already staged
|
||||
* 4. lock - bind-mount an empty read-only dir over the staging dir
|
||||
* 5. settings - switch off the update-related com.webos.settingsservice keys
|
||||
* 1. updater - stop /usr/sbin/update from ever being launched
|
||||
* 2. hosts - point LG's firmware update servers at 127.0.0.1
|
||||
* 3. dismiss - close the "software update available" alert at boot
|
||||
* 4. purge - delete the firmware image the TV already staged
|
||||
* 5. lock - bind-mount an empty read-only dir over the staging dir
|
||||
* 6. settings - switch off the update-related com.webos.settingsservice keys
|
||||
*
|
||||
* / is read-only on webOS, and /tmp, /etc and the mount namespace are reset on
|
||||
* every boot, so each layer is re-applied by a boot hook script dropped into
|
||||
* /var/lib/webosbrew/init.d (run by the Homebrew Channel startup script).
|
||||
* every boot, so the layers that live there are re-applied by a boot hook
|
||||
* script dropped into /var/lib/webosbrew/init.d (run by the Homebrew Channel
|
||||
* startup script).
|
||||
*
|
||||
* Why the hosts block alone is not enough: /usr/sbin/update is started by
|
||||
* systemd (webos-mbd.target) and runs its version check against snu.lge.com
|
||||
* roughly 20 seconds before the Homebrew Channel gets as far as running the
|
||||
* hooks in init.d. On a CX that check therefore *succeeds* on every boot and
|
||||
* the popup is already on screen by the time anything of ours runs - which is
|
||||
* also why the Homebrew Channel's own "block system updates" toggle does not
|
||||
* stop it. Nothing persistent gates that check (automaticUpdate,
|
||||
* support/softwareUpdateEnable, hotelMode/swUpdateEnable and the
|
||||
* .UpdateIsInprogress flag were all measured to make no difference), and there
|
||||
* is no writable directory early enough in the boot to win the race: every
|
||||
* systemd unit path except /run is a read-only overlay. So the popup is closed
|
||||
* instead of prevented, and the hosts block stops the download and every
|
||||
* later check.
|
||||
* Why the hosts block alone is not enough: /usr/sbin/update runs its version
|
||||
* check against snu.lge.com roughly 20 seconds before the Homebrew Channel
|
||||
* gets as far as running the hooks in init.d. On a CX that check therefore
|
||||
* *succeeds* on every boot and the popup is already on screen by the time
|
||||
* anything of ours runs - which is also why the Homebrew Channel's own "block
|
||||
* system updates" toggle does not stop it, and why closing the popup after the
|
||||
* fact leaves it visible for those 20 seconds.
|
||||
*
|
||||
* The updater layer removes the race instead of trying to win it. The updater
|
||||
* is not a normal daemon: it is launched on demand by ls-hubd from a D-Bus
|
||||
* service file, which ls-hubd finds through a manifest. ls-hubd.conf lists
|
||||
* ManifestsVolatileDirectories under /var - writable, persistent, and how
|
||||
* webOS itself ships manifest updates - and a manifest there with the same id
|
||||
* and a higher version replaces the read-only one. Dropping in a copy whose
|
||||
* serviceFiles list is empty leaves every role and permission file in place
|
||||
* but removes the launch entry, so nothing on the bus can start the updater:
|
||||
* no version check, no alert, nothing to dismiss. Callers get an immediate
|
||||
* "Service does not exist" instead of hanging, and deleting the one file puts
|
||||
* it all back.
|
||||
*
|
||||
* Things that do *not* work, all measured on a CX before settling on the
|
||||
* above: automaticUpdate, support/softwareUpdateEnable, hotelMode/swUpdateEnable
|
||||
* and the .UpdateIsInprogress flag change nothing; no systemd unit directory is
|
||||
* writable, so update.service cannot be masked; breaking update.service's ping
|
||||
* via its /var/systemd/system/env override does make that ping fail, but
|
||||
* something else on the bus activates the updater anyway.
|
||||
*
|
||||
* Written in ES5 without dependencies - it runs on the TV's own node with the
|
||||
* platform-provided webos-service module and nothing else.
|
||||
@@ -84,6 +99,15 @@ var CACHE_GUARD = '/mnt/lg/';
|
||||
var UPDATE_DAEMON_LOG = '/tmp/var/log/update.log';
|
||||
var ALERT_ID_PATTERN = 'com\\.webos\\.service\\.update-[0-9]*';
|
||||
|
||||
/* ls-hubd launches the updater on demand; its config says which manifest
|
||||
* directories are read-only and which are the writable ones we may override
|
||||
* from. Everything about the updater - its manifest, its D-Bus service file,
|
||||
* even the path of its binary - is discovered from there rather than assumed,
|
||||
* so this works the same on a firmware that moves them. */
|
||||
var LS_HUBD_CONF = '/etc/luna-service2/ls-hubd.conf';
|
||||
var UPDATER_SERVICE = 'com.webos.service.update';
|
||||
var UPDATER_BINARY_FALLBACK = '/usr/sbin/update';
|
||||
|
||||
var SETTINGS_CATEGORIES = ['option', 'general', 'network', 'commercial', 'support', 'hotelMode'];
|
||||
var SETTINGS_KEY_PATTERN = /(update|upgrade|firmware)/i;
|
||||
/* "ota" and "nsu" are substrings of perfectly innocent keys - screenRotation,
|
||||
@@ -96,6 +120,7 @@ function isUpdateKey(key) {
|
||||
}
|
||||
|
||||
var DEFAULT_CONFIG = {
|
||||
blockUpdater: true,
|
||||
blockHosts: true,
|
||||
dismissPopup: true,
|
||||
purgeCache: true,
|
||||
@@ -202,6 +227,185 @@ function anyEnabled(config) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- updater */
|
||||
|
||||
/** Semicolon-separated directory list out of ls-hubd.conf. */
|
||||
function lsHubdDirs(key) {
|
||||
var match = readFile(LS_HUBD_CONF, '').match(new RegExp('^[ \\t]*' + key + '[ \\t]*=(.*)$', 'm'));
|
||||
if (!match) return [];
|
||||
return match[1].split(';').map(function (dir) {
|
||||
return dir.trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
/** The read-only manifest whose serviceFiles make the updater launchable. */
|
||||
function findUpdaterManifest() {
|
||||
var found = null;
|
||||
lsHubdDirs('ManifestsDirectories').forEach(function (dir) {
|
||||
if (found || !isDirectory(dir)) return;
|
||||
var entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dir);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
entries.forEach(function (entry) {
|
||||
if (found || !/\.json$/.test(entry)) return;
|
||||
var file = path.join(dir, entry);
|
||||
var manifest = readJson(file, null);
|
||||
if (!manifest || !manifest.id || !Array.isArray(manifest.serviceFiles)) return;
|
||||
var launchesUpdater = manifest.serviceFiles.some(function (svc) {
|
||||
return path.basename(svc) === UPDATER_SERVICE + '.service';
|
||||
});
|
||||
if (launchesUpdater) found = { file: file, manifest: manifest };
|
||||
});
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Where our replacement goes - the first writable manifest dir ls-hubd scans. */
|
||||
function overridePath(base) {
|
||||
var dirs = lsHubdDirs('ManifestsVolatileDirectories');
|
||||
var dir = dirs.filter(isDirectory)[0] || dirs[0];
|
||||
return dir ? path.join(dir, base.manifest.id + '.json') : null;
|
||||
}
|
||||
|
||||
/** A version that outranks the stock manifest, so ls-hubd prefers ours. */
|
||||
function outrankVersion(version) {
|
||||
var major = parseInt(String(version || '').split('.')[0], 10);
|
||||
return Math.max((major >= 0 ? major : 0) + 1, 99) + '.0.0';
|
||||
}
|
||||
|
||||
/* Same id, higher version, same roles and permissions - only the serviceFiles
|
||||
* list is emptied, which is what takes away ls-hubd's ability to launch it. */
|
||||
function buildOverride(base) {
|
||||
var override = { id: base.manifest.id, version: outrankVersion(base.manifest.version) };
|
||||
['roleFiles', 'roleFilesPub', 'roleFilesPrv', 'apiPermissionFiles', 'clientPermissionFiles']
|
||||
.forEach(function (key) {
|
||||
if (base.manifest[key]) override[key] = base.manifest[key];
|
||||
});
|
||||
override.serviceFiles = [];
|
||||
return override;
|
||||
}
|
||||
|
||||
/* An empty serviceFiles list is not something a real manifest ever has, so it
|
||||
* doubles as the signature that a file at that path was written by us. */
|
||||
function isOverrideOurs(target, base) {
|
||||
var manifest = readJson(target, null);
|
||||
return !!manifest && manifest.id === base.manifest.id &&
|
||||
Array.isArray(manifest.serviceFiles) && manifest.serviceFiles.length === 0;
|
||||
}
|
||||
|
||||
/** Exec= out of the updater's D-Bus service file. */
|
||||
function updaterBinary(base) {
|
||||
var files = base.manifest.serviceFiles || [];
|
||||
for (var i = 0; i < files.length; i += 1) {
|
||||
var match = readFile(files[i], '').match(/^[ \t]*Exec[ \t]*=[ \t]*(\S+)/m);
|
||||
if (match) return match[1];
|
||||
}
|
||||
return UPDATER_BINARY_FALLBACK;
|
||||
}
|
||||
|
||||
function updaterPids(binary) {
|
||||
var pids = [];
|
||||
try {
|
||||
fs.readdirSync('/proc').forEach(function (entry) {
|
||||
if (!/^[0-9]+$/.test(entry)) return;
|
||||
/* cmdline is NUL-separated; argv[0] is what ls-hubd launched */
|
||||
if (readFile('/proc/' + entry + '/cmdline', '').split('\0')[0] === binary) {
|
||||
pids.push(parseInt(entry, 10));
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
/* /proc unreadable - nothing we can do */
|
||||
}
|
||||
return pids;
|
||||
}
|
||||
|
||||
function stopUpdater(binary) {
|
||||
var stopped = 0;
|
||||
updaterPids(binary).forEach(function (pid) {
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
stopped += 1;
|
||||
} catch (err) {
|
||||
/* already gone */
|
||||
}
|
||||
});
|
||||
return stopped;
|
||||
}
|
||||
|
||||
function blockUpdater(log) {
|
||||
var base = findUpdaterManifest();
|
||||
if (!base) {
|
||||
log.add('! no ' + UPDATER_SERVICE + ' manifest on this TV - leaving the updater alone');
|
||||
return false;
|
||||
}
|
||||
var target = overridePath(base);
|
||||
if (!target) {
|
||||
log.add('! ls-hubd has no writable manifest directory on this TV');
|
||||
return false;
|
||||
}
|
||||
if (exists(target) && !isOverrideOurs(target, base)) {
|
||||
log.add('! ' + target + ' exists and is not ours - leaving it alone');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirp(path.dirname(target));
|
||||
writeJson(target, buildOverride(base));
|
||||
} catch (err) {
|
||||
log.add('! could not write ' + target + ': ' + err.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
var res = sh('ls-control scan-services');
|
||||
if (!res.ok) log.add('! ls-control scan-services failed: ' + res.output);
|
||||
log.add('updater disabled - ' + UPDATER_SERVICE + ' can no longer be launched');
|
||||
|
||||
var binary = updaterBinary(base);
|
||||
var stopped = stopUpdater(binary);
|
||||
if (stopped) log.add('stopped ' + stopped + ' running ' + binary + ' process(es)');
|
||||
return true;
|
||||
}
|
||||
|
||||
function unblockUpdater(log) {
|
||||
var base = findUpdaterManifest();
|
||||
var target = base && overridePath(base);
|
||||
if (!target || !exists(target)) {
|
||||
log.add('updater was not disabled');
|
||||
return;
|
||||
}
|
||||
if (!isOverrideOurs(target, base)) {
|
||||
log.add('! ' + target + ' is not ours - leaving it alone');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(target);
|
||||
} catch (err) {
|
||||
log.add('! could not remove ' + target + ': ' + err.message);
|
||||
return;
|
||||
}
|
||||
sh('ls-control scan-services');
|
||||
log.add('updater re-enabled - removed ' + target);
|
||||
}
|
||||
|
||||
function updaterStatus() {
|
||||
var base = findUpdaterManifest();
|
||||
if (!base) return { found: false, blocked: false, running: false };
|
||||
var target = overridePath(base);
|
||||
var binary = updaterBinary(base);
|
||||
return {
|
||||
found: true,
|
||||
service: UPDATER_SERVICE,
|
||||
binary: binary,
|
||||
manifest: base.file,
|
||||
overridePath: target,
|
||||
blocked: !!(target && exists(target) && isOverrideOurs(target, base)),
|
||||
running: updaterPids(binary).length > 0
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ hosts */
|
||||
|
||||
function readDomains() {
|
||||
@@ -692,6 +896,37 @@ function restoreSettings(log, callback) {
|
||||
|
||||
/* -------------------------------------------------------------- boot hook */
|
||||
|
||||
/* The updater override lives on a persistent partition, so unlike the other
|
||||
* layers it normally survives a reboot on its own. The hook only puts it back
|
||||
* if something removed it - a firmware update, or an app install that rewrote
|
||||
* the volatile manifest directory. */
|
||||
function updaterHookLines(config) {
|
||||
var base = config.blockUpdater ? findUpdaterManifest() : null;
|
||||
var target = base && overridePath(base);
|
||||
if (!target) return [];
|
||||
return [
|
||||
'UPDATER_MANIFEST=' + target,
|
||||
'UPDATER_BIN=' + updaterBinary(base),
|
||||
'',
|
||||
'if [ ! -f "$UPDATER_MANIFEST" ]; then',
|
||||
' mkdir -p "$(dirname "$UPDATER_MANIFEST")"',
|
||||
' cat > "$UPDATER_MANIFEST" <<\'LGUB_MANIFEST\'',
|
||||
JSON.stringify(buildOverride(base), null, 2),
|
||||
'LGUB_MANIFEST',
|
||||
' ls-control scan-services >/dev/null 2>&1',
|
||||
' echo "restored updater override $UPDATER_MANIFEST"',
|
||||
'fi',
|
||||
'# it cannot be launched any more, but kill one that slipped through',
|
||||
'for proc in /proc/[0-9]*; do',
|
||||
' [ -r "$proc/cmdline" ] || continue',
|
||||
' case "$(tr \'\\0\' \' \' < "$proc/cmdline")" in',
|
||||
' "$UPDATER_BIN "*) kill "${proc#/proc/}" 2>/dev/null && echo "stopped $UPDATER_BIN" ;;',
|
||||
' esac',
|
||||
'done',
|
||||
''
|
||||
];
|
||||
}
|
||||
|
||||
function bootHookScript(config, domains) {
|
||||
return [
|
||||
'#!/bin/sh',
|
||||
@@ -712,9 +947,10 @@ function bootHookScript(config, domains) {
|
||||
'if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 65536 ]; then rm -f "$LOG"; fi',
|
||||
'exec >>"$LOG" 2>&1',
|
||||
'echo "--- $(date) LG Update Blocker ---"',
|
||||
'',
|
||||
'# First, because by now the popup has been on screen for ~20 seconds: the',
|
||||
'# updater runs its version check from webos-mbd.target, long before the',
|
||||
''
|
||||
].concat(updaterHookLines(config)).concat([
|
||||
'# Fallback for when the updater layer is off: by now the popup has been on',
|
||||
'# screen for ~20 seconds, because the version check runs long before the',
|
||||
'# Homebrew Channel gets round to running these hooks.',
|
||||
'if [ "$DISMISS_POPUP" = 1 ]; then',
|
||||
' id=$(sed -n \'s/.*_gAlertWindowId : \\(' + ALERT_ID_PATTERN + '\\).*/\\1/p\' \\',
|
||||
@@ -787,7 +1023,7 @@ function bootHookScript(config, domains) {
|
||||
'',
|
||||
'echo "done"',
|
||||
''
|
||||
].join('\n');
|
||||
]).join('\n');
|
||||
}
|
||||
|
||||
/* Long-lived companion to the boot hook: subscribes to the notification
|
||||
@@ -888,6 +1124,7 @@ function baseStatus() {
|
||||
uid: typeof process.getuid === 'function' ? process.getuid() : -1,
|
||||
root: isRoot(),
|
||||
config: readConfig(),
|
||||
updater: updaterStatus(),
|
||||
hosts: {
|
||||
path: HOSTS_PATH,
|
||||
writable: hostsWritable(),
|
||||
@@ -933,6 +1170,13 @@ service.register('apply', function (message) {
|
||||
mkdirp(STATE_DIR);
|
||||
writeJson(CONFIG_PATH, config);
|
||||
|
||||
/* first: with the updater gone there is no version check to lose a race to */
|
||||
if (config.blockUpdater) {
|
||||
blockUpdater(log);
|
||||
} else {
|
||||
unblockUpdater(log);
|
||||
}
|
||||
|
||||
if (config.blockHosts) {
|
||||
applyHosts(log);
|
||||
} else {
|
||||
@@ -992,6 +1236,7 @@ service.register('revert', function (message) {
|
||||
});
|
||||
|
||||
try {
|
||||
unblockUpdater(log);
|
||||
removeHosts(log);
|
||||
unlockCache(log);
|
||||
if (stopWatcher()) log.add('alert watcher stopped');
|
||||
|
||||
Reference in New Issue
Block a user