Build / build (push) Successful in 48s
Closing the popup after the fact left it on screen for the 15-20 seconds between the updater's version check and the Homebrew Channel running its init.d hooks. That race cannot be won, so remove it. /usr/sbin/update is launched on demand by ls-hubd via a manifest, and ls-hubd.conf lists ManifestsVolatileDirectories under /var - writable, persistent, and how webOS itself ships manifest updates. A manifest there with the same id and a higher version replaces the read-only one, so drop in a copy that keeps every role and permission file and only empties serviceFiles. Nothing on the bus can start the updater after that: no version check, no alert, nothing to dismiss. Callers get an immediate "Service does not exist" rather than a hang, and deleting the file undoes it. Verified on a CX (webOS 5, 04.60.65) across reboots: no update process, /tmp/var/log/update.log never created, no alert. Previously that log was 55 kB with two server checks and an _gAlertWindowId per boot. Revert puts the updater back on the bus. The manifest, its D-Bus service file and the updater binary are all discovered from ls-hubd.conf rather than hardcoded. The popup-closing layer stays as a fallback for when this one is off. Also tried and rejected, now documented in the README: update-related settings, masking update.service (no writable unit directory), and breaking its ping through /var/systemd/system/env/update.env - the ping does fail, but something else on the bus activates the updater anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1286 lines
40 KiB
JavaScript
1286 lines
40 KiB
JavaScript
/**
|
|
* LG Update Blocker - root service.
|
|
*
|
|
* Everything this service does is reversible and is applied in layers:
|
|
*
|
|
* 1. updater - stop /usr/sbin/update from ever being launched
|
|
* 2. hosts - point LG's firmware update servers at 127.0.0.1
|
|
* 3. dismiss - close the "software update available" alert at boot
|
|
* 4. purge - delete the firmware image the TV already staged
|
|
* 5. lock - bind-mount an empty read-only dir over the staging dir
|
|
* 6. settings - switch off the update-related com.webos.settingsservice keys
|
|
*
|
|
* / is read-only on webOS, and /tmp, /etc and the mount namespace are reset on
|
|
* every boot, so the layers that live there are re-applied by a boot hook
|
|
* script dropped into /var/lib/webosbrew/init.d (run by the Homebrew Channel
|
|
* startup script).
|
|
*
|
|
* Why the hosts block alone is not enough: /usr/sbin/update runs its version
|
|
* check against snu.lge.com roughly 20 seconds before the Homebrew Channel
|
|
* gets as far as running the hooks in init.d. On a CX that check therefore
|
|
* *succeeds* on every boot and the popup is already on screen by the time
|
|
* anything of ours runs - which is also why the Homebrew Channel's own "block
|
|
* system updates" toggle does not stop it, and why closing the popup after the
|
|
* fact leaves it visible for those 20 seconds.
|
|
*
|
|
* The updater layer removes the race instead of trying to win it. The updater
|
|
* is not a normal daemon: it is launched on demand by ls-hubd from a D-Bus
|
|
* service file, which ls-hubd finds through a manifest. ls-hubd.conf lists
|
|
* ManifestsVolatileDirectories under /var - writable, persistent, and how
|
|
* webOS itself ships manifest updates - and a manifest there with the same id
|
|
* and a higher version replaces the read-only one. Dropping in a copy whose
|
|
* serviceFiles list is empty leaves every role and permission file in place
|
|
* but removes the launch entry, so nothing on the bus can start the updater:
|
|
* no version check, no alert, nothing to dismiss. Callers get an immediate
|
|
* "Service does not exist" instead of hanging, and deleting the one file puts
|
|
* it all back.
|
|
*
|
|
* Things that do *not* work, all measured on a CX before settling on the
|
|
* above: automaticUpdate, support/softwareUpdateEnable, hotelMode/swUpdateEnable
|
|
* and the .UpdateIsInprogress flag change nothing; no systemd unit directory is
|
|
* writable, so update.service cannot be masked; breaking update.service's ping
|
|
* via its /var/systemd/system/env override does make that ping fail, but
|
|
* something else on the bus activates the updater anyway.
|
|
*
|
|
* Written in ES5 without dependencies - it runs on the TV's own node with the
|
|
* platform-provided webos-service module and nothing else.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var childProcess = require('child_process');
|
|
var Service = require('webos-service');
|
|
|
|
var pkgInfo = require('./package.json');
|
|
|
|
var service = new Service(pkgInfo.name);
|
|
service.activityManager.idleTimeout = 300;
|
|
|
|
var SLUG = 'lgupdateblocker';
|
|
var STATE_DIR = '/var/lib/webosbrew/' + SLUG;
|
|
var CONFIG_PATH = STATE_DIR + '/config.json';
|
|
var HOSTS_LIST_PATH = STATE_DIR + '/hosts.txt';
|
|
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';
|
|
/* 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';
|
|
var MARKER_BEGIN = '# >>> lg-update-blocker >>>';
|
|
var MARKER_END = '# <<< lg-update-blocker <<<';
|
|
|
|
var BUNDLED_HOSTS = path.join(__dirname, 'data', 'lg-update-hosts.txt');
|
|
|
|
/* Directories where the TV stages a downloaded firmware image. A staged image
|
|
* keeps the "update is ready" popup coming back even when the update servers
|
|
* are unreachable. Only paths below /mnt/lg are ever touched. */
|
|
var CACHE_DIRS = [
|
|
'/mnt/lg/cmn_data/swupdate',
|
|
'/mnt/lg/swupdate',
|
|
'/mnt/lg/cmn_data/var/palm/data/com.webos.service.swupdate'
|
|
];
|
|
var CACHE_GUARD = '/mnt/lg/';
|
|
|
|
/* /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]*';
|
|
|
|
/* ls-hubd launches the updater on demand; its config says which manifest
|
|
* directories are read-only and which are the writable ones we may override
|
|
* from. Everything about the updater - its manifest, its D-Bus service file,
|
|
* even the path of its binary - is discovered from there rather than assumed,
|
|
* so this works the same on a firmware that moves them. */
|
|
var LS_HUBD_CONF = '/etc/luna-service2/ls-hubd.conf';
|
|
var UPDATER_SERVICE = 'com.webos.service.update';
|
|
var UPDATER_BINARY_FALLBACK = '/usr/sbin/update';
|
|
|
|
var SETTINGS_CATEGORIES = ['option', 'general', 'network', 'commercial', 'support', 'hotelMode'];
|
|
var SETTINGS_KEY_PATTERN = /(update|upgrade|firmware)/i;
|
|
/* "ota" and "nsu" are substrings of perfectly innocent keys - screenRotation,
|
|
* 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 = {
|
|
blockUpdater: true,
|
|
blockHosts: true,
|
|
dismissPopup: true,
|
|
purgeCache: true,
|
|
lockCache: false,
|
|
disableSettings: true
|
|
};
|
|
var CONFIG_KEYS = Object.keys(DEFAULT_CONFIG);
|
|
|
|
/* ------------------------------------------------------------------ utils */
|
|
|
|
function isRoot() {
|
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
}
|
|
|
|
function exists(target) {
|
|
try {
|
|
fs.statSync(target);
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isDirectory(target) {
|
|
try {
|
|
return fs.statSync(target).isDirectory();
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function mkdirp(dir) {
|
|
if (isDirectory(dir)) return;
|
|
mkdirp(path.dirname(dir));
|
|
try {
|
|
fs.mkdirSync(dir);
|
|
} catch (err) {
|
|
if (!isDirectory(dir)) throw err;
|
|
}
|
|
}
|
|
|
|
function readFile(target, fallback) {
|
|
try {
|
|
return fs.readFileSync(target, 'utf8');
|
|
} catch (err) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function readJson(target, fallback) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
} catch (err) {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function writeJson(target, value) {
|
|
mkdirp(path.dirname(target));
|
|
fs.writeFileSync(target, JSON.stringify(value, null, 2) + '\n');
|
|
}
|
|
|
|
function sh(command) {
|
|
try {
|
|
var out = childProcess.execSync(command + ' 2>&1', { encoding: 'utf8', timeout: 30000 });
|
|
return { ok: true, output: String(out || '').trim() };
|
|
} catch (err) {
|
|
var output = (err && (err.stdout || err.message)) || '';
|
|
return { ok: false, output: String(output).trim() };
|
|
}
|
|
}
|
|
|
|
function Log() {
|
|
this.lines = [];
|
|
}
|
|
Log.prototype.add = function (line) {
|
|
console.info(line);
|
|
this.lines.push(line);
|
|
return this;
|
|
};
|
|
|
|
/* ----------------------------------------------------------------- config */
|
|
|
|
function readConfig() {
|
|
var stored = readJson(CONFIG_PATH, {});
|
|
var config = {};
|
|
CONFIG_KEYS.forEach(function (key) {
|
|
config[key] = typeof stored[key] === 'boolean' ? stored[key] : DEFAULT_CONFIG[key];
|
|
});
|
|
return config;
|
|
}
|
|
|
|
function mergeConfig(config, payload) {
|
|
var merged = {};
|
|
CONFIG_KEYS.forEach(function (key) {
|
|
merged[key] = typeof payload[key] === 'boolean' ? payload[key] : config[key];
|
|
});
|
|
return merged;
|
|
}
|
|
|
|
function anyEnabled(config) {
|
|
return CONFIG_KEYS.some(function (key) {
|
|
return config[key];
|
|
});
|
|
}
|
|
|
|
/* ---------------------------------------------------------------- updater */
|
|
|
|
/** Semicolon-separated directory list out of ls-hubd.conf. */
|
|
function lsHubdDirs(key) {
|
|
var match = readFile(LS_HUBD_CONF, '').match(new RegExp('^[ \\t]*' + key + '[ \\t]*=(.*)$', 'm'));
|
|
if (!match) return [];
|
|
return match[1].split(';').map(function (dir) {
|
|
return dir.trim();
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
/** The read-only manifest whose serviceFiles make the updater launchable. */
|
|
function findUpdaterManifest() {
|
|
var found = null;
|
|
lsHubdDirs('ManifestsDirectories').forEach(function (dir) {
|
|
if (found || !isDirectory(dir)) return;
|
|
var entries;
|
|
try {
|
|
entries = fs.readdirSync(dir);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
entries.forEach(function (entry) {
|
|
if (found || !/\.json$/.test(entry)) return;
|
|
var file = path.join(dir, entry);
|
|
var manifest = readJson(file, null);
|
|
if (!manifest || !manifest.id || !Array.isArray(manifest.serviceFiles)) return;
|
|
var launchesUpdater = manifest.serviceFiles.some(function (svc) {
|
|
return path.basename(svc) === UPDATER_SERVICE + '.service';
|
|
});
|
|
if (launchesUpdater) found = { file: file, manifest: manifest };
|
|
});
|
|
});
|
|
return found;
|
|
}
|
|
|
|
/** Where our replacement goes - the first writable manifest dir ls-hubd scans. */
|
|
function overridePath(base) {
|
|
var dirs = lsHubdDirs('ManifestsVolatileDirectories');
|
|
var dir = dirs.filter(isDirectory)[0] || dirs[0];
|
|
return dir ? path.join(dir, base.manifest.id + '.json') : null;
|
|
}
|
|
|
|
/** A version that outranks the stock manifest, so ls-hubd prefers ours. */
|
|
function outrankVersion(version) {
|
|
var major = parseInt(String(version || '').split('.')[0], 10);
|
|
return Math.max((major >= 0 ? major : 0) + 1, 99) + '.0.0';
|
|
}
|
|
|
|
/* Same id, higher version, same roles and permissions - only the serviceFiles
|
|
* list is emptied, which is what takes away ls-hubd's ability to launch it. */
|
|
function buildOverride(base) {
|
|
var override = { id: base.manifest.id, version: outrankVersion(base.manifest.version) };
|
|
['roleFiles', 'roleFilesPub', 'roleFilesPrv', 'apiPermissionFiles', 'clientPermissionFiles']
|
|
.forEach(function (key) {
|
|
if (base.manifest[key]) override[key] = base.manifest[key];
|
|
});
|
|
override.serviceFiles = [];
|
|
return override;
|
|
}
|
|
|
|
/* An empty serviceFiles list is not something a real manifest ever has, so it
|
|
* doubles as the signature that a file at that path was written by us. */
|
|
function isOverrideOurs(target, base) {
|
|
var manifest = readJson(target, null);
|
|
return !!manifest && manifest.id === base.manifest.id &&
|
|
Array.isArray(manifest.serviceFiles) && manifest.serviceFiles.length === 0;
|
|
}
|
|
|
|
/** Exec= out of the updater's D-Bus service file. */
|
|
function updaterBinary(base) {
|
|
var files = base.manifest.serviceFiles || [];
|
|
for (var i = 0; i < files.length; i += 1) {
|
|
var match = readFile(files[i], '').match(/^[ \t]*Exec[ \t]*=[ \t]*(\S+)/m);
|
|
if (match) return match[1];
|
|
}
|
|
return UPDATER_BINARY_FALLBACK;
|
|
}
|
|
|
|
function updaterPids(binary) {
|
|
var pids = [];
|
|
try {
|
|
fs.readdirSync('/proc').forEach(function (entry) {
|
|
if (!/^[0-9]+$/.test(entry)) return;
|
|
/* cmdline is NUL-separated; argv[0] is what ls-hubd launched */
|
|
if (readFile('/proc/' + entry + '/cmdline', '').split('\0')[0] === binary) {
|
|
pids.push(parseInt(entry, 10));
|
|
}
|
|
});
|
|
} catch (err) {
|
|
/* /proc unreadable - nothing we can do */
|
|
}
|
|
return pids;
|
|
}
|
|
|
|
function stopUpdater(binary) {
|
|
var stopped = 0;
|
|
updaterPids(binary).forEach(function (pid) {
|
|
try {
|
|
process.kill(pid, 'SIGTERM');
|
|
stopped += 1;
|
|
} catch (err) {
|
|
/* already gone */
|
|
}
|
|
});
|
|
return stopped;
|
|
}
|
|
|
|
function blockUpdater(log) {
|
|
var base = findUpdaterManifest();
|
|
if (!base) {
|
|
log.add('! no ' + UPDATER_SERVICE + ' manifest on this TV - leaving the updater alone');
|
|
return false;
|
|
}
|
|
var target = overridePath(base);
|
|
if (!target) {
|
|
log.add('! ls-hubd has no writable manifest directory on this TV');
|
|
return false;
|
|
}
|
|
if (exists(target) && !isOverrideOurs(target, base)) {
|
|
log.add('! ' + target + ' exists and is not ours - leaving it alone');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
mkdirp(path.dirname(target));
|
|
writeJson(target, buildOverride(base));
|
|
} catch (err) {
|
|
log.add('! could not write ' + target + ': ' + err.message);
|
|
return false;
|
|
}
|
|
|
|
var res = sh('ls-control scan-services');
|
|
if (!res.ok) log.add('! ls-control scan-services failed: ' + res.output);
|
|
log.add('updater disabled - ' + UPDATER_SERVICE + ' can no longer be launched');
|
|
|
|
var binary = updaterBinary(base);
|
|
var stopped = stopUpdater(binary);
|
|
if (stopped) log.add('stopped ' + stopped + ' running ' + binary + ' process(es)');
|
|
return true;
|
|
}
|
|
|
|
function unblockUpdater(log) {
|
|
var base = findUpdaterManifest();
|
|
var target = base && overridePath(base);
|
|
if (!target || !exists(target)) {
|
|
log.add('updater was not disabled');
|
|
return;
|
|
}
|
|
if (!isOverrideOurs(target, base)) {
|
|
log.add('! ' + target + ' is not ours - leaving it alone');
|
|
return;
|
|
}
|
|
try {
|
|
fs.unlinkSync(target);
|
|
} catch (err) {
|
|
log.add('! could not remove ' + target + ': ' + err.message);
|
|
return;
|
|
}
|
|
sh('ls-control scan-services');
|
|
log.add('updater re-enabled - removed ' + target);
|
|
}
|
|
|
|
function updaterStatus() {
|
|
var base = findUpdaterManifest();
|
|
if (!base) return { found: false, blocked: false, running: false };
|
|
var target = overridePath(base);
|
|
var binary = updaterBinary(base);
|
|
return {
|
|
found: true,
|
|
service: UPDATER_SERVICE,
|
|
binary: binary,
|
|
manifest: base.file,
|
|
overridePath: target,
|
|
blocked: !!(target && exists(target) && isOverrideOurs(target, base)),
|
|
running: updaterPids(binary).length > 0
|
|
};
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ hosts */
|
|
|
|
function readDomains() {
|
|
var raw = readFile(BUNDLED_HOSTS, null);
|
|
if (raw === null) raw = readFile(HOSTS_LIST_PATH, '');
|
|
return raw
|
|
.split('\n')
|
|
.map(function (line) {
|
|
return line.replace(/#.*$/, '').trim();
|
|
})
|
|
.filter(function (line) {
|
|
return line.length > 0 && line.indexOf(' ') === -1;
|
|
});
|
|
}
|
|
|
|
function mountTargets() {
|
|
return readFile('/proc/mounts', '')
|
|
.split('\n')
|
|
.map(function (line) {
|
|
return line.split(' ')[1];
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function isMountPoint(target) {
|
|
return mountTargets().indexOf(target) !== -1;
|
|
}
|
|
|
|
function hostsWritable() {
|
|
try {
|
|
fs.appendFileSync(HOSTS_PATH, '');
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* / is mounted read-only, so /etc/hosts gets replaced by a bind mount of a
|
|
* writable copy in /tmp - the same trick the Homebrew Channel startup script
|
|
* uses (and it happily stacks with it). */
|
|
function makeHostsWritable(log) {
|
|
if (hostsWritable()) return true;
|
|
try {
|
|
fs.writeFileSync(HOSTS_TMP, readFile(HOSTS_PATH, ''));
|
|
fs.chmodSync(HOSTS_TMP, parseInt('644', 8));
|
|
} catch (err) {
|
|
log.add('! could not stage a writable hosts file: ' + err.message);
|
|
return false;
|
|
}
|
|
var res = sh('mount --bind ' + HOSTS_TMP + ' ' + HOSTS_PATH);
|
|
if (!res.ok || !hostsWritable()) {
|
|
log.add('! bind mount of ' + HOSTS_PATH + ' failed: ' + res.output);
|
|
return false;
|
|
}
|
|
log.add('bind-mounted a writable ' + HOSTS_PATH + ' (from ' + HOSTS_TMP + ')');
|
|
return true;
|
|
}
|
|
|
|
function stripHostsBlock(content) {
|
|
var out = [];
|
|
var inside = false;
|
|
content.split('\n').forEach(function (line) {
|
|
if (line.indexOf(MARKER_BEGIN) === 0) {
|
|
inside = true;
|
|
return;
|
|
}
|
|
if (inside) {
|
|
if (line.indexOf(MARKER_END) === 0) inside = false;
|
|
return;
|
|
}
|
|
out.push(line);
|
|
});
|
|
return out.join('\n').replace(/\n{3,}$/, '\n');
|
|
}
|
|
|
|
function buildHostsBlock(domains) {
|
|
var lines = [MARKER_BEGIN];
|
|
domains.forEach(function (domain) {
|
|
lines.push('127.0.0.1 ' + domain);
|
|
lines.push('::1 ' + domain);
|
|
});
|
|
lines.push(MARKER_END);
|
|
return lines.join('\n') + '\n';
|
|
}
|
|
|
|
function hostsBlockedDomains() {
|
|
var content = readFile(HOSTS_PATH, '');
|
|
if (content.indexOf(MARKER_BEGIN) === -1) return 0;
|
|
return readDomains().filter(function (domain) {
|
|
return content.indexOf(' ' + domain) !== -1;
|
|
}).length;
|
|
}
|
|
|
|
function applyHosts(log) {
|
|
var domains = readDomains();
|
|
if (!domains.length) {
|
|
log.add('! no update domains found to block');
|
|
return false;
|
|
}
|
|
if (!makeHostsWritable(log)) return false;
|
|
|
|
var content = stripHostsBlock(readFile(HOSTS_PATH, ''));
|
|
if (content.length && content.charAt(content.length - 1) !== '\n') content += '\n';
|
|
fs.writeFileSync(HOSTS_PATH, content + buildHostsBlock(domains));
|
|
log.add('blocked ' + domains.length + ' update hostnames in ' + HOSTS_PATH);
|
|
return true;
|
|
}
|
|
|
|
function removeHosts(log) {
|
|
var content = readFile(HOSTS_PATH, '');
|
|
if (content.indexOf(MARKER_BEGIN) === -1) {
|
|
log.add('no hosts entries to remove');
|
|
return true;
|
|
}
|
|
if (!hostsWritable()) {
|
|
log.add('! ' + HOSTS_PATH + ' is not writable, entries stay until reboot');
|
|
return false;
|
|
}
|
|
fs.writeFileSync(HOSTS_PATH, stripHostsBlock(content));
|
|
log.add('removed update hostnames from ' + HOSTS_PATH);
|
|
return true;
|
|
}
|
|
|
|
/* ------------------------------------------------------- staged firmware */
|
|
|
|
function rmrf(target) {
|
|
if (target.indexOf(CACHE_GUARD) !== 0) throw new Error('refusing to delete ' + target);
|
|
var stat;
|
|
try {
|
|
stat = fs.lstatSync(target);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
if (stat.isDirectory()) {
|
|
fs.readdirSync(target).forEach(function (entry) {
|
|
rmrf(path.join(target, entry));
|
|
});
|
|
fs.rmdirSync(target);
|
|
} else {
|
|
fs.unlinkSync(target);
|
|
}
|
|
}
|
|
|
|
function dirBytes(target, depth) {
|
|
var total = 0;
|
|
var stat;
|
|
try {
|
|
stat = fs.lstatSync(target);
|
|
} catch (err) {
|
|
return 0;
|
|
}
|
|
if (!stat.isDirectory()) return stat.size;
|
|
if (depth > 8) return 0;
|
|
fs.readdirSync(target).forEach(function (entry) {
|
|
total += dirBytes(path.join(target, entry), depth + 1);
|
|
});
|
|
return total;
|
|
}
|
|
|
|
function cacheStatus() {
|
|
return CACHE_DIRS.filter(isDirectory).map(function (dir) {
|
|
var entries = [];
|
|
try {
|
|
entries = fs.readdirSync(dir);
|
|
} catch (err) {
|
|
entries = [];
|
|
}
|
|
return {
|
|
path: dir,
|
|
entries: entries,
|
|
bytes: dirBytes(dir, 0),
|
|
locked: isMountPoint(dir)
|
|
};
|
|
});
|
|
}
|
|
|
|
function purgeCache(log) {
|
|
var dirs = CACHE_DIRS.filter(isDirectory);
|
|
if (!dirs.length) {
|
|
log.add('no firmware staging directory present on this TV');
|
|
return;
|
|
}
|
|
dirs.forEach(function (dir) {
|
|
if (isMountPoint(dir)) {
|
|
log.add(dir + ' is locked (empty read-only mount), nothing staged');
|
|
return;
|
|
}
|
|
var entries;
|
|
try {
|
|
entries = fs.readdirSync(dir);
|
|
} catch (err) {
|
|
log.add('! cannot read ' + dir + ': ' + err.message);
|
|
return;
|
|
}
|
|
if (!entries.length) {
|
|
log.add(dir + ' is already empty');
|
|
return;
|
|
}
|
|
var removed = 0;
|
|
entries.forEach(function (entry) {
|
|
try {
|
|
rmrf(path.join(dir, entry));
|
|
removed += 1;
|
|
} catch (err) {
|
|
log.add('! could not delete ' + entry + ' in ' + dir + ': ' + err.message);
|
|
}
|
|
});
|
|
log.add('purged ' + removed + '/' + entries.length + ' staged item(s) from ' + dir);
|
|
});
|
|
}
|
|
|
|
function lockCache(log) {
|
|
var dirs = CACHE_DIRS.filter(isDirectory);
|
|
if (!dirs.length) {
|
|
log.add('no firmware staging directory to lock');
|
|
return;
|
|
}
|
|
mkdirp(EMPTY_DIR);
|
|
dirs.forEach(function (dir) {
|
|
if (isMountPoint(dir)) {
|
|
log.add(dir + ' is already locked');
|
|
return;
|
|
}
|
|
var res = sh('mount -o bind,ro ' + EMPTY_DIR + ' ' + dir);
|
|
if (!res.ok) {
|
|
log.add('! could not lock ' + dir + ': ' + res.output);
|
|
return;
|
|
}
|
|
// Older busybox mount ignores "ro" on the initial bind, so remount.
|
|
sh('mount -o bind,remount,ro ' + EMPTY_DIR + ' ' + dir);
|
|
log.add('locked ' + dir + ' (empty, read-only)');
|
|
});
|
|
}
|
|
|
|
function unlockCache(log) {
|
|
CACHE_DIRS.forEach(function (dir) {
|
|
if (!isMountPoint(dir)) return;
|
|
var res = sh('umount ' + dir);
|
|
log.add(res.ok ? 'unlocked ' + dir : '! could not unlock ' + dir + ': ' + res.output);
|
|
});
|
|
}
|
|
|
|
/* ------------------------------------------------------------ update popup */
|
|
|
|
/* 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 {
|
|
fs.unlinkSync(WATCHER_PID_PATH);
|
|
} catch (err) {
|
|
/* already gone */
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function startWatcher(log) {
|
|
if (watcherPid()) {
|
|
log.add('alert watcher already running');
|
|
return;
|
|
}
|
|
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();
|
|
});
|
|
}
|
|
|
|
/* --------------------------------------------------------------- settings */
|
|
|
|
function callLuna(uri, params, callback) {
|
|
var done = false;
|
|
var timer = setTimeout(function () {
|
|
if (done) return;
|
|
done = true;
|
|
callback({ returnValue: false, errorText: 'timed out' });
|
|
}, 4000);
|
|
|
|
function finish(payload) {
|
|
if (done) return;
|
|
done = true;
|
|
clearTimeout(timer);
|
|
callback(payload);
|
|
}
|
|
|
|
try {
|
|
service.call(uri, params || {}, function (message) {
|
|
finish((message && message.payload) || {});
|
|
});
|
|
} catch (err) {
|
|
finish({ returnValue: false, errorText: err.message });
|
|
}
|
|
}
|
|
|
|
/** Value that turns a setting off, or null when it is off/not a toggle. */
|
|
function disabledValueFor(value) {
|
|
if (value === true) return false;
|
|
if (typeof value === 'string') {
|
|
var normalized = value.toLowerCase();
|
|
if (normalized === 'on') return 'off';
|
|
if (normalized === 'true') return 'false';
|
|
if (normalized === 'yes') return 'no';
|
|
if (normalized === 'enable' || normalized === 'enabled') return 'disable';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function discoverSettings(callback) {
|
|
var found = [];
|
|
var index = 0;
|
|
|
|
function next() {
|
|
if (index >= SETTINGS_CATEGORIES.length) {
|
|
callback(found);
|
|
return;
|
|
}
|
|
var category = SETTINGS_CATEGORIES[index];
|
|
index += 1;
|
|
callLuna('luna://com.webos.settingsservice/getSystemSettings', { category: category }, function (payload) {
|
|
var settings = (payload && payload.settings) || {};
|
|
Object.keys(settings).forEach(function (key) {
|
|
if (!isUpdateKey(key)) return;
|
|
found.push({
|
|
category: category,
|
|
key: key,
|
|
value: settings[key],
|
|
canDisable: disabledValueFor(settings[key]) !== null
|
|
});
|
|
});
|
|
next();
|
|
});
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
function disableSettings(log, callback) {
|
|
discoverSettings(function (found) {
|
|
var targets = found.filter(function (entry) {
|
|
return entry.canDisable;
|
|
});
|
|
|
|
if (!found.length) {
|
|
log.add('no update-related system settings exposed by this firmware');
|
|
callback();
|
|
return;
|
|
}
|
|
if (!targets.length) {
|
|
log.add('update-related settings are already off: ' + found.map(function (e) {
|
|
return e.key + '=' + JSON.stringify(e.value);
|
|
}).join(', '));
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
var backup = readJson(SETTINGS_BACKUP_PATH, {});
|
|
targets.forEach(function (entry) {
|
|
var id = entry.category + '.' + entry.key;
|
|
if (!(id in backup)) backup[id] = entry.value;
|
|
});
|
|
try {
|
|
writeJson(SETTINGS_BACKUP_PATH, backup);
|
|
} catch (err) {
|
|
log.add('! could not save settings backup: ' + err.message);
|
|
}
|
|
|
|
var index = 0;
|
|
function next() {
|
|
if (index >= targets.length) {
|
|
callback();
|
|
return;
|
|
}
|
|
var entry = targets[index];
|
|
index += 1;
|
|
var settings = {};
|
|
settings[entry.key] = disabledValueFor(entry.value);
|
|
callLuna(
|
|
'luna://com.webos.settingsservice/setSystemSettings',
|
|
{ category: entry.category, settings: settings },
|
|
function (payload) {
|
|
if (payload && payload.returnValue) {
|
|
log.add('turned off ' + entry.category + '/' + entry.key +
|
|
' (' + JSON.stringify(entry.value) + ' -> ' + JSON.stringify(settings[entry.key]) + ')');
|
|
} else {
|
|
log.add('! could not change ' + entry.category + '/' + entry.key + ': ' +
|
|
((payload && payload.errorText) || 'unknown error'));
|
|
}
|
|
next();
|
|
}
|
|
);
|
|
}
|
|
next();
|
|
});
|
|
}
|
|
|
|
function restoreSettings(log, callback) {
|
|
var backup = readJson(SETTINGS_BACKUP_PATH, null);
|
|
if (!backup || !Object.keys(backup).length) {
|
|
log.add('no system settings to restore');
|
|
callback();
|
|
return;
|
|
}
|
|
var ids = Object.keys(backup);
|
|
var index = 0;
|
|
function next() {
|
|
if (index >= ids.length) {
|
|
try {
|
|
fs.unlinkSync(SETTINGS_BACKUP_PATH);
|
|
} catch (err) {
|
|
/* nothing to clean up */
|
|
}
|
|
callback();
|
|
return;
|
|
}
|
|
var id = ids[index];
|
|
index += 1;
|
|
var split = id.indexOf('.');
|
|
var category = id.slice(0, split);
|
|
var key = id.slice(split + 1);
|
|
var settings = {};
|
|
settings[key] = backup[id];
|
|
callLuna(
|
|
'luna://com.webos.settingsservice/setSystemSettings',
|
|
{ category: category, settings: settings },
|
|
function (payload) {
|
|
log.add((payload && payload.returnValue ? 'restored ' : '! could not restore ') +
|
|
category + '/' + key + ' = ' + JSON.stringify(backup[id]));
|
|
next();
|
|
}
|
|
);
|
|
}
|
|
next();
|
|
}
|
|
|
|
/* -------------------------------------------------------------- boot hook */
|
|
|
|
/* The updater override lives on a persistent partition, so unlike the other
|
|
* layers it normally survives a reboot on its own. The hook only puts it back
|
|
* if something removed it - a firmware update, or an app install that rewrote
|
|
* the volatile manifest directory. */
|
|
function updaterHookLines(config) {
|
|
var base = config.blockUpdater ? findUpdaterManifest() : null;
|
|
var target = base && overridePath(base);
|
|
if (!target) return [];
|
|
return [
|
|
'UPDATER_MANIFEST=' + target,
|
|
'UPDATER_BIN=' + updaterBinary(base),
|
|
'',
|
|
'if [ ! -f "$UPDATER_MANIFEST" ]; then',
|
|
' mkdir -p "$(dirname "$UPDATER_MANIFEST")"',
|
|
' cat > "$UPDATER_MANIFEST" <<\'LGUB_MANIFEST\'',
|
|
JSON.stringify(buildOverride(base), null, 2),
|
|
'LGUB_MANIFEST',
|
|
' ls-control scan-services >/dev/null 2>&1',
|
|
' echo "restored updater override $UPDATER_MANIFEST"',
|
|
'fi',
|
|
'# it cannot be launched any more, but kill one that slipped through',
|
|
'for proc in /proc/[0-9]*; do',
|
|
' [ -r "$proc/cmdline" ] || continue',
|
|
' case "$(tr \'\\0\' \' \' < "$proc/cmdline")" in',
|
|
' "$UPDATER_BIN "*) kill "${proc#/proc/}" 2>/dev/null && echo "stopped $UPDATER_BIN" ;;',
|
|
' esac',
|
|
'done',
|
|
''
|
|
];
|
|
}
|
|
|
|
function bootHookScript(config, domains) {
|
|
return [
|
|
'#!/bin/sh',
|
|
'# LG Update Blocker boot hook - generated by ' + pkgInfo.name + ' v' + pkgInfo.version + '.',
|
|
'# Regenerated every time the settings are applied; delete it to disable.',
|
|
'',
|
|
'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),
|
|
'CACHE_DIRS="' + CACHE_DIRS.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 ---"',
|
|
''
|
|
].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 ! (: >> /etc/hosts) 2>/dev/null; then',
|
|
' cp /etc/hosts "$HOSTS_TMP" && chmod 644 "$HOSTS_TMP" \\',
|
|
' && mount --bind "$HOSTS_TMP" /etc/hosts \\',
|
|
' && echo "bind-mounted writable /etc/hosts"',
|
|
' fi',
|
|
' if grep -q "lg-update-blocker" /etc/hosts 2>/dev/null; then',
|
|
' echo "hosts entries already present"',
|
|
' elif (: >> /etc/hosts) 2>/dev/null; then',
|
|
' {',
|
|
' echo "' + MARKER_BEGIN + '"',
|
|
' sed -e "s/#.*//" -e "s/[[:space:]]*$//" -e "/^$/d" "$STATE/hosts.txt" | while read -r domain; do',
|
|
' echo "127.0.0.1 $domain"',
|
|
' echo "::1 $domain"',
|
|
' done',
|
|
' echo "' + MARKER_END + '"',
|
|
' } >> /etc/hosts && echo "hosts entries added"',
|
|
' else',
|
|
' echo "could not make /etc/hosts writable"',
|
|
' fi',
|
|
'fi',
|
|
'',
|
|
'if [ "$PURGE_CACHE" = 1 ]; then',
|
|
' for dir in $CACHE_DIRS; do',
|
|
' [ -d "$dir" ] || continue',
|
|
' case "$dir" in /mnt/lg/*) ;; *) continue ;; esac',
|
|
' entries=$(ls -A "$dir" 2>/dev/null)',
|
|
' [ -n "$entries" ] || continue',
|
|
' rm -rf "$dir"/* "$dir"/.[!.]* 2>/dev/null',
|
|
' echo "purged staged item(s) from $dir: $(echo "$entries" | tr \'\\n\' \' \')"',
|
|
' done',
|
|
'fi',
|
|
'',
|
|
'if [ "$LOCK_CACHE" = 1 ]; then',
|
|
' mkdir -p "$STATE/empty"',
|
|
' for dir in $CACHE_DIRS; do',
|
|
' [ -d "$dir" ] || continue',
|
|
' case "$dir" in /mnt/lg/*) ;; *) continue ;; esac',
|
|
' if awk \'{print $2}\' /proc/mounts | grep -qx "$dir"; then',
|
|
' echo "$dir already locked"',
|
|
' continue',
|
|
' fi',
|
|
' if mount -o bind,ro "$STATE/empty" "$dir"; then',
|
|
' mount -o bind,remount,ro "$STATE/empty" "$dir" 2>/dev/null',
|
|
' echo "locked $dir"',
|
|
' else',
|
|
' echo "could not lock $dir"',
|
|
' fi',
|
|
' done',
|
|
'fi',
|
|
'',
|
|
'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');
|
|
}
|
|
|
|
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(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) {
|
|
var removed = [BOOT_HOOK_PATH, LEGACY_BOOT_HOOK_PATH].filter(function (target) {
|
|
if (!exists(target)) return false;
|
|
try {
|
|
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 */
|
|
|
|
function baseStatus() {
|
|
return {
|
|
version: pkgInfo.version,
|
|
uid: typeof process.getuid === 'function' ? process.getuid() : -1,
|
|
root: isRoot(),
|
|
config: readConfig(),
|
|
updater: updaterStatus(),
|
|
hosts: {
|
|
path: HOSTS_PATH,
|
|
writable: hostsWritable(),
|
|
bindMounted: isMountPoint(HOSTS_PATH),
|
|
blockedDomains: hostsBlockedDomains(),
|
|
totalDomains: readDomains().length
|
|
},
|
|
cache: cacheStatus(),
|
|
popup: {
|
|
lastAlertId: lastAlertId(),
|
|
watcherPid: watcherPid()
|
|
},
|
|
bootHook: {
|
|
path: BOOT_HOOK_PATH,
|
|
installed: exists(BOOT_HOOK_PATH)
|
|
}
|
|
};
|
|
}
|
|
|
|
function respondError(message, text) {
|
|
message.respond({ returnValue: false, errorText: text });
|
|
}
|
|
|
|
service.register('status', function (message) {
|
|
var status = baseStatus();
|
|
discoverSettings(function (settings) {
|
|
status.settings = settings;
|
|
status.returnValue = true;
|
|
message.respond(status);
|
|
});
|
|
});
|
|
|
|
service.register('apply', function (message) {
|
|
if (!isRoot()) {
|
|
respondError(message, 'Service is not running as root - elevate it through the Homebrew Channel first.');
|
|
return;
|
|
}
|
|
|
|
var log = new Log();
|
|
var config = mergeConfig(readConfig(), message.payload || {});
|
|
|
|
try {
|
|
mkdirp(STATE_DIR);
|
|
writeJson(CONFIG_PATH, config);
|
|
|
|
/* first: with the updater gone there is no version check to lose a race to */
|
|
if (config.blockUpdater) {
|
|
blockUpdater(log);
|
|
} else {
|
|
unblockUpdater(log);
|
|
}
|
|
|
|
if (config.blockHosts) {
|
|
applyHosts(log);
|
|
} else {
|
|
removeHosts(log);
|
|
}
|
|
|
|
if (config.purgeCache) purgeCache(log);
|
|
|
|
if (config.lockCache) {
|
|
lockCache(log);
|
|
} else {
|
|
unlockCache(log);
|
|
}
|
|
|
|
if (anyEnabled(config)) {
|
|
installBootHook(config, log);
|
|
} else {
|
|
removeBootHook(log);
|
|
}
|
|
} catch (err) {
|
|
log.add('! ' + err.message);
|
|
message.respond({ returnValue: false, errorText: err.message, log: log.lines, config: config });
|
|
return;
|
|
}
|
|
|
|
function finish() {
|
|
log.add('done - reboot the TV to verify the popup is gone');
|
|
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) {
|
|
disableSettings(log, afterSettings);
|
|
} else {
|
|
restoreSettings(log, afterSettings);
|
|
}
|
|
});
|
|
|
|
service.register('revert', function (message) {
|
|
if (!isRoot()) {
|
|
respondError(message, 'Service is not running as root - elevate it through the Homebrew Channel first.');
|
|
return;
|
|
}
|
|
|
|
var log = new Log();
|
|
var config = {};
|
|
CONFIG_KEYS.forEach(function (key) {
|
|
config[key] = false;
|
|
});
|
|
|
|
try {
|
|
unblockUpdater(log);
|
|
removeHosts(log);
|
|
unlockCache(log);
|
|
if (stopWatcher()) log.add('alert watcher stopped');
|
|
removeBootHook(log);
|
|
mkdirp(STATE_DIR);
|
|
writeJson(CONFIG_PATH, config);
|
|
} catch (err) {
|
|
log.add('! ' + err.message);
|
|
}
|
|
|
|
restoreSettings(log, function () {
|
|
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() });
|
|
});
|
|
});
|
|
|
|
service.register('purge', function (message) {
|
|
if (!isRoot()) {
|
|
respondError(message, 'Service is not running as root - elevate it through the Homebrew Channel first.');
|
|
return;
|
|
}
|
|
var log = new Log();
|
|
purgeCache(log);
|
|
message.respond({ returnValue: true, log: log.lines, status: baseStatus() });
|
|
});
|
|
|
|
service.register('diagnostics', function (message) {
|
|
var status = baseStatus();
|
|
discoverSettings(function (settings) {
|
|
message.respond({
|
|
returnValue: true,
|
|
status: status,
|
|
settings: settings,
|
|
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;
|
|
})
|
|
});
|
|
});
|
|
});
|
|
|
|
console.info(pkgInfo.name + ' v' + pkgInfo.version + ' started, uid=' +
|
|
(typeof process.getuid === 'function' ? process.getuid() : 'unknown'));
|