37507dd866427c06516211cafc76051be2124cec
[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_error(std::system_error &exc, const std::string &service_name)
138 {
139     std::cerr << "Service '" << service_name << "', error reading service description: " << exc.what() << "\n";
140 }
141
142 static void report_dir_error(const char *service_name, const std::string &dirpath)
143 {
144     std::cerr << "Service '" << service_name << "', error reading dependencies from directory " << dirpath
145             << ": " << strerror(errno) << "\n";
146 }
147
148 // Process a dependency directory - filenames contained within correspond to service names which
149 // are loaded and added as a dependency of the given type. Expected use is with a directory
150 // containing symbolic links to other service descriptions, but this isn't required.
151 // Failure to read the directory contents, or to find a service listed within, is not considered
152 // a fatal error.
153 static void process_dep_dir(const char *servicename,
154         const string &service_filename,
155         std::list<prelim_dep> &deplist, const std::string &depdirpath,
156         dependency_type dep_type)
157 {
158     std::string depdir_fname = combine_paths(parent_path(service_filename), depdirpath.c_str());
159
160     DIR *depdir = opendir(depdir_fname.c_str());
161     if (depdir == nullptr) {
162         report_dir_error(servicename, depdirpath);
163         return;
164     }
165
166     errno = 0;
167     dirent * dent = readdir(depdir);
168     while (dent != nullptr) {
169         char * name =  dent->d_name;
170         if (name[0] != '.') {
171             deplist.emplace_back(name, dep_type);
172         }
173         dent = readdir(depdir);
174     }
175
176     if (errno != 0) {
177         report_dir_error(servicename, depdirpath);
178     }
179
180     closedir(depdir);
181 }
182
183 // TODO: this is pretty much copy-paste from load_service.cc. Need to factor out common structure.
184 service_record *load_service(service_set_t &services, const std::string &name,
185         const service_dir_pathlist &service_dirs)
186 {
187     using namespace std;
188     using namespace dinit_load;
189
190     auto found = services.find(name);
191     if (found != services.end()) {
192         return found->second;
193     }
194
195     string service_filename;
196     ifstream service_file;
197
198     // Couldn't find one. Have to load it.
199     for (auto &service_dir : service_dirs) {
200         service_filename = service_dir.get_dir();
201         if (*(service_filename.rbegin()) != '/') {
202             service_filename += '/';
203         }
204         service_filename += name;
205
206         service_file.open(service_filename.c_str(), ios::in);
207         if (service_file) break;
208     }
209
210     if (! service_file) {
211         throw service_not_found(string(name));
212     }
213
214     string command;
215     list<pair<unsigned,unsigned>> command_offsets;
216     string stop_command;
217     list<pair<unsigned,unsigned>> stop_command_offsets;
218     string working_dir;
219     string pid_file;
220     string env_file;
221
222     bool do_sub_vars = false;
223
224     service_type_t service_type = service_type_t::PROCESS;
225     std::list<prelim_dep> depends;
226     string logfile;
227     service_flags_t onstart_flags;
228     int term_signal = -1;  // additional termination signal
229     bool auto_restart = false;
230     bool smooth_recovery = false;
231     string socket_path;
232     int socket_perms = 0666;
233     // Note: Posix allows that uid_t and gid_t may be unsigned types, but eg chown uses -1 as an
234     // invalid value, so it's safe to assume that we can do the same:
235     uid_t socket_uid = -1;
236     gid_t socket_gid = -1;
237     // Restart limit interval / count; default is 10 seconds, 3 restarts:
238     timespec restart_interval = { .tv_sec = 10, .tv_nsec = 0 };
239     int max_restarts = 3;
240     timespec restart_delay = { .tv_sec = 0, .tv_nsec = 200000000 };
241     timespec stop_timeout = { .tv_sec = 10, .tv_nsec = 0 };
242     timespec start_timeout = { .tv_sec = 60, .tv_nsec = 0 };
243     std::vector<service_rlimits> rlimits;
244
245     int readiness_fd = -1;      // readiness fd in service process
246     std::string readiness_var;  // environment var to hold readiness fd
247
248     uid_t run_as_uid = -1;
249     gid_t run_as_gid = -1;
250
251     string chain_to_name;
252
253     #if USE_UTMPX
254     char inittab_id[sizeof(utmpx().ut_id)] = {0};
255     char inittab_line[sizeof(utmpx().ut_line)] = {0};
256     #endif
257
258     string line;
259     service_file.exceptions(ios::badbit);
260
261     try {
262         process_service_file(name, service_file,
263                 [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void {
264             try {
265                 if (setting == "command") {
266                     command = read_setting_value(i, end, &command_offsets);
267                 }
268                 else if (setting == "working-dir") {
269                     working_dir = read_setting_value(i, end, nullptr);
270                 }
271                 else if (setting == "env-file") {
272                     env_file = read_setting_value(i, end, nullptr);
273                 }
274                 else if (setting == "socket-listen") {
275                     socket_path = read_setting_value(i, end, nullptr);
276                 }
277                 else if (setting == "socket-permissions") {
278                     string sock_perm_str = read_setting_value(i, end, nullptr);
279                     std::size_t ind = 0;
280                     try {
281                         socket_perms = std::stoi(sock_perm_str, &ind, 8);
282                         if (ind != sock_perm_str.length()) {
283                             throw std::logic_error("");
284                         }
285                     }
286                     catch (std::logic_error &exc) {
287                         throw service_description_exc(name, "socket-permissions: Badly-formed or "
288                                 "out-of-range numeric value");
289                     }
290                 }
291                 else if (setting == "socket-uid") {
292                     string sock_uid_s = read_setting_value(i, end, nullptr);
293                     socket_uid = parse_uid_param(sock_uid_s, name, &socket_gid);
294                 }
295                 else if (setting == "socket-gid") {
296                     string sock_gid_s = read_setting_value(i, end, nullptr);
297                     socket_gid = parse_gid_param(sock_gid_s, name);
298                 }
299                 else if (setting == "stop-command") {
300                     stop_command = read_setting_value(i, end, &stop_command_offsets);
301                 }
302                 else if (setting == "pid-file") {
303                     pid_file = read_setting_value(i, end);
304                 }
305                 else if (setting == "depends-on") {
306                     string dependency_name = read_setting_value(i, end);
307                     depends.emplace_back(std::move(dependency_name), dependency_type::REGULAR);
308                 }
309                 else if (setting == "depends-ms") {
310                     string dependency_name = read_setting_value(i, end);
311                     depends.emplace_back(dependency_name, dependency_type::MILESTONE);
312                 }
313                 else if (setting == "waits-for") {
314                     string dependency_name = read_setting_value(i, end);
315                     depends.emplace_back(dependency_name, dependency_type::WAITS_FOR);
316                 }
317                 else if (setting == "waits-for.d") {
318                     string waitsford = read_setting_value(i, end);
319                     process_dep_dir(name.c_str(), service_filename, depends, waitsford,
320                             dependency_type::WAITS_FOR);
321                 }
322                 else if (setting == "logfile") {
323                     logfile = read_setting_value(i, end);
324                 }
325                 else if (setting == "restart") {
326                     string restart = read_setting_value(i, end);
327                     auto_restart = (restart == "yes" || restart == "true");
328                 }
329                 else if (setting == "smooth-recovery") {
330                     string recovery = read_setting_value(i, end);
331                     smooth_recovery = (recovery == "yes" || recovery == "true");
332                 }
333                 else if (setting == "type") {
334                     string type_str = read_setting_value(i, end);
335                     if (type_str == "scripted") {
336                         service_type = service_type_t::SCRIPTED;
337                     }
338                     else if (type_str == "process") {
339                         service_type = service_type_t::PROCESS;
340                     }
341                     else if (type_str == "bgprocess") {
342                         service_type = service_type_t::BGPROCESS;
343                     }
344                     else if (type_str == "internal") {
345                         service_type = service_type_t::INTERNAL;
346                     }
347                     else {
348                         throw service_description_exc(name, "Service type must be one of: \"scripted\","
349                             " \"process\", \"bgprocess\" or \"internal\"");
350                     }
351                 }
352                 else if (setting == "options") {
353                     std::list<std::pair<unsigned,unsigned>> indices;
354                     string onstart_cmds = read_setting_value(i, end, &indices);
355                     for (auto indexpair : indices) {
356                         string option_txt = onstart_cmds.substr(indexpair.first,
357                                 indexpair.second - indexpair.first);
358                         if (option_txt == "starts-rwfs") {
359                             onstart_flags.rw_ready = true;
360                         }
361                         else if (option_txt == "starts-log") {
362                             onstart_flags.log_ready = true;
363                         }
364                         else if (option_txt == "no-sigterm") {
365                             onstart_flags.no_sigterm = true;
366                         }
367                         else if (option_txt == "runs-on-console") {
368                             onstart_flags.runs_on_console = true;
369                             // A service that runs on the console necessarily starts on console:
370                             onstart_flags.starts_on_console = true;
371                             onstart_flags.shares_console = false;
372                         }
373                         else if (option_txt == "starts-on-console") {
374                             onstart_flags.starts_on_console = true;
375                             onstart_flags.shares_console = false;
376                         }
377                         else if (option_txt == "shares-console") {
378                             onstart_flags.shares_console = true;
379                             onstart_flags.runs_on_console = false;
380                             onstart_flags.starts_on_console = false;
381                         }
382                         else if (option_txt == "pass-cs-fd") {
383                             onstart_flags.pass_cs_fd = true;
384                         }
385                         else if (option_txt == "start-interruptible") {
386                             onstart_flags.start_interruptible = true;
387                         }
388                         else if (option_txt == "skippable") {
389                             onstart_flags.skippable = true;
390                         }
391                         else if (option_txt == "signal-process-only") {
392                             onstart_flags.signal_process_only = true;
393                         }
394                         else {
395                             throw service_description_exc(name, "Unknown option: " + option_txt);
396                         }
397                     }
398                 }
399                 else if (setting == "load-options") {
400                     std::list<std::pair<unsigned,unsigned>> indices;
401                     string load_opts = read_setting_value(i, end, &indices);
402                     for (auto indexpair : indices) {
403                         string option_txt = load_opts.substr(indexpair.first,
404                                 indexpair.second - indexpair.first);
405                         if (option_txt == "sub-vars") {
406                             // substitute environment variables in command line
407                             do_sub_vars = true;
408                         }
409                         else if (option_txt == "no-sub-vars") {
410                             do_sub_vars = false;
411                         }
412                         else {
413                             throw service_description_exc(name, "Unknown load option: " + option_txt);
414                         }
415                     }
416                 }
417                 else if (setting == "term-signal" || setting == "termsignal") {
418                     // Note: "termsignal" supported for legacy reasons.
419                     string signame = read_setting_value(i, end, nullptr);
420                     int signo = signal_name_to_number(signame);
421                     if (signo == -1) {
422                         throw service_description_exc(name, "Unknown/unsupported termination signal: "
423                                 + signame);
424                     }
425                     else {
426                         term_signal = signo;
427                     }
428                 }
429                 else if (setting == "restart-limit-interval") {
430                     string interval_str = read_setting_value(i, end, nullptr);
431                     parse_timespec(interval_str, name, "restart-limit-interval", restart_interval);
432                 }
433                 else if (setting == "restart-delay") {
434                     string rsdelay_str = read_setting_value(i, end, nullptr);
435                     parse_timespec(rsdelay_str, name, "restart-delay", restart_delay);
436                 }
437                 else if (setting == "restart-limit-count") {
438                     string limit_str = read_setting_value(i, end, nullptr);
439                     max_restarts = parse_unum_param(limit_str, name, std::numeric_limits<int>::max());
440                 }
441                 else if (setting == "stop-timeout") {
442                     string stoptimeout_str = read_setting_value(i, end, nullptr);
443                     parse_timespec(stoptimeout_str, name, "stop-timeout", stop_timeout);
444                 }
445                 else if (setting == "start-timeout") {
446                     string starttimeout_str = read_setting_value(i, end, nullptr);
447                     parse_timespec(starttimeout_str, name, "start-timeout", start_timeout);
448                 }
449                 else if (setting == "run-as") {
450                     string run_as_str = read_setting_value(i, end, nullptr);
451                     run_as_uid = parse_uid_param(run_as_str, name, &run_as_gid);
452                 }
453                 else if (setting == "chain-to") {
454                     chain_to_name = read_setting_value(i, end, nullptr);
455                 }
456                 else if (setting == "ready-notification") {
457                     string notify_setting = read_setting_value(i, end, nullptr);
458                     if (starts_with(notify_setting, "pipefd:")) {
459                         readiness_fd = parse_unum_param(notify_setting.substr(7 /* len 'pipefd:' */),
460                                 name, std::numeric_limits<int>::max());
461                     }
462                     else if (starts_with(notify_setting, "pipevar:")) {
463                         readiness_var = notify_setting.substr(8 /* len 'pipevar:' */);
464                         if (readiness_var.empty()) {
465                             throw service_description_exc(name, "Invalid pipevar variable name "
466                                     "in ready-notification");
467                         }
468                     }
469                     else {
470                         throw service_description_exc(name, "Unknown ready-notification setting: "
471                                 + notify_setting);
472                     }
473                 }
474                 else if (setting == "inittab-id") {
475                     string inittab_setting = read_setting_value(i, end, nullptr);
476                     #if USE_UTMPX
477                     if (inittab_setting.length() > sizeof(inittab_id)) {
478                         throw service_description_exc(name, "inittab-id setting is too long");
479                     }
480                     strncpy(inittab_id, inittab_setting.c_str(), sizeof(inittab_id));
481                     #endif
482                 }
483                 else if (setting == "inittab-line") {
484                     string inittab_setting = read_setting_value(i, end, nullptr);
485                     #if USE_UTMPX
486                     if (inittab_setting.length() > sizeof(inittab_line)) {
487                         throw service_description_exc(name, "inittab-line setting is too long");
488                     }
489                     strncpy(inittab_line, inittab_setting.c_str(), sizeof(inittab_line));
490                     #endif
491                 }
492                 else if (setting == "rlimit-nofile") {
493                     string nofile_setting = read_setting_value(i, end, nullptr);
494                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_NOFILE);
495                     parse_rlimit(line, name, "rlimit-nofile", nofile_limits);
496                 }
497                 else if (setting == "rlimit-core") {
498                     string nofile_setting = read_setting_value(i, end, nullptr);
499                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_CORE);
500                     parse_rlimit(line, name, "rlimit-core", nofile_limits);
501                 }
502                 else if (setting == "rlimit-data") {
503                     string nofile_setting = read_setting_value(i, end, nullptr);
504                     service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_DATA);
505                     parse_rlimit(line, name, "rlimit-data", nofile_limits);
506                 }
507                 else if (setting == "rlimit-addrspace") {
508                     #if defined(RLIMIT_AS)
509                         string nofile_setting = read_setting_value(i, end, nullptr);
510                         service_rlimits &nofile_limits = find_rlimits(rlimits, RLIMIT_AS);
511                         parse_rlimit(line, name, "rlimit-addrspace", nofile_limits);
512                     #endif
513                 }
514                 else {
515                     report_unknown_setting_error(name, setting.c_str());
516                 }
517             }
518             catch (setting_exception &exc) {
519                 report_error(exc, name, setting.c_str());
520             }
521         });
522     }
523     catch (std::system_error &sys_err)
524     {
525         report_error(sys_err, name);
526         return nullptr;
527     }
528
529     return new service_record(name, depends);
530 }