Fix --help option in dinitcheck
[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(const 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_p, std::list<prelim_dep> dependencies_p)
44             : name(name_p), dependencies(dependencies_p) {}
45
46     std::string name;
47     std::list<prelim_dep> dependencies;
48
49     bool visited = false;  // flag used to detect cyclic dependencies
50     bool cycle_free = false;
51 };
52
53 using service_set_t = std::map<std::string, service_record *>;
54
55 service_record *load_service(service_set_t &services, const std::string &name,
56         const service_dir_pathlist &service_dirs);
57
58 // Add some missing standard library functionality...
59 template <typename T> bool contains(std::vector<T> vec, const T& elem)
60 {
61     return std::find(vec.begin(), vec.end(), elem) != vec.end();
62 }
63
64 static bool errors_found = false;
65
66 int main(int argc, char **argv)
67 {
68     using namespace std;
69
70     service_dir_opt service_dir_opts;
71     bool am_system_init = (getuid() == 0);
72
73     std::vector<std::string> services_to_check;
74
75     // Process command line
76     if (argc > 1) {
77         for (int i = 1; i < argc; i++) {
78             if (argv[i][0] == '-') {
79                 // An option...
80                 if (strcmp(argv[i], "--services-dir") == 0 || strcmp(argv[i], "-d") == 0) {
81                     if (++i < argc) {
82                         service_dir_opts.set_specified_service_dir(argv[i]);
83                     }
84                     else {
85                         cerr << "dinitcheck: '--services-dir' (-d) requires an argument" << endl;
86                         return 1;
87                     }
88                 }
89                 else if (strcmp(argv[i], "--help") == 0) {
90                     cout << "dinitcheck: check dinit service descriptions\n"
91                             " --help                       display help\n"
92                             " --services-dir <dir>, -d <dir>\n"
93                             "                              set base directory for service description\n"
94                             "                              files\n"
95                             " <service-name>               check service with name <service-name>\n";
96                     return EXIT_SUCCESS;
97                 }
98                 else {
99                     std::cerr << "dinitcheck: Unrecognized option: '" << argv[i] << "' (use '--help' for help)\n";
100                     return EXIT_FAILURE;
101                 }
102             }
103             else {
104                 services_to_check.push_back(argv[i]);
105             }
106         }
107     }
108
109     service_dir_opts.build_paths(am_system_init);
110
111     if (services_to_check.empty()) {
112         services_to_check.push_back("boot");
113     }
114
115     size_t num_services_to_check = services_to_check.size();
116
117     // Load named service(s)
118     // - load the service, store dependencies as strings
119     // - recurse
120
121     std::map<std::string, service_record *> service_set;
122
123     for (size_t i = 0; i < services_to_check.size(); ++i) {
124         const std::string &name = services_to_check[i];
125         std::cout << "Checking service: " << name << "...\n";
126         try {
127             service_record *sr = load_service(service_set, name, service_dir_opts.get_paths());
128             service_set[name] = sr;
129             // add dependencies to services_to_check
130             for (auto &dep : sr->dependencies) {
131                 if (service_set.count(dep.name) == 0 && !contains(services_to_check, dep.name)) {
132                     services_to_check.push_back(dep.name);
133                 }
134             }
135         }
136         catch (service_load_exc &exc) {
137             std::cerr << "Unable to load service '" << name << "': " << exc.exc_description << "\n";
138             errors_found = true;
139         }
140     }
141
142     // Check for circular dependencies
143     std::vector<std::tuple<service_record *, size_t>> service_chain;
144
145     for (size_t i = 0; i < num_services_to_check; ++i) {
146         service_record *root = service_set[services_to_check[i]];
147         if (root->visited) continue;
148
149         // invariant: service_chain is empty
150         service_chain.emplace_back(root, 0);
151
152         // Depth first traversal. If we find a link (dependency) on a service already visited (but not
153         // marked as cycle-free), we know then that we've found a cycle.
154         while (true) {
155             auto n = service_chain.size() - 1;
156             auto &last = service_chain[n];
157             service_record *last_record = std::get<0>(last);
158             size_t &index = std::get<1>(last);
159             if (index >= last_record->dependencies.size()) {
160                 // Processed all dependencies, go back up:
161                 last_record->cycle_free = true;
162                 service_chain.pop_back();
163                 if (n == 0) break;
164                 size_t &prev_index = std::get<1>(service_chain[n - 1]);
165                 ++prev_index;
166                 continue;
167             }
168             // Down the tree:
169             auto dep_it = std::next(last_record->dependencies.begin(), index);
170             service_record *next_link = service_set[dep_it->name];
171             if (next_link == nullptr) {
172                 ++index;
173                 continue;
174             }
175             if (next_link->visited) {
176                 if (! next_link->cycle_free) {
177                     // We've found a cycle. Clear entries before the beginning of the cycle, then
178                     // exit the loop.
179                     auto first = std::find_if(service_chain.begin(), service_chain.end(),
180                             [next_link](std::tuple<service_record *, size_t> &a) -> bool {
181                        return std::get<0>(a) == next_link;
182                     });
183                     service_chain.erase(service_chain.begin(), first);
184                     break;
185                 }
186             }
187             next_link->visited = true;
188             service_chain.emplace_back(next_link, 0);
189         }
190
191         // Report only one cycle; otherwise difficult to avoid reporting duplicates or overlapping
192         // cycles.
193         if (!service_chain.empty()) break;
194     }
195
196     if (!service_chain.empty()) {
197         errors_found = true;
198         std::cerr << "Found dependency cycle:\n";
199         for (auto chain_link : service_chain) {
200             std::cerr << "    " << std::get<0>(chain_link)->name << " ->\n";
201         }
202         std::cerr << "    " << std::get<0>(service_chain[0])->name << ".\n";
203     }
204
205     // TODO additional: check chain-to, other lint
206
207     if (! errors_found) {
208         std::cout << "No problems found.\n";
209     }
210     else {
211         std::cout << "One or more errors found.\n";
212     }
213
214     return errors_found ? EXIT_FAILURE : EXIT_SUCCESS;
215 }
216
217 static void report_service_description_exc(service_description_exc &exc)
218 {
219     std::cerr << "Service '" << exc.service_name << "': " << exc.exc_description << "\n";
220     errors_found = true;
221 }
222
223 static void report_error(std::system_error &exc, const std::string &service_name)
224 {
225     std::cerr << "Service '" << service_name << "', error reading service description: " << exc.what() << "\n";
226     errors_found = true;
227 }
228
229 static void report_dir_error(const char *service_name, const std::string &dirpath)
230 {
231     std::cerr << "Service '" << service_name << "', error reading dependencies from directory " << dirpath
232             << ": " << strerror(errno) << "\n";
233     errors_found = true;
234 }
235
236 // Process a dependency directory - filenames contained within correspond to service names which
237 // are loaded and added as a dependency of the given type. Expected use is with a directory
238 // containing symbolic links to other service descriptions, but this isn't required.
239 // Failure to read the directory contents, or to find a service listed within, is not considered
240 // a fatal error.
241 static void process_dep_dir(const char *servicename,
242         const string &service_filename,
243         std::list<prelim_dep> &deplist, const std::string &depdirpath,
244         dependency_type dep_type)
245 {
246     std::string depdir_fname = combine_paths(parent_path(service_filename), depdirpath.c_str());
247
248     DIR *depdir = opendir(depdir_fname.c_str());
249     if (depdir == nullptr) {
250         report_dir_error(servicename, depdirpath);
251         return;
252     }
253
254     errno = 0;
255     dirent * dent = readdir(depdir);
256     while (dent != nullptr) {
257         char * name =  dent->d_name;
258         if (name[0] != '.') {
259             deplist.emplace_back(name, dep_type);
260         }
261         dent = readdir(depdir);
262     }
263
264     if (errno != 0) {
265         report_dir_error(servicename, depdirpath);
266     }
267
268     closedir(depdir);
269 }
270
271 service_record *load_service(service_set_t &services, const std::string &name,
272         const service_dir_pathlist &service_dirs)
273 {
274     using namespace std;
275     using namespace dinit_load;
276
277     auto found = services.find(name);
278     if (found != services.end()) {
279         return found->second;
280     }
281
282     string service_filename;
283     ifstream service_file;
284
285     // Couldn't find one. Have to load it.
286     for (auto &service_dir : service_dirs) {
287         service_filename = service_dir.get_dir();
288         if (*(service_filename.rbegin()) != '/') {
289             service_filename += '/';
290         }
291         service_filename += name;
292
293         service_file.open(service_filename.c_str(), ios::in);
294         if (service_file) break;
295     }
296
297     if (! service_file) {
298         throw service_not_found(string(name));
299     }
300
301     service_settings_wrapper<prelim_dep> settings;
302
303     string line;
304     service_file.exceptions(ios::badbit);
305
306     try {
307         process_service_file(name, service_file,
308                 [&](string &line, string &setting, string_iterator &i, string_iterator &end) -> void {
309
310             auto process_dep_dir_n = [&](std::list<prelim_dep> &deplist, const std::string &waitsford,
311                     dependency_type dep_type) -> void {
312                 process_dep_dir(name.c_str(), service_filename, deplist, waitsford, dep_type);
313             };
314
315             auto load_service_n = [&](const string &dep_name) -> const string & {
316                 return dep_name;
317             };
318
319             try {
320                 process_service_line(settings, name.c_str(), line, setting, i, end, load_service_n, process_dep_dir_n);
321             }
322             catch (service_description_exc &exc) {
323                 report_service_description_exc(exc);
324             }
325         });
326     }
327     catch (std::system_error &sys_err)
328     {
329         report_error(sys_err, name);
330         return nullptr;
331     }
332
333     return new service_record(name, settings.depends);
334 }