'use strict';
'require dom';
'require form';
'require fs';
'require poll';
'require rpc';
'require view';
const DEFAULT_CONFIG_FILE = '/etc/adguardhome/adguardhome.yaml';
const DEFAULT_WORK_DIR = '/var/lib/adguardhome';
const DEFAULT_USER = 'adguardhome';
const DEFAULT_GROUP = DEFAULT_USER;
const DEFAULT_GOGC = '0';
const DEFAULT_GOMAXPROCS = '0';
const DEFAULT_GOMEMLIMIT = '0';
const PATH_REGEX = new RegExp('^/etc(/[^/]+)?/?$');
const POLL_INTERVAL = 5;
const RUNNING_SPAN = `${_('Running')}`;
const NOT_RUNNING_SPAN = `${_('Not running')}`;
const STORAGE_KEY = 'luci-app-adguardhome';
function getServiceInfo(name) {
const fn = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { [name]: { instances: { [name]: {} }}},
});
return () => fn(name);
}
const getAGHServiceInfo = getServiceInfo('adguardhome');
async function getStatus() {
try {
const res = await getAGHServiceInfo();
const isRunning = res?.instances?.adguardhome?.running;
return isRunning ?? false;
} catch (e) {
console.error(e);
return false;
}
}
function getStatusValue(isRunning) {
return isRunning ? RUNNING_SPAN : NOT_RUNNING_SPAN;
}
async function getVersion() {
try {
const res = await fs.exec('/usr/bin/AdGuardHome', ['--version']);
const version = res.stdout
? (res.stdout.match(/version\s+(.*)/) || [null, res.stdout.trim()])[1]
: '';
return version;
} catch (e) {
console.error(e);
return 'unknown version';
}
}
function updateStatus(node) {
const output = node?.querySelector('output');
return output
? async () => {
const isRunning = await getStatus();
dom.content(output, getStatusValue(isRunning));
}
: () => {};
}
function validateConfigFile(_unused, value) {
if (value == null || value === '') {
return true;
}
if (!value.startsWith('/')) {
return _('Path must be absolute.');
}
if (value.endsWith('/')) {
return _('Path must not end with a slash.');
}
if (PATH_REGEX.test(value)) {
return _('Configuration file must be stored in its own directory, and not in \'/etc\'.');
}
return true;
}
function validateWorkDir(_unused, value) {
if (value == null || value === '') {
return true;
}
if (!value.startsWith('/')) {
return _('Path must be absolute.');
}
return true;
}
return view.extend({
load() {
return Promise.all([
getStatus(),
getVersion(),
]);
},
async render([isRunning, version]) {
const map = new form.Map('adguardhome', _('AdGuard Home'));
const statusSect = map.section(form.TypedSection, 'status');
statusSect.anonymous = true;
statusSect.cfgsections = () => ['status_section'];
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
versionOpt.cfgvalue = () => version;
const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status'));
statusOpt.rawhtml = true;
statusOpt.cfgvalue = () => getStatusValue(isRunning);
const mainSect = map.section(form.TypedSection, 'adguardhome');
mainSect.anonymous = true;
mainSect.tab('general', _('General Settings'));
mainSect.tab(
'jail',
_('File System Access'),
_('Files and directories that AdGuard Home should have read-only or read-write access to.'),
);
mainSect.tab(
'advanced',
_('Advanced Settings'),
_('Go environment variables that tune garbage collector and memory management.') +
' ' + _('Modify at your own risk.'),
);
const configFileOpt = mainSect.taboption(
'general',
form.Value,
'config_file',
_('Configuration file'),
_('Configuration file must be stored in its own directory, and not in \'/etc\'.') +
'
' + _('Parent directory will be owned by the service user.') +
'
' + _('If empty, defaults to') + ` '${DEFAULT_CONFIG_FILE}'.`,
);
configFileOpt.placeholder = DEFAULT_CONFIG_FILE;
configFileOpt.validate = validateConfigFile;
const workDirOpt = mainSect.taboption(
'general',
form.Value,
'work_dir',
_('Working directory'),
_('Directory where filters, logs, and statistics are stored.') +
'
' + _('Will be owned by the service user.') +
'
' + _('If empty, defaults to') + ` '${DEFAULT_WORK_DIR}'.`,
);
workDirOpt.placeholder = DEFAULT_WORK_DIR;
workDirOpt.validate = validateWorkDir;
const userOpt = mainSect.taboption(
'general',
form.Value,
'user',
_('Service user'),
_('User the service runs under.') + ' ' + _('If empty, defaults to') +
` '${DEFAULT_USER}'.`,
);
userOpt.placeholder = DEFAULT_USER;
const groupOpt = mainSect.taboption(
'general',
form.Value,
'group',
_('Service group'),
_('Group the service runs under.') + ' ' + _('If empty, defaults to') +
` '${DEFAULT_GROUP}'.`,
);
groupOpt.placeholder = DEFAULT_GROUP;
const verboseOpt = mainSect.taboption(
'general',
form.Flag,
'verbose',
_('Verbose logging'),
);
verboseOpt.default = '0';
const advSettingsOpt = mainSect.taboption(
'general',
form.Flag,
'advanced_settings',
_('Advanced Settings'),
);
advSettingsOpt.default = '0';
advSettingsOpt.rmempty = false;
advSettingsOpt.load = () => sessionStorage.getItem(STORAGE_KEY) || '0';
advSettingsOpt.remove = () => {};
advSettingsOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY, value);
mainSect.taboption('jail', form.DynamicList, 'jail_mount', _('Read-only access'));
mainSect.taboption('jail', form.DynamicList, 'jail_mount_rw', _('Read-write access'));
const gcOpt = mainSect.taboption(
'advanced',
form.Value,
'gc',
'GOGC',
_('Tunes the garbage collector\'s aggressiveness by setting the percentage of heap ' +
'growth allowed before the next collection cycle triggers.') + '
' +
_('If empty, defaults to') + ' ' + _('unset and 100') + '.',
'https://go.dev/doc/gc-guide#GOGC'
);
gcOpt.datatype = 'uinteger';
gcOpt.depends('advanced_settings', '1');
gcOpt.placeholder = DEFAULT_GOGC;
gcOpt.retain = true;
const maxProcsOpt = mainSect.taboption(
'advanced',
form.Value,
'maxprocs',
'GOMAXPROCS',
_('The maximum number of operating system threads that can execute user-level Go code' +
' simultaneously.') + '
' +
_('If empty, defaults to') + ' ' + _('unset and matching the number of CPUs') + '.',
);
maxProcsOpt.datatype = 'uinteger';
maxProcsOpt.depends('advanced_settings', '1');
maxProcsOpt.placeholder = DEFAULT_GOMAXPROCS;
maxProcsOpt.retain = true;
const memLimitOpt = mainSect.taboption(
'advanced',
form.Value,
'memlimit',
'GOMEMLIMIT',
_('A soft memory cap for the Go runtime, allowing the garbage collector to run more ' +
'frequently as usage approaches the limit to prevent Out-of-Memory (OOM) kills.') +
'
' +
_('If empty, defaults to') + ' ' + _('unset') + '.',
);
memLimitOpt.datatype = 'uinteger';
memLimitOpt.depends('advanced_settings', '1');
memLimitOpt.placeholder = DEFAULT_GOMEMLIMIT;
memLimitOpt.retain = true;
const rendered = await map.render();
const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
poll.add(updateStatus(statusNode), POLL_INTERVAL);
return rendered;
},
});