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