Compare commits
2
Commits
v1.0.0
...
ef8b6b96c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef8b6b96c4 | ||
|
|
ae8229588e |
@@ -3,3 +3,4 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
*.ipk
|
*.ipk
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
tv-diag*.txt
|
||||||
|
|||||||
@@ -13,32 +13,63 @@ 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),
|
`/usr/sbin/update` is started by **systemd** (`update.service`, pulled in by
|
||||||
* it does nothing about a firmware image the TV **already downloaded** — a
|
`webos-mbd.target`) and runs its version check against
|
||||||
staged update keeps prompting even when the update servers are unreachable,
|
`https://snu.lge.com/CheckSWAutoUpdate.laf` roughly **20 seconds before** the
|
||||||
* it leaves the auto-update settings and the update services alone.
|
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
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Protection layers
|
## Protection layers
|
||||||
|
|
||||||
| Layer | What it does | Default |
|
| Layer | What it does | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **Block LG update servers** | Points `snu`/`su`/`nsu.lge.com`, their TLS variants, the `-dev` fallbacks and the CDN aliases at `127.0.0.1` in `/etc/hosts` | on |
|
| **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** | 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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
## 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 +98,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -89,8 +120,9 @@ Other buttons: **Purge staged update** (wipe a downloaded image right now),
|
|||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `/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
|
||||||
@@ -103,7 +135,13 @@ replaced.
|
|||||||
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 look at the
|
||||||
boot-hook log at the bottom. It records, per boot, whether the hosts entries
|
boot-hook log at the bottom. It records, per boot, whether the hosts entries
|
||||||
were added, what was purged and which jobs were stopped.
|
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.
|
||||||
* **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.
|
||||||
|
|||||||
+10
-8
@@ -18,6 +18,11 @@
|
|||||||
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: 'The TV asks LG about updates ~20s before homebrew starts, so the popup is closed instead'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'purgeCache',
|
key: 'purgeCache',
|
||||||
title: 'Delete staged firmware',
|
title: 'Delete staged firmware',
|
||||||
@@ -32,11 +37,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'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
blockHosts: true,
|
blockHosts: true,
|
||||||
|
dismissPopup: true,
|
||||||
purgeCache: true,
|
purgeCache: true,
|
||||||
lockCache: false,
|
lockCache: false,
|
||||||
disableSettings: true,
|
disableSettings: true
|
||||||
stopServices: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var focusables = [];
|
var focusables = [];
|
||||||
@@ -229,7 +229,9 @@
|
|||||||
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 || [];
|
||||||
|
|||||||
+13
-6
@@ -1,22 +1,29 @@
|
|||||||
<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>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>
|
||||||
|
|
||||||
<p>It applies up to five reversible layers and re-applies them on every boot
|
<p>It applies up to five 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>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> - closes the alert raised during boot, then
|
||||||
unreachable, which is why blocking alone often is not enough.</li>
|
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
|
||||||
|
|||||||
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.1.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,
|
||||||
|
|||||||
+236
-67
@@ -4,15 +4,29 @@
|
|||||||
* 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. hosts - point LG's firmware update servers at 127.0.0.1
|
||||||
* 2. purge - delete the firmware image the TV already staged
|
* 2. dismiss - close the "software update available" alert at boot
|
||||||
* 3. lock - bind-mount an empty read-only dir over the staging dir
|
* 3. purge - delete the firmware image the TV already staged
|
||||||
* 4. settings - switch off the update-related com.webos.settingsservice keys
|
* 4. lock - bind-mount an empty read-only dir over the staging dir
|
||||||
* 5. services - stop the update related upstart jobs (advanced, opt-in)
|
* 5. 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 each layer is re-applied by a boot hook script dropped into
|
||||||
* /var/lib/webosbrew/init.d (run by the Homebrew Channel startup script).
|
* /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.
|
||||||
|
*
|
||||||
* 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 +51,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 +76,31 @@ 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'];
|
var SETTINGS_CATEGORIES = ['option', 'general', 'network', 'commercial', 'support', 'hotelMode'];
|
||||||
var SETTINGS_KEY_PATTERN = /(update|upgrade|firmware|ota)/i;
|
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 = {
|
||||||
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);
|
||||||
|
|
||||||
@@ -411,34 +444,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 +577,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 +692,7 @@ function restoreSettings(log, callback) {
|
|||||||
|
|
||||||
/* -------------------------------------------------------------- boot hook */
|
/* -------------------------------------------------------------- boot hook */
|
||||||
|
|
||||||
function bootHookScript(config, domains, jobs) {
|
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,18 +701,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 ---"',
|
||||||
'',
|
'',
|
||||||
|
'# 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',
|
||||||
|
'# 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',
|
||||||
' cp /etc/hosts "$HOSTS_TMP" && chmod 644 "$HOSTS_TMP" \\',
|
' cp /etc/hosts "$HOSTS_TMP" && chmod 644 "$HOSTS_TMP" \\',
|
||||||
@@ -682,42 +785,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');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function installBootHook(config, jobs, log) {
|
/* 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
try {
|
||||||
fs.unlinkSync(BOOT_HOOK_PATH);
|
fs.unlinkSync(target);
|
||||||
log.add('removed boot hook ' + BOOT_HOOK_PATH);
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.add('! could not remove boot hook: ' + err.message);
|
log.add('! could not remove boot hook: ' + err.message);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
log.add(removed.length ? 'removed boot hook ' + removed.join(', ') : 'no boot hook installed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------- methods */
|
/* ---------------------------------------------------------------- methods */
|
||||||
@@ -736,7 +896,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)
|
||||||
@@ -784,13 +947,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 +963,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() });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.disableSettings) {
|
function afterSettings() {
|
||||||
disableSettings(log, finish);
|
if (config.dismissPopup) {
|
||||||
|
dismissPopup(log, finish);
|
||||||
} else {
|
} else {
|
||||||
restoreSettings(log, finish);
|
if (stopWatcher()) log.add('alert watcher stopped');
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.disableSettings) {
|
||||||
|
disableSettings(log, afterSettings);
|
||||||
|
} else {
|
||||||
|
restoreSettings(log, afterSettings);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -827,6 +994,7 @@ service.register('revert', function (message) {
|
|||||||
try {
|
try {
|
||||||
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 +1003,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 +1028,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