base-files: add support for ucode based init.d scripts
authorJohn Crispin <john@phrozen.org>
Mon, 14 Oct 2024 10:19:19 +0000 (12:19 +0200)
committerJohn Crispin <john@phrozen.org>
Wed, 23 Oct 2024 12:14:38 +0000 (14:14 +0200)
Signed-off-by: John Crispin <john@phrozen.org>
package/base-files/files/usr/share/ucode/procd.uc [new file with mode: 0644]

diff --git a/package/base-files/files/usr/share/ucode/procd.uc b/package/base-files/files/usr/share/ucode/procd.uc
new file mode 100644 (file)
index 0000000..c54a965
--- /dev/null
@@ -0,0 +1,431 @@
+'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);
+}