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