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