Compare commits
3
Commits
bf2daac146
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e2d7e5fa | ||
|
|
ef8b6b96c4 | ||
|
|
ae8229588e |
@@ -3,3 +3,4 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
*.ipk
|
*.ipk
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
tv-diag*.txt
|
||||||
|
|||||||
@@ -13,32 +13,99 @@ only ever probes for things and acts on what it actually finds on your TV.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Why the popup keeps coming back
|
## Why blocking the update servers is not enough
|
||||||
|
|
||||||
The Homebrew Channel's own *Block system updates* toggle only writes four
|
Blocking `snu.lge.com` in `/etc/hosts` is the obvious fix, and it is what the
|
||||||
hostnames into `/etc/hosts` at boot. That is not enough in practice:
|
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.
|
||||||
|
|
||||||
* it misses several update hostnames (`nsu`, `*-ssl`, the CDN aliases),
|
The updater runs its version check against
|
||||||
* it does nothing about a firmware image the TV **already downloaded** — a
|
`https://snu.lge.com/CheckSWAutoUpdate.laf` roughly **20 seconds before** the
|
||||||
staged update keeps prompting even when the update servers are unreachable,
|
Homebrew Channel gets as far as running its `init.d` hooks. So on every boot
|
||||||
* it leaves the auto-update settings and the update services alone.
|
the check reaches a real LG server, gets `RESULT_CD 900 / Success` back, and
|
||||||
|
the popup is already on screen by the time any hosts entry exists:
|
||||||
|
|
||||||
This app covers all of that, and re-applies everything on each boot (webOS
|
```text
|
||||||
resets `/`, `/tmp` and the mount namespace on every start).
|
00:20:52 [tNsuMainTask] pRequestUpdateUrl - https://snu.lge.com/CheckSWAutoUpdate.laf
|
||||||
|
00:20:52 [updateTimer] NSU_CreateUpdateAlert - "…Version: 04.64.00…"
|
||||||
|
00:21:11 lg-update-blocker: hosts entries added ← 19 seconds too late
|
||||||
|
```
|
||||||
|
|
||||||
|
That race cannot be won — nothing unprivileged runs earlier than those hooks —
|
||||||
|
so the app removes it instead.
|
||||||
|
|
||||||
|
`/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
|
## Protection layers
|
||||||
|
|
||||||
| Layer | What it does | Default |
|
| 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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
| **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 |
|
||||||
| **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
|
Every layer is undone by **Remove protection**, which also restores the
|
||||||
settings from the backup it made.
|
settings from the backup it made.
|
||||||
|
|
||||||
|
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
|
## Requirements
|
||||||
|
|
||||||
* A rooted webOS TV with the [Homebrew Channel](https://github.com/webosbrew/webos-homebrew-channel)
|
* A rooted webOS TV with the [Homebrew Channel](https://github.com/webosbrew/webos-homebrew-channel)
|
||||||
@@ -67,7 +134,7 @@ to the newest release.
|
|||||||
Download the `.ipk` from the release and:
|
Download the `.ipk` from the release and:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ares-install com.rkievits.lgupdateblocker_1.0.0_all.ipk
|
ares-install com.rkievits.lgupdateblocker_<version>_all.ipk
|
||||||
ares-launch com.rkievits.lgupdateblocker
|
ares-launch com.rkievits.lgupdateblocker
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -88,9 +155,11 @@ Other buttons: **Purge staged update** (wipe a downloaded image right now),
|
|||||||
|
|
||||||
| Path | Purpose |
|
| 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 |
|
| `/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/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` |
|
| `/var/lib/webosbrew/lgupdateblocker/` | `config.json`, `hosts.txt`, `settings-backup.json`, `boot.log`, `alert-watch.sh` |
|
||||||
|
| `/tmp/var/log/update.log` | Read only — the updater logs the id of the alert it raised, which is the only way to close a popup that opened before we were subscribed |
|
||||||
| `/mnt/lg/cmn_data/swupdate` | Staged firmware, emptied (and optionally locked) |
|
| `/mnt/lg/cmn_data/swupdate` | Staged firmware, emptied (and optionally locked) |
|
||||||
|
|
||||||
Nothing is written to system partitions, and no LG binaries are patched or
|
Nothing is written to system partitions, and no LG binaries are patched or
|
||||||
@@ -101,9 +170,18 @@ replaced.
|
|||||||
* **Badge says "no root"** — open the Homebrew Channel, verify *Root status* is
|
* **Badge says "no root"** — open the Homebrew Channel, verify *Root status* is
|
||||||
`ok`, then relaunch this app. If the Homebrew Channel itself is unelevated,
|
`ok`, then relaunch this app. If the Homebrew Channel itself is unelevated,
|
||||||
fix that first (its Settings screen has a button for it).
|
fix that first (its Settings screen has a button for it).
|
||||||
* **Popup still appears after a reboot** — run *Diagnostics* and look at the
|
* **Popup still appears after a reboot** — run *Diagnostics* and check
|
||||||
boot-hook log at the bottom. It records, per boot, whether the hosts entries
|
`updater` in the status block. `blocked` should be `true` and `running`
|
||||||
were added, what was purged and which jobs were stopped.
|
`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
|
* **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
|
`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.
|
well is the one measure that also survives a factory reset.
|
||||||
|
|||||||
+23
-8
@@ -13,11 +13,21 @@
|
|||||||
'/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service';
|
'/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service';
|
||||||
|
|
||||||
var TOGGLES = [
|
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',
|
key: 'blockHosts',
|
||||||
title: 'Block LG update servers',
|
title: 'Block LG update servers',
|
||||||
desc: 'Points snu/su/nsu.lge.com at 127.0.0.1 in /etc/hosts, re-applied on every boot'
|
desc: 'Points snu/su/nsu.lge.com at 127.0.0.1 in /etc/hosts, re-applied on every boot'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'dismissPopup',
|
||||||
|
title: 'Close the update popup',
|
||||||
|
desc: 'Fallback: closes a popup that appears anyway, e.g. with the updater left running'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'purgeCache',
|
key: 'purgeCache',
|
||||||
title: 'Delete staged firmware',
|
title: 'Delete staged firmware',
|
||||||
@@ -32,11 +42,6 @@
|
|||||||
key: 'disableSettings',
|
key: 'disableSettings',
|
||||||
title: 'Turn off auto-update settings',
|
title: 'Turn off auto-update settings',
|
||||||
desc: 'Switches off every update-related key com.webos.settingsservice exposes'
|
desc: 'Switches off every update-related key com.webos.settingsservice exposes'
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'stopServices',
|
|
||||||
title: 'Stop update services at boot',
|
|
||||||
desc: 'Advanced: stops the update upstart jobs found on this TV after every boot'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -49,11 +54,12 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
|
blockUpdater: true,
|
||||||
blockHosts: true,
|
blockHosts: true,
|
||||||
|
dismissPopup: true,
|
||||||
purgeCache: true,
|
purgeCache: true,
|
||||||
lockCache: false,
|
lockCache: false,
|
||||||
disableSettings: true,
|
disableSettings: true
|
||||||
stopServices: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var focusables = [];
|
var focusables = [];
|
||||||
@@ -224,12 +230,21 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
row('Service user', status.root ? 'root' : 'uid ' + status.uid + ' (not elevated)');
|
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 +
|
row('Hosts entries', status.hosts.blockedDomains + ' / ' + status.hosts.totalDomains +
|
||||||
(status.hosts.bindMounted ? ' (bind-mounted)' : ''));
|
(status.hosts.bindMounted ? ' (bind-mounted)' : ''));
|
||||||
row('Staging folders', stagedDirs ? stagedDirs + ' found' : 'none on this TV');
|
row('Staging folders', stagedDirs ? stagedDirs + ' found' : 'none on this TV');
|
||||||
row('Staged firmware', stagedDirs ? bytes(staged) : '-');
|
row('Staged firmware', stagedDirs ? bytes(staged) : '-');
|
||||||
row('Staging locked', stagedDirs ? locked + ' / ' + stagedDirs : '-');
|
row('Staging locked', stagedDirs ? locked + ' / ' + stagedDirs : '-');
|
||||||
row('Update jobs found', (status.jobs || []).length ? status.jobs.join(', ') : 'none');
|
var popup = status.popup || {};
|
||||||
|
row('Update popup this boot', popup.lastAlertId ? 'raised, then closed' : 'none raised');
|
||||||
|
row('Popup watcher', popup.watcherPid ? 'running (pid ' + popup.watcherPid + ')' : 'not running');
|
||||||
row('Boot hook', status.bootHook.installed ? 'installed' : 'not installed');
|
row('Boot hook', status.bootHook.installed ? 'installed' : 'not installed');
|
||||||
|
|
||||||
var settings = status.settings || [];
|
var settings = status.settings || [];
|
||||||
|
|||||||
+24
-8
@@ -1,23 +1,39 @@
|
|||||||
<p><b>LG Update Blocker</b> stops the "a new software version is available" popup
|
<p><b>LG Update Blocker</b> stops the "a new software version is available" popup
|
||||||
that webOS shows on every boot, without touching the firmware itself.</p>
|
that webOS shows on every boot, without touching the firmware itself.</p>
|
||||||
|
|
||||||
<p>It applies up to five reversible layers and re-applies them on every boot
|
<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 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 six reversible layers and re-applies them on every boot
|
||||||
through a Homebrew Channel startup hook:</p>
|
through a Homebrew Channel startup hook:</p>
|
||||||
|
|
||||||
<ul>
|
<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
|
<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.</li>
|
and CDN aliases at 127.0.0.1 in /etc/hosts, which stops the download and
|
||||||
<li><b>Delete staged firmware</b> - wipes /mnt/lg/cmn_data/swupdate. An update
|
every later check in the session.</li>
|
||||||
that was already downloaded keeps prompting even when the servers are
|
<li><b>Close the update popup</b> - fallback for a popup that appears anyway:
|
||||||
unreachable, which is why blocking alone often is not enough.</li>
|
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
|
<li><b>Lock the staging folder</b> - bind-mounts an empty read-only directory
|
||||||
over it so nothing can be staged again.</li>
|
over it so nothing can be staged again.</li>
|
||||||
<li><b>Turn off auto-update settings</b> - switches off every update related
|
<li><b>Turn off auto-update settings</b> - switches off every update related
|
||||||
key that com.webos.settingsservice exposes on your firmware, remembering
|
key that com.webos.settingsservice exposes on your firmware, remembering
|
||||||
the original values.</li>
|
the original values.</li>
|
||||||
<li><b>Stop update services at boot</b> - optional, stops the update upstart
|
|
||||||
jobs found on the TV.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Requires root and the Homebrew Channel: the app asks the Homebrew Channel to
|
<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>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "com.rkievits.lgupdateblocker",
|
"name": "com.rkievits.lgupdateblocker",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "com.rkievits.lgupdateblocker",
|
"name": "com.rkievits.lgupdateblocker",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@webos-tools/cli": "^3.2.1"
|
"@webos-tools/cli": "^3.2.1"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.rkievits.lgupdateblocker",
|
"name": "com.rkievits.lgupdateblocker",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"title": "LG Update Blocker",
|
"title": "LG Update Blocker",
|
||||||
"description": "Blocks the recurring LG webOS firmware update popup on rooted TVs",
|
"description": "Blocks the recurring LG webOS firmware update popup on rooted TVs",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
+487
-73
@@ -3,15 +3,44 @@
|
|||||||
*
|
*
|
||||||
* Everything this service does is reversible and is applied in layers:
|
* Everything this service does is reversible and is applied in layers:
|
||||||
*
|
*
|
||||||
* 1. hosts - point LG's firmware update servers at 127.0.0.1
|
* 1. updater - stop /usr/sbin/update from ever being launched
|
||||||
* 2. purge - delete the firmware image the TV already staged
|
* 2. hosts - point LG's firmware update servers at 127.0.0.1
|
||||||
* 3. lock - bind-mount an empty read-only dir over the staging dir
|
* 3. dismiss - close the "software update available" alert at boot
|
||||||
* 4. settings - switch off the update-related com.webos.settingsservice keys
|
* 4. purge - delete the firmware image the TV already staged
|
||||||
* 5. services - stop the update related upstart jobs (advanced, opt-in)
|
* 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
|
* / 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
|
* every boot, so the layers that live there are re-applied by a boot hook
|
||||||
* /var/lib/webosbrew/init.d (run by the Homebrew Channel startup script).
|
* 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 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
|
* Written in ES5 without dependencies - it runs on the TV's own node with the
|
||||||
* platform-provided webos-service module and nothing else.
|
* platform-provided webos-service module and nothing else.
|
||||||
@@ -37,7 +66,13 @@ var SETTINGS_BACKUP_PATH = STATE_DIR + '/settings-backup.json';
|
|||||||
var EMPTY_DIR = STATE_DIR + '/empty';
|
var EMPTY_DIR = STATE_DIR + '/empty';
|
||||||
var BOOT_LOG_PATH = STATE_DIR + '/boot.log';
|
var BOOT_LOG_PATH = STATE_DIR + '/boot.log';
|
||||||
var BOOT_HOOK_DIR = '/var/lib/webosbrew/init.d';
|
var BOOT_HOOK_DIR = '/var/lib/webosbrew/init.d';
|
||||||
var BOOT_HOOK_PATH = BOOT_HOOK_DIR + '/' + SLUG;
|
/* run-parts runs the hooks in lexical order and the popup is already on screen
|
||||||
|
* by then, so sort ahead of the other homebrew hooks. Older versions installed
|
||||||
|
* the hook under the bare slug; installBootHook removes that leftover. */
|
||||||
|
var BOOT_HOOK_PATH = BOOT_HOOK_DIR + '/00-' + SLUG;
|
||||||
|
var LEGACY_BOOT_HOOK_PATH = BOOT_HOOK_DIR + '/' + SLUG;
|
||||||
|
var WATCHER_PATH = STATE_DIR + '/alert-watch.sh';
|
||||||
|
var WATCHER_PID_PATH = '/tmp/' + SLUG + '-watch.pid';
|
||||||
|
|
||||||
var HOSTS_PATH = '/etc/hosts';
|
var HOSTS_PATH = '/etc/hosts';
|
||||||
var HOSTS_TMP = '/tmp/' + SLUG + '-hosts';
|
var HOSTS_TMP = '/tmp/' + SLUG + '-hosts';
|
||||||
@@ -56,18 +91,41 @@ var CACHE_DIRS = [
|
|||||||
];
|
];
|
||||||
var CACHE_GUARD = '/mnt/lg/';
|
var CACHE_GUARD = '/mnt/lg/';
|
||||||
|
|
||||||
var UPSTART_DIR = '/etc/init';
|
/* /usr/sbin/update logs its own alert id here as
|
||||||
var JOB_PATTERN = /(swupdate|softwareupdate|firmware|fota|update|upgrade|nsu)/i;
|
* _NSU_CreateAlertCallback - _gAlertWindowId : com.webos.service.update-<ms>
|
||||||
|
* which is the only way to learn the id of a popup that opened before we did:
|
||||||
|
* com.webos.notification only pushes alerts to clients already subscribed, and
|
||||||
|
* closeAllAlerts rejects every source id we can pass it. */
|
||||||
|
var UPDATE_DAEMON_LOG = '/tmp/var/log/update.log';
|
||||||
|
var ALERT_ID_PATTERN = 'com\\.webos\\.service\\.update-[0-9]*';
|
||||||
|
|
||||||
var SETTINGS_CATEGORIES = ['option', 'general', 'network', 'commercial'];
|
/* ls-hubd launches the updater on demand; its config says which manifest
|
||||||
var SETTINGS_KEY_PATTERN = /(update|upgrade|firmware|ota)/i;
|
* 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,
|
||||||
|
* consumerMode - and switching one of those off would be a nasty surprise, so
|
||||||
|
* only match them at a word or camelCase boundary. */
|
||||||
|
var SETTINGS_ABBREV_PATTERN = /(^|[^A-Za-z])(ota|nsu)|Ota|OTA|Nsu|NSU/;
|
||||||
|
|
||||||
|
function isUpdateKey(key) {
|
||||||
|
return SETTINGS_KEY_PATTERN.test(key) || SETTINGS_ABBREV_PATTERN.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
var DEFAULT_CONFIG = {
|
var DEFAULT_CONFIG = {
|
||||||
|
blockUpdater: true,
|
||||||
blockHosts: true,
|
blockHosts: true,
|
||||||
|
dismissPopup: true,
|
||||||
purgeCache: true,
|
purgeCache: true,
|
||||||
lockCache: false,
|
lockCache: false,
|
||||||
disableSettings: true,
|
disableSettings: true
|
||||||
stopServices: false
|
|
||||||
};
|
};
|
||||||
var CONFIG_KEYS = Object.keys(DEFAULT_CONFIG);
|
var CONFIG_KEYS = Object.keys(DEFAULT_CONFIG);
|
||||||
|
|
||||||
@@ -169,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 */
|
/* ------------------------------------------------------------------ hosts */
|
||||||
|
|
||||||
function readDomains() {
|
function readDomains() {
|
||||||
@@ -411,34 +648,83 @@ function unlockCache(log) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------- upstart jobs */
|
/* ------------------------------------------------------------ update popup */
|
||||||
|
|
||||||
function discoverJobs() {
|
/* Id of the last update alert the daemon raised this boot, recovered from its
|
||||||
var entries;
|
* own log. /tmp is wiped at boot, so at boot time this is the popup currently
|
||||||
try {
|
* on screen; later in a session it may already have been closed. Closing an
|
||||||
entries = fs.readdirSync(UPSTART_DIR);
|
* alert that is gone is a no-op, so it is safe either way. */
|
||||||
} catch (err) {
|
function lastAlertId() {
|
||||||
return [];
|
var log = readFile(UPDATE_DAEMON_LOG, '');
|
||||||
}
|
var matches = log.match(/_gAlertWindowId\s*:\s*(com\.webos\.service\.update-[0-9]+)/g);
|
||||||
return entries
|
if (!matches || !matches.length) return null;
|
||||||
.filter(function (entry) {
|
return matches[matches.length - 1].replace(/^.*:\s*/, '');
|
||||||
return /\.conf$/.test(entry) && JOB_PATTERN.test(entry);
|
|
||||||
})
|
|
||||||
.map(function (entry) {
|
|
||||||
return entry.replace(/\.conf$/, '');
|
|
||||||
})
|
|
||||||
.sort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopJobs(jobs, log) {
|
function closeAlert(alertId, callback) {
|
||||||
if (!jobs.length) {
|
callLuna('luna://com.webos.notification/closeAlert', { alertId: alertId }, callback);
|
||||||
log.add('no update-related upstart jobs found');
|
}
|
||||||
|
|
||||||
|
function watcherPid() {
|
||||||
|
var pid = parseInt(readFile(WATCHER_PID_PATH, ''), 10);
|
||||||
|
if (!pid || !exists('/proc/' + pid)) return 0;
|
||||||
|
/* the pid file survives a crash; make sure it is still our watcher */
|
||||||
|
var cmdline = readFile('/proc/' + pid + '/cmdline', '');
|
||||||
|
return cmdline.indexOf('alert-watch') >= 0 ? pid : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopWatcher() {
|
||||||
|
var pid = watcherPid();
|
||||||
|
if (!pid) return false;
|
||||||
|
/* the watcher is a pipeline in its own session (setsid / detached spawn), so
|
||||||
|
* kill the whole group - killing the script alone orphans its luna-send */
|
||||||
|
sh('kill -TERM -' + pid + ' 2>/dev/null || kill ' + pid);
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(WATCHER_PID_PATH);
|
||||||
|
} catch (err) {
|
||||||
|
/* already gone */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWatcher(log) {
|
||||||
|
if (watcherPid()) {
|
||||||
|
log.add('alert watcher already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
jobs.forEach(function (job) {
|
if (!exists(WATCHER_PATH)) {
|
||||||
var res = sh('initctl stop ' + job);
|
log.add('! alert watcher missing at ' + WATCHER_PATH);
|
||||||
if (!res.ok) res = sh('stop ' + job);
|
return;
|
||||||
log.add(res.ok ? 'stopped upstart job ' + job : '! could not stop ' + job + ': ' + res.output);
|
}
|
||||||
|
try {
|
||||||
|
var child = childProcess.spawn('/bin/sh', [WATCHER_PATH], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
log.add('alert watcher started');
|
||||||
|
} catch (err) {
|
||||||
|
log.add('! could not start alert watcher: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close a popup that is on screen right now, then keep watching for the next
|
||||||
|
* one. Used by apply(); at boot the hook does the same thing in shell. */
|
||||||
|
function dismissPopup(log, done) {
|
||||||
|
var alertId = lastAlertId();
|
||||||
|
if (!alertId) {
|
||||||
|
log.add('no update popup raised this boot');
|
||||||
|
startWatcher(log);
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
closeAlert(alertId, function (res) {
|
||||||
|
log.add(
|
||||||
|
res && res.returnValue
|
||||||
|
? 'closed update popup ' + alertId
|
||||||
|
: '! could not close ' + alertId + ': ' + ((res && res.errorText) || 'no response')
|
||||||
|
);
|
||||||
|
startWatcher(log);
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,7 +781,7 @@ function discoverSettings(callback) {
|
|||||||
callLuna('luna://com.webos.settingsservice/getSystemSettings', { category: category }, function (payload) {
|
callLuna('luna://com.webos.settingsservice/getSystemSettings', { category: category }, function (payload) {
|
||||||
var settings = (payload && payload.settings) || {};
|
var settings = (payload && payload.settings) || {};
|
||||||
Object.keys(settings).forEach(function (key) {
|
Object.keys(settings).forEach(function (key) {
|
||||||
if (!SETTINGS_KEY_PATTERN.test(key)) return;
|
if (!isUpdateKey(key)) return;
|
||||||
found.push({
|
found.push({
|
||||||
category: category,
|
category: category,
|
||||||
key: key,
|
key: key,
|
||||||
@@ -610,7 +896,38 @@ function restoreSettings(log, callback) {
|
|||||||
|
|
||||||
/* -------------------------------------------------------------- boot hook */
|
/* -------------------------------------------------------------- boot hook */
|
||||||
|
|
||||||
function bootHookScript(config, domains, jobs) {
|
/* 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 [
|
return [
|
||||||
'#!/bin/sh',
|
'#!/bin/sh',
|
||||||
'# LG Update Blocker boot hook - generated by ' + pkgInfo.name + ' v' + pkgInfo.version + '.',
|
'# LG Update Blocker boot hook - generated by ' + pkgInfo.name + ' v' + pkgInfo.version + '.',
|
||||||
@@ -619,17 +936,39 @@ function bootHookScript(config, domains, jobs) {
|
|||||||
'STATE=' + STATE_DIR,
|
'STATE=' + STATE_DIR,
|
||||||
'LOG=' + BOOT_LOG_PATH,
|
'LOG=' + BOOT_LOG_PATH,
|
||||||
'HOSTS_TMP=' + HOSTS_TMP,
|
'HOSTS_TMP=' + HOSTS_TMP,
|
||||||
|
'WATCHER=' + WATCHER_PATH,
|
||||||
'BLOCK_HOSTS=' + (config.blockHosts ? 1 : 0),
|
'BLOCK_HOSTS=' + (config.blockHosts ? 1 : 0),
|
||||||
|
'DISMISS_POPUP=' + (config.dismissPopup ? 1 : 0),
|
||||||
'PURGE_CACHE=' + (config.purgeCache ? 1 : 0),
|
'PURGE_CACHE=' + (config.purgeCache ? 1 : 0),
|
||||||
'LOCK_CACHE=' + (config.lockCache ? 1 : 0),
|
'LOCK_CACHE=' + (config.lockCache ? 1 : 0),
|
||||||
'STOP_SERVICES=' + (config.stopServices ? 1 : 0),
|
|
||||||
'CACHE_DIRS="' + CACHE_DIRS.join(' ') + '"',
|
'CACHE_DIRS="' + CACHE_DIRS.join(' ') + '"',
|
||||||
'JOBS="' + jobs.join(' ') + '"',
|
|
||||||
'',
|
'',
|
||||||
'mkdir -p "$STATE"',
|
'mkdir -p "$STATE"',
|
||||||
'if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 65536 ]; then rm -f "$LOG"; fi',
|
'if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 65536 ]; then rm -f "$LOG"; fi',
|
||||||
'exec >>"$LOG" 2>&1',
|
'exec >>"$LOG" 2>&1',
|
||||||
'echo "--- $(date) LG Update Blocker ---"',
|
'echo "--- $(date) LG Update Blocker ---"',
|
||||||
|
''
|
||||||
|
].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\' \\',
|
||||||
|
' ' + UPDATE_DAEMON_LOG + ' 2>/dev/null | tail -1)',
|
||||||
|
' if [ -n "$id" ] && luna-send -t 1 -f luna://com.webos.notification/closeAlert \\',
|
||||||
|
' "{\\"alertId\\":\\"$id\\"}" >/dev/null 2>&1; then',
|
||||||
|
' echo "dismissed update popup $id"',
|
||||||
|
' fi',
|
||||||
|
' # and stay subscribed for the rest of the session in case it comes back',
|
||||||
|
' if [ -x "$WATCHER" ]; then',
|
||||||
|
' if command -v setsid >/dev/null 2>&1; then',
|
||||||
|
' setsid "$WATCHER" >/dev/null 2>&1 </dev/null &',
|
||||||
|
' else',
|
||||||
|
' "$WATCHER" >/dev/null 2>&1 </dev/null &',
|
||||||
|
' fi',
|
||||||
|
' echo "alert watcher started"',
|
||||||
|
' fi',
|
||||||
|
'fi',
|
||||||
'',
|
'',
|
||||||
'if [ "$BLOCK_HOSTS" = 1 ] && [ -f "$STATE/hosts.txt" ]; then',
|
'if [ "$BLOCK_HOSTS" = 1 ] && [ -f "$STATE/hosts.txt" ]; then',
|
||||||
' if ! (: >> /etc/hosts) 2>/dev/null; then',
|
' if ! (: >> /etc/hosts) 2>/dev/null; then',
|
||||||
@@ -682,42 +1021,99 @@ function bootHookScript(config, domains, jobs) {
|
|||||||
' done',
|
' done',
|
||||||
'fi',
|
'fi',
|
||||||
'',
|
'',
|
||||||
'if [ "$STOP_SERVICES" = 1 ] && [ -n "$JOBS" ]; then',
|
|
||||||
' for job in $JOBS; do',
|
|
||||||
' if initctl stop "$job" >/dev/null 2>&1 || stop "$job" >/dev/null 2>&1; then',
|
|
||||||
' echo "stopped $job"',
|
|
||||||
' else',
|
|
||||||
' echo "could not stop $job (not running?)"',
|
|
||||||
' fi',
|
|
||||||
' done',
|
|
||||||
'fi',
|
|
||||||
'',
|
|
||||||
'echo "done"',
|
'echo "done"',
|
||||||
''
|
''
|
||||||
|
]).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Long-lived companion to the boot hook: subscribes to the notification
|
||||||
|
* manager and closes update alerts as they open. Kept as a separate file so
|
||||||
|
* the hook itself stays a short, ordinary run-parts script. */
|
||||||
|
function watcherScript() {
|
||||||
|
return [
|
||||||
|
'#!/bin/sh',
|
||||||
|
'# LG Update Blocker alert watcher - generated by ' + pkgInfo.name + ' v' + pkgInfo.version + '.',
|
||||||
|
'# Closes LG\'s "a new software version is available" alert as it opens.',
|
||||||
|
'',
|
||||||
|
'LOG=' + BOOT_LOG_PATH,
|
||||||
|
'PIDFILE=' + WATCHER_PID_PATH,
|
||||||
|
'',
|
||||||
|
'echo $$ > "$PIDFILE"',
|
||||||
|
'# the INT/TERM handler has to exit explicitly: a trap that just returns',
|
||||||
|
'# resumes the script, which would then re-subscribe and outlive the kill',
|
||||||
|
'trap \'rm -f "$PIDFILE"\' EXIT',
|
||||||
|
'trap \'rm -f "$PIDFILE"; exit 0\' INT TERM',
|
||||||
|
'',
|
||||||
|
'note() { echo "$(date) $*" >> "$LOG"; }',
|
||||||
|
'',
|
||||||
|
'close_alert() {',
|
||||||
|
' [ -n "$1" ] || return 1',
|
||||||
|
' luna-send -t 1 -f luna://com.webos.notification/closeAlert \\',
|
||||||
|
' "{\\"alertId\\":\\"$1\\"}" >/dev/null 2>&1',
|
||||||
|
'}',
|
||||||
|
'',
|
||||||
|
'# getAlertNotification only pushes to clients that were already subscribed,',
|
||||||
|
'# so it never reports the popup that is on screen right now - and',
|
||||||
|
'# closeAllAlerts rejects every source id it accepts arguments for. The',
|
||||||
|
'# subscription below therefore only covers alerts raised from now on; the',
|
||||||
|
'# boot hook handles the one already open, using the updater\'s own log.',
|
||||||
|
'while :; do',
|
||||||
|
' luna-send -i -f luna://com.webos.notification/getAlertNotification \\',
|
||||||
|
' \'{"subscribe":true}\' 2>/dev/null |',
|
||||||
|
' while read -r line; do',
|
||||||
|
' case "$line" in',
|
||||||
|
' *com.webos.service.update-*) ;;',
|
||||||
|
' *) continue ;;',
|
||||||
|
' esac',
|
||||||
|
' id=$(echo "$line" | sed -n \'s/.*\\(' + ALERT_ID_PATTERN + '\\).*/\\1/p\')',
|
||||||
|
' close_alert "$id" || continue',
|
||||||
|
' # one line per popup, not per notification the bus repeats',
|
||||||
|
' [ "$id" = "$last" ] && continue',
|
||||||
|
' last=$id',
|
||||||
|
' note "dismissed update popup $id"',
|
||||||
|
' done',
|
||||||
|
' # the bus dropped us; wait before re-subscribing rather than spinning',
|
||||||
|
' sleep 30',
|
||||||
|
'done',
|
||||||
|
''
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function installBootHook(config, jobs, log) {
|
function installBootHook(config, log) {
|
||||||
var domains = readDomains();
|
var domains = readDomains();
|
||||||
mkdirp(STATE_DIR);
|
mkdirp(STATE_DIR);
|
||||||
mkdirp(BOOT_HOOK_DIR);
|
mkdirp(BOOT_HOOK_DIR);
|
||||||
fs.writeFileSync(HOSTS_LIST_PATH, domains.join('\n') + '\n');
|
fs.writeFileSync(HOSTS_LIST_PATH, domains.join('\n') + '\n');
|
||||||
fs.writeFileSync(BOOT_HOOK_PATH, bootHookScript(config, domains, jobs));
|
fs.writeFileSync(WATCHER_PATH, watcherScript());
|
||||||
|
fs.chmodSync(WATCHER_PATH, parseInt('755', 8));
|
||||||
|
fs.writeFileSync(BOOT_HOOK_PATH, bootHookScript(config, domains));
|
||||||
fs.chmodSync(BOOT_HOOK_PATH, parseInt('755', 8));
|
fs.chmodSync(BOOT_HOOK_PATH, parseInt('755', 8));
|
||||||
log.add('boot hook installed at ' + BOOT_HOOK_PATH);
|
log.add('boot hook installed at ' + BOOT_HOOK_PATH);
|
||||||
|
|
||||||
|
/* versions before 1.1 installed the hook under the bare slug, which
|
||||||
|
* run-parts would then run a second time */
|
||||||
|
if (exists(LEGACY_BOOT_HOOK_PATH)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(LEGACY_BOOT_HOOK_PATH);
|
||||||
|
log.add('removed old boot hook ' + LEGACY_BOOT_HOOK_PATH);
|
||||||
|
} catch (err) {
|
||||||
|
log.add('! could not remove old boot hook: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBootHook(log) {
|
function removeBootHook(log) {
|
||||||
if (!exists(BOOT_HOOK_PATH)) {
|
var removed = [BOOT_HOOK_PATH, LEGACY_BOOT_HOOK_PATH].filter(function (target) {
|
||||||
log.add('no boot hook installed');
|
if (!exists(target)) return false;
|
||||||
return;
|
try {
|
||||||
}
|
fs.unlinkSync(target);
|
||||||
try {
|
return true;
|
||||||
fs.unlinkSync(BOOT_HOOK_PATH);
|
} catch (err) {
|
||||||
log.add('removed boot hook ' + BOOT_HOOK_PATH);
|
log.add('! could not remove boot hook: ' + err.message);
|
||||||
} catch (err) {
|
return false;
|
||||||
log.add('! could not remove boot hook: ' + err.message);
|
}
|
||||||
}
|
});
|
||||||
|
log.add(removed.length ? 'removed boot hook ' + removed.join(', ') : 'no boot hook installed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- methods */
|
/* ---------------------------------------------------------------- methods */
|
||||||
@@ -728,6 +1124,7 @@ function baseStatus() {
|
|||||||
uid: typeof process.getuid === 'function' ? process.getuid() : -1,
|
uid: typeof process.getuid === 'function' ? process.getuid() : -1,
|
||||||
root: isRoot(),
|
root: isRoot(),
|
||||||
config: readConfig(),
|
config: readConfig(),
|
||||||
|
updater: updaterStatus(),
|
||||||
hosts: {
|
hosts: {
|
||||||
path: HOSTS_PATH,
|
path: HOSTS_PATH,
|
||||||
writable: hostsWritable(),
|
writable: hostsWritable(),
|
||||||
@@ -736,7 +1133,10 @@ function baseStatus() {
|
|||||||
totalDomains: readDomains().length
|
totalDomains: readDomains().length
|
||||||
},
|
},
|
||||||
cache: cacheStatus(),
|
cache: cacheStatus(),
|
||||||
jobs: discoverJobs(),
|
popup: {
|
||||||
|
lastAlertId: lastAlertId(),
|
||||||
|
watcherPid: watcherPid()
|
||||||
|
},
|
||||||
bootHook: {
|
bootHook: {
|
||||||
path: BOOT_HOOK_PATH,
|
path: BOOT_HOOK_PATH,
|
||||||
installed: exists(BOOT_HOOK_PATH)
|
installed: exists(BOOT_HOOK_PATH)
|
||||||
@@ -770,6 +1170,13 @@ service.register('apply', function (message) {
|
|||||||
mkdirp(STATE_DIR);
|
mkdirp(STATE_DIR);
|
||||||
writeJson(CONFIG_PATH, config);
|
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) {
|
if (config.blockHosts) {
|
||||||
applyHosts(log);
|
applyHosts(log);
|
||||||
} else {
|
} else {
|
||||||
@@ -784,13 +1191,8 @@ service.register('apply', function (message) {
|
|||||||
unlockCache(log);
|
unlockCache(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobs = discoverJobs();
|
|
||||||
if (config.stopServices) {
|
|
||||||
stopJobs(jobs, log);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anyEnabled(config)) {
|
if (anyEnabled(config)) {
|
||||||
installBootHook(config, config.stopServices ? jobs : [], log);
|
installBootHook(config, log);
|
||||||
} else {
|
} else {
|
||||||
removeBootHook(log);
|
removeBootHook(log);
|
||||||
}
|
}
|
||||||
@@ -805,10 +1207,19 @@ service.register('apply', function (message) {
|
|||||||
message.respond({ returnValue: true, log: log.lines, config: config, status: baseStatus() });
|
message.respond({ returnValue: true, log: log.lines, config: config, status: baseStatus() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function afterSettings() {
|
||||||
|
if (config.dismissPopup) {
|
||||||
|
dismissPopup(log, finish);
|
||||||
|
} else {
|
||||||
|
if (stopWatcher()) log.add('alert watcher stopped');
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.disableSettings) {
|
if (config.disableSettings) {
|
||||||
disableSettings(log, finish);
|
disableSettings(log, afterSettings);
|
||||||
} else {
|
} else {
|
||||||
restoreSettings(log, finish);
|
restoreSettings(log, afterSettings);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -825,8 +1236,10 @@ service.register('revert', function (message) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
unblockUpdater(log);
|
||||||
removeHosts(log);
|
removeHosts(log);
|
||||||
unlockCache(log);
|
unlockCache(log);
|
||||||
|
if (stopWatcher()) log.add('alert watcher stopped');
|
||||||
removeBootHook(log);
|
removeBootHook(log);
|
||||||
mkdirp(STATE_DIR);
|
mkdirp(STATE_DIR);
|
||||||
writeJson(CONFIG_PATH, config);
|
writeJson(CONFIG_PATH, config);
|
||||||
@@ -835,7 +1248,7 @@ service.register('revert', function (message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
restoreSettings(log, function () {
|
restoreSettings(log, function () {
|
||||||
log.add('everything reverted; stopped services come back after a reboot');
|
log.add('everything reverted - the update popup will be back on the next boot');
|
||||||
message.respond({ returnValue: true, log: log.lines, config: config, status: baseStatus() });
|
message.respond({ returnValue: true, log: log.lines, config: config, status: baseStatus() });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -860,6 +1273,7 @@ service.register('diagnostics', function (message) {
|
|||||||
osInfo: readJson('/var/run/nyx/os_info.json', null),
|
osInfo: readJson('/var/run/nyx/os_info.json', null),
|
||||||
hostsFile: readFile(HOSTS_PATH, '(unreadable)').split('\n').slice(-40).join('\n'),
|
hostsFile: readFile(HOSTS_PATH, '(unreadable)').split('\n').slice(-40).join('\n'),
|
||||||
bootLog: readFile(BOOT_LOG_PATH, '(no boot log yet)').split('\n').slice(-40).join('\n'),
|
bootLog: readFile(BOOT_LOG_PATH, '(no boot log yet)').split('\n').slice(-40).join('\n'),
|
||||||
|
updateLog: readFile(UPDATE_DAEMON_LOG, '(no update log)').split('\n').slice(-40).join('\n'),
|
||||||
mounts: mountTargets().filter(function (target) {
|
mounts: mountTargets().filter(function (target) {
|
||||||
return target === HOSTS_PATH || target.indexOf('/mnt/lg') === 0;
|
return target === HOSTS_PATH || target.indexOf('/mnt/lg') === 0;
|
||||||
})
|
})
|
||||||
|
|||||||
Executable
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Dumps everything needed to work out why the update popup still appears.
|
||||||
|
# Read-only: it changes nothing on the TV.
|
||||||
|
#
|
||||||
|
# ssh root@<tv-ip> 'sh -s' < tools/tv-diag.sh > tv-diag.txt
|
||||||
|
#
|
||||||
|
# Busybox-friendly on purpose - no bashisms, every command guarded.
|
||||||
|
|
||||||
|
SLUG=lgupdateblocker
|
||||||
|
STATE=/var/lib/webosbrew/$SLUG
|
||||||
|
|
||||||
|
section() { echo; echo "=================== $* ==================="; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
section "identity"
|
||||||
|
id
|
||||||
|
cat /etc/starfish-release 2>/dev/null || echo "(no /etc/starfish-release)"
|
||||||
|
cat /var/run/nyx/device_info.json 2>/dev/null | head -c 600
|
||||||
|
echo
|
||||||
|
uptime
|
||||||
|
date
|
||||||
|
|
||||||
|
section "did our boot hook run this boot?"
|
||||||
|
ls -l /var/lib/webosbrew/init.d/ 2>&1
|
||||||
|
echo "--- last 60 lines of $STATE/boot.log ---"
|
||||||
|
tail -60 "$STATE/boot.log" 2>&1
|
||||||
|
echo "--- config ---"
|
||||||
|
cat "$STATE/config.json" 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- hosts list the hook uses ---"
|
||||||
|
cat "$STATE/hosts.txt" 2>&1
|
||||||
|
|
||||||
|
section "homebrew channel startup"
|
||||||
|
ls -la /var/lib/webosbrew/ 2>&1
|
||||||
|
ls -la /var/luna/preferences/ 2>/dev/null | grep -i webosbrew
|
||||||
|
echo "--- hbc startup script ---"
|
||||||
|
ls -l /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/startup.sh 2>&1
|
||||||
|
|
||||||
|
section "/etc/hosts"
|
||||||
|
cat /etc/hosts 2>&1
|
||||||
|
echo "--- is it bind-mounted? ---"
|
||||||
|
grep -i hosts /proc/mounts 2>&1
|
||||||
|
|
||||||
|
section "does blocking actually work?"
|
||||||
|
for h in su.lge.com snu.lge.com nsu.lge.com su-ssl.lge.com; do
|
||||||
|
if have nslookup; then
|
||||||
|
echo "$h -> $(nslookup "$h" 2>&1 | tail -3 | tr '\n' ' ')"
|
||||||
|
else
|
||||||
|
echo "$h -> $(ping -c 1 -W 2 "$h" 2>&1 | head -1)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
section "staged firmware / update data"
|
||||||
|
find /mnt/lg -maxdepth 4 \( -iname '*swupdate*' -o -iname '*epk*' -o -iname '*upgrade*' \) 2>/dev/null | head -40
|
||||||
|
for d in /mnt/lg/cmn_data/swupdate /mnt/lg/swupdate /mnt/lg/user/swupdate; do
|
||||||
|
[ -e "$d" ] || continue
|
||||||
|
echo "--- $d ---"
|
||||||
|
ls -la "$d" 2>&1
|
||||||
|
du -sh "$d" 2>&1
|
||||||
|
done
|
||||||
|
echo "--- anything mounted over them? ---"
|
||||||
|
grep -i -E 'swupdate|/mnt/lg' /proc/mounts 2>&1
|
||||||
|
|
||||||
|
section "update services"
|
||||||
|
echo "--- upstart jobs ---"
|
||||||
|
ls /etc/init 2>/dev/null | grep -i -E 'update|upgrade|swup|ota'
|
||||||
|
echo "--- luna services ---"
|
||||||
|
ls /usr/palm/services 2>/dev/null | grep -i -E 'update|upgrade|swup|ota'
|
||||||
|
ls /var/palm/ls2/roles/prv 2>/dev/null | grep -i -E 'update|upgrade|swup|ota'
|
||||||
|
echo "--- running processes ---"
|
||||||
|
ps 2>/dev/null | grep -i -E 'swupdate|updater|upgrade' | grep -v grep
|
||||||
|
|
||||||
|
section "update-related settings"
|
||||||
|
for c in option general network commercial system; do
|
||||||
|
echo "--- category $c ---"
|
||||||
|
luna-send -n 1 -f "luna://com.webos.settingsservice/getSystemSettings" \
|
||||||
|
"{\"category\":\"$c\"}" 2>&1 |
|
||||||
|
tr ',' '\n' | grep -i -E 'update|upgrade|firmware|ota|epk'
|
||||||
|
done
|
||||||
|
|
||||||
|
section "what the update service itself says"
|
||||||
|
for svc in com.webos.service.swupdate com.webos.service.swupdater com.webos.service.update com.webos.service.upgrade; do
|
||||||
|
echo "--- $svc/getStatus ---"
|
||||||
|
luna-send -n 1 -t 3 -f "luna://$svc/getStatus" '{}' 2>&1 | head -c 500
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
section "boot log: who raised the popup"
|
||||||
|
for f in /var/log/messages /var/log/syslog; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
echo "--- $f (update-related, last 60) ---"
|
||||||
|
grep -i -E 'swupdate|softwareupdate|upgrade|firmware' "$f" 2>/dev/null | tail -60
|
||||||
|
done
|
||||||
|
echo "--- notifications / alerts, last 40 ---"
|
||||||
|
grep -i -E 'createAlert|notificationmgr|systemAlert' /var/log/messages 2>/dev/null | tail -40
|
||||||
|
|
||||||
|
section "luna preferences that mention update"
|
||||||
|
grep -ril update /var/luna/preferences/ 2>/dev/null | head -20
|
||||||
|
for f in $(grep -ril update /var/luna/preferences/ 2>/dev/null | head -20); do
|
||||||
|
echo "--- $f ---"
|
||||||
|
head -c 300 "$f"
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
section "done"
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Second probe: find where the pending-update record lives, what the real
|
||||||
|
# auto-update switch is, and whether /etc/hosts is honoured.
|
||||||
|
# Read-only: it changes nothing on the TV.
|
||||||
|
#
|
||||||
|
# ssh root@<tv-ip> 'sh -s' < tools/tv-diag2.sh | tee tv-diag2.txt
|
||||||
|
|
||||||
|
section() { echo; echo "=================== $* ==================="; }
|
||||||
|
have() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
# luna-send writes progress chatter to stderr; keep only the payload
|
||||||
|
lsend() { luna-send -n 1 -t 5 -f "luna://$1" "$2" 2>/dev/null; }
|
||||||
|
|
||||||
|
section "is /etc/hosts actually honoured? (getaddrinfo, not DNS)"
|
||||||
|
for h in su.lge.com snu.lge.com nsu.lge.com su-ssl.lge.com; do
|
||||||
|
echo "--- $h ---"
|
||||||
|
ping -c 1 -W 3 "$h" 2>&1 | head -2
|
||||||
|
done
|
||||||
|
echo "--- can it still reach an update server over http? ---"
|
||||||
|
have wget && wget -q -T 5 -O - http://su.lge.com/ 2>&1 | head -c 200
|
||||||
|
echo
|
||||||
|
|
||||||
|
section "com.webos.service.update - what does it expose?"
|
||||||
|
grep -rl "com.webos.service.update" /usr/share/luna-service2/ 2>/dev/null | head -10
|
||||||
|
for f in $(grep -rl "com.webos.service.update" /usr/share/luna-service2/api-permissions.d/ 2>/dev/null | head -4); do
|
||||||
|
echo "--- $f ---"
|
||||||
|
head -c 1500 "$f"
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
echo "--- service file ---"
|
||||||
|
grep -rh "com.webos.service.update" /usr/share/luna-service2/services.d/ 2>/dev/null | head -10
|
||||||
|
|
||||||
|
section "com.webos.service.update - current state"
|
||||||
|
for m in getStatus getSettings getUpdateInfo getCurrentSWInformation getUpdateStatus checkUpdate; do
|
||||||
|
echo "--- /$m ---"
|
||||||
|
lsend "com.webos.service.update/$m" '{}' | head -c 700
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
section "settingsservice: every key mentioning update (via db8)"
|
||||||
|
lsend com.webos.service.db/find '{"query":{"from":"com.webos.settings.system:1"}}' |
|
||||||
|
tr ',' '\n' | grep -i -E 'update|upgrade|firmware|ota|nsu|epk' | head -60
|
||||||
|
echo "--- same for the description kind (tells us allowed values) ---"
|
||||||
|
lsend com.webos.service.db/find '{"query":{"from":"com.webos.settings.desc.system:1"}}' |
|
||||||
|
tr ',' '\n' | grep -i -E 'update|upgrade|firmware|ota|nsu' | head -60
|
||||||
|
|
||||||
|
section "settingsservice: direct reads"
|
||||||
|
for k in autoUpdate appUpdateMode swUpdateMode updateMode; do
|
||||||
|
echo "--- option/$k ---"
|
||||||
|
lsend com.webos.settingsservice/getSystemSettings "{\"category\":\"option\",\"keys\":[\"$k\"]}" | head -c 300
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
echo "--- no category, just the key ---"
|
||||||
|
lsend com.webos.settingsservice/getSystemSettings '{"keys":["autoUpdate","appUpdateMode"]}' | head -c 400
|
||||||
|
echo
|
||||||
|
|
||||||
|
section "luna preferences: full update-related keys"
|
||||||
|
for f in /var/luna/preferences/option /var/luna/preferences/general /var/luna/preferences/network; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
echo "--- $f ---"
|
||||||
|
tr ',' '\n' < "$f" | grep -i -E 'update|upgrade|firmware|ota|nsu|epk'
|
||||||
|
done
|
||||||
|
echo "--- any other preference file with an update key ---"
|
||||||
|
for f in /var/luna/preferences/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
hits=$(tr ',' '\n' < "$f" 2>/dev/null | grep -i -E '"[a-z]*(update|upgrade|nsu)[a-z]*"' | head -5)
|
||||||
|
[ -n "$hits" ] && { echo "--- $f ---"; echo "$hits"; }
|
||||||
|
done
|
||||||
|
|
||||||
|
section "where is the pending update recorded?"
|
||||||
|
echo "--- files under /mnt/lg mentioning swupdate/nsu/epk ---"
|
||||||
|
find /mnt/lg -maxdepth 3 -type f -size -2M 2>/dev/null |
|
||||||
|
while read -r f; do
|
||||||
|
if grep -l -i -E 'nsuimage|swupdate|epk_|otaVersion' "$f" >/dev/null 2>&1; then
|
||||||
|
echo "$f ($(wc -c < "$f") bytes, $(date -r "$f" 2>/dev/null))"
|
||||||
|
fi
|
||||||
|
done | head -20
|
||||||
|
echo "--- update-ish paths anywhere under /mnt/lg and /var ---"
|
||||||
|
find /mnt/lg /var -maxdepth 4 \( -iname '*swupdate*' -o -iname '*nsu*' -o -iname '*otaid*' \) 2>/dev/null | head -30
|
||||||
|
echo "--- tvservice config dirs ---"
|
||||||
|
ls -la /mnt/lg/cmn_data/ 2>/dev/null | head -60
|
||||||
|
|
||||||
|
section "the popup at boot: full log window"
|
||||||
|
ls -la /var/log/ 2>/dev/null | head -20
|
||||||
|
echo "--- first 120 update/alert lines in the current log ---"
|
||||||
|
grep -i -E 'swupdate|softwareupdate|createAlert|notification|popup|otaid' /var/log/messages 2>/dev/null | head -120
|
||||||
|
|
||||||
|
section "who is running the updater"
|
||||||
|
ps 2>/dev/null | grep -i -E 'broadcast|update' | grep -v grep
|
||||||
|
echo "--- upstart job contents ---"
|
||||||
|
for j in /etc/init/update.conf /etc/init/software-update-complete.conf; do
|
||||||
|
[ -f "$j" ] || continue
|
||||||
|
echo "--- $j ---"
|
||||||
|
cat "$j"
|
||||||
|
done
|
||||||
|
echo "--- are they actually stopped? ---"
|
||||||
|
initctl list 2>/dev/null | grep -i -E 'update|broadcast'
|
||||||
|
|
||||||
|
section "done"
|
||||||
Reference in New Issue
Block a user