@@ -0,0 +1,871 @@
|
||||
/**
|
||||
* LG Update Blocker - root service.
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* / 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).
|
||||
*
|
||||
* 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';
|
||||
var BOOT_HOOK_PATH = BOOT_HOOK_DIR + '/' + SLUG;
|
||||
|
||||
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/';
|
||||
|
||||
var UPSTART_DIR = '/etc/init';
|
||||
var JOB_PATTERN = /(swupdate|softwareupdate|firmware|fota|update|upgrade|nsu)/i;
|
||||
|
||||
var SETTINGS_CATEGORIES = ['option', 'general', 'network', 'commercial'];
|
||||
var SETTINGS_KEY_PATTERN = /(update|upgrade|firmware|ota)/i;
|
||||
|
||||
var DEFAULT_CONFIG = {
|
||||
blockHosts: true,
|
||||
purgeCache: true,
|
||||
lockCache: false,
|
||||
disableSettings: true,
|
||||
stopServices: false
|
||||
};
|
||||
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];
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ 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);
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- upstart jobs */
|
||||
|
||||
function discoverJobs() {
|
||||
var entries;
|
||||
try {
|
||||
entries = fs.readdirSync(UPSTART_DIR);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
.filter(function (entry) {
|
||||
return /\.conf$/.test(entry) && JOB_PATTERN.test(entry);
|
||||
})
|
||||
.map(function (entry) {
|
||||
return entry.replace(/\.conf$/, '');
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
function stopJobs(jobs, log) {
|
||||
if (!jobs.length) {
|
||||
log.add('no update-related upstart jobs found');
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- 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 (!SETTINGS_KEY_PATTERN.test(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 */
|
||||
|
||||
function bootHookScript(config, domains, jobs) {
|
||||
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,
|
||||
'BLOCK_HOSTS=' + (config.blockHosts ? 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 ---"',
|
||||
'',
|
||||
'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',
|
||||
'',
|
||||
'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) {
|
||||
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.chmodSync(BOOT_HOOK_PATH, parseInt('755', 8));
|
||||
log.add('boot hook installed at ' + BOOT_HOOK_PATH);
|
||||
}
|
||||
|
||||
function removeBootHook(log) {
|
||||
if (!exists(BOOT_HOOK_PATH)) {
|
||||
log.add('no boot hook installed');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(BOOT_HOOK_PATH);
|
||||
log.add('removed boot hook ' + BOOT_HOOK_PATH);
|
||||
} catch (err) {
|
||||
log.add('! could not remove boot hook: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- methods */
|
||||
|
||||
function baseStatus() {
|
||||
return {
|
||||
version: pkgInfo.version,
|
||||
uid: typeof process.getuid === 'function' ? process.getuid() : -1,
|
||||
root: isRoot(),
|
||||
config: readConfig(),
|
||||
hosts: {
|
||||
path: HOSTS_PATH,
|
||||
writable: hostsWritable(),
|
||||
bindMounted: isMountPoint(HOSTS_PATH),
|
||||
blockedDomains: hostsBlockedDomains(),
|
||||
totalDomains: readDomains().length
|
||||
},
|
||||
cache: cacheStatus(),
|
||||
jobs: discoverJobs(),
|
||||
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);
|
||||
|
||||
if (config.blockHosts) {
|
||||
applyHosts(log);
|
||||
} else {
|
||||
removeHosts(log);
|
||||
}
|
||||
|
||||
if (config.purgeCache) purgeCache(log);
|
||||
|
||||
if (config.lockCache) {
|
||||
lockCache(log);
|
||||
} else {
|
||||
unlockCache(log);
|
||||
}
|
||||
|
||||
var jobs = discoverJobs();
|
||||
if (config.stopServices) {
|
||||
stopJobs(jobs, log);
|
||||
}
|
||||
|
||||
if (anyEnabled(config)) {
|
||||
installBootHook(config, config.stopServices ? jobs : [], 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() });
|
||||
}
|
||||
|
||||
if (config.disableSettings) {
|
||||
disableSettings(log, finish);
|
||||
} else {
|
||||
restoreSettings(log, finish);
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
removeHosts(log);
|
||||
unlockCache(log);
|
||||
removeBootHook(log);
|
||||
mkdirp(STATE_DIR);
|
||||
writeJson(CONFIG_PATH, config);
|
||||
} catch (err) {
|
||||
log.add('! ' + err.message);
|
||||
}
|
||||
|
||||
restoreSettings(log, function () {
|
||||
log.add('everything reverted; stopped services come back after a reboot');
|
||||
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'),
|
||||
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'));
|
||||
Reference in New Issue
Block a user