Service description loading: improve error messages
[oweals/dinit.git] / src / dinitcheck.cc
1 #include <algorithm>
2 #include <iostream>
3 #include <fstream>
4 #include <cstring>
5 #include <string>
6 #include <vector>
7 #include <list>
8 #include <map>
9
10 #include <unistd.h>
11 #include <sys/types.h>
12 #include <sys/time.h>
13 #include <sys/resource.h>
14 #include <pwd.h>
15 #include <dirent.h>
16
17 #include "dinit-util.h"
18 #include "service-constants.h"
19 #include "load-service.h"
20 #include "options-processing.h"
21
22 // dinitcheck:  utility to check Dinit configuration for correctness/lint
23
24 using string = std::string;
25 using string_iterator = std::string::iterator;
26
27 // prelim_dep: A preliminary (unresolved) service dependency
28 class prelim_dep
29 {
30     public:
31     std::string name;
32     dependency_type dep_type;
33
34     prelim_dep(std::string &name_p, dependency_type dep_type_p)
35         : name(name_p), dep_type(dep_type_p) { }
36     prelim_dep(std::string &&name_p, dependency_type dep_type_p)
37         : name(std::move(name_p)), dep_type(dep_type_p) { }
38 };
39
40 class service_record
41 {
42 public:
43     service_record(std::string name, std::list<prelim_dep> dependencies_p) : dependencies(dependencies_p) {}
44
45     std::string name;
46     bool finished_loading = false;  // flag used to detect cyclic dependencies
47     std::list<prelim_dep> dependencies;
48 };
49
50 using service_set_t = std::map<std::string, service_record *>;
51
52 service_record *load_service(service_set_t &services, const std::string &name,
53         const service_dir_pathlist &service_dirs);
54
55 // Add some missing standard library functionality...
56 template <typename T> bool contains(std::vector<T> vec, const T& elem)
57 {
58     return std::find(vec.begin(), vec.end(), elem) != vec.end();
59 }
60
61 int main(int argc, char **argv)
62 {
63     using namespace std;
64
65     service_dir_opt service_dir_opts;
66     bool am_system_init = (getuid() == 0);
67
68     std::vector<std::string> services_to_check;
69
70     // Process command line
71     if (argc > 1) {
72         for (int i = 1; i < argc; i++) {
73             if (argv[i][0] == '-') {
74                 if (argv[i][0] == '-') {
75                     // An option...
76                     if (strcmp(argv[i], "--services-dir") == 0 || strcmp(argv[i], "-d") == 0) {
77                         if (++i < argc) {
78                             service_dir_opts.set_specified_service_dir(argv[i]);
79                         }
80                         else {
81                             cerr << "dinitcheck: '--services-dir' (-d) requires an argument" << endl;
82                             return 1;
83                         }
84                     }
85                 }
86                 // TODO handle other options, err if unrecognized
87             }
88         }
89     }
90
91     service_dir_opts.build_paths(am_system_init);
92
93     // Temporary, for testing:
94     services_to_check.push_back("boot");
95
96     // Load named service(s)
97     std::map<std::string, service_record *> service_set;
98
99     // - load the service, store dependencies as strings
100     // - recurse
101
102     // TODO additional: check chain-to, other lint
103
104     for (size_t i = 0; i < services_to_check.size(); ++i) {
105         const std::string &name = services_to_check[i];
106         std::cout << "Checking service: " << name << "...\n";
107         try {
108             service_record *sr = load_service(service_set, name, service_dir_opts.get_paths());
109             service_set[name] = sr;
110             // add dependencies to services_to_check
111             for (auto &dep : sr->dependencies) {
112                 if (service_set.count(dep.name) == 0 && !contains(services_to_check, dep.name)) {
113                     services_to_check.push_back(dep.name);
114                 }
115             }
116         }
117         catch (service_load_exc &exc) {
118             std::cerr << "Unable to load service '" << name << "': " << exc.exc_description << "\n";
119         }
120     }
121
122     // TODO check for circular dependencies
123
124     return 0;
125 }
126
127 static void report_unknown_setting_error(const std::string &service_name, const char *setting_name)
128 {
129     std::cerr << "Service '" << service_name << "', unknown setting: '" << setting_name << "'.\n";
130 }
131
132 static void report_error(dinit_load::setting_exception &exc, const std::string &service_name, const char *setting_name)
133 {
134     std::cerr << "Service '" << service_name << "', " << setting_name << ": " << exc.get_info() << "\n";
135 }
136
137 static void report_service_description_exc(service_description_exc &exc)
138 {
139     std::cerr << "Service '" << exc.service_name << "': " << exc.exc_description << "\n";
140 }
141
142 static void report_error(std::system_error &exc, const std::string &service_name)
143 {
144     std::cerr << "Service '" << service_name << "', error reading service description: " << exc.what() << "\n";
145 }
146
147 static void report_dir_error(const char *service_name, const std::string &dirpath)
148 {
149     std::cerr << "Service '" << service_name << "', error reading dependencies from directory " << dirpath
150             << ": " << strerror(errno) << "\n";
151 }
152
153 // Process a dependency directory - filenames contained within correspond to service names which
154 // are loaded and added as a dependency of the given type. Expected use is with a directory
155 // containing symbolic links to other service descriptions, but this isn't required.
156 // Failure to read the directory contents, or to find a service listed within, is not considered
157 // a fatal error.
158 static void process_dep_dir(const char *servicename,
159         const string &service_filename,
160         std::list<prelim_dep> &deplist, const std::string &depdirpath,
161         dependency_type dep_type)
162 {
163     std::string depdir_fname = combine_paths(parent_path(service_filename), depdirpath.c_str());
164
165     DIR *depdir = opendir(depdir_fname.c_str());
166     if (depdir == nullptr) {
167         report_dir_error(servicename, depdirpath);
168         return;
169     }
170
171     errno = 0;
172     dirent * dent = readdir(depdir);
173     while (dent != nullptr) {
174         char * name =  dent->d_name;
175         if (name[0] != '.') {
176             deplist.emplace_back(name, dep_type);
177         }
178         dent = readdir(depdir);
179     }
180
181     if (errno != 0) {
182         report_dir_error(servicename, depdirpath);
183     }
184
185     closedir(depdir);
186 }
187
188 // TODO: this is pretty much copy-paste from load_service.cc. Need to factor out common structure.
189 service_record *load_service(service_set_t &services, const std::string &name,
190         const service_dir_pathlist &service_dirs)
191 {
192     using namespace std;
193     using namespace dinit_load;
194
195     auto found = services.find(name);
196     if (found != services.end()) {
197         return found->second;
198     }
199
200     string service_filename;
201     ifstream service_file;
202
203     // Couldn't find one. Have to load it.
204     for (auto &service_dir : service_dirs) {
205         service_filename = service_dir.get_dir();
206         if (*(service_filename.rbegin()) != '/') {
207             service_filename += '/';
208         }
209         service_filename += name;
210
211         service_file.open(service_filename.c_str(), ios::in);
212         if (service_file) break;
213     }
214
215     if (! service_file) {
216         throw service_not_found(string(name));
217     }
218
219     string command;
220     list<pair<unsigned,unsigned>> command_offsets;
221     string stop_command;
222     list<pair<unsigned,unsigned>> stop_command_offsets;
223     string working_dir;
224     string pid_file;
225     string env_file;
226
227     bool do_sub_vars = false;
228
229     service_type_t service_type = service_type_t::PROCESS;
230     std::list<prelim_dep> depends;
231     string logfile;
232     service_flags_t onstart_flags;
233     int term_signal = -1;  // additional termination signal
234     bool auto_restart = false;
235     bool smooth_recovery = false;
236     string socket_path;
237     int socket_perms = 0666;
238     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
239     // invalid value, so it's safe to assume that we can do the same:
240     uid_t socket_uid = -1;
241     gid_t socket_gid = -1;
242     // Restart limit interval / count; default is 10 seconds, 3 restarts:
243     timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
244     int max_restarts = 3;
245     timespec restart_delay = { .tv_sec = 0, .tv_nsec = 200000000 };
246     timespec stop_timeout = { .tv_sec = 10, .tv_nsec = 0 };
247     timespec start_timeout = { .tv_sec = 60, .tv_nsec = 0 };
248     std::vector<service_rlimits> rlimits;
249
250     int readiness_fd = -1;      // readiness fd in service process
251     std::string readiness_var;  // environment var to hold readiness fd
252
253     uid_t run_as_uid = -1;
254     gid_t run_as_gid = -1;
255
256     string chain_to_name;
257
258     #if USE_UTMPX
259     char inittab_id[sizeof(utmpx().ut_id)] = {0};
260     char inittab_line[sizeof(utmpx().ut_line)] = {0};
261     #endif
262
263     string line;
264     service_file.exceptions(ios::badbit);
265
266     try {
267         process_service_file(name, service_file,
268                 [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void {
269             try {
270                 if (setting == "command") {
271                     command = read_setting_value(i, end, &command_offsets);
272                 }
273                 else if (setting == "working-dir") {
274                     working_dir = read_setting_value(i, end, nullptr);
275                 }
276                 else if (setting == "env-file") {
277                     env_file = read_setting_value(i, end, nullptr);
278                 }
279                 else if (setting == "socket-listen") {
280                     socket_path = read_setting_value(i, end, nullptr);
281                 }
282                 else if (setting == "socket-permissions") {
283                     string sock_perm_str = read_setting_value(i, end, nullptr);
284                     std::size_t ind = 0;
285                     try {
286                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
287                         if (ind != sock_perm_str.length()) {
288                             throw std::logic_error("");
289                         }
290                     }
291                     catch (std::logic_error &exc) {
292                         throw service_description_exc(name, "socket-permissions: Badly-formed or "
293                                 "out-of-range numeric value");
294                     }
295                 }
296                 else if (setting == "socket-uid") {
297                     string sock_uid_s = read_setting_value(i, end, nullptr);
298                     socket_uid = parse_uid_param(sock_uid_s, name, "socket-uid", &socket_gid);
299                 }
300                 else if (setting == "socket-gid") {
301                     string sock_gid_s = read_setting_value(i, end, nullptr);
302                     socket_gid = parse_gid_param(sock_gid_s, "socket-gid", name);
303                 }
304                 else if (setting == "stop-command") {
305                     stop_command = read_setting_value(i, end, &stop_command_offsets);
306                 }
307                 else if (setting == "pid-file") {
308                     pid_file = read_setting_value(i, end);
309                 }
310                 else if (setting == "depends-on") {
311                     string dependency_name = read_setting_value(i, end);
312                     depends.emplace_back(std::move(dependency_name), dependency_type::REGULAR);
313                 }
314                 else if (setting == "depends-ms") {
315                     string dependency_name = read_setting_value(i, end);
316                     depends.emplace_back(dependency_name, dependency_type::MILESTONE);
317                 }
318                 else if (setting == "waits-for") {
319                     string dependency_name = read_setting_value(i, end);
320                     depends.emplace_back(dependency_name, dependency_type::WAITS_FOR);
321                 }
322                 else if (setting == "waits-for.d") {
323                     string waitsford = read_setting_value(i, end);
324                     process_dep_dir(name.c_str(), service_filename, depends, waitsford,
325                             dependency_type::WAITS_FOR);
326                 }
327                 else if (setting == "logfile") {
328                     logfile = read_setting_value(i, end);
329                 }
330                 else if (setting == "restart") {
331                     string restart = read_setting_value(i, end);
332                     auto_restart = (restart == "yes" || restart == "true");
333                 }
334                 else if (setting == "smooth-recovery") {
335                     string recovery = read_setting_value(i, end);
336                     smooth_recovery = (recovery == "yes" || recovery == "true");
337                 }
338                 else if (setting == "type") {
339                     string type_str = read_setting_value(i, end);
340                     if (type_str == "scripted") {
341                         service_type = service_type_t::SCRIPTED;
342                     }
343                     else if (type_str == "process") {
344                         service_type = service_type_t::PROCESS;
345                     }
346                     else if (type_str == "bgprocess") {
347                         service_type = service_type_t::BGPROCESS;
348                     }
349                     else if (type_str == "internal") {
350                         service_type = service_type_t::INTERNAL;
351                     }
352                     else {
353                         throw service_description_exc(name, "Service type must be one of: \"scripted\","
354                             " \"process\", \"bgprocess\" or \"internal\"");
355                     }
356                 }
357                 else if (setting == "options") {
358                     std::list<std::pair<unsigned,unsigned>> indices;
359                     string onstart_cmds = read_setting_value(i, end, &indices);
360                     for (auto indexpair : indices) {
361                         string option_txt = onstart_cmds.substr(indexpair.first,
362                                 indexpair.second - indexpair.first);
363                         if (option_txt == "starts-rwfs") {
364                             onstart_flags.rw_ready = true;
365                         }
366                         else if (option_txt == "starts-log") {
367                             onstart_flags.log_ready = true;
368                         }
369                         else if (option_txt == "no-sigterm") {
370                             onstart_flags.no_sigterm = true;
371                         }
372                         else if (option_txt == "runs-on-console") {
373                             onstart_flags.runs_on_console = true;
374                             // A service that runs on the console necessarily starts on console:
375                             onstart_flags.starts_on_console = true;
376                             onstart_flags.shares_console = false;
377                         }
378                         else if (option_txt == "starts-on-console") {
379                             onstart_flags.starts_on_console = true;
380                             onstart_flags.shares_console = false;
381                         }
382                         else if (option_txt == "shares-console") {
383                             onstart_flags.shares_console = true;
384                             onstart_flags.runs_on_console = false;
385                             onstart_flags.starts_on_console = false;
386                         }
387                         else if (option_txt == "pass-cs-fd") {
388                             onstart_flags.pass_cs_fd = true;
389                         }
390                         else if (option_txt == "start-interruptible") {
391                             onstart_flags.start_interruptible = true;
392                         }
393                         else if (option_txt == "skippable") {
394                             onstart_flags.skippable = true;
395                         }
396                         else if (option_txt == "signal-process-only") {
397                             onstart_flags.signal_process_only = true;
398                         }
399                         else {
400                             throw service_description_exc(name, "Unknown option: " + option_txt);
401                         }
402                     }
403                 }
404                 else if (setting == "load-options") {
405                     std::list<std::pair<unsigned,unsigned>> indices;
406                     string load_opts = read_setting_value(i, end, &indices);
407                     for (auto indexpair : indices) {
408                         string option_txt = load_opts.substr(indexpair.first,
409                                 indexpair.second - indexpair.first);
410                         if (option_txt == "sub-vars") {
411                             // substitute environment variables in command line
412                             do_sub_vars = true;
413                         }
414                         else if (option_txt == "no-sub-vars") {
415                             do_sub_vars = false;
416                         }
417                         else {
418                             throw service_description_exc(name, "Unknown load option: " + option_txt);
419                         }
420                     }
421                 }
422                 else if (setting == "term-signal" || setting == "termsignal") {
423                     // Note: "termsignal" supported for legacy reasons.
424                     string signame = read_setting_value(i, end, nullptr);
425                     int signo = signal_name_to_number(signame);
426                     if (signo == -1) {
427                         throw service_description_exc(name, "Unknown/unsupported termination signal: "
428                                 + signame);
429                     }
430                     else {
431                         term_signal = signo;
432                     }
433                 }
434                 else if (setting == "restart-limit-interval") {
435                     string interval_str = read_setting_value(i, end, nullptr);
436                     parse_timespec(interval_str, name, "restart-limit-interval", restart_interval);
437                 }
438                 else if (setting == "restart-delay") {
439                     string rsdelay_str = read_setting_value(i, end, nullptr);
440                     parse_timespec(rsdelay_str, name, "restart-delay", restart_delay);
441                 }
442                 else if (setting == "restart-limit-count") {
443                     string limit_str = read_setting_value(i, end, nullptr);
444                     max_restarts = parse_unum_param(limit_str, name, std::numeric_limits<int>::max());
445                 }
446                 else if (setting == "stop-timeout") {
447                     string stoptimeout_str = read_setting_value(i, end, nullptr);
448                     parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout);
449                 }
450                 else if (setting == "start-timeout") {
451                     string starttimeout_str = read_setting_value(i, end, nullptr);
452                     parse_timespec(starttimeout_str, name, "start-timeout", start_timeout);
453                 }
454                 else if (setting == "run-as") {
455                     string run_as_str = read_setting_value(i, end, nullptr);
456                     run_as_uid = parse_uid_param(run_as_str, name, "run-as", &run_as_gid);
457                 }
458                 else if (setting == "chain-to") {
459                     chain_to_name = read_setting_value(i, end, nullptr);
460                 }
461                 else if (setting == "ready-notification") {
462                     string notify_setting = read_setting_value(i, end, nullptr);
463                     if (starts_with(notify_setting, "pipefd:")) {
464                         readiness_fd = parse_unum_param(notify_setting.substr(7 /* len 'pipefd:' */),
465                                 name, std::numeric_limits<int>::max());
466                     }
467                     else if (starts_with(notify_setting, "pipevar:")) {
468                         readiness_var = notify_setting.substr(8 /* len 'pipevar:' */);
469                         if (readiness_var.empty()) {
470                             throw service_description_exc(name, "Invalid pipevar variable name "
471                                     "in ready-notification");
472                         }
473                     }
474                     else {
475                         throw service_description_exc(name, "Unknown ready-notification setting: "
476                                 + notify_setting);
477                     }
478                 }
479                 else if (setting == "inittab-id") {
480                     string inittab_setting = read_setting_value(i, end, nullptr);
481                     #if USE_UTMPX
482                     if (inittab_setting.length() > sizeof(inittab_id)) {
483                         throw service_description_exc(name, "inittab-id setting is too long");
484                     }
485                     strncpy(inittab_id, inittab_setting.c_str(), sizeof(inittab_id));
486                     #endif
487                 }
488                 else if (setting == "inittab-line") {
489                     string inittab_setting = read_setting_value(i, end, nullptr);
490                     #if USE_UTMPX
491                     if (inittab_setting.length() > sizeof(inittab_line)) {
492                         throw service_description_exc(name, "inittab-line setting is too long");
493                     }
494                     strncpy(inittab_line, inittab_setting.c_str(), sizeof(inittab_line));
495                     #endif
496                 }
497                 else if (setting == "rlimit-nofile") {
498                     string nofile_setting = read_setting_value(i, end, nullptr);
499                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_NOFILE);
500                     parse_rlimit(line, name, "rlimit-nofile", nofile_limits);
501                 }
502                 else if (setting == "rlimit-core") {
503                     string nofile_setting = read_setting_value(i, end, nullptr);
504                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_CORE);
505                     parse_rlimit(line, name, "rlimit-core", nofile_limits);
506                 }
507                 else if (setting == "rlimit-data") {
508                     string nofile_setting = read_setting_value(i, end, nullptr);
509                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_DATA);
510                     parse_rlimit(line, name, "rlimit-data", nofile_limits);
511                 }
512                 else if (setting == "rlimit-addrspace") {
513                     #if defined(RLIMIT_AS)
514                         string nofile_setting = read_setting_value(i, end, nullptr);
515                         service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_AS);
516                         parse_rlimit(line, name, "rlimit-addrspace", nofile_limits);
517                     #endif
518                 }
519                 else {
520                     report_unknown_setting_error(name, setting.c_str());
521                 }
522             }
523             catch (service_description_exc &exc) {
524                 report_service_description_exc(exc);
525             }
526             catch (setting_exception &exc) {
527                 report_error(exc, name, setting.c_str());
528             }
529         });
530     }
531     catch (std::system_error &sys_err)
532     {
533         report_error(sys_err, name);
534         return nullptr;
535     }
536
537     return new service_record(name, depends);
538 }