#include <string>
#include <fstream>
#include <locale>
-#include <iostream>
#include <limits>
+#include <list>
#include <cstring>
#include <cstdlib>
const char *servicename,
const string &service_filename,
std::list<prelim_dep> &deplist, const std::string &depdirpath,
- dependency_type dep_type)
+ dependency_type dep_type,
+ const service_record *avoid_circular)
{
std::string depdir_fname = combine_paths(parent_path(service_filename), depdirpath.c_str());
// problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
// if a memory allocation failure occurs.
//
-service_record * dirload_service_set::load_service(const char * name)
+service_record * dirload_service_set::load_service(const char * name, const service_record *avoid_circular)
{
using std::string;
using std::ifstream;
// First try and find an existing record...
service_record * rval = find_service(string(name));
- if (rval != 0) {
- if (rval->is_dummy()) {
+ if (rval != nullptr) {
+ if (rval == avoid_circular || rval->is_dummy()) {
throw service_cyclic_dependency(name);
}
return rval;
auto process_dep_dir_n = [&](std::list<prelim_dep> &deplist, const std::string &waitsford,
dependency_type dep_type) -> void {
- process_dep_dir(*this, name, service_filename, deplist, waitsford, dep_type);
+ process_dep_dir(*this, name, service_filename, deplist, waitsford, dep_type, avoid_circular);
};
auto load_service_n = [&](const string &dep_name) -> service_record * {
- return load_service(dep_name.c_str());
+ return load_service(dep_name.c_str(), avoid_circular);
};
process_service_line(settings, name, line, setting, i, end, load_service_n, process_dep_dir_n);
throw;
}
}
+
+// Update the dependencies of the specified service atomically. May fail with bad_alloc.
+static void update_depenencies(service_record *service,
+ dinit_load::service_settings_wrapper<prelim_dep> &settings)
+{
+ std::list<service_dep> &deps = service->get_dependencies();
+ auto first_preexisting = deps.begin();
+
+ // build a set of services currently issuing acquisition
+ std::unordered_set<service_record *> deps_with_acqs;
+ for (auto i = deps.begin(), e = deps.end(); i != e; ++i) {
+ if (i->holding_acq) {
+ deps_with_acqs.insert(i->get_to());
+ }
+ }
+
+ try {
+ // Insert all the new dependencies before the first pre-existing dependency
+ for (auto &new_dep : settings.depends) {
+ bool has_acq = deps_with_acqs.count(new_dep.to);
+ service->add_dep(new_dep.to, new_dep.dep_type, first_preexisting, has_acq);
+ }
+ }
+ catch (...) {
+ // remove the inserted dependencies
+ for (auto i = deps.begin(); i != first_preexisting; ++i) {
+ i = service->rm_dep(i);
+ }
+
+ // re-throw the exception
+ throw;
+ }
+
+ // Now remove all pre-existing dependencies (no exceptions possible from here).
+ for( ; first_preexisting != deps.end(); ) {
+ first_preexisting = service->rm_dep(first_preexisting);
+ }
+}
+
+// Update the command, and dependencies, of the specified service atomically. May fail with bad_alloc.
+static void update_command_and_dependencies(base_process_service *service,
+ dinit_load::service_settings_wrapper<prelim_dep> &settings)
+{
+ // Get the current command parts
+ std::string orig_cmd; std::vector<const char *> orig_arg_parts;
+ service->get_command(orig_cmd, orig_arg_parts);
+
+ // Separate the new command parts and set
+ std::vector<const char *> cmd_arg_parts = separate_args(settings.command, settings.command_offsets);
+ service->set_command(std::move(settings.command), std::move(cmd_arg_parts));
+
+ try {
+ update_depenencies(service, settings);
+ }
+ catch (...) {
+ // restore original command
+ service->set_command(std::move(orig_cmd), std::move(orig_arg_parts));
+
+ // re-throw the exception
+ throw;
+ }
+}
+
+service_record * dirload_service_set::reload_service(service_record * service)
+{
+ // We have the following problems:
+ // - ideally want to allow changing service type, at least for stopped services. That implies creating
+ // a new (replacement) service_record object, at least in cases where the type does change.
+ // - dependencies may change (including addition of new dependencies which aren't yet loaded). We need
+ // to prevent cyclic dependencies forming.
+ // - We want atomicity. If any new settings are not valid/alterable, or if a cyclic dependency is
+ // created, nothing should change. Ideally this would extend to unloading any dependencies which were
+ // loaded as part of the reload attempt.
+ // - We need to either transfer handles referring to the old service (so that they refer to the new
+ // service), or make them invalid. Or, we alter the original service without creating a new one
+ // (which we can only do if the type doesn't change).
+
+ // Approach:
+ // - remember the initial service count, so we can remove services loaded as part of the reload
+ // operation if we want to abort it later (i.e. if service count changed from N to N+X, remove the
+ // last X services)
+ // - check that the new settings are valid (if the service is running, check if the settings can be
+ // altered, though we may just defer some changes until service is restarted)
+ // - check all dependencies of the newly created service record for cyclic dependencies, via depth-first
+ // traversal.
+ // - If changing type:
+ // - create the service initially just as if loading a new service (but with no dummy placeholder,
+ // use the original service for that).
+ // - switch all dependents to depend on the new record. Copy necessary runtime data from the original
+ // to the new service record. Remove dependencies from the old record, and release any dependency
+ // services as appropriate (so they stop if no longer needed). Finally, remove the old service
+ // record and delete it.
+ // Otherwise:
+ // - copy the new settings to the existing service
+ // - fix dependencies
+ //
+ // Limitations:
+ // - caller must check there are no handles (or only a single requesting handle) to the service before
+ // calling
+ // - cannot change the type of a non-stopped service
+
+ using std::string;
+ using std::ifstream;
+ using std::ios;
+ using std::ios_base;
+ using std::locale;
+ using std::isspace;
+
+ using std::list;
+ using std::pair;
+
+ using namespace dinit_load;
+
+ service_record *rval = nullptr;
+ const string &name = service->get_name();
+
+ ifstream service_file;
+ string service_filename;
+
+ // Couldn't find one. Have to load it.
+ for (auto &service_dir : service_dirs) {
+ service_filename = service_dir.get_dir();
+ if (*(service_filename.rbegin()) != '/') {
+ service_filename += '/';
+ }
+ service_filename += name;
+
+ service_file.open(service_filename.c_str(), ios::in);
+ if (service_file) break;
+ }
+
+ if (! service_file) {
+ throw service_not_found(string(name));
+ }
+
+ service_settings_wrapper<prelim_dep> settings;
+
+ string line;
+ // getline can set failbit if it reaches end-of-file, we don't want an exception in that case. There's
+ // no good way to handle an I/O error however, so we'll have exceptions thrown on badbit:
+ service_file.exceptions(ios::badbit);
+
+ bool create_new_record = true;
+
+ try {
+ process_service_file(name, service_file,
+ [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void {
+
+ auto process_dep_dir_n = [&](std::list<prelim_dep> &deplist, const std::string &waitsford,
+ dependency_type dep_type) -> void {
+ process_dep_dir(*this, name.c_str(), service_filename, deplist, waitsford, dep_type, service);
+ };
+
+ auto load_service_n = [&](const string &dep_name) -> service_record * {
+ return load_service(dep_name.c_str(), service);
+ };
+
+ process_service_line(settings, name.c_str(), line, setting, i, end, load_service_n, process_dep_dir_n);
+ });
+
+ service_file.close();
+
+ auto service_type = settings.service_type;
+
+ if (service_type == service_type_t::PROCESS || service_type == service_type_t::BGPROCESS
+ || service_type == service_type_t::SCRIPTED) {
+ if (settings.command.length() == 0) {
+ throw service_description_exc(name, "Service command not specified.");
+ }
+ }
+
+ // Make sure settings are able to be changed/are compatible
+ if (service->get_state() != service_state_t::STOPPED) {
+ // Can not change type of a running service.
+ if (service_type != service->get_type()) {
+ throw service_description_exc(name, "Cannot change type of non-stopped service.");
+ }
+ // Can not alter a starting/stopping service, at least for now.
+ if (service->get_state() != service_state_t::STARTED) {
+ throw service_description_exc(name,
+ "Cannot alter settings for service which is currently starting/stopping.");
+ }
+
+ // Check validity of dependencies (if started, regular deps must be started)
+ for (auto &new_dep : settings.depends) {
+ if (new_dep.dep_type == dependency_type::REGULAR) {
+ if (new_dep.to->get_state() != service_state_t::STARTED) {
+ throw service_description_exc(name,
+ std::string("Cannot add non-started dependency '")
+ + new_dep.to->get_name() + "'.");
+ }
+ }
+ }
+
+ // XXX cannot change pid_file
+ // XXX cannot change service flags: runs_on_console, shares_console
+ // XXX cannot change inittab_id/inittab_line
+
+ // Already started; we must replace settings on existing service record
+ create_new_record = false;
+ }
+
+ // Note, we need to be very careful to handle exceptions properly and roll back any changes that
+ // we've made before the exception occurred.
+
+ if (service_type == service_type_t::PROCESS) {
+ do_env_subst(settings.command, settings.command_offsets, settings.do_sub_vars);
+ process_service *rvalps;
+ if (create_new_record) {
+ rvalps = new process_service(this, string(name), std::move(settings.command),
+ settings.command_offsets, settings.depends);
+ }
+ else {
+ rvalps = static_cast<process_service *>(service);
+ update_command_and_dependencies(rvalps, settings);
+ }
+ rval = rvalps;
+ // All of the following should be noexcept or must perform rollback on exception
+ rvalps->set_working_dir(std::move(settings.working_dir));
+ rvalps->set_env_file(std::move(settings.env_file));
+ rvalps->set_rlimits(std::move(settings.rlimits));
+ rvalps->set_restart_interval(settings.restart_interval, settings.max_restarts);
+ rvalps->set_restart_delay(settings.restart_delay);
+ rvalps->set_stop_timeout(settings.stop_timeout);
+ rvalps->set_start_timeout(settings.start_timeout);
+ rvalps->set_extra_termination_signal(settings.term_signal);
+ rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
+ rvalps->set_notification_fd(settings.readiness_fd);
+ rvalps->set_notification_var(std::move(settings.readiness_var));
+ #if USE_UTMPX
+ rvalps->set_utmp_id(settings.inittab_id);
+ rvalps->set_utmp_line(settings.inittab_line);
+ #endif
+ }
+ else if (service_type == service_type_t::BGPROCESS) {
+ do_env_subst(settings.command, settings.command_offsets, settings.do_sub_vars);
+ bgproc_service *rvalps;
+ if (create_new_record) {
+ rvalps = new bgproc_service(this, string(name), std::move(settings.command),
+ settings.command_offsets, settings.depends);
+ }
+ else {
+ rvalps = static_cast<bgproc_service *>(service);
+ update_command_and_dependencies(rvalps, settings);
+ }
+ rval = rvalps;
+ // All of the following should be noexcept or must perform rollback on exception
+ rvalps->set_working_dir(std::move(settings.working_dir));
+ rvalps->set_env_file(std::move(settings.env_file));
+ rvalps->set_rlimits(std::move(settings.rlimits));
+ rvalps->set_pid_file(std::move(settings.pid_file));
+ rvalps->set_restart_interval(settings.restart_interval, settings.max_restarts);
+ rvalps->set_restart_delay(settings.restart_delay);
+ rvalps->set_stop_timeout(settings.stop_timeout);
+ rvalps->set_start_timeout(settings.start_timeout);
+ rvalps->set_extra_termination_signal(settings.term_signal);
+ rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
+ settings.onstart_flags.runs_on_console = false;
+ }
+ else if (service_type == service_type_t::SCRIPTED) {
+ do_env_subst(settings.command, settings.command_offsets, settings.do_sub_vars);
+ std::vector<const char *> stop_arg_parts = separate_args(settings.stop_command, settings.stop_command_offsets);
+ scripted_service *rvalps;
+ if (create_new_record) {
+ rvalps = new scripted_service(this, string(name), std::move(settings.command),
+ settings.command_offsets, settings.depends);
+ }
+ else {
+ rvalps = static_cast<scripted_service *>(service);
+ update_command_and_dependencies(rvalps, settings);
+ }
+ rval = rvalps;
+ // All of the following should be noexcept or must perform rollback on exception
+ rvalps->set_stop_command(std::move(settings.stop_command), std::move(stop_arg_parts));
+ rvalps->set_working_dir(std::move(settings.working_dir));
+ rvalps->set_env_file(std::move(settings.env_file));
+ rvalps->set_rlimits(std::move(settings.rlimits));
+ rvalps->set_stop_timeout(settings.stop_timeout);
+ rvalps->set_start_timeout(settings.start_timeout);
+ rvalps->set_extra_termination_signal(settings.term_signal);
+ rvalps->set_run_as_uid_gid(settings.run_as_uid, settings.run_as_gid);
+ }
+ else {
+ if (create_new_record) {
+ rval = new service_record(this, string(name), service_type, settings.depends);
+ }
+ else {
+ rval = service;
+ update_depenencies(rval, settings);
+ }
+ }
+
+ rval->set_log_file(std::move(settings.logfile));
+ rval->set_auto_restart(settings.auto_restart);
+ rval->set_smooth_recovery(settings.smooth_recovery);
+ rval->set_flags(settings.onstart_flags);
+ rval->set_socket_details(std::move(settings.socket_path), settings.socket_perms,
+ settings.socket_uid, settings.socket_gid);
+ rval->set_chain_to(std::move(settings.chain_to_name));
+
+ if (create_new_record) {
+ // switch dependencies to old record so that they refer to the new record
+
+ // Add dependent-link for all dependencies. Add to the new service first, so we can rollback
+ // on failure:
+ int added_dep_links = 0;
+ try {
+ for (auto &dep : rval->get_dependencies()) {
+ dep.get_to()->get_dependents().push_back(&dep);
+ added_dep_links++;
+ }
+ }
+ catch (...) {
+ for (auto &dep : rval->get_dependencies()) {
+ if (added_dep_links-- == 0) break;
+ dep.get_to()->get_dependents().pop_back();
+ }
+ throw;
+ }
+
+ // Remove dependent-link for all dependencies from the original:
+ service->prepare_for_unload();
+
+ // Set links in all dependents to the original to point to the new service:
+ rval->get_dependents() = std::move(service->get_dependents());
+ for (auto n : rval->get_dependents()) {
+ n->set_to(rval);
+ }
+ }
+
+ return rval;
+ }
+ catch (setting_exception &setting_exc)
+ {
+ if (create_new_record) delete rval;
+ throw service_description_exc(name, std::move(setting_exc.get_info()));
+ }
+ catch (std::system_error &sys_err)
+ {
+ if (create_new_record) delete rval;
+ throw service_description_exc(name, sys_err.what());
+ }
+ catch (...) // (should only be std::bad_alloc / service_description_exc)
+ {
+ if (create_new_record) delete rval;
+ throw;
+ }
+}