Allow decimal comma separator for times expressed as decimals.
[oweals/dinit.git] / src / load_service.cc
1 #include <algorithm>
2 #include <string>
3 #include <fstream>
4 #include <locale>
5 #include <iostream>
6 #include <limits>
7
8 #include <sys/stat.h>
9 #include <sys/types.h>
10 #include <pwd.h>
11 #include <grp.h>
12
13 #include "service.h"
14
15 using string = std::string;
16 using string_iterator = std::string::iterator;
17
18 // Utility function to skip white space. Returns an iterator at the
19 // first non-white-space position (or at end).
20 static string_iterator skipws(string_iterator i, string_iterator end)
21 {
22     using std::locale;
23     using std::isspace;
24     
25     while (i != end) {
26       if (! isspace(*i, locale::classic())) {
27         break;
28       }
29       ++i;
30     }
31     return i;
32 }
33
34 // Read a setting name.
35 static string read_setting_name(string_iterator & i, string_iterator end)
36 {
37     using std::locale;
38     using std::ctype;
39     using std::use_facet;
40     
41     const ctype<char> & facet = use_facet<ctype<char> >(locale::classic());
42
43     string rval;
44     // Allow alphabetical characters, and dash (-) in setting name
45     while (i != end && (*i == '-' || facet.is(ctype<char>::alpha, *i))) {
46         rval += *i;
47         ++i;
48     }
49     return rval;
50 }
51
52 namespace {
53     class setting_exception
54     {
55         std::string info;
56         
57         public:
58         setting_exception(const std::string &&exc_info) : info(std::move(exc_info))
59         {
60         }
61         
62         std::string &get_info()
63         {
64             return info;
65         }
66     };
67 }
68
69
70 // Read a setting value
71 //
72 // In general a setting value is a single-line string. It may contain multiple parts
73 // separated by white space (which is normally collapsed). A hash mark - # - denotes
74 // the end of the value and the beginning of a comment (it should be preceded by
75 // whitespace).
76 //
77 // Part of a value may be quoted using double quote marks, which prevents collapse
78 // of whitespace and interpretation of most special characters (the quote marks will
79 // not be considered part of the value). A backslash can precede a character (such
80 // as '#' or '"' or another backslash) to remove its special meaning. Newline
81 // characters are not allowed in values and cannot be quoted.
82 //
83 // This function expects the string to be in an ASCII-compatible, single byte
84 // encoding (the "classic" locale).
85 //
86 // Params:
87 //    service_name - the name of the service to which the setting applies
88 //    i  -  reference to string iterator through the line
89 //    end -   iterator at end of line
90 //    part_positions -  list of <int,int> to which the position of each setting value
91 //                      part will be added as [start,end). May be null.
92 static string read_setting_value(string_iterator & i, string_iterator end,
93         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
94 {
95     using std::locale;
96     using std::isspace;
97
98     i = skipws(i, end);
99     
100     string rval;
101     bool new_part = true;
102     int part_start;
103     
104     while (i != end) {
105         char c = *i;
106         if (c == '\"') {
107             if (new_part) {
108                 part_start = rval.length();
109                 new_part = false;
110             }
111             // quoted string
112             ++i;
113             while (i != end) {
114                 c = *i;
115                 if (c == '\"') break;
116                 if (c == '\n') {
117                     throw setting_exception("Line end inside quoted string");
118                 }
119                 else if (c == '\\') {
120                     // A backslash escapes the following character.
121                     ++i;
122                     if (i != end) {
123                         c = *i;
124                         if (c == '\n') {
125                             throw setting_exception("Line end follows backslash escape character (`\\')");
126                         }
127                         rval += c;
128                     }
129                 }
130                 else {
131                     rval += c;
132                 }
133                 ++i;
134             }
135             if (i == end) {
136                 // String wasn't terminated
137                 throw setting_exception("Unterminated quoted string");
138             }
139         }
140         else if (c == '\\') {
141             if (new_part) {
142                 part_start = rval.length();
143                 new_part = false;
144             }
145             // A backslash escapes the next character
146             ++i;
147             if (i != end) {
148                 rval += *i;
149             }
150             else {
151                 throw setting_exception("Backslash escape (`\\') not followed by character");
152             }
153         }
154         else if (isspace(c, locale::classic())) {
155             if (! new_part && part_positions != nullptr) {
156                 part_positions->emplace_back(part_start, rval.length());
157                 new_part = true;
158             }
159             i = skipws(i, end);
160             if (i == end) break;
161             if (*i == '#') break; // comment
162             rval += ' ';  // collapse ws to a single space
163             continue;
164         }
165         else if (c == '#') {
166             // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental
167             // comments in setting values.
168             throw setting_exception("hashmark (`#') comment must be separated from setting value by whitespace");
169         }
170         else {
171             if (new_part) {
172                 part_start = rval.length();
173                 new_part = false;
174             }
175             rval += c;
176         }
177         ++i;
178     }
179
180     // Got to end:
181     if (part_positions != nullptr) {
182         part_positions->emplace_back(part_start, rval.length());
183     }
184
185     return rval;
186 }
187
188 static int signal_name_to_number(std::string &signame)
189 {
190     if (signame == "HUP") return SIGHUP;
191     if (signame == "INT") return SIGINT;
192     if (signame == "QUIT") return SIGQUIT;
193     if (signame == "USR1") return SIGUSR1;
194     if (signame == "USR2") return SIGUSR2;
195     return -1;
196 }
197
198 static const char * uid_err_msg = "Specified user id contains invalid numeric characters or is outside allowed range.";
199
200 // Parse a userid parameter which may be a numeric user ID or a username. If a name, the
201 // userid is looked up via the system user database (getpwnam() function). In this case,
202 // the associated group is stored in the location specified by the group_p parameter iff
203 // it is not null and iff it contains the value -1.
204 static uid_t parse_uid_param(const std::string &param, const std::string &service_name, gid_t *group_p)
205 {
206     // Could be a name or a numeric id. But we should assume numeric first, just in case
207     // a user manages to give themselves a username that parses as a number.
208     std::size_t ind = 0;
209     try {
210         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
211         // is is probably safe to assume that valid values are positive. We'll also assert
212         // that the value range fits within "unsigned long long" since it seems unlikely
213         // that would ever not be the case.
214         static_assert((uintmax_t)std::numeric_limits<uid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "uid_t is too large");
215         unsigned long long v = std::stoull(param, &ind, 0);
216         if (v > static_cast<unsigned long long>(std::numeric_limits<uid_t>::max()) || ind != param.length()) {
217             throw service_description_exc(service_name, uid_err_msg);
218         }
219         return v;
220     }
221     catch (std::out_of_range &exc) {
222         throw service_description_exc(service_name, uid_err_msg);
223     }
224     catch (std::invalid_argument &exc) {
225         // Ok, so it doesn't look like a number: proceed...
226     }
227
228     errno = 0;
229     struct passwd * pwent = getpwnam(param.c_str());
230     if (pwent == nullptr) {
231         // Maybe an error, maybe just no entry.
232         if (errno == 0) {
233             throw service_description_exc(service_name, "Specified user \"" + param + "\" does not exist in system database.");
234         }
235         else {
236             throw service_description_exc(service_name, std::string("Error accessing user database: ") + strerror(errno));
237         }
238     }
239     
240     if (group_p && *group_p != (gid_t)-1) {
241         *group_p = pwent->pw_gid;
242     }
243     
244     return pwent->pw_uid;
245 }
246
247 static const char * num_err_msg = "Specified value contains invalid numeric characters or is outside allowed range.";
248
249 // Parse an unsigned numeric parameter value
250 static unsigned long long parse_unum_param(const std::string &param, const std::string &service_name,
251         unsigned long long max = std::numeric_limits<unsigned long long>::max())
252 {
253     std::size_t ind = 0;
254     try {
255         unsigned long long v = std::stoull(param, &ind, 0);
256         if (v > max || ind != param.length()) {
257             throw service_description_exc(service_name, num_err_msg);
258         }
259         return v;
260     }
261     catch (std::out_of_range &exc) {
262         throw service_description_exc(service_name, num_err_msg);
263     }
264     catch (std::invalid_argument &exc) {
265         throw service_description_exc(service_name, num_err_msg);
266     }
267 }
268
269 static const char * gid_err_msg = "Specified group id contains invalid numeric characters or is outside allowed range.";
270
271 static gid_t parse_gid_param(const std::string &param, const std::string &service_name)
272 {
273     // Could be a name or a numeric id. But we should assume numeric first, just in case
274     // a user manages to give themselves a username that parses as a number.
275     std::size_t ind = 0;
276     try {
277         // POSIX does not specify whether uid_t is an signed or unsigned, but regardless
278         // is is probably safe to assume that valid values are positive. We'll also assume
279         // that the value range fits with "unsigned long long" since it seems unlikely
280         // that would ever not be the case.
281         static_assert((uintmax_t)std::numeric_limits<gid_t>::max() <= (uintmax_t)std::numeric_limits<unsigned long long>::max(), "gid_t is too large");
282         unsigned long long v = std::stoull(param, &ind, 0);
283         if (v > static_cast<unsigned long long>(std::numeric_limits<gid_t>::max()) || ind != param.length()) {
284             throw service_description_exc(service_name, gid_err_msg);
285         }
286         return v;
287     }
288     catch (std::out_of_range &exc) {
289         throw service_description_exc(service_name, gid_err_msg);
290     }
291     catch (std::invalid_argument &exc) {
292         // Ok, so it doesn't look like a number: proceed...
293     }
294
295     errno = 0;
296     struct group * grent = getgrnam(param.c_str());
297     if (grent == nullptr) {
298         // Maybe an error, maybe just no entry.
299         if (errno == 0) {
300             throw service_description_exc(service_name, "Specified group \"" + param + "\" does not exist in system database.");
301         }
302         else {
303             throw service_description_exc(service_name, std::string("Error accessing group database: ") + strerror(errno));
304         }
305     }
306     
307     return grent->gr_gid;
308 }
309
310 // Parse a time, specified as a decimal number of seconds (with optional fractional component after decimal
311 // point or decimal comma).
312 static void parse_timespec(const std::string &paramval, const std::string &servicename,
313         const char * paramname, timespec &ts)
314 {
315     decltype(ts.tv_sec) isec = 0;
316     decltype(ts.tv_nsec) insec = 0;
317     auto max_secs = std::numeric_limits<decltype(isec)>::max() / 10;
318     auto len = paramval.length();
319     decltype(len) i;
320     for (i = 0; i < len; i++) {
321         char ch = paramval[i];
322         if (ch == '.' || ch == ',') {
323             i++;
324             break;
325         }
326         if (ch < '0' || ch > '9') {
327             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
328         }
329         // check for overflow
330         if (isec >= max_secs) {
331            throw service_description_exc(servicename, std::string("Too-large value for ") + paramname);
332         }
333         isec *= 10;
334         isec += ch - '0';
335     }
336     decltype(insec) insec_m = 100000000; // 10^8
337     for ( ; i < len; i++) {
338         char ch = paramval[i];
339         if (ch < '0' || ch > '9') {
340             throw service_description_exc(servicename, std::string("Bad value for ") + paramname);
341         }
342         insec += (ch - '0') * insec_m;
343         insec_m /= 10;
344     }
345     ts.tv_sec = isec;
346     ts.tv_nsec = insec;
347 }
348
349 // Find a service record, or load it from file. If the service has
350 // dependencies, load those also.
351 //
352 // Might throw a ServiceLoadExc exception if a dependency cycle is found or if another
353 // problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
354 // if a memory allocation failure occurs.
355 service_record * dirload_service_set::load_service(const char * name)
356 {
357     using std::string;
358     using std::ifstream;
359     using std::ios;
360     using std::ios_base;
361     using std::locale;
362     using std::isspace;
363     
364     using std::list;
365     using std::pair;
366     
367     // First try and find an existing record...
368     service_record * rval = find_service(string(name));
369     if (rval != 0) {
370         if (rval->isDummy()) {
371             throw service_cyclic_dependency(name);
372         }
373         return rval;
374     }
375
376     // Couldn't find one. Have to load it.    
377     string service_filename = service_dir;
378     if (*(service_filename.rbegin()) != '/') {
379         service_filename += '/';
380     }
381     service_filename += name;
382     
383     string command;
384     list<pair<unsigned,unsigned>> command_offsets;
385     string stop_command;
386     list<pair<unsigned,unsigned>> stop_command_offsets;
387     string pid_file;
388
389     service_type service_type = service_type::PROCESS;
390     std::list<prelim_dep> depends;
391     string logfile;
392     onstart_flags_t onstart_flags;
393     int term_signal = -1;  // additional termination signal
394     bool auto_restart = false;
395     bool smooth_recovery = false;
396     string socket_path;
397     int socket_perms = 0666;
398     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
399     // invalid value, so it's safe to assume that we can do the same:
400     uid_t socket_uid = -1;
401     gid_t socket_gid = -1;
402     // Restart limit interval / count; default is 10 seconds, 3 restarts:
403     timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
404     int max_restarts = 3;
405     timespec restart_delay = { .tv_sec = 0, .tv_nsec = 200000000 };
406     timespec stop_timeout = { .tv_sec = 10, .tv_nsec = 0 };
407     
408     string line;
409     ifstream service_file;
410     service_file.exceptions(ios::badbit | ios::failbit);
411     
412     try {
413         service_file.open(service_filename.c_str(), ios::in);
414     }
415     catch (std::ios_base::failure &exc) {
416         throw service_not_found(name);
417     }
418     
419     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
420     rval = new service_record(this, string(name));
421     add_service(rval);
422     
423     try {
424         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
425         service_file.exceptions(ios::badbit);
426         
427         while (! (service_file.rdstate() & ios::eofbit)) {
428             getline(service_file, line);
429             string::iterator i = line.begin();
430             string::iterator end = line.end();
431           
432             i = skipws(i, end);
433             if (i != end) {
434                 if (*i == '#') {
435                     continue;  // comment line
436                 }
437                 string setting = read_setting_name(i, end);
438                 i = skipws(i, end);
439                 if (i == end || (*i != '=' && *i != ':')) {
440                     throw service_description_exc(name, "Badly formed line.");
441                 }
442                 i = skipws(++i, end);
443                 
444                 if (setting == "command") {
445                     command = read_setting_value(i, end, &command_offsets);
446                 }
447                 else if (setting == "socket-listen") {
448                     socket_path = read_setting_value(i, end, nullptr);
449                 }
450                 else if (setting == "socket-permissions") {
451                     string sock_perm_str = read_setting_value(i, end, nullptr);
452                     std::size_t ind = 0;
453                     try {
454                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
455                         if (ind != sock_perm_str.length()) {
456                             throw std::logic_error("");
457                         }
458                     }
459                     catch (std::logic_error &exc) {
460                         throw service_description_exc(name, "socket-permissions: Badly-formed or out-of-range numeric value");
461                     }
462                 }
463                 else if (setting == "socket-uid") {
464                     string sock_uid_s = read_setting_value(i, end, nullptr);
465                     socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid);
466                 }
467                 else if (setting == "socket-gid") {
468                     string sock_gid_s = read_setting_value(i, end, nullptr);
469                     socket_gid = parse_gid_param(sock_gid_s, name);
470                 }
471                 else if (setting == "stop-command") {
472                     stop_command = read_setting_value(i, end, &stop_command_offsets);
473                 }
474                 else if (setting == "pid-file") {
475                     pid_file = read_setting_value(i, end);
476                 }
477                 else if (setting == "depends-on") {
478                     string dependency_name = read_setting_value(i, end);
479                     depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::REGULAR);
480                 }
481                 else if (setting == "depends-ms") {
482                     string dependency_name = read_setting_value(i, end);
483                     depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::MILESTONE);
484                 }
485                 else if (setting == "waits-for") {
486                     string dependency_name = read_setting_value(i, end);
487                     depends.emplace_back(load_service(dependency_name.c_str()), dependency_type::WAITS_FOR);
488                 }
489                 else if (setting == "logfile") {
490                     logfile = read_setting_value(i, end);
491                 }
492                 else if (setting == "restart") {
493                     string restart = read_setting_value(i, end);
494                     auto_restart = (restart == "yes" || restart == "true");
495                 }
496                 else if (setting == "smooth-recovery") {
497                     string recovery = read_setting_value(i, end);
498                     smooth_recovery = (recovery == "yes" || recovery == "true");
499                 }
500                 else if (setting == "type") {
501                     string type_str = read_setting_value(i, end);
502                     if (type_str == "scripted") {
503                         service_type = service_type::SCRIPTED;
504                     }
505                     else if (type_str == "process") {
506                         service_type = service_type::PROCESS;
507                     }
508                     else if (type_str == "bgprocess") {
509                         service_type = service_type::BGPROCESS;
510                     }
511                     else if (type_str == "internal") {
512                         service_type = service_type::INTERNAL;
513                     }
514                     else {
515                         throw service_description_exc(name, "Service type must be one of: \"scripted\","
516                             " \"process\", \"bgprocess\" or \"internal\"");
517                     }
518                 }
519                 else if (setting == "options") {
520                     std::list<std::pair<unsigned,unsigned>> indices;
521                     string onstart_cmds = read_setting_value(i, end, &indices);
522                     for (auto indexpair : indices) {
523                         string option_txt = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
524                         if (option_txt == "starts-rwfs") {
525                             onstart_flags.rw_ready = true;
526                         }
527                         else if (option_txt == "starts-log") {
528                             onstart_flags.log_ready = true;
529                         }
530                         else if (option_txt == "no-sigterm") {
531                             onstart_flags.no_sigterm = true;
532                         }
533                         else if (option_txt == "runs-on-console") {
534                             onstart_flags.runs_on_console = true;
535                             // A service that runs on the console necessarily starts on console:
536                             onstart_flags.starts_on_console = true;
537                         }
538                         else if (option_txt == "starts-on-console") {
539                             onstart_flags.starts_on_console = true;
540                         }
541                         else if (option_txt == "pass-cs-fd") {
542                             onstart_flags.pass_cs_fd = true;
543                         }
544                         else {
545                             throw service_description_exc(name, "Unknown option: " + option_txt);
546                         }
547                     }
548                 }
549                 else if (setting == "termsignal") {
550                     string signame = read_setting_value(i, end, nullptr);
551                     int signo = signal_name_to_number(signame);
552                     if (signo == -1) {
553                         throw service_description_exc(name, "Unknown/unsupported termination signal: " + signame);
554                     }
555                     else {
556                         term_signal = signo;
557                     }
558                 }
559                 else if (setting == "restart-limit-interval") {
560                     string interval_str = read_setting_value(i, end, nullptr);
561                     parse_timespec(interval_str, name, "restart-limit-interval", restart_interval);
562                 }
563                 else if (setting == "restart-delay") {
564                     string rsdelay_str = read_setting_value(i, end, nullptr);
565                     parse_timespec(rsdelay_str, name, "restart-delay", restart_delay);
566                 }
567                 else if (setting == "restart-limit-count") {
568                     string limit_str = read_setting_value(i, end, nullptr);
569                     max_restarts = parse_unum_param(limit_str, name, std::numeric_limits<int>::max());
570                 }
571                 else if (setting == "stop-timeout") {
572                     string stoptimeout_str = read_setting_value(i, end, nullptr);
573                     parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout);
574                 }
575                 else {
576                     throw service_description_exc(name, "Unknown setting: " + setting);
577                 }
578             }
579         }
580         
581         service_file.close();
582         
583         if (service_type == service_type::PROCESS || service_type == service_type::BGPROCESS || service_type == service_type::SCRIPTED) {
584             if (command.length() == 0) {
585                 throw service_description_exc(name, "Service command not specified");
586             }
587         }
588         
589         // Now replace the dummy service record with a real record:
590         for (auto iter = records.begin(); iter != records.end(); iter++) {
591             if (*iter == rval) {
592                 // We've found the dummy record
593                 delete rval;
594                 if (service_type == service_type::PROCESS) {
595                     auto rvalps = new process_service(this, string(name), std::move(command),
596                             command_offsets, depends);
597                     rvalps->set_restart_interval(restart_interval, max_restarts);
598                     rvalps->set_restart_delay(restart_delay);
599                     rvalps->set_stop_timeout(stop_timeout);
600                     rval = rvalps;
601                 }
602                 else if (service_type == service_type::BGPROCESS) {
603                     auto rvalps = new bgproc_service(this, string(name), std::move(command),
604                             command_offsets, depends);
605                     rvalps->set_pid_file(std::move(pid_file));
606                     rvalps->set_restart_interval(restart_interval, max_restarts);
607                     rvalps->set_restart_delay(restart_delay);
608                     rvalps->set_stop_timeout(stop_timeout);
609                     rval = rvalps;
610                 }
611                 else if (service_type == service_type::SCRIPTED) {
612                     auto rvalps = new scripted_service(this, string(name), std::move(command),
613                             command_offsets, depends);
614                     rvalps->set_stop_command(stop_command, stop_command_offsets);
615                     rvalps->set_stop_timeout(stop_timeout);
616                     rval = rvalps;
617                 }
618                 else {
619                     rval = new service_record(this, string(name), service_type, depends);
620                 }
621                 rval->set_log_file(logfile);
622                 rval->set_auto_restart(auto_restart);
623                 rval->set_smooth_recovery(smooth_recovery);
624                 rval->set_flags(onstart_flags);
625                 rval->set_extra_termination_signal(term_signal);
626                 rval->set_socket_details(std::move(socket_path), socket_perms, socket_uid, socket_gid);
627                 *iter = rval;
628                 break;
629             }
630         }
631         
632         return rval;
633     }
634     catch (setting_exception &setting_exc)
635     {
636         // Must remove the dummy service record.
637         std::remove(records.begin(), records.end(), rval);
638         delete rval;
639         throw service_description_exc(name, std::move(setting_exc.get_info()));
640     }
641     catch (...) {
642         // Must remove the dummy service record.
643         std::remove(records.begin(), records.end(), rval);
644         delete rval;
645         throw;
646     }
647 }