fix: close the update popup instead of racing to prevent it
Blocking snu.lge.com in /etc/hosts never stopped the boot popup on a CX, and the reason only showed up on the TV itself: /usr/sbin/update is a systemd unit (webos-mbd.target) that runs its version check against https://snu.lge.com/CheckSWAutoUpdate.laf 15-20 seconds before the Homebrew Channel gets as far as running its init.d hooks. The check therefore succeeds on every boot and the alert is already on screen before any hosts entry exists. Homebrew Channel's own "block system updates" toggle loses the same race. Nothing running that late can win it: every systemd unit path is a read-only overlay except tmpfs /run, and no persistent setting gates the check - automaticUpdate, support/softwareUpdateEnable, hotelMode/swUpdateEnable and .UpdateIsInprogress were each measured by restarting the daemon and counting its requests. So dismiss the popup instead. The boot hook recovers the alert id from the updater's own log (_gAlertWindowId), which is the only way to reach an alert that opened before we could subscribe - com.webos.notification never reports it to a late subscriber and closeAllAlerts rejects every source id it accepts. A companion alert-watch.sh then stays subscribed for the rest of the session. Also drop two things that were never true. There is no staged firmware image driving the popup (the staging dir is empty at boot; the size the daemon reports is in-memory only), and there is no update service to stop - /etc/init is dead upstart leftovers on a systemd TV, so the old stopServices layer printed "stopped update" while doing nothing. - rename the hook to 00-lgupdateblocker so run-parts runs it first, removing the legacy file on apply - add support/ and hotelMode/ to the scanned settings categories - stop matching "ota" inside screenRotation, which would have switched screen rotation off - kill the watcher by process group, and make its TERM trap exit - a trap that only returns resumes the script, which then re-subscribes Verified on an LG OLED55CX8LB (webOS 5, 04.60.65): after a reboot the boot log records "dismissed update popup com.webos.service.update-1788650782451", matching the id the updater logged that boot. Raised 01:26:22, closed 01:26:37 - so it is visible for ~15s and then goes away on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae8229588e
commit
122bdb0ef0
@@ -3,3 +3,4 @@ build/
|
||||
dist/
|
||||
*.ipk
|
||||
.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
|
||||
hostnames into `/etc/hosts` at boot. That is not enough in practice:
|
||||
Blocking `snu.lge.com` in `/etc/hosts` is the obvious fix, and it is what the
|
||||
Homebrew Channel's own *Block system updates* toggle does. On a CX it does not
|
||||
stop the boot popup, and it took a session on the TV to work out why.
|
||||
|
||||
* it misses several update hostnames (`nsu`, `*-ssl`, the CDN aliases),
|
||||
* it does nothing about a firmware image the TV **already downloaded** — a
|
||||
staged update keeps prompting even when the update servers are unreachable,
|
||||
* it leaves the auto-update settings and the update services alone.
|
||||
`/usr/sbin/update` is started by **systemd** (`update.service`, pulled in by
|
||||
`webos-mbd.target`) and runs its version check against
|
||||
`https://snu.lge.com/CheckSWAutoUpdate.laf` roughly **20 seconds before** the
|
||||
Homebrew Channel gets as far as running its `init.d` hooks. So on every boot
|
||||
the check reaches a real LG server, gets `RESULT_CD 900 / Success` back, and
|
||||
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
|
||||
resets `/`, `/tmp` and the mount namespace on every start).
|
||||
```text
|
||||
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
|
||||
|
||||
| Layer | What it does | Default |
|
||||
| --- | --- | --- |
|
||||
| **Block LG update servers** | Points `snu`/`su`/`nsu.lge.com`, their TLS variants, the `-dev` fallbacks and the CDN aliases at `127.0.0.1` in `/etc/hosts` | on |
|
||||
| **Delete staged firmware** | Wipes the update image staged in `/mnt/lg/cmn_data/swupdate` | on |
|
||||
| **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 |
|
||||
| **Turn off auto-update settings** | Reads every `com.webos.settingsservice` category, switches off each update-related key it finds, and remembers the original values | on |
|
||||
| **Stop update services at boot** | Stops the update-related upstart jobs discovered in `/etc/init` (advanced) | off |
|
||||
| **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 |
|
||||
|
||||
Every layer is undone by **Remove protection**, which also restores the
|
||||
settings from the backup it made.
|
||||
|
||||
Be honest about what this buys you: the popup is closed within a fraction of a
|
||||
second of the Homebrew Channel starting its hooks, but the updater raised it
|
||||
15–20 seconds earlier, so on a cold boot **you will see it appear and then
|
||||
vanish by itself**. It is gone before you can act on it, and you never have to
|
||||
dismiss it. The hook is installed as `00-lgupdateblocker` so `run-parts` runs
|
||||
it before the other homebrew hooks, which is as early as anything unprivileged
|
||||
can go. Measured on a CX: alert raised at `01:26:22`, closed at `01:26:37`.
|
||||
|
||||
## Requirements
|
||||
|
||||
* 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:
|
||||
|
||||
```sh
|
||||
ares-install com.rkievits.lgupdateblocker_1.0.0_all.ipk
|
||||
ares-install com.rkievits.lgupdateblocker_<version>_all.ipk
|
||||
ares-launch com.rkievits.lgupdateblocker
|
||||
```
|
||||
|
||||
@@ -89,8 +120,9 @@ Other buttons: **Purge staged update** (wipe a downloaded image right now),
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `/etc/hosts` | Blocked hostnames, between `# >>> lg-update-blocker >>>` markers. Bind-mounted from `/tmp/lgupdateblocker-hosts` because `/` is read-only |
|
||||
| `/var/lib/webosbrew/init.d/lgupdateblocker` | Boot hook, run by the Homebrew Channel startup script; re-applies the enabled layers |
|
||||
| `/var/lib/webosbrew/lgupdateblocker/` | `config.json`, `hosts.txt`, `settings-backup.json`, `boot.log` |
|
||||
| `/var/lib/webosbrew/init.d/00-lgupdateblocker` | Boot hook, run by the Homebrew Channel startup script; re-applies the enabled layers. The `00-` prefix makes `run-parts` run it first |
|
||||
| `/var/lib/webosbrew/lgupdateblocker/` | `config.json`, `hosts.txt`, `settings-backup.json`, `boot.log`, `alert-watch.sh` |
|
||||
| `/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) |
|
||||
|
||||
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).
|
||||
* **Popup still appears after a reboot** — run *Diagnostics* and look at the
|
||||
boot-hook log at the bottom. It records, per boot, whether the hosts entries
|
||||
were added, what was purged and which jobs were stopped.
|
||||
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
|
||||
`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.
|
||||
|
||||
+10
-8
@@ -18,6 +18,11 @@
|
||||
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'
|
||||
},
|
||||
{
|
||||
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',
|
||||
title: 'Delete staged firmware',
|
||||
@@ -32,11 +37,6 @@
|
||||
key: 'disableSettings',
|
||||
title: 'Turn off auto-update settings',
|
||||
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 = {
|
||||
blockHosts: true,
|
||||
dismissPopup: true,
|
||||
purgeCache: true,
|
||||
lockCache: false,
|
||||
disableSettings: true,
|
||||
stopServices: false
|
||||
disableSettings: true
|
||||
};
|
||||
|
||||
var focusables = [];
|
||||
@@ -229,7 +229,9 @@
|
||||
row('Staging folders', stagedDirs ? stagedDirs + ' found' : 'none on this TV');
|
||||
row('Staged firmware', stagedDirs ? bytes(staged) : '-');
|
||||
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');
|
||||
|
||||
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
|
||||
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
|
||||
through a Homebrew Channel startup hook:</p>
|
||||
|
||||
<ul>
|
||||
<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>
|
||||
<li><b>Delete staged firmware</b> - wipes /mnt/lg/cmn_data/swupdate. An update
|
||||
that was already downloaded keeps prompting even when the servers are
|
||||
unreachable, which is why blocking alone often is not enough.</li>
|
||||
and CDN aliases at 127.0.0.1 in /etc/hosts, which stops the download and
|
||||
every later check in the session.</li>
|
||||
<li><b>Close the update popup</b> - closes the alert raised during boot, then
|
||||
stays subscribed to the notification manager and closes any later one as
|
||||
it opens.</li>
|
||||
<li><b>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
|
||||
over it so nothing can be staged again.</li>
|
||||
<li><b>Turn off auto-update settings</b> - switches off every update related
|
||||
key that com.webos.settingsservice exposes on your firmware, remembering
|
||||
the original values.</li>
|
||||
<li><b>Stop update services at boot</b> - optional, stops the update upstart
|
||||
jobs found on the TV.</li>
|
||||
</ul>
|
||||
|
||||
<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",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "com.rkievits.lgupdateblocker",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@webos-tools/cli": "^3.2.1"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.rkievits.lgupdateblocker",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"title": "LG Update Blocker",
|
||||
"description": "Blocks the recurring LG webOS firmware update popup on rooted TVs",
|
||||
"private": true,
|
||||
|
||||
+233
-64
@@ -4,15 +4,29 @@
|
||||
* Everything this service does is reversible and is applied in layers:
|
||||
*
|
||||
* 1. hosts - point LG's firmware update servers at 127.0.0.1
|
||||
* 2. purge - delete the firmware image the TV already staged
|
||||
* 3. lock - bind-mount an empty read-only dir over the staging dir
|
||||
* 4. settings - switch off the update-related com.webos.settingsservice keys
|
||||
* 5. services - stop the update related upstart jobs (advanced, opt-in)
|
||||
* 2. dismiss - close the "software update available" alert at boot
|
||||
* 3. purge - delete the firmware image the TV already staged
|
||||
* 4. lock - bind-mount an empty read-only dir over the staging dir
|
||||
* 5. settings - switch off the update-related com.webos.settingsservice keys
|
||||
*
|
||||
* / is read-only on webOS, and /tmp, /etc and the mount namespace are reset on
|
||||
* every boot, so each layer is re-applied by a boot hook script dropped into
|
||||
* /var/lib/webosbrew/init.d (run by the Homebrew Channel startup script).
|
||||
*
|
||||
* 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
|
||||
* 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 BOOT_LOG_PATH = STATE_DIR + '/boot.log';
|
||||
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_TMP = '/tmp/' + SLUG + '-hosts';
|
||||
@@ -56,18 +76,31 @@ var CACHE_DIRS = [
|
||||
];
|
||||
var CACHE_GUARD = '/mnt/lg/';
|
||||
|
||||
var UPSTART_DIR = '/etc/init';
|
||||
var JOB_PATTERN = /(swupdate|softwareupdate|firmware|fota|update|upgrade|nsu)/i;
|
||||
/* /usr/sbin/update logs its own alert id here as
|
||||
* _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_KEY_PATTERN = /(update|upgrade|firmware|ota)/i;
|
||||
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 = {
|
||||
blockHosts: true,
|
||||
dismissPopup: true,
|
||||
purgeCache: true,
|
||||
lockCache: false,
|
||||
disableSettings: true,
|
||||
stopServices: false
|
||||
disableSettings: true
|
||||
};
|
||||
var CONFIG_KEYS = Object.keys(DEFAULT_CONFIG);
|
||||
|
||||
@@ -411,34 +444,83 @@ function unlockCache(log) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- upstart jobs */
|
||||
/* ------------------------------------------------------------ update popup */
|
||||
|
||||
function discoverJobs() {
|
||||
var entries;
|
||||
/* Id of the last update alert the daemon raised this boot, recovered from its
|
||||
* own log. /tmp is wiped at boot, so at boot time this is the popup currently
|
||||
* on screen; later in a session it may already have been closed. Closing an
|
||||
* alert that is gone is a no-op, so it is safe either way. */
|
||||
function lastAlertId() {
|
||||
var log = readFile(UPDATE_DAEMON_LOG, '');
|
||||
var matches = log.match(/_gAlertWindowId\s*:\s*(com\.webos\.service\.update-[0-9]+)/g);
|
||||
if (!matches || !matches.length) return null;
|
||||
return matches[matches.length - 1].replace(/^.*:\s*/, '');
|
||||
}
|
||||
|
||||
function closeAlert(alertId, callback) {
|
||||
callLuna('luna://com.webos.notification/closeAlert', { alertId: alertId }, callback);
|
||||
}
|
||||
|
||||
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 {
|
||||
entries = fs.readdirSync(UPSTART_DIR);
|
||||
fs.unlinkSync(WATCHER_PID_PATH);
|
||||
} catch (err) {
|
||||
return [];
|
||||
/* already gone */
|
||||
}
|
||||
return entries
|
||||
.filter(function (entry) {
|
||||
return /\.conf$/.test(entry) && JOB_PATTERN.test(entry);
|
||||
})
|
||||
.map(function (entry) {
|
||||
return entry.replace(/\.conf$/, '');
|
||||
})
|
||||
.sort();
|
||||
return true;
|
||||
}
|
||||
|
||||
function stopJobs(jobs, log) {
|
||||
if (!jobs.length) {
|
||||
log.add('no update-related upstart jobs found');
|
||||
function startWatcher(log) {
|
||||
if (watcherPid()) {
|
||||
log.add('alert watcher already running');
|
||||
return;
|
||||
}
|
||||
jobs.forEach(function (job) {
|
||||
var res = sh('initctl stop ' + job);
|
||||
if (!res.ok) res = sh('stop ' + job);
|
||||
log.add(res.ok ? 'stopped upstart job ' + job : '! could not stop ' + job + ': ' + res.output);
|
||||
if (!exists(WATCHER_PATH)) {
|
||||
log.add('! alert watcher missing at ' + WATCHER_PATH);
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
var settings = (payload && payload.settings) || {};
|
||||
Object.keys(settings).forEach(function (key) {
|
||||
if (!SETTINGS_KEY_PATTERN.test(key)) return;
|
||||
if (!isUpdateKey(key)) return;
|
||||
found.push({
|
||||
category: category,
|
||||
key: key,
|
||||
@@ -610,7 +692,7 @@ function restoreSettings(log, callback) {
|
||||
|
||||
/* -------------------------------------------------------------- boot hook */
|
||||
|
||||
function bootHookScript(config, domains, jobs) {
|
||||
function bootHookScript(config, domains) {
|
||||
return [
|
||||
'#!/bin/sh',
|
||||
'# LG Update Blocker boot hook - generated by ' + pkgInfo.name + ' v' + pkgInfo.version + '.',
|
||||
@@ -619,18 +701,39 @@ function bootHookScript(config, domains, jobs) {
|
||||
'STATE=' + STATE_DIR,
|
||||
'LOG=' + BOOT_LOG_PATH,
|
||||
'HOSTS_TMP=' + HOSTS_TMP,
|
||||
'WATCHER=' + WATCHER_PATH,
|
||||
'BLOCK_HOSTS=' + (config.blockHosts ? 1 : 0),
|
||||
'DISMISS_POPUP=' + (config.dismissPopup ? 1 : 0),
|
||||
'PURGE_CACHE=' + (config.purgeCache ? 1 : 0),
|
||||
'LOCK_CACHE=' + (config.lockCache ? 1 : 0),
|
||||
'STOP_SERVICES=' + (config.stopServices ? 1 : 0),
|
||||
'CACHE_DIRS="' + CACHE_DIRS.join(' ') + '"',
|
||||
'JOBS="' + jobs.join(' ') + '"',
|
||||
'',
|
||||
'mkdir -p "$STATE"',
|
||||
'if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 65536 ]; then rm -f "$LOG"; fi',
|
||||
'exec >>"$LOG" 2>&1',
|
||||
'echo "--- $(date) LG Update Blocker ---"',
|
||||
'',
|
||||
'# First, because by now the popup has been on screen for ~20 seconds: the',
|
||||
'# updater runs its version check from webos-mbd.target, long before the',
|
||||
'# 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 ! (: >> /etc/hosts) 2>/dev/null; then',
|
||||
' cp /etc/hosts "$HOSTS_TMP" && chmod 644 "$HOSTS_TMP" \\',
|
||||
@@ -682,42 +785,99 @@ function bootHookScript(config, domains, jobs) {
|
||||
' done',
|
||||
'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"',
|
||||
''
|
||||
].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();
|
||||
mkdirp(STATE_DIR);
|
||||
mkdirp(BOOT_HOOK_DIR);
|
||||
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));
|
||||
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) {
|
||||
if (!exists(BOOT_HOOK_PATH)) {
|
||||
log.add('no boot hook installed');
|
||||
return;
|
||||
}
|
||||
var removed = [BOOT_HOOK_PATH, LEGACY_BOOT_HOOK_PATH].filter(function (target) {
|
||||
if (!exists(target)) return false;
|
||||
try {
|
||||
fs.unlinkSync(BOOT_HOOK_PATH);
|
||||
log.add('removed boot hook ' + BOOT_HOOK_PATH);
|
||||
fs.unlinkSync(target);
|
||||
return true;
|
||||
} catch (err) {
|
||||
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 */
|
||||
@@ -736,7 +896,10 @@ function baseStatus() {
|
||||
totalDomains: readDomains().length
|
||||
},
|
||||
cache: cacheStatus(),
|
||||
jobs: discoverJobs(),
|
||||
popup: {
|
||||
lastAlertId: lastAlertId(),
|
||||
watcherPid: watcherPid()
|
||||
},
|
||||
bootHook: {
|
||||
path: BOOT_HOOK_PATH,
|
||||
installed: exists(BOOT_HOOK_PATH)
|
||||
@@ -784,13 +947,8 @@ service.register('apply', function (message) {
|
||||
unlockCache(log);
|
||||
}
|
||||
|
||||
var jobs = discoverJobs();
|
||||
if (config.stopServices) {
|
||||
stopJobs(jobs, log);
|
||||
}
|
||||
|
||||
if (anyEnabled(config)) {
|
||||
installBootHook(config, config.stopServices ? jobs : [], log);
|
||||
installBootHook(config, log);
|
||||
} else {
|
||||
removeBootHook(log);
|
||||
}
|
||||
@@ -805,10 +963,19 @@ service.register('apply', function (message) {
|
||||
message.respond({ returnValue: true, log: log.lines, config: config, status: baseStatus() });
|
||||
}
|
||||
|
||||
if (config.disableSettings) {
|
||||
disableSettings(log, finish);
|
||||
function afterSettings() {
|
||||
if (config.dismissPopup) {
|
||||
dismissPopup(log, finish);
|
||||
} 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 {
|
||||
removeHosts(log);
|
||||
unlockCache(log);
|
||||
if (stopWatcher()) log.add('alert watcher stopped');
|
||||
removeBootHook(log);
|
||||
mkdirp(STATE_DIR);
|
||||
writeJson(CONFIG_PATH, config);
|
||||
@@ -835,7 +1003,7 @@ service.register('revert', function (message) {
|
||||
}
|
||||
|
||||
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() });
|
||||
});
|
||||
});
|
||||
@@ -860,6 +1028,7 @@ service.register('diagnostics', function (message) {
|
||||
osInfo: readJson('/var/run/nyx/os_info.json', null),
|
||||
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'),
|
||||
updateLog: readFile(UPDATE_DAEMON_LOG, '(no update log)').split('\n').slice(-40).join('\n'),
|
||||
mounts: mountTargets().filter(function (target) {
|
||||
return target === HOSTS_PATH || target.indexOf('/mnt/lg') === 0;
|
||||
})
|
||||
|
||||
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