Browse Source

Implement externally triggered services

These require an external trigger (possible via "dinitctl trigger") to
start; their start will be delayed until the trigger occurs.
Davin McCall 1 year ago
parent
commit
b3e161953b

+ 7 - 1
doc/manpages/dinit-service.5.m4

@@ -28,7 +28,7 @@ specified via property settings, the format of which are documented in the
 .\"
 .SS SERVICE TYPES
 .\"
-There are four basic types of service:
+There are five basic types of service:
 .IP \(bu
 \fBProcess\fR services. This kind of service runs as a single process; starting
 the service simply requires starting the process; stopping the service is
@@ -48,6 +48,12 @@ They can not be supervised.
 \fBInternal\fR services do not run as an external process at all. They can
 be started and stopped without any external action.
 They are useful for grouping other services (via service dependencies).
+.IP \(bu
+\fbTriggered\fR services are similar to internal processes, but an external
+trigger is required before they will start (i.e. their startup will pause until
+the trigger occurs).
+The \fBdinitctl trigger\fR command can be used to trigger such a service;
+see \fBdinitctl\fR(8).
 .LP
 Independent of their type, the state of services can be linked to other
 services via dependency relationships, which are discussed in the next section.

+ 15 - 0
doc/manpages/dinitctl.8.m4

@@ -54,6 +54,12 @@ dinitctl \- control services supervised by Dinit
 [\fIoptions\fR] \fBdisable\fR [\fB\-\-from\fR \fIfrom-service\fR] \fIto-service\fR
 .HP
 .B dinitctl
+[\fIoptions\fR] \fBtrigger\fR \fIservice-name\fR
+.HP
+.B dinitctl
+[\fIoptions\fR] \fBuntrigger\fR \fIservice-name\fR
+.HP
+.B dinitctl
 [\fIoptions\fR] \fBsetenv\fR [\fIname\fR[=\fIvalue\fR] \fI...\fR]
 .\"
 .PD
@@ -249,6 +255,15 @@ Note that the \fBdisable\fR command affects only the dependency specified (or im
 It has no other effect, and a service that is "disabled" may still be started if it is a dependency of
 another started service.
 .TP
+\fBtrigger\fR
+Mark the specified service (which must be a \fItriggered\fR service) as having its external trigger set.
+This will allow the service to finish starting. 
+.TP
+\fBuntrigger\fR
+Clear the trigger for the specified service (which must be a \fItriggered\fR service).
+This will delay the service from starting, until the trigger is set. If the service has already started,
+this will have no immediate effect.
+.TP
 \fBsetenv\fR
 Export one or more variables into the activation environment.
 The value can be provided on the command line or retrieved from the environment \fBdinitctl\fR is

+ 42 - 1
src/control.cc

@@ -10,13 +10,17 @@
 // Server-side control protocol implementation. This implements the functionality that allows
 // clients (such as dinitctl) to query service state and issue commands to control services.
 
+// Control protocol versions:
+// 1 - dinit 0.16 and prior
+// 2 - dinit 0.17 (adds DINIT_CP_SETTRIGGER)
+
 namespace {
     constexpr auto OUT_EVENTS = dasynq::OUT_EVENTS;
     constexpr auto IN_EVENTS = dasynq::IN_EVENTS;
 
     // Control protocol minimum compatible version and current version:
     constexpr uint16_t min_compat_version = 1;
-    constexpr uint16_t cp_version = 1;
+    constexpr uint16_t cp_version = 2;
 
     // check for value in a set
     template <typename T, int N, typename U>
@@ -109,6 +113,9 @@ bool control_conn_t::process_packet()
     if (pktType == DINIT_CP_SETENV) {
         return process_setenv();
     }
+    if (pktType == DINIT_CP_SETTRIGGER) {
+        return process_set_trigger();
+    }
 
     // Unrecognized: give error response
     char outbuf[] = { DINIT_RP_BADREQ };
@@ -917,6 +924,40 @@ badreq:
     return true;
 }
 
+bool control_conn_t::process_set_trigger()
+{
+    // 1 byte packet type
+    // handle: service
+    // 1 byte trigger value
+    constexpr int pkt_size = 2 + sizeof(handle_t);
+
+    if (rbuf.get_length() < pkt_size) {
+        chklen = pkt_size;
+        return true;
+    }
+
+    handle_t handle;
+    char trigger_val;
+
+    rbuf.extract(&handle, 1, sizeof(handle));
+    rbuf.extract(&trigger_val, 1 + sizeof(handle), sizeof(trigger_val));
+    rbuf.consume(pkt_size);
+    chklen = 0;
+
+    service_record *service = find_service_for_key(handle);
+    if (service == nullptr || service->get_type() != service_type_t::TRIGGERED) {
+        char nak_rep[] = { DINIT_RP_NAK };
+        return queue_packet(nak_rep, 1);
+    }
+
+    triggered_service *tservice = static_cast<triggered_service *>(service);
+    tservice->set_trigger(trigger_val != 0);
+    services->process_queues();
+
+    char ack_rep[] = { DINIT_RP_ACK };
+    return queue_packet(ack_rep, 1);
+}
+
 bool control_conn_t::query_load_mech()
 {
     rbuf.consume(1);

+ 50 - 1
src/dinitctl.cc

@@ -51,6 +51,7 @@ static int add_remove_dependency(int socknum, cpbuffer_t &rbuffer, bool add, con
 static int enable_disable_service(int socknum, cpbuffer_t &rbuffer, const char *from, const char *to,
         bool enable, bool verbose);
 static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector<const char *> &env_names);
+static int trigger_service(int socknum, cpbuffer_t &rbuffer, const char *service_name, bool trigger_value);
 
 static const char * describeState(bool stopped)
 {
@@ -80,6 +81,8 @@ enum class command_t {
     ENABLE_SERVICE,
     DISABLE_SERVICE,
     SETENV,
+    SET_TRIGGER,
+    UNSET_TRIGGER,
 };
 
 class dinit_protocol_error
@@ -215,6 +218,12 @@ int dinitctl_main(int argc, char **argv)
             else if (strcmp(argv[i], "setenv") == 0) {
                 command = command_t::SETENV;
             }
+            else if (strcmp(argv[i], "trigger") == 0) {
+                command = command_t::SET_TRIGGER;
+            }
+            else if (strcmp(argv[i], "untrigger") == 0) {
+                command = command_t::UNSET_TRIGGER;
+            }
             else {
                 cerr << "dinitctl: unrecognized command: " << argv[i] << " (use --help for help)\n";
                 return 1;
@@ -322,6 +331,8 @@ int dinitctl_main(int argc, char **argv)
           "    dinitctl [options] rm-dep <type> <from-service> <to-service>\n"
           "    dinitctl [options] enable [--from <from-service>] <to-service>\n"
           "    dinitctl [options] disable [--from <from-service>] <to-service>\n"
+          "    dinitctl [options] trigger <service-name>\n"
+          "    dinitctl [options] untrigger <service-name>\n"
           "    dinitctl [options] setenv [name[=value] ...]\n"
           "\n"
           "Note: An activated service continues running when its dependents stop.\n"
@@ -363,7 +374,7 @@ int dinitctl_main(int argc, char **argv)
 
         // Start by querying protocol version:
         cpbuffer_t rbuffer;
-        check_protocol_version(min_cp_version, max_cp_version, rbuffer, socknum);
+        uint16_t daemon_protocol_ver = check_protocol_version(min_cp_version, max_cp_version, rbuffer, socknum);
 
         if (command == command_t::UNPIN_SERVICE) {
             return unpin_service(socknum, rbuffer, service_name, verbose);
@@ -398,6 +409,12 @@ int dinitctl_main(int argc, char **argv)
         else if (command == command_t::SETENV) {
             return do_setenv(socknum, rbuffer, cmd_args);
         }
+        else if (command == command_t::SET_TRIGGER || command == command_t::UNSET_TRIGGER) {
+            if (daemon_protocol_ver < 2) {
+                throw cp_old_server_exception();
+            }
+            return trigger_service(socknum, rbuffer, service_name, (command == command_t::SET_TRIGGER));
+        }
         else {
             return start_stop_service(socknum, rbuffer, service_name, command, do_pin, do_force,
                     wait_for_service, ignore_unstarted, verbose);
@@ -1635,3 +1652,35 @@ static int do_setenv(int socknum, cpbuffer_t &rbuffer, std::vector<const char *>
 
     return 0;
 }
+
+static int trigger_service(int socknum, cpbuffer_t &rbuffer, const char *service_name, bool trigger_value)
+{
+    using namespace std;
+
+    handle_t handle;
+    if (!load_service(socknum, rbuffer, service_name, &handle, nullptr, true)) {
+        return 1;
+    }
+
+    // Issue SET_TRIGGER command.
+    {
+        auto m = membuf()
+                .append<char>(DINIT_CP_SETTRIGGER)
+                .append(handle)
+                .append<char>(trigger_value);
+        write_all_x(socknum, m);
+
+        wait_for_reply(rbuffer, socknum);
+        if (rbuffer[0] == DINIT_RP_NAK) {
+            cerr << "dinitctl: cannot trigger a service that is not of 'triggered' type.\n";
+            return 1;
+        }
+        if (rbuffer[0] != DINIT_RP_ACK) {
+            cerr << "dinitctl: protocol error.\n";
+            return 1;
+        }
+        rbuffer.consume(1);
+    }
+
+    return 0;
+}

+ 4 - 4
src/igr-tests/check-lint/expected.txt

@@ -1,7 +1,7 @@
 Checking service: boot...
-Service 'boot': 'command' specified, but 'type' is internal (or not specified).
-Service 'boot': 'working-dir' specified, but 'type' is internal (or not specified).
-Service 'boot': 'run-as' specified, but 'type' is internal (or not specified).
-Service 'boot': 'socket-listen' specified, but 'type' is internal (or not specified).
+Service 'boot': 'command' specified, but ignored for the specified (or default) service type.
+Service 'boot': 'working-dir' specified, but ignored for the specified (or default) service type.
+Service 'boot': 'run-as' specified, but ignored for the specified (or default) service type.
+Service 'boot': 'socket-listen' specified, but ignored for the specified (or default) service type'.
 Service 'boot': could not stat command executable '/this/command/does/not/exist': No such file or directory
 One or more errors/warnings issued.

+ 3 - 0
src/includes/control-cmds.h

@@ -54,6 +54,9 @@ constexpr static int DINIT_CP_SETENV = 17;
 // Query status of an individual service
 constexpr static int DINIT_CP_SERVICESTATUS = 18;
 
+// Set trigger value for triggered services
+constexpr static int DINIT_CP_SETTRIGGER = 19;
+
 
 // Replies:
 

+ 3 - 0
src/includes/control.h

@@ -155,6 +155,9 @@ class control_conn_t : private service_listener
     // Process a SETENV packet.
     bool process_setenv();
 
+    // Process a SETTRIGGER packet.
+    bool process_set_trigger();
+
     // List all loaded services and their state.
     bool list_services();
 

+ 16 - 12
src/includes/load-service.h

@@ -813,40 +813,41 @@ class service_settings_wrapper
             }
         }
 
-        if (do_report_lint && service_type == service_type_t::INTERNAL) {
+        if (do_report_lint && (service_type == service_type_t::INTERNAL
+                || service_type == service_type_t::TRIGGERED)) {
             if (!command.empty()) {
-                report_lint("'command' specified, but 'type' is internal (or not specified).");
+                report_lint("'command' specified, but ignored for the specified (or default) service type.");
             }
             if (!stop_command.empty()) {
-                report_lint("'stop-command' specified, but 'type' is internal (or not specified).");
+                report_lint("'stop-command' specified, but ignored for the specified (or default) service type.");
             }
             if (!working_dir.empty()) {
-                report_lint("'working-dir' specified, but 'type' is internal (or not specified).");
+                report_lint("'working-dir' specified, but ignored for the specified (or default) service type.");
             }
             #if SUPPORT_CGROUPS
             if (!run_in_cgroup.empty()) {
-                report_lint("'run-in-cgroup' specified, but 'type' is internal (or not specified).");
+                report_lint("'run-in-cgroup' specified, but ignored for the specified (or default) service type.");
             }
             #endif
             if (run_as_uid != (uid_t)-1) {
-                report_lint("'run-as' specified, but 'type' is internal (or not specified).");
+                report_lint("'run-as' specified, but ignored for the specified (or default) service type.");
             }
             if (!socket_path.empty()) {
-                report_lint("'socket-listen' specified, but 'type' is internal (or not specified).");
+                report_lint("'socket-listen' specified, but ignored for the specified (or default) service type'.");
             }
             #if USE_UTMPX
             if (inittab_id[0] != 0 || inittab_line[0] != 0) {
-                report_lint("'inittab_line' or 'inittab_id' specified, but 'type' is internal (or not specified).");
+                report_lint("'inittab_line' or 'inittab_id' specified, but ignored for the specified (or default) service type.");
             }
             #endif
             if (onstart_flags.signal_process_only || onstart_flags.start_interruptible) {
-                report_lint("signal options were specified, but 'type' is internal (or not specified).");
+                report_lint("signal options were specified, but ignored for the specified (or default) service type.");
             }
             if (onstart_flags.pass_cs_fd) {
-                report_lint("option 'pass_cs_fd' was specified, but 'type' is internal (or not specified).");
+                report_lint("option 'pass_cs_fd' was specified, but ignored for the specified (or default) service type.");
             }
             if (onstart_flags.skippable) {
-                report_lint("option 'skippable' was specified, but 'type' is internal (or not specified).");
+                report_lint("option 'skippable' was specified, but ignored for the specified (or default) service type.");
             }
         }
 
@@ -1016,9 +1017,12 @@ void process_service_line(settings_wrapper &settings, const char *name, string &
         else if (type_str == "internal") {
             settings.service_type = service_type_t::INTERNAL;
         }
+        else if (type_str == "triggered") {
+            settings.service_type = service_type_t::TRIGGERED;
+        }
         else {
             throw service_description_exc(name, "service type must be one of: \"scripted\","
-                " \"process\", \"bgprocess\" or \"internal\"", line_num);
+                " \"process\", \"bgprocess\", \"internal\" or \"triggered\"", line_num);
         }
     }
     else if (setting == "options") {

+ 2 - 1
src/includes/service-constants.h

@@ -20,7 +20,8 @@ enum class service_type_t {
                 // "background".
     SCRIPTED,   // Service requires an external command to start,
                 // and a second command to stop
-    INTERNAL    // Internal service, runs no external process
+    INTERNAL,   // Internal service, runs no external process
+    TRIGGERED   // Externally triggered service
 };
 
 /* Service events */

+ 25 - 0
src/includes/service.h

@@ -789,6 +789,31 @@ class service_record
     }
 };
 
+// Externally triggered service. This is essentially the same as an internal service, but does not start
+// until the external trigger is set.
+class triggered_service : public service_record {
+    private:
+    bool is_triggered = false;
+
+    public:
+    using service_record::service_record;
+
+    bool bring_up() noexcept override;
+
+    bool can_interrupt_start() noexcept override
+    {
+        return true;
+    }
+
+    void set_trigger(bool new_trigger) noexcept
+    {
+        is_triggered = new_trigger;
+        if (is_triggered && get_state() == service_state_t::STARTING && !waiting_for_deps) {
+            started();
+        }
+    }
+};
+
 inline auto extract_prop_queue(service_record *sr) -> decltype(sr->prop_queue_node) &
 {
     return sr->prop_queue_node;

+ 11 - 1
src/load-service.cc

@@ -409,6 +409,10 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
                 // Already started; we must replace settings on existing service record
                 create_new_record = false;
             }
+            else if (service_type != service->get_type()) {
+                // No need to create a new record if the type hasn't changed
+                create_new_record = false;
+            }
         }
 
         // Note, we need to be very careful to handle exceptions properly and roll back any changes that
@@ -540,7 +544,13 @@ service_record * dirload_service_set::load_reload_service(const char *name, serv
         }
         else {
             if (create_new_record) {
-                rval = new service_record(this, string(name), service_type, settings.depends);
+                if (service_type == service_type_t::INTERNAL) {
+                    rval = new service_record(this, string(name), service_type, settings.depends);
+                }
+                else {
+                    /* TRIGGERED */
+                    rval = new triggered_service(this, string(name), service_type, settings.depends);
+                }
                 if (reload_svc != nullptr) {
                     check_cycle(settings.depends, reload_svc);
                 }

+ 10 - 2
src/service.cc

@@ -392,7 +392,7 @@ bool service_record::check_deps_started() noexcept
 
 void service_record::all_deps_started() noexcept
 {
-    if (onstart_flags.starts_on_console && ! have_console) {
+    if (onstart_flags.starts_on_console && !have_console) {
         queue_for_console();
         return;
     }
@@ -426,7 +426,7 @@ void service_record::acquired_console() noexcept
 void service_record::started() noexcept
 {
     // If we start on console but don't keep it, release it now:
-    if (have_console && ! onstart_flags.runs_on_console) {
+    if (have_console && !onstart_flags.runs_on_console) {
         bp_sys::tcsetpgrp(0, bp_sys::getpgrp());
         release_console();
     }
@@ -836,3 +836,11 @@ void service_set::service_inactive(service_record *sr) noexcept
 {
     active_services--;
 }
+
+bool triggered_service::bring_up() noexcept
+{
+    if (is_triggered) {
+        started();
+    }
+    return true;
+}