From 75bbea5ddef86412d2a8eb2f5acaedb2a7176a1b Mon Sep 17 00:00:00 2001 From: Davin McCall Date: Mon, 15 Oct 2018 18:57:52 +0100 Subject: [PATCH] Factor out service description loading to a separate header. This allows for reading service descriptions directly from dinitctl, which is useful for planned "enable/disable service" functionality. --- src/includes/load-service.h | 259 ++++++++++++++++++ src/includes/service.h | 42 +-- src/load-service.cc | 506 ++++++++++++------------------------ 3 files changed, 422 insertions(+), 385 deletions(-) create mode 100644 src/includes/load-service.h diff --git a/src/includes/load-service.h b/src/includes/load-service.h new file mode 100644 index 0000000..397b2cc --- /dev/null +++ b/src/includes/load-service.h @@ -0,0 +1,259 @@ +#include +#include + +// Exception while loading a service +class service_load_exc +{ + public: + std::string serviceName; + std::string excDescription; + + protected: + service_load_exc(const std::string &serviceName, std::string &&desc) noexcept + : serviceName(serviceName), excDescription(std::move(desc)) + { + } +}; + +class service_not_found : public service_load_exc +{ + public: + service_not_found(const std::string &serviceName) noexcept + : service_load_exc(serviceName, "Service description not found.") + { + } +}; + +class service_cyclic_dependency : public service_load_exc +{ + public: + service_cyclic_dependency(const std::string &serviceName) noexcept + : service_load_exc(serviceName, "Has cyclic dependency.") + { + } +}; + +class service_description_exc : public service_load_exc +{ + public: + service_description_exc(const std::string &serviceName, std::string &&extraInfo) noexcept + : service_load_exc(serviceName, std::move(extraInfo)) + { + } +}; + +namespace dinit_load { + +using string = std::string; +using string_iterator = std::string::iterator; + +// exception thrown when encountering a syntax issue when reading a setting value +class setting_exception +{ + std::string info; + + public: + setting_exception(const std::string &&exc_info) : info(std::move(exc_info)) + { + } + + std::string &get_info() + { + return info; + } +}; + + +// Utility function to skip white space. Returns an iterator at the +// first non-white-space position (or at end). +inline string_iterator skipws(string_iterator i, string_iterator end) +{ + using std::locale; + using std::isspace; + + while (i != end) { + if (! isspace(*i, locale::classic())) { + break; + } + ++i; + } + return i; +} + +// Read a setting name. +inline string read_setting_name(string_iterator & i, string_iterator end) +{ + using std::locale; + using std::ctype; + using std::use_facet; + + const ctype & facet = use_facet >(locale::classic()); + + string rval; + // Allow alphabetical characters, and dash (-) in setting name + while (i != end && (*i == '-' || facet.is(ctype::alpha, *i))) { + rval += *i; + ++i; + } + return rval; +} + +// Read a setting value +// +// In general a setting value is a single-line string. It may contain multiple parts +// separated by white space (which is normally collapsed). A hash mark - # - denotes +// the end of the value and the beginning of a comment (it should be preceded by +// whitespace). +// +// Part of a value may be quoted using double quote marks, which prevents collapse +// of whitespace and interpretation of most special characters (the quote marks will +// not be considered part of the value). A backslash can precede a character (such +// as '#' or '"' or another backslash) to remove its special meaning. Newline +// characters are not allowed in values and cannot be quoted. +// +// This function expects the string to be in an ASCII-compatible, single byte +// encoding (the "classic" locale). +// +// Params: +// service_name - the name of the service to which the setting applies +// i - reference to string iterator through the line +// end - iterator at end of line +// part_positions - list of to which the position of each setting value +// part will be added as [start,end). May be null. +inline string read_setting_value(string_iterator & i, string_iterator end, + std::list> * part_positions = nullptr) +{ + using std::locale; + using std::isspace; + + i = skipws(i, end); + + string rval; + bool new_part = true; + int part_start; + + while (i != end) { + char c = *i; + if (c == '\"') { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + // quoted string + ++i; + while (i != end) { + c = *i; + if (c == '\"') break; + if (c == '\n') { + throw setting_exception("Line end inside quoted string"); + } + else if (c == '\\') { + // A backslash escapes the following character. + ++i; + if (i != end) { + c = *i; + if (c == '\n') { + throw setting_exception("Line end follows backslash escape character (`\\')"); + } + rval += c; + } + } + else { + rval += c; + } + ++i; + } + if (i == end) { + // String wasn't terminated + throw setting_exception("Unterminated quoted string"); + } + } + else if (c == '\\') { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + // A backslash escapes the next character + ++i; + if (i != end) { + rval += *i; + } + else { + throw setting_exception("Backslash escape (`\\') not followed by character"); + } + } + else if (isspace(c, locale::classic())) { + if (! new_part && part_positions != nullptr) { + part_positions->emplace_back(part_start, rval.length()); + new_part = true; + } + i = skipws(i, end); + if (i == end) break; + if (*i == '#') break; // comment + rval += ' '; // collapse ws to a single space + continue; + } + else if (c == '#') { + // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental + // comments in setting values. + throw setting_exception("hashmark (`#') comment must be separated from setting value by whitespace"); + } + else { + if (new_part) { + part_start = rval.length(); + new_part = false; + } + rval += c; + } + ++i; + } + + // Got to end: + if (part_positions != nullptr) { + part_positions->emplace_back(part_start, rval.length()); + } + + return rval; +} + +// Process an opened service file, line by line. +// name - the service name +// service_file - the service file input stream +// func - a function of the form: +// void(string &line, string &setting, string_iterator i, string_iterator end) +// Called with: +// line - the complete line +// setting - the setting name, from the beginning of the line +// i - iterator at the beginning of the setting value +// end - iterator marking the end of the line +// +// May throw service load exceptions or I/O exceptions if enabled on stream. +template +void process_service_file(string name, std::istream &service_file, T func) +{ + string line; + + while (getline(service_file, line)) { + string::iterator i = line.begin(); + string::iterator end = line.end(); + + i = skipws(i, end); + if (i != end) { + if (*i == '#') { + continue; // comment line + } + string setting = read_setting_name(i, end); + i = skipws(i, end); + if (i == end || (*i != '=' && *i != ':')) { + throw service_description_exc(name, "Badly formed line."); + } + i = skipws(++i, end); + + func(line, setting, i, end); + } + } +} + +} // namespace dinit_load + +using dinit_load::process_service_file; diff --git a/src/includes/service.h b/src/includes/service.h index 9d250aa..390d5d0 100644 --- a/src/includes/service.h +++ b/src/includes/service.h @@ -14,6 +14,7 @@ #include "control.h" #include "service-listener.h" #include "service-constants.h" +#include "load-service.h" #include "dinit-ll.h" #include "dinit-log.h" @@ -131,47 +132,6 @@ struct service_flags_t { } }; -// Exception while loading a service -class service_load_exc -{ - public: - std::string serviceName; - std::string excDescription; - - protected: - service_load_exc(const std::string &serviceName, std::string &&desc) noexcept - : serviceName(serviceName), excDescription(std::move(desc)) - { - } -}; - -class service_not_found : public service_load_exc -{ - public: - service_not_found(const std::string &serviceName) noexcept - : service_load_exc(serviceName, "Service description not found.") - { - } -}; - -class service_cyclic_dependency : public service_load_exc -{ - public: - service_cyclic_dependency(const std::string &serviceName) noexcept - : service_load_exc(serviceName, "Has cyclic dependency.") - { - } -}; - -class service_description_exc : public service_load_exc -{ - public: - service_description_exc(const std::string &serviceName, std::string &&extraInfo) noexcept - : service_load_exc(serviceName, std::move(extraInfo)) - { - } -}; - class service_record; class service_set; class base_process_service; diff --git a/src/load-service.cc b/src/load-service.cc index 6bfe776..1aeddb8 100644 --- a/src/load-service.cc +++ b/src/load-service.cc @@ -20,176 +20,7 @@ using string = std::string; using string_iterator = std::string::iterator; -// Utility function to skip white space. Returns an iterator at the -// first non-white-space position (or at end). -static string_iterator skipws(string_iterator i, string_iterator end) -{ - using std::locale; - using std::isspace; - - while (i != end) { - if (! isspace(*i, locale::classic())) { - break; - } - ++i; - } - return i; -} - -// Read a setting name. -static string read_setting_name(string_iterator & i, string_iterator end) -{ - using std::locale; - using std::ctype; - using std::use_facet; - - const ctype & facet = use_facet >(locale::classic()); - - string rval; - // Allow alphabetical characters, and dash (-) in setting name - while (i != end && (*i == '-' || facet.is(ctype::alpha, *i))) { - rval += *i; - ++i; - } - return rval; -} - -namespace { - class setting_exception - { - std::string info; - - public: - setting_exception(const std::string &&exc_info) : info(std::move(exc_info)) - { - } - - std::string &get_info() - { - return info; - } - }; -} - - -// Read a setting value -// -// In general a setting value is a single-line string. It may contain multiple parts -// separated by white space (which is normally collapsed). A hash mark - # - denotes -// the end of the value and the beginning of a comment (it should be preceded by -// whitespace). -// -// Part of a value may be quoted using double quote marks, which prevents collapse -// of whitespace and interpretation of most special characters (the quote marks will -// not be considered part of the value). A backslash can precede a character (such -// as '#' or '"' or another backslash) to remove its special meaning. Newline -// characters are not allowed in values and cannot be quoted. -// -// This function expects the string to be in an ASCII-compatible, single byte -// encoding (the "classic" locale). -// -// Params: -// service_name - the name of the service to which the setting applies -// i - reference to string iterator through the line -// end - iterator at end of line -// part_positions - list of to which the position of each setting value -// part will be added as [start,end). May be null. -static string read_setting_value(string_iterator & i, string_iterator end, - std::list> * part_positions = nullptr) -{ - using std::locale; - using std::isspace; - - i = skipws(i, end); - - string rval; - bool new_part = true; - int part_start; - - while (i != end) { - char c = *i; - if (c == '\"') { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - // quoted string - ++i; - while (i != end) { - c = *i; - if (c == '\"') break; - if (c == '\n') { - throw setting_exception("Line end inside quoted string"); - } - else if (c == '\\') { - // A backslash escapes the following character. - ++i; - if (i != end) { - c = *i; - if (c == '\n') { - throw setting_exception("Line end follows backslash escape character (`\\')"); - } - rval += c; - } - } - else { - rval += c; - } - ++i; - } - if (i == end) { - // String wasn't terminated - throw setting_exception("Unterminated quoted string"); - } - } - else if (c == '\\') { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - // A backslash escapes the next character - ++i; - if (i != end) { - rval += *i; - } - else { - throw setting_exception("Backslash escape (`\\') not followed by character"); - } - } - else if (isspace(c, locale::classic())) { - if (! new_part && part_positions != nullptr) { - part_positions->emplace_back(part_start, rval.length()); - new_part = true; - } - i = skipws(i, end); - if (i == end) break; - if (*i == '#') break; // comment - rval += ' '; // collapse ws to a single space - continue; - } - else if (c == '#') { - // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental - // comments in setting values. - throw setting_exception("hashmark (`#') comment must be separated from setting value by whitespace"); - } - else { - if (new_part) { - part_start = rval.length(); - new_part = false; - } - rval += c; - } - ++i; - } - - // Got to end: - if (part_positions != nullptr) { - part_positions->emplace_back(part_start, rval.length()); - } - - return rval; -} - +// Convert a signal name to the corresponding signal number static int signal_name_to_number(std::string &signame) { if (signame == "HUP") return SIGHUP; @@ -465,6 +296,8 @@ service_record * dirload_service_set::load_service(const char * name) using std::list; using std::pair; + using namespace dinit_load; + // First try and find an existing record... service_record * rval = find_service(string(name)); if (rval != 0) { @@ -536,204 +369,189 @@ service_record * dirload_service_set::load_service(const char * name) // getline can set failbit if it reaches end-of-file, we don't want an exception in that case: service_file.exceptions(ios::badbit); - while (getline(service_file, line)) { - string::iterator i = line.begin(); - string::iterator end = line.end(); - - i = skipws(i, end); - if (i != end) { - if (*i == '#') { - continue; // comment line - } - string setting = read_setting_name(i, end); - i = skipws(i, end); - if (i == end || (*i != '=' && *i != ':')) { - throw service_description_exc(name, "Badly formed line."); - } - i = skipws(++i, end); - - if (setting == "command") { - command = read_setting_value(i, end, &command_offsets); - } - else if (setting == "working-dir") { - working_dir = read_setting_value(i, end, nullptr); - } - else if (setting == "socket-listen") { - socket_path = read_setting_value(i, end, nullptr); - } - else if (setting == "socket-permissions") { - string sock_perm_str = read_setting_value(i, end, nullptr); - std::size_t ind = 0; - try { - socket_perms = std::stoi(sock_perm_str, &ind, 8); - if (ind != sock_perm_str.length()) { - throw std::logic_error(""); - } - } - catch (std::logic_error &exc) { - throw service_description_exc(name, "socket-permissions: Badly-formed or " - "out-of-range numeric value"); + process_service_file(name, service_file, + [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void { + if (setting == "command") { + command = read_setting_value(i, end, &command_offsets); + } + else if (setting == "working-dir") { + working_dir = read_setting_value(i, end, nullptr); + } + else if (setting == "socket-listen") { + socket_path = read_setting_value(i, end, nullptr); + } + else if (setting == "socket-permissions") { + string sock_perm_str = read_setting_value(i, end, nullptr); + std::size_t ind = 0; + try { + socket_perms = std::stoi(sock_perm_str, &ind, 8); + if (ind != sock_perm_str.length()) { + throw std::logic_error(""); } } - else if (setting == "socket-uid") { - string sock_uid_s = read_setting_value(i, end, nullptr); - socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid); - } - else if (setting == "socket-gid") { - string sock_gid_s = read_setting_value(i, end, nullptr); - socket_gid = parse_gid_param(sock_gid_s, name); - } - else if (setting == "stop-command") { - stop_command = read_setting_value(i, end, &stop_command_offsets); - } - else if (setting == "pid-file") { - pid_file = read_setting_value(i, end); - } - else if (setting == "depends-on") { - string dependency_name = read_setting_value(i, end); - depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::REGULAR); + catch (std::logic_error &exc) { + throw service_description_exc(name, "socket-permissions: Badly-formed or " + "out-of-range numeric value"); } - else if (setting == "depends-ms") { - string dependency_name = read_setting_value(i, end); - depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::MILESTONE); - } - else if (setting == "waits-for") { - string dependency_name = read_setting_value(i, end); - depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::WAITS_FOR); + } + else if (setting == "socket-uid") { + string sock_uid_s = read_setting_value(i, end, nullptr); + socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid); + } + else if (setting == "socket-gid") { + string sock_gid_s = read_setting_value(i, end, nullptr); + socket_gid = parse_gid_param(sock_gid_s, name); + } + else if (setting == "stop-command") { + stop_command = read_setting_value(i, end, &stop_command_offsets); + } + else if (setting == "pid-file") { + pid_file = read_setting_value(i, end); + } + else if (setting == "depends-on") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::REGULAR); + } + else if (setting == "depends-ms") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::MILESTONE); + } + else if (setting == "waits-for") { + string dependency_name = read_setting_value(i, end); + depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::WAITS_FOR); + } + else if (setting == "waits-for.d") { + string waitsford = read_setting_value(i, end); + process_dep_dir(*this, name, depends, waitsford, dependency_type::WAITS_FOR); + } + else if (setting == "logfile") { + logfile = read_setting_value(i, end); + } + else if (setting == "restart") { + string restart = read_setting_value(i, end); + auto_restart = (restart == "yes" || restart == "true"); + } + else if (setting == "smooth-recovery") { + string recovery = read_setting_value(i, end); + smooth_recovery = (recovery == "yes" || recovery == "true"); + } + else if (setting == "type") { + string type_str = read_setting_value(i, end); + if (type_str == "scripted") { + service_type = service_type_t::SCRIPTED; } - else if (setting == "waits-for.d") { - string waitsford = read_setting_value(i, end); - process_dep_dir(*this, name, depends, waitsford, dependency_type::WAITS_FOR); + else if (type_str == "process") { + service_type = service_type_t::PROCESS; } - else if (setting == "logfile") { - logfile = read_setting_value(i, end); + else if (type_str == "bgprocess") { + service_type = service_type_t::BGPROCESS; } - else if (setting == "restart") { - string restart = read_setting_value(i, end); - auto_restart = (restart == "yes" || restart == "true"); + else if (type_str == "internal") { + service_type = service_type_t::INTERNAL; } - else if (setting == "smooth-recovery") { - string recovery = read_setting_value(i, end); - smooth_recovery = (recovery == "yes" || recovery == "true"); + else { + throw service_description_exc(name, "Service type must be one of: \"scripted\"," + " \"process\", \"bgprocess\" or \"internal\""); } - else if (setting == "type") { - string type_str = read_setting_value(i, end); - if (type_str == "scripted") { - service_type = service_type_t::SCRIPTED; + } + else if (setting == "options") { + std::list> indices; + string onstart_cmds = read_setting_value(i, end, &indices); + for (auto indexpair : indices) { + string option_txt = onstart_cmds.substr(indexpair.first, + indexpair.second - indexpair.first); + if (option_txt == "starts-rwfs") { + onstart_flags.rw_ready = true; } - else if (type_str == "process") { - service_type = service_type_t::PROCESS; + else if (option_txt == "starts-log") { + onstart_flags.log_ready = true; } - else if (type_str == "bgprocess") { - service_type = service_type_t::BGPROCESS; + else if (option_txt == "no-sigterm") { + onstart_flags.no_sigterm = true; } - else if (type_str == "internal") { - service_type = service_type_t::INTERNAL; + else if (option_txt == "runs-on-console") { + onstart_flags.runs_on_console = true; + // A service that runs on the console necessarily starts on console: + onstart_flags.starts_on_console = true; } - else { - throw service_description_exc(name, "Service type must be one of: \"scripted\"," - " \"process\", \"bgprocess\" or \"internal\""); + else if (option_txt == "starts-on-console") { + onstart_flags.starts_on_console = true; } - } - else if (setting == "options") { - std::list> indices; - string onstart_cmds = read_setting_value(i, end, &indices); - for (auto indexpair : indices) { - string option_txt = onstart_cmds.substr(indexpair.first, - indexpair.second - indexpair.first); - if (option_txt == "starts-rwfs") { - onstart_flags.rw_ready = true; - } - else if (option_txt == "starts-log") { - onstart_flags.log_ready = true; - } - else if (option_txt == "no-sigterm") { - onstart_flags.no_sigterm = true; - } - else if (option_txt == "runs-on-console") { - onstart_flags.runs_on_console = true; - // A service that runs on the console necessarily starts on console: - onstart_flags.starts_on_console = true; - } - else if (option_txt == "starts-on-console") { - onstart_flags.starts_on_console = true; - } - else if (option_txt == "pass-cs-fd") { - onstart_flags.pass_cs_fd = true; - } - else if (option_txt == "start-interruptible") { - onstart_flags.start_interruptible = true; - } - else if (option_txt == "skippable") { - onstart_flags.skippable = true; - } - else if (option_txt == "signal-process-only") { - onstart_flags.signal_process_only = true; - } - else { - throw service_description_exc(name, "Unknown option: " + option_txt); - } + else if (option_txt == "pass-cs-fd") { + onstart_flags.pass_cs_fd = true; } - } - else if (setting == "load-options") { - std::list> indices; - string load_opts = read_setting_value(i, end, &indices); - for (auto indexpair : indices) { - string option_txt = load_opts.substr(indexpair.first, - indexpair.second - indexpair.first); - if (option_txt == "sub-vars") { - // substitute environment variables in command line - do_sub_vars = true; - } - else if (option_txt == "no-sub-vars") { - do_sub_vars = false; - } - else { - throw service_description_exc(name, "Unknown load option: " + option_txt); - } + else if (option_txt == "start-interruptible") { + onstart_flags.start_interruptible = true; } - } - else if (setting == "termsignal") { - string signame = read_setting_value(i, end, nullptr); - int signo = signal_name_to_number(signame); - if (signo == -1) { - throw service_description_exc(name, "Unknown/unsupported termination signal: " - + signame); + else if (option_txt == "skippable") { + onstart_flags.skippable = true; + } + else if (option_txt == "signal-process-only") { + onstart_flags.signal_process_only = true; } else { - term_signal = signo; + throw service_description_exc(name, "Unknown option: " + option_txt); } } - else if (setting == "restart-limit-interval") { - string interval_str = read_setting_value(i, end, nullptr); - parse_timespec(interval_str, name, "restart-limit-interval", restart_interval); - } - else if (setting == "restart-delay") { - string rsdelay_str = read_setting_value(i, end, nullptr); - parse_timespec(rsdelay_str, name, "restart-delay", restart_delay); - } - else if (setting == "restart-limit-count") { - string limit_str = read_setting_value(i, end, nullptr); - max_restarts = parse_unum_param(limit_str, name, std::numeric_limits::max()); - } - else if (setting == "stop-timeout") { - string stoptimeout_str = read_setting_value(i, end, nullptr); - parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout); - } - else if (setting == "start-timeout") { - string starttimeout_str = read_setting_value(i, end, nullptr); - parse_timespec(starttimeout_str, name, "start-timeout", start_timeout); + } + else if (setting == "load-options") { + std::list> indices; + string load_opts = read_setting_value(i, end, &indices); + for (auto indexpair : indices) { + string option_txt = load_opts.substr(indexpair.first, + indexpair.second - indexpair.first); + if (option_txt == "sub-vars") { + // substitute environment variables in command line + do_sub_vars = true; + } + else if (option_txt == "no-sub-vars") { + do_sub_vars = false; + } + else { + throw service_description_exc(name, "Unknown load option: " + option_txt); + } } - else if (setting == "run-as") { - string run_as_str = read_setting_value(i, end, nullptr); - run_as_uid = parse_uid_param(run_as_str, name, &run_as_gid); + } + else if (setting == "termsignal") { + string signame = read_setting_value(i, end, nullptr); + int signo = signal_name_to_number(signame); + if (signo == -1) { + throw service_description_exc(name, "Unknown/unsupported termination signal: " + + signame); } else { - throw service_description_exc(name, "Unknown setting: " + setting); + term_signal = signo; } } - } - + else if (setting == "restart-limit-interval") { + string interval_str = read_setting_value(i, end, nullptr); + parse_timespec(interval_str, name, "restart-limit-interval", restart_interval); + } + else if (setting == "restart-delay") { + string rsdelay_str = read_setting_value(i, end, nullptr); + parse_timespec(rsdelay_str, name, "restart-delay", restart_delay); + } + else if (setting == "restart-limit-count") { + string limit_str = read_setting_value(i, end, nullptr); + max_restarts = parse_unum_param(limit_str, name, std::numeric_limits::max()); + } + else if (setting == "stop-timeout") { + string stoptimeout_str = read_setting_value(i, end, nullptr); + parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout); + } + else if (setting == "start-timeout") { + string starttimeout_str = read_setting_value(i, end, nullptr); + parse_timespec(starttimeout_str, name, "start-timeout", start_timeout); + } + else if (setting == "run-as") { + string run_as_str = read_setting_value(i, end, nullptr); + run_as_uid = parse_uid_param(run_as_str, name, &run_as_gid); + } + else { + throw service_description_exc(name, "Unknown setting: " + setting); + } + }); + service_file.close(); if (service_type == service_type_t::PROCESS || service_type == service_type_t::BGPROCESS -- 2.25.1