Move to asynchronous handling of child exec status.
[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     using std::list;
191     using std::pair;
192     
193     // First try and find an existing record...
194     ServiceRecord * rval = findService(string(name));
195     if (rval != 0) {
196         if (rval->isDummy()) {
197             throw ServiceCyclicDependency(name);
198         }
199         return rval;
200     }
201
202     // Couldn't find one. Have to load it.    
203     string service_filename = service_dir;
204     if (*(service_filename.rbegin()) != '/') {
205         service_filename += '/';
206     }
207     service_filename += name;
208     
209     string command;
210     list<pair<unsigned,unsigned>> command_offsets;
211     string stop_command;
212     list<pair<unsigned,unsigned>> stop_command_offsets;
213
214     ServiceType service_type = ServiceType::PROCESS;
215     std::list<ServiceRecord *> depends_on;
216     std::list<ServiceRecord *> depends_soft;
217     string logfile;
218     OnstartFlags onstart_flags;
219     int term_signal = -1;  // additional termination signal
220     
221     string line;
222     bool auto_restart = false;
223     ifstream service_file;
224     service_file.exceptions(ios::badbit | ios::failbit);
225     
226     try {
227         service_file.open(service_filename.c_str(), ios::in);
228     }
229     catch (std::ios_base::failure &exc) {
230         throw ServiceNotFound(name);
231     }
232     
233     // Add a dummy service record now to prevent infinite recursion in case of cyclic dependency
234     rval = new ServiceRecord(this, string(name));
235     records.push_back(rval);
236     
237     try {
238         // getline can set failbit if it reaches end-of-file, we don't want an exception in that case:
239         service_file.exceptions(ios::badbit);
240         
241         while (! (service_file.rdstate() & ios::eofbit)) {
242             getline(service_file, line);
243             string::iterator i = line.begin();
244             string::iterator end = line.end();
245           
246             i = skipws(i, end);
247             if (i != end) {
248                 if (*i == '#') {
249                     continue;  // comment line
250                 }
251                 string setting = read_setting_name(i, end);
252                 i = skipws(i, end);
253                 if (i == end || (*i != '=' && *i != ':')) {
254                     throw ServiceDescriptionExc(name, "Badly formed line.");
255                 }
256                 i = skipws(++i, end);
257                 
258                 if (setting == "command") {
259                     command = read_setting_value(i, end, &command_offsets);
260                 }
261                 else if (setting == "stop-command") {
262                     stop_command = read_setting_value(i, end, &stop_command_offsets);
263                 }
264                 else if (setting == "depends-on") {
265                     string dependency_name = read_setting_value(i, end);
266                     depends_on.push_back(loadServiceRecord(dependency_name.c_str()));
267                 }
268                 else if (setting == "waits-for") {
269                     string dependency_name = read_setting_value(i, end);
270                     depends_soft.push_back(loadServiceRecord(dependency_name.c_str()));
271                 }
272                 else if (setting == "logfile") {
273                     logfile = read_setting_value(i, end);
274                 }
275                 else if (setting == "restart") {
276                     string restart = read_setting_value(i, end);
277                     auto_restart = (restart == "yes" || restart == "true");
278                 }
279                 else if (setting == "type") {
280                     string type_str = read_setting_value(i, end);
281                     if (type_str == "scripted") {
282                         service_type = ServiceType::SCRIPTED;
283                     }
284                     else if (type_str == "process") {
285                         service_type = ServiceType::PROCESS;
286                     }
287                     else if (type_str == "internal") {
288                         service_type = ServiceType::INTERNAL;
289                     }
290                     else {
291                         throw ServiceDescriptionExc(name, "Service type must be \"scripted\""
292                             " or \"process\" or \"internal\"");
293                     }
294                 }
295                 else if (setting == "onstart") {
296                     std::list<std::pair<unsigned,unsigned>> indices;
297                     string onstart_cmds = read_setting_value(i, end, &indices);
298                     for (auto indexpair : indices) {
299                         string onstart_cmd = onstart_cmds.substr(indexpair.first, indexpair.second - indexpair.first);
300                         if (onstart_cmd == "rw_ready") {
301                             onstart_flags.rw_ready = true;
302                         }
303                         else {
304                             throw new ServiceDescriptionExc(name, "Unknown onstart command: " + onstart_cmd);
305                         }
306                     }
307                 }
308                 else if (setting == "termsignal") {
309                     string signame = read_setting_value(i, end, nullptr);
310                     int signo = signalNameToNumber(signame);
311                     if (signo == -1) {
312                         throw new ServiceDescriptionExc(name, "Unknown/unsupported termination signal: " + signame);
313                     }
314                     else {
315                         term_signal = signo;
316                     }
317                 }
318                 else if (setting == "nosigterm") {
319                     string sigtermsetting = read_setting_value(i, end);
320                     onstart_flags.no_sigterm = (sigtermsetting == "yes" || sigtermsetting == "true");
321                 }
322                 else if (setting == "runs-on-console") {
323                     string runconsolesetting = read_setting_value(i, end);
324                     onstart_flags.runs_on_console = (runconsolesetting == "yes" || runconsolesetting == "true");
325                 }
326                 else {
327                     throw ServiceDescriptionExc(name, "Unknown setting: " + setting);
328                 }
329             }
330         }
331         
332         service_file.close();
333         // TODO check we actually have all the settings - type, command
334         
335         // Now replace the dummy service record with a real record:
336         for (auto iter = records.begin(); iter != records.end(); iter++) {
337             if (*iter == rval) {
338                 // We've found the dummy record
339                 delete rval;
340                 rval = new ServiceRecord(this, string(name), service_type, std::move(command), command_offsets,
341                         & depends_on, & depends_soft);
342                 rval->setStopCommand(stop_command, stop_command_offsets);
343                 rval->setLogfile(logfile);
344                 rval->setAutoRestart(auto_restart);
345                 rval->setOnstartFlags(onstart_flags);
346                 rval->setExtraTerminationSignal(term_signal);
347                 *iter = rval;
348                 break;
349             }
350         }
351         
352         return rval;
353     }
354     catch (...) {
355         // Must remove the dummy service record.
356         std::remove(records.begin(), records.end(), rval);
357         delete rval;
358         throw;
359     }
360 }