Improve error message logged when failing to load a service (give
[oweals/dinit.git] / load_service.cc
1 #include "service.h"
2 #include <algorithm>
3 #include <string>
4 #include <fstream>
5 #include <locale>
6 #include <iostream>
7
8 typedef std::string string;
9 typedef std::string::iterator string_iterator;
10
11 // Utility function to skip white space. Returns an iterator at the
12 // first non-white-space position (or at end).
13 static string_iterator skipws(string_iterator i, string_iterator end)
14 {
15     using std::locale;
16     using std::isspace;
17     
18     while (i != end) {
19       if (! isspace(*i, locale::classic())) {
20         break;
21       }
22       ++i;
23     }
24     return i;
25 }
26
27 // Read a setting name.
28 static string read_setting_name(string_iterator & i, string_iterator end)
29 {
30     using std::locale;
31     using std::ctype;
32     using std::use_facet;
33     
34     const ctype<char> & facet = use_facet<ctype<char> >(locale::classic());
35
36     string rval;
37     // Allow alphabetical characters, and dash (-) in setting name
38     while (i != end && (*i == '-' || facet.is(ctype<char>::alpha, *i))) {
39         rval += *i;
40         ++i;
41     }
42     return rval;
43 }
44
45 // Read a setting value
46 //
47 // In general a setting value is a single-line string. It may contain multiple parts
48 // separated by white space (which is normally collapsed). A hash mark - # - denotes
49 // the end of the value and the beginning of a comment (it should be preceded by
50 // whitespace).
51 //
52 // Part of a value may be quoted using double quote marks, which prevents collapse
53 // of whitespace and interpretation of most special characters (the quote marks will
54 // not be considered part of the value). A backslash can precede a character (such
55 // as '#' or '"' or another backslash) to remove its special meaning. Newline
56 // characters are not allowed in values and cannot be quoted.
57 //
58 // This function expects the string to be in an ASCII-compatible, single byte
59 // encoding (the "classic" locale).
60 //
61 // Params:
62 //    i  -  reference to string iterator through the line
63 //    end -   iterator at end of line
64 //    part_positions -  list of <int,int> to which the position of each setting value
65 //                      part will be added as [start,end). May be null.
66 static string read_setting_value(string_iterator & i, string_iterator end,
67         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
68 {
69     using std::locale;
70     using std::isspace;
71
72     i = skipws(i, end);
73     
74     string rval;
75     bool new_part = true;
76     int part_start;
77     
78     while (i != end) {
79         char c = *i;
80         if (c == '\"') {
81             if (new_part) {
82                 part_start = rval.length();
83                 new_part = false;
84             }
85             // quoted string
86             ++i;
87             while (i != end) {
88                 c = *i;
89                 if (c == '\"') break;
90                 if (c == '\n') {
91                     // TODO error here.
92                 }
93                 else if (c == '\\') {
94                     // A backslash escapes the following character.
95                     ++i;
96                     if (i != end) {
97                         c = *i;
98                         if (c == '\n') {
99                             // TODO error here.
100                         }
101                         rval += c;
102                     }
103                 }
104                 else {
105                     rval += c;
106                 }
107                 ++i;
108             }
109             if (i == end) {
110                 // String wasn't terminated
111                 // TODO error here
112                 break;
113             }
114         }
115         else if (c == '\\') {
116             if (new_part) {
117                 part_start = rval.length();
118                 new_part = false;
119             }
120             // A backslash escapes the next character
121             ++i;
122             if (i != end) {
123                 rval += *i;
124             }
125             else {
126                 // TODO error here
127             }
128         }
129         else if (isspace(c, locale::classic())) {
130             if (! new_part && part_positions != nullptr) {
131                 part_positions->emplace_back(part_start, rval.length());
132                 new_part = true;
133             }
134             i = skipws(i, end);
135             if (i == end) break;
136             if (*i == '#') break; // comment
137             rval += ' ';  // collapse ws to a single space
138             continue;
139         }
140         else if (c == '#') {
141             // hmm... comment? Probably, though they should have put a space
142             // before it really. TODO throw an exception, and document
143             // that '#' for comments must be preceded by space, and in values
144             // must be quoted.
145             break;
146         }
147         else {
148             if (new_part) {
149                 part_start = rval.length();
150                 new_part = false;
151             }
152             rval += c;
153         }
154         ++i;
155     }
156
157     // Got to end:
158     if (part_positions != nullptr) {
159         part_positions->emplace_back(part_start, rval.length());
160     }
161
162     return rval;
163 }
164
165 static int signalNameToNumber(std::string &signame)
166 {
167     if (signame == "HUP") return SIGHUP;
168     if (signame == "INT") return SIGINT;
169     if (signame == "QUIT") return SIGQUIT;
170     if (signame == "USR1") return SIGUSR1;
171     if (signame == "USR2") return SIGUSR2;
172     return -1;
173 }
174
175 // Find a service record, or load it from file. If the service has
176 // dependencies, load those also.
177 //
178 // Might throw a ServiceLoadExc exception if a dependency cycle is found or if another
179 // problem occurs (I/O error, service description not found etc). Throws std::bad_alloc
180 // if a memory allocation failure occurs.
181 ServiceRecord * ServiceSet::loadServiceRecord(const char * name)
182 {
183     using std::string;
184     using std::ifstream;
185     using std::ios;
186     using std::ios_base;
187     using std::locale;
188     using std::isspace;
189     
190     // First try and find an existing record...
191     ServiceRecord * rval = findService(string(name));
192     if (rval != 0) {
193         if (rval->isDummy()) {
194             throw ServiceCyclicDependency(name);
195         }
196         return rval;
197     }
198
199     // Couldn't find one. Have to load it.    
200     string service_filename = service_dir;
201     if (*(service_filename.rbegin()) != '/') {
202         service_filename += '/';
203     }
204     service_filename += name;
205     
206     string command;
207     std::list<std::pair<unsigned,unsigned>> command_offsets;
208
209     ServiceType service_type = ServiceType::PROCESS;
210     std::list<ServiceRecord *> depends_on;
211     std::list<ServiceRecord *> depends_soft;
212     string logfile;
213     OnstartFlags onstart_flags;
214     int term_signal = -1;  // additional termination signal
215     
216     string line;
217     bool auto_restart = false;
218     ifstream service_file;
219     service_file.exceptions(ios::badbit | ios::failbit);
220     
221     try {
222         service_file.open(service_filename.c_str(), ios::in);
223     }
224     catch (std::ios_base::failure &exc) {
225         throw ServiceNotFound(name);
226     }
227     
228     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
229     rval = new ServiceRecord(this, string(name));
230     records.push_back(rval);
231     
232     try {
233         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
234         service_file.exceptions(ios::badbit);
235         
236         while (! (service_file.rdstate() & ios::eofbit)) {
237             getline(service_file, line);
238             string::iterator i = line.begin();
239             string::iterator end = line.end();
240           
241             i = skipws(i, end);
242             if (i != end) {
243                 if (*i == '#') {
244                     continue;  // comment line
245                 }
246                 string setting = read_setting_name(i, end);
247                 i = skipws(i, end);
248                 if (i == end || (*i != '=' && *i != ':')) {
249                     throw ServiceDescriptionExc(name, "Badly formed line.");
250                 }
251                 i = skipws(++i, end);
252                 
253                 if (setting == "command") {
254                     command = read_setting_value(i, end, &command_offsets);
255                 }
256                 else if (setting == "depends-on") {
257                     string dependency_name = read_setting_value(i, end);
258                     depends_on.push_back(loadServiceRecord(dependency_name.c_str()));
259                 }
260                 else if (setting == "waits-for") {
261                     string dependency_name = read_setting_value(i, end);
262                     depends_soft.push_back(loadServiceRecord(dependency_name.c_str()));
263                 }
264                 else if (setting == "logfile") {
265                     logfile = read_setting_value(i, end);
266                 }
267                 else if (setting == "restart") {
268                     string restart = read_setting_value(i, end);
269                     auto_restart = (restart == "yes" || restart == "true");
270                 }
271                 else if (setting == "type") {
272                     string type_str = read_setting_value(i, end);
273                     if (type_str == "scripted") {
274                         service_type = ServiceType::SCRIPTED;
275                     }
276                     else if (type_str == "process") {
277                         service_type = ServiceType::PROCESS;
278                     }
279                     else if (type_str == "internal") {
280                         service_type = ServiceType::INTERNAL;
281                     }
282                     else {
283                         throw ServiceDescriptionExc(name, "Service type must be \"scripted\""
284                             " or \"process\" or \"internal\"");
285                     }
286                 }
287                 else if (setting == "onstart") {
288                     std::list<std::pair<unsigned,unsigned>> indices;
289                     string onstart_cmds = read_setting_value(i, end, &indices);
290                     for (auto indexpair : indices) {
291                         string onstart_cmd = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
292                         if (onstart_cmd == "release_console") {
293                             onstart_flags.release_console = true;
294                         }
295                         else if (onstart_cmd == "rw_ready") {
296                             onstart_flags.rw_ready = true;
297                         }
298                         else {
299                             throw new ServiceDescriptionExc(name, "Unknown onstart command: " + onstart_cmd);
300                         }
301                     }
302                 }
303                 else if (setting == "termsignal") {
304                     string signame = read_setting_value(i, end, nullptr);
305                     int signo = signalNameToNumber(signame);
306                     if (signo == -1) {
307                         throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame);
308                     }
309                     else {
310                         term_signal = signo;
311                     }
312                 }
313                 else if (setting == "nosigterm") {
314                     string sigtermsetting = read_setting_value(i, end);
315                     onstart_flags.no_sigterm = (sigtermsetting == "yes" || sigtermsetting == "true");
316                 }
317                 else {
318                     throw ServiceDescriptionExc(name, "Unknown setting: " + setting);
319                 }
320             }
321         }
322         
323         service_file.close();
324         // TODO check we actually have all the settings - type, command
325         
326         // Now replace the dummy service record with a real record:
327         for (auto iter = records.begin(); iter != records.end(); iter++) {
328             if (*iter == rval) {
329                 // We've found the dummy record
330                 delete rval;
331                 rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets,
332                         & depends_on, & depends_soft);
333                 rval->setLogfile(logfile);
334                 rval->setAutoRestart(auto_restart);
335                 rval->setOnstartFlags(onstart_flags);
336                 rval->setExtraTerminationSignal(term_signal);
337                 *iter = rval;
338                 break;
339             }
340         }
341         
342         return rval;
343     }
344     catch (...) {
345         // Must remove the dummy service record.
346         std::remove(records.begin(), records.end(), rval);
347         delete rval;
348         throw;
349     }
350 }