--- /dev/null
+'use strict';
+
+import * as libubus from "ubus";
+import * as libuci from "uci";
+import * as fs from "fs";
+
+let ubus = libubus.connect();
+let uci = libuci.cursor();
+
+let script = ARGV[0];
+let command = ARGV[1];
+let parameter = ARGV[2];
+let script_name = fs.basename(script);
+let procd_boot = false;
+
+let help = {
+ "start": "Start the service",
+ "stop": "Stop the service",
+ "restart": "Restart the service",
+ "reload": "Reload configuration files (or restart if service does not implement reload)",
+ "enable": "Enable service autostart",
+ "disable": "Disable service autostart",
+ "enabled": "Check if service is started on boot",
+ "running": "Check if service is running",
+ "status": "Service status",
+ "trace": "Start with syscall trace",
+ "info": "Dump procd service info",
+};
+
+function abort(msg, retval) {
+ retval ??= 1;
+ warn(`${msg}\n`);
+ exit(retval);
+}
+
+function procd_get_network_devices(data) {
+ if (type(data) == 'string')
+ data = [ data ];
+ if (type(data) != 'array')
+ abort('Invalid parameter passed to get_network_devices');
+
+ let networks = [];
+ for (let interface in data) {
+ if (type(interface) != 'string')
+ abort('Invalid network passed to get_network_devices');
+
+ let l3_device = ubus.call('network.interface', 'status', { interface })?.l3_device;
+
+ if (l3_device)
+ push(networks, l3_device);
+ }
+
+ return networks;
+};
+
+function procd_ubus_wait_for(object, timeout) {
+ timeout ??= 5;
+ if (type(object) != 'string')
+ abort('Invalid object while waiting for ubus');
+ if (type(timeout) != 'int')
+ abort('Invalid timeout while waiting for ubus');
+
+ while(timeout--) {
+ let list = ubus.list();
+
+ for (let k, v in list)
+ if (object == v)
+ return;
+ }
+}
+
+function procd_default_respawn() {
+ return [ 3600, 5, 5 ];
+}
+
+function procd_get_mountpoints() {
+
+}
+
+function procd_send_signal(service, instance, signal) {
+ let msg = { service };
+
+ if (instance)
+ msg.instance = instance;
+ if (signal)
+ msg.signal = signal;
+
+ ubus.call('service', 'signal', msg);
+}
+
+function procd_config_reload_trigger(config) {
+ if (type(config) != 'string')
+ abort('Invalid reload trigger');
+
+ return [
+ "config.change",
+ [
+ "if",
+ [
+ "eq", "package", config
+ ], [
+ "run_script", script, "reload"
+ ]
+ ],
+ 1000
+ ];
+}
+
+function procd_interface_trigger(network) {
+ if (type(network) != 'string')
+ abort('Invalid interface trigger');
+
+ return [
+ "interface.*",
+ [
+ "if",
+ [
+ "eq", "interface", network
+ ], [
+ "run_script", script, "reload" ]
+ ],
+ 1000
+ ];
+}
+
+function procd_raw_trigger(event, command, timeout) {
+ timeout ??= 1000;
+ if (type(event) != 'string' || type(command) != 'array' || type(timeout) != 'int')
+ abort('Invalid raw trigger');
+
+ return [
+ event,
+ [
+ [
+ "run_script",
+ ...command,
+ ]
+ ],
+ timeout
+ ];
+}
+
+function procd_update_trigger(path, timeout) {
+ if (type(path) != 'string')
+ abort('Invalid update_trigger path');
+
+ timeout ??= 1000;
+ if (type(timeout) != 'int')
+ abort('Invalid update_trigger timeout');
+
+ return [
+ "instance.update",
+ [
+ [
+ "run_script", ...split(path, ' ')
+ ]
+ ],
+ timeout || 1000
+ ];
+}
+
+function procd_core_dump() {
+ return 'core="unlimited"';
+}
+
+function procd_instance_add(name, data, trace) {
+ if (type(data.command) == 'function')
+ data.command = data.command();
+ if (type(data.command) == 'string')
+ data.command = split(data.command, ' ');
+ if (type(data.command) != 'array')
+ abort('Instance is missing the command line');
+
+ let instance = {
+ command: data.command,
+ triggers: [],
+ };
+
+ if (trace)
+ instance.trace = 1;
+
+ if (data.capabilities) {
+ if (!fs.stat(data.capabilities))
+ abort(`Capabilities file "${data.capabilities}" file is missing`);
+
+ instance.capabilities = data.capabilities;
+ }
+
+ if (data.user) {
+ if (type(data.user) != 'string')
+ abort('Invalid user type');
+
+ instance.user = data.user;
+ }
+
+ if (data.group) {
+ if (type(data.group) != 'string')
+ abort('Invalid group type');
+
+ instance.group = data.group;
+ }
+
+ if (data.seccomp) {
+ if (type(data.seccomp) != 'string')
+ abort('Invalid seccomp type');
+ if (!fs.stat(data.seccomp))
+ abort(`Seccomp file is missing: ${data.seccomp}`);
+
+ instance.seccomp = data.seccomp;
+ }
+
+ if (data.respawn) {
+ if (type(data.respawn) != 'array')
+ abort('Invalid respawn type');
+ if (length(data.respawn) != 3)
+ abort('Invalid respawn values');
+
+ instance.respawn = [];
+ for (let respawn in data.respawn)
+ push(instance.respawn, '' + respawn);
+ }
+
+ if (data.trigger) {
+ if (type(data.trigger) != 'array')
+ abort('Invalid trigger type');
+ for (let trigger in triggers) {
+ if (type(trigger) != 'array')
+ abort('Invalid trigger type');
+
+ push(instance.triggers, trigger);
+ }
+ }
+
+ if (data.update_trigger) {
+ if (type(data.update_trigger) != 'array')
+ abort('Invalid update_trigger type');
+
+ push(instance.triggers, data.update_trigger);
+ }
+
+ if (data.jail && fs.stat('/sbin/ujail')) {
+ instance.jail = { name };
+
+ for (let permission in [ "log", "ubus", "procfs", "sysfs", "ronly", "requirejail", "netns", "userns", "cgroupsns" ])
+ if (permission in data.jail_permissions)
+ instance.jail[permission] = true;
+
+ if (data.jail_mounts) {
+ instance.jail.mounts = {};
+ for (let mount in data.jail_mounts) {
+ if (type(mount) != 'string')
+ abort('Invalid jail mount type');
+
+ instance.jail.mounts[mount] = 0;
+ }
+ }
+
+ data.no_new_privs ??= 1;
+ }
+
+ if (data.no_new_privs)
+ instance.no_new_privs = !!data.no_new_privs;
+
+ return instance;
+}
+
+function service_start(name, data, trace) {
+ if (!data.instances)
+ return;
+
+ let service = {
+ name,
+ script,
+ instances: {},
+ data: {},
+ };
+
+ if (type(data.service_triggers) == 'function')
+ service.triggers = data.service_triggers();
+
+ let instances = data.instances();
+ if (type(instances) == 'object')
+ instances = [ instances ];
+
+ let idx = 1;
+ for (let instance in instances)
+ service.instances[instance.name ?? ('instance' + idx++)] = procd_instance_add(name, instance, trace);
+
+ ubus.call('service', 'set', service);
+
+ //printf('%.J\n', service);
+
+ if (type(data.service_started) == 'function')
+ data.service_started();
+}
+
+function service_stop(name) {
+ ubus.call('service', 'delete', { name });
+}
+
+function service_status(name, verbose) {
+ let service = ubus.call('service', 'list', { name, verbose: true });
+
+ service = service?.[name];
+ if (!service)
+ abort('inactive', 3);
+ if (!length(service.instances))
+ abort('active with no instances', 0);
+
+ let running = 0, stopped = 0, total = 0;
+ for (let name, instance in service.instances) {
+ if (parameter && name != parameter)
+ continue;
+
+ total++;
+ if (instance.running)
+ running++;
+ else
+ stopped++;
+ }
+
+ if (!verbose)
+ exit(!!running);
+
+ if (!total)
+ abort(`unkown instance ${parameter}`, 0);
+ if (running && !stopped)
+ abort('running', 0);
+ if (running)
+ abort(`running ${running}/${total}`, 0);
+ abort('not running', 5);
+}
+
+function procd_service(name, data) {
+ if (type(name) != 'string')
+ abort('Invalid service name');
+
+ switch(command) {
+ case 'boot':
+ procd_boot = true;
+ if (type(data.boot) == 'function')
+ return data.boot();
+ /* fall through */
+
+ case 'start':
+ service_start(name, data);
+ break;
+
+ case 'trace':
+ service_start(name, data, true);
+ break;
+
+ case 'shutdown':
+ case 'stop':
+ service_stop(name);
+ break;
+
+ case 'restart':
+ service_stop(name);
+ service_start(name, data);
+ break;
+
+ case 'reload':
+ if (type(data.service_reload) != 'function')
+ service_start(name, data);
+ else
+ data.service_reload();
+ break;
+
+ case 'info':
+ printf('%.J\n', ubus.call('service', 'list', { name, verbose: true }) || "");
+ break;
+
+ case 'status':
+ service_status(name, true);
+ break;
+
+ case 'running':
+ service_status(name, false);
+ break;
+
+ case 'disable':
+ for (let file in filter(fs.lsdir('/etc/rc.d/'), (f) => match(f, regexp('^[SK]..' + script_name + '$'))))
+ fs.unlink('/etc/rc.d/' + file);
+ break;
+
+ case 'enable':
+ if (type(data.start) == 'int')
+ fs.symlink('../init.d/' + script_name, '/etc/rc.d/S' + data.start + script_name);
+ if (type(data.stop) == 'int')
+ fs.symlink('../init.d/' + script_name, '/etc/rc.d/K' + data.stop + script_name);
+ break;
+
+ case 'help':
+ default:
+ printf(`Syntax: ${script} [command]\n\nAvailable commands:\n`);
+ for (let k, v in help)
+ printf(`\t${k}\t\t${v}\n`);
+ printf('\n');
+ break;
+ };
+};
+
+const scope = {
+ procd_config_reload_trigger,
+ procd_get_network_devices,
+ procd_interface_trigger,
+ procd_get_mountpoints,
+ procd_default_respawn,
+ procd_update_trigger,
+ procd_ubus_wait_for,
+ procd_instance_add,
+ procd_raw_trigger,
+ procd_core_dump,
+ procd_service,
+ procd_boot,
+
+ script_name,
+ script,
+
+ ubus,
+ uci,
+ fs
+};
+
+try {
+ include(script, scope);
+} catch (e) {
+ printf("Unable to include path '%s': %s\n\n", script, e);
+ printf('%s\n', e.stacktrace[0].context);
+}