/** * LG Update Blocker - frontend. * * No frameworks and no build step: Luna calls go straight through * PalmServiceBridge, which every webOS webapp gets injected for free. */ (function () { 'use strict'; var SERVICE = 'luna://__SERVICE_ID__'; var HBCHANNEL = 'luna://org.webosbrew.hbchannel.service'; var HBCHANNEL_ELEVATE = '/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service'; var TOGGLES = [ { key: 'blockHosts', 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: 'purgeCache', title: 'Delete staged firmware', desc: 'Wipes an already downloaded update - the usual reason the popup keeps returning' }, { key: 'lockCache', title: 'Lock the staging folder', desc: 'Mounts an empty read-only folder over it so no update can be staged again' }, { 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' } ]; var ACTIONS = [ { key: 'apply', label: 'Apply protection', primary: true }, { key: 'purge', label: 'Purge staged update' }, { key: 'revert', label: 'Remove protection' }, { key: 'refresh', label: 'Refresh status' }, { key: 'diagnostics', label: 'Diagnostics' } ]; var config = { blockHosts: true, purgeCache: true, lockCache: false, disableSettings: true, stopServices: false }; var focusables = []; var focusIndex = 0; var busy = false; var bridges = []; /* ----------------------------------------------------------- luna calls */ function luna(uri, params, onSuccess, onFailure) { if (typeof PalmServiceBridge === 'undefined') { if (onFailure) onFailure({ returnValue: false, errorText: 'no Luna bus (not running on a TV)' }); return; } var bridge = new PalmServiceBridge(); bridges.push(bridge); bridge.onservicecallback = function (raw) { var index = bridges.indexOf(bridge); if (index !== -1) bridges.splice(index, 1); var response; try { response = JSON.parse(raw); } catch (err) { response = { returnValue: false, errorText: 'malformed response: ' + raw }; } if (response.returnValue === false || response.errorText) { if (onFailure) onFailure(response); } else if (onSuccess) { onSuccess(response); } }; bridge.call(uri, JSON.stringify(params || {})); } /* ------------------------------------------------------------------- ui */ function el(id) { return document.getElementById(id); } function log(text) { var pane = el('log'); var stamp = new Date().toTimeString().slice(0, 8); pane.textContent += '[' + stamp + '] ' + text + '\n'; pane.scrollTop = pane.scrollHeight; } function logLines(lines) { (lines || []).forEach(function (line) { log(line); }); } function setHint(text, isBusy) { var hint = el('hint'); hint.textContent = text || ''; hint.className = isBusy ? 'busy' : ''; } function setBusy(state, text) { busy = state; setHint(text || (state ? 'working…' : ''), state); } function bytes(value) { if (!value) return '0 B'; var units = ['B', 'kB', 'MB', 'GB']; var index = 0; var size = value; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } return (index === 0 ? size : size.toFixed(1)) + ' ' + units[index]; } function buildUi() { var toggles = el('toggles'); TOGGLES.forEach(function (toggle) { var row = document.createElement('div'); row.className = 'row'; row.innerHTML = '
' + '
'; row.querySelector('.title').textContent = toggle.title; row.querySelector('.desc').textContent = toggle.desc; row.dataset.toggle = toggle.key; row.onActivate = function () { config[toggle.key] = !config[toggle.key]; renderToggles(); log((config[toggle.key] ? 'enabled: ' : 'disabled: ') + toggle.title + ' (press Apply protection to write it to the TV)'); }; toggles.appendChild(row); focusables.push(row); }); var actions = el('actions'); ACTIONS.forEach(function (action) { var button = document.createElement('div'); button.className = 'button' + (action.primary ? ' primary' : ''); button.textContent = action.label; button.onActivate = function () { runAction(action.key); }; actions.appendChild(button); focusables.push(button); }); renderToggles(); renderFocus(); } function renderToggles() { TOGGLES.forEach(function (toggle, index) { var row = focusables[index]; var on = !!config[toggle.key]; row.className = 'row' + (on ? ' on' : '') + (index === focusIndex ? ' focused' : ''); row.querySelector('.state').textContent = on ? 'ON' : 'OFF'; }); } function renderFocus() { focusables.forEach(function (node, index) { var focused = index === focusIndex; if (focused) { if (node.className.indexOf('focused') === -1) node.className += ' focused'; if (node.scrollIntoView) node.scrollIntoView(false); } else { node.className = node.className.replace(/\s*focused/g, ''); } }); } function move(delta) { focusIndex = (focusIndex + delta + focusables.length) % focusables.length; renderFocus(); } function badge(id, text, kind) { var node = el(id); node.textContent = text; node.className = 'badge' + (kind ? ' badge-' + kind : ''); } function renderStatus(status) { var list = el('status'); list.innerHTML = ''; function row(label, value) { var wrapper = document.createElement('div'); var dt = document.createElement('dt'); var dd = document.createElement('dd'); dt.textContent = label; dd.textContent = value; wrapper.appendChild(dt); wrapper.appendChild(dd); list.appendChild(wrapper); } var staged = 0; var stagedDirs = 0; var locked = 0; (status.cache || []).forEach(function (entry) { stagedDirs += 1; staged += entry.bytes || 0; if (entry.locked) locked += 1; }); row('Service user', status.root ? 'root' : 'uid ' + status.uid + ' (not elevated)'); row('Hosts entries', status.hosts.blockedDomains + ' / ' + status.hosts.totalDomains + (status.hosts.bindMounted ? ' (bind-mounted)' : '')); 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'); row('Boot hook', status.bootHook.installed ? 'installed' : 'not installed'); var settings = status.settings || []; if (settings.length) { settings.forEach(function (entry) { row(entry.category + '/' + entry.key, JSON.stringify(entry.value)); }); } else { row('Update settings', 'none exposed'); } badge('badge-root', status.root ? 'root' : 'no root', status.root ? 'ok' : 'bad'); badge('badge-version', 'v' + status.version); if (status.config) { Object.keys(config).forEach(function (key) { if (typeof status.config[key] === 'boolean') config[key] = status.config[key]; }); renderToggles(); } } /* -------------------------------------------------------------- actions */ function refresh(onDone) { luna(SERVICE + '/status', {}, function (status) { renderStatus(status); if (onDone) onDone(status); }, function (err) { log('status failed: ' + (err.errorText || JSON.stringify(err))); badge('badge-root', 'service error', 'bad'); if (onDone) onDone(null); }); } function runAction(key) { if (busy) return; if (key === 'refresh') { setBusy(true, 'refreshing…'); refresh(function () { setBusy(false); log('status refreshed'); }); return; } if (key === 'diagnostics') { setBusy(true, 'collecting diagnostics…'); luna(SERVICE + '/diagnostics', {}, function (res) { setBusy(false); log('--- diagnostics ---'); log('os: ' + JSON.stringify(res.osInfo)); log('mounts: ' + JSON.stringify(res.mounts)); log('settings: ' + JSON.stringify(res.settings)); log('hosts (tail):\n' + res.hostsFile); log('boot hook log (tail):\n' + res.bootLog); log('--- end ---'); }, function (err) { setBusy(false); log('diagnostics failed: ' + (err.errorText || JSON.stringify(err))); }); return; } var uri = SERVICE + '/' + key; var params = key === 'apply' ? config : {}; setBusy(true, key === 'revert' ? 'removing…' : 'applying…'); luna(uri, params, function (res) { setBusy(false); logLines(res.log); if (res.status) renderStatus(res.status); else refresh(); }, function (err) { setBusy(false); logLines(err.log); log('failed: ' + (err.errorText || JSON.stringify(err))); }); } /* ------------------------------------------------------------ elevation */ function elevate(onDone) { log('service is not running as root, asking the Homebrew Channel to elevate it…'); function afterElevation() { // The running instance keeps its old (unprivileged) process; quitting it // makes the bus start a fresh, elevated one on the next call. luna(SERVICE + '/quit', {}, function () { window.setTimeout(recheck, 2500); }, function () { window.setTimeout(recheck, 2500); }); } function recheck() { refresh(function (status) { if (status && status.root) { log('service elevated - ready'); } else { log('still not elevated. Open the Homebrew Channel, make sure "Root status" says ' + '"ok", then relaunch this app.'); } setBusy(false); if (onDone) onDone(); }); } luna(HBCHANNEL + '/elevateService', { id: '__SERVICE_ID__' }, function () { log('elevateService succeeded, restarting our service…'); afterElevation(); }, function (err) { log('elevateService unavailable (' + (err.errorText || 'unknown') + '), trying exec fallback…'); luna(HBCHANNEL + '/exec', { command: HBCHANNEL_ELEVATE + ' __SERVICE_ID__' }, function () { log('elevate-service executed, restarting our service…'); afterElevation(); }, function (execErr) { log('could not elevate: ' + (execErr.errorText || JSON.stringify(execErr))); log('Is the Homebrew Channel installed and rooted? Its service must be elevated first.'); setBusy(false); if (onDone) onDone(); }); }); } /* ---------------------------------------------------------------- input */ document.addEventListener('keydown', function (event) { switch (event.keyCode) { case 38: // up move(-1); break; case 40: // down move(1); break; case 37: // left move(-1); break; case 39: // right move(1); break; case 13: // OK case 32: // space (keyboard/dev) if (!busy && focusables[focusIndex] && focusables[focusIndex].onActivate) { focusables[focusIndex].onActivate(); } break; case 461: // back case 27: // esc window.close(); break; default: return; } event.preventDefault(); }); /* ----------------------------------------------------------------- boot */ buildUi(); log('LG Update Blocker v__VERSION__'); setBusy(true, 'checking service…'); refresh(function (status) { if (!status) { setBusy(false); return; } if (status.root) { setBusy(false); log('service is running as root - ready'); } else { elevate(); } }); })();