My first C program: simple process supervisor like daemontools/runit in 359 LoC
uggedal.github.com
uggedal.github.com
As a constructive note, think about reversing your conditions (check if something NOT happened) and break huge chunks of code into functions. This will guarantee a 'sane' amount of nested 'ifs' and improve code readability. Ex:
for (int i = dn - 1; i >= 0; i--) {
if (!has_child(dlist[i]->d_name)) {
if ((fp = fopen(path, "r")) != NULL) {
.............
} else {
slog(LOG_ERR, "Can't read %s: %m", path);
}
}
}
Notice how the error message is 20 lines far from the actual condition. You can rework that as: if ((fp = fopen(path, "r")) != NULL) {
do_some_work_with(fp);
} else {
slog(LOG_ERR, "Can't read %s: %m", path);
}
or if (!(fp = fopen(path, "r")) {
slog(LOG_ERR, "Can't read %s: %m", path);
continue;
}
//removed 1 nesting level
........ // The user has given and illegal number or type of arguments. The program
// usage is printed to the standard error stream and we exit abnormally.
fprintf(stderr, USAGE);
exit(EX_USAGE);
What else is it supposed to be? This is, even for me who does not use C(++) on a regular basis, obvious.It looks like you are interested in writing well documented code. I think a good example you can learn a lot from is Redis[1]. Also written in C.
[1] https://github.com/antirez/redis
//edit: fixed typo
I question the need for yet-another-process-monitor, but I acknowledge that upstart, monit, etc are by and large overcomplicated and messy. They provide more features, of course, but they violate my interpretation of the unix philosophy.
I'm sure many people on HN will condemn you for writing this but, I for one am very excited to try it.
I had a similar idea of a daemon that would allow inter-process signaling for users to signal processes which shared their GID, to simplify web application deployment without relying on sudo privilege restrictions.
You may have just inspired me to take your lead, and just do it - a few hundred lines of C, and smart documentation have a lot of value as learning, experience, proof-of-concept and a discussion point; and that in itself is reason enough for this to exist!
My recommendation is to have one comment to explain a large chunk of code and let the programmer discern what part is doing what instead of annotating it piece by piece.
I understand, though, that explaining each and every line of code was done for the sake of learning C.
And of course, redundancy spawns inconsistency: the code says "spawn_unquarantined_children()", but the comment says "All quarantined (a newly initialized child structure is quarantined by default) children is spawned for the first time." Which is it, quarantined or unquarantined? And the comment breaks number agreement, too.
I find it remarkably illustrative that comment rot has already set in into version 1.0 of a "359 LoC" program (actually 797, according to github).
I suppose this is up to the individual, but I don't like literate programming partly because it interferes with a dynamic that coders around the world have. Most people include comments only when it's necessary. It signifies something important to take note of and clarifies unusual behavior. Literate programming partly strips that away without giving you a really long-term benefit... It seems useful to me mostly for the initial implementation and ends up making maintenance a chore.
If I can modify my original comment a bit for literate programming: Make a comment at the beginning of your function with a bullet list of how the function will work, step by step, and then just write the code. Easier to read and you still have your [mostly-]literate program.
I liked the comments, not because I needed them to help understand what the program is doing (this is a bog standard C program that I think most Unix developers have written several times over) but as document of someone learning C, and as something I can show other people who want to learn C.
Edit: ok I forgot this was his first C program.
It's his first C program. Mine were similarly prolix. Another habit I got out of after a year or so was wrapping every use of every new API in a me-friendly wrapper function. Also, of writing my own linked list code over and over again.
1) // style commands are C99 only. If you don't have other C99 dependencies it's somewhat a shame it could not compile in older systems just for this.
2) inline should rarely be used if not into performance critical code.
3) two space indent is not C-ish ;)
4) While 'bool' is valid C99 code, it is more or less never used in C code.
Are people using such a wide range of font sizes, and font aspect ratios, that 4 spaces doesn't make sense for everyone?
problem solved ;-)
But I'm also a single-return fetishist, so I tend to nest deeper than normal.
I disagree. It's used quite a bit in the code I work on. Little things like an explicit statement that this variable is only true or false significantly reduce the cognitive load when reading code.
Anyway, even if it were true that nobody uses bool, there's absolutely no reason to discourage its use.
Since it's allowed to use bool in arithmetic context (some_int + some_bool will compile fine, for example), having a bool type is useless. (Useless in C and C++ that is; Java, for example, has sane bool handling.)
I have no idea why C99 introduced _Bool type when they could have just inserted typedef char bool; in stdbool.h. Has anybody ever used for something useful the fact that memory representation of _Bool is constrained to read as 0 or 1?
2. C lets you commit all kinds of atrocities in arithmetic contexts with or without it.
3. Having a standard typedef saves us from all having to make our own
4. None of your points support the case of "don't use stdbool"
Huh? I was saying that the language did not have to be extended to get a bool typedef. C99 defines a new type, _Bool (name from the reserved identifier space), which is different from existing types. stdbool is a simple typedef of _Bool to bool.
Also, introducing bool would have had meaning if implicit conversion to int wasn't allowed. But since it is, it's a useless type.
My viewpoint is that types exist for enforcing semantic constraints; what use of a bool type that allows performing semantically nonsense operation, such as implicit conversion to int in arithmetic context? The C99 committee messed it up big with bool.
C's approach to typing is, in general, pretty weak. Mostly they're there so the compiler can decide how much space to set aside. There's no point singling out this particular instance of nonsense when everywhere you turn in C you're greeted by other forms of the same nonsense. There's no point being pedantic about bool if you're not going to be pedantic about all the other types.
Enough bikeshedding. Use it or don't.
By your reasoning, since nonsense already exists, it's pointless to try to do the right thing. And they could have done it.
for (tail_ch = head_ch; tail_ch; tail_ch = tail_ch->next);
return tail_ch;Going to want to add a test case with more than one child at a time. I think as is it loses all but the most recent child.
Comments – I see you have alarmed the HN crowd, but I was reading the annotated page and it makes sense in that context. As a data point, I only looked at the lefthand column paragraphs about twice, related to the quarantine handling in the forever loop. There is an odd effect if children are dying faster than the quarantine period, but I convinced myself that there was a finite number of children and they would eventually get quarantined if they kept that up allowing the period to finally expire. I looked at the titles though.
Design – I'd lose the fixed size string buffers in the child_t. 256 sounds like a lot for a command until someone uses a wildcard in a directory. strdup() and free() are your friends here. I see you have a centralized free function for the child_t and are using valgrind. I trust you will not leak or shoot off a body part.
Portability – You are using "%m" to get the strerror(errno) into your log messages. This is a GNU libc extension. I don't have a pure BSD machine around, but it doesn't work on OS X. This will likely also crop up for very small Linux systems that use alternative C libraries for space reasons, such as OpenWRT.
strncmp() and the strnXXX() functions in general – These are not really what you want. Think of them as functions for fixed length string fields with optional NUL terminators. Then never use them.
Consider…
strncmp("foo", "foobar", 3)
It returns 0, that means equal. That is seldom what you actually intended. In your case, strcmp() is fine because you are working with guaranteed NUL terminated strings.I see a strnlen()…
inline bool str_not_empty(char *str) {
return strnlen(str, 1) == 1;
}
Most C programmers would rather see… inline bool str_not_empty(char *str) {
return *str != 0;
}
… but they'd also leave off the "inline" and use "int" instead of bool. "inline" is the compiler's problem, let it take care of that. The part you might do for performance is to mark the argument as "const" since you aren't going to change it. That gives the optimizer more freedom around the calls, though in this case the optimizer will probably inline it and it won't matter. "const" is also a nice hint to people reading your code.I suppose summing up strings, "char *" is not an opaque type to C programmers.
Signals and file descriptors across forks – Signals are generally a bit tricky, and what to do with open file descriptors across a fork is also fussy. I'll leave that to you. (Frequently in cases like this, closing all file descriptors is the right answer. A shame there isn't a portable way to do that. Remember you might have something open from the syslog functions.)
* What's the harm in `inline`? I would encourage its use, rather than discourage?
At one time (shortly after the Morris worm) people flocked to using strncpy() for security. That was also an error. strncpy() will happily leave you with a missing NUL terminator, plus it has sad performance implications since it fills the whole destination with NUL, so if you make a "plenty big" buffer you do a lot of useless memory writing.
To reiterate: The strnXXX functions are not for NUL terminated strings. They are for fixed size buffers of characters padded with optional NUL characters.
As to "inline*. The compiler knows better and will probably ignore it anyway. I think the place where it is wanted is if you are declaring inline functions in header files. This prevents the duplicate function errors at link time that result if included in more than one object file. The standards and compilers are a mess on precisely how to do this.
Why use int, when there's bool in C99 that clearly communicates the intent?
You expect that "true" is success, but you can't know without checking, and then you're back in the same situation as with int.
In practice, idiomatically, you write something like if(!some_function(blah)), and in this context it totally doesn't matter whether the function returns bool or int.
The C cultural expectation is that anything named like a predicate is going to return an int for true or false. In another language with a different culture I would use a Boolean or whatever it had.
main() {
printf("Hello, world\n");
}#include <stdio.h>
main() { printf("hello, world\n"); }
;)