1 /* -*- c-file-style: "java"; indent-tabs-mode: nil; tab-width: 4; fill-column: 78 -*-
2 *
3 * distcc -- A simple distributed compiler system
4 *
5 * Copyright (C) 2002, 2003, 2004, 2009 by Martin Pool <mbp@samba.org>
6 * Copyright 2004 Google Inc.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
21 * USA.
22 */
23
24 /* dcc_randomize_host_list() and friends:
25 * Author: Josh Hyman <joshh@google.com>
26 */
27
28
29 /* The lyf so short, the craft so long to lerne.
30 * -- Chaucer */
31
32
33
34 /**
35 * @file
36 *
37 * Routines to parse <tt>$DISTCC_HOSTS</tt>. Actual decisions about
38 * where to run a job are in where.c.
39 *
40 * The grammar of this variable is, informally:
41 *
42 DISTCC_HOSTS = HOSTSPEC ...
43 HOSTSPEC = LOCAL_HOST | SSH_HOST | TCP_HOST | OLDSTYLE_TCP_HOST
44 | GLOBAL_OPTION
45 LOCAL_HOST = localhost[/LIMIT]
46 SSH_HOST = [USER]@HOSTID[/LIMIT][:COMMAND][OPTIONS]
47 TCP_HOST = HOSTID[:PORT][/LIMIT][OPTIONS]
48 OLDSTYLE_TCP_HOST = HOSTID[/LIMIT][:PORT][OPTIONS]
49 HOSTID = HOSTNAME | IPV4
50 OPTIONS = ,OPTION[OPTIONS]
51 OPTION = lzo | cpp
52 GLOBAL_OPTION = --randomize
53 *
54 * Any amount of whitespace may be present between hosts.
55 *
56 * The command specified for SSH defines the location of the remote
57 * server, e.g. "/usr/local/bin/distccd". This is provided as a
58 * convenience who have trouble getting their PATH set correctly for
59 * sshd to find distccd, and should not normally be needed.
60 *
61 * If you need to specify special options for ssh, they should be put
62 * in ~/.ssh/config and referenced by the hostname.
63 *
64 * The TCP port defaults to 3632 and should not normally need to be
65 * overridden.
66 *
67 * IPv6 literals are not supported yet. They will need to be
68 * surrounded by square brackets because they may contain a colon,
69 * which would otherwise be ambiguous. This is consistent with other
70 * URL-like schemes.
71 */
72
73
74 /*
75 Alexandre Oliva writes
76
77 I take this opportunity to plead people to consider such issues when
78 proposing additional syntax for DISTCC_HOSTS: if it was possible to
79 handle DISTCC_HOSTS as a single shell word (perhaps after turning
80 blanks into say commas), without the risk of any shell active
81 characters such as {, }, ~, $, quotes getting in the way, outputting
82 distcc commands that override DISTCC_HOSTS would be far
83 simpler.
84
85 TODO: Perhaps entries in the host list that "look like files" (start
86 with '/' or '~') should be read in as files? This could even be
87 recursive.
88 */
89
90 #include <config.h>
91
92 #include <stdio.h>
93 #include <stdlib.h>
94 #include <unistd.h>
95 #include <string.h>
96 #include <errno.h>
97 #include <time.h>
98 #include <ctype.h>
99 #include <sys/time.h>
100 #include <sys/types.h>
101
102 #include "distcc.h"
103 #include "trace.h"
104 #include "util.h"
105 #include "hosts.h"
106 #include "exitcode.h"
107 #include "snprintf.h"
108 #include "config.h"
109 #ifdef HAVE_AVAHI
110 #include "zeroconf.h"
111 #define ZEROCONF_MAGIC "+zeroconf"
112 #endif
113
114 const int dcc_default_port = DISTCC_DEFAULT_PORT;
115
116 /***
117 * A simple container which would hold a host -> rand int pair
118 ***/
119 struct rand_container {
120 struct dcc_hostdef *host;
121 int rand;
122 };
123
124 int dcc_randomize_host_list(struct dcc_hostdef **host_list, int length);
125
126 int dcc_compare_container(const void *a, const void *b);
127
128
129 #ifndef HAVE_STRNDUP
130 /**
131 * Copy at most @p size characters from @p src, plus a terminating nul.
132 *
133 * Really this needs to be in util.c, but it's only used here.
134 **/
135 char *strndup(const char *src, size_t size);
strndup(const char * src,size_t size)136 char *strndup(const char *src, size_t size)
137 {
138 char *dst;
139
140 dst = malloc(size + 1);
141 if (dst == NULL)
142 return NULL;
143 strncpy(dst, src, size);
144 dst[size] = '\0';
145
146 return dst;
147 }
148 #endif
149
150 /**
151 * Get a list of hosts to use.
152 *
153 * Hosts are taken from DISTCC_HOSTS, if that exists. Otherwise, they are
154 * taken from $DISTCC_DIR/hosts, if that exists. Otherwise, they are taken
155 * from ${sysconfdir}/distcc/hosts, if that exists. Otherwise, we fail.
156 **/
dcc_get_hostlist(struct dcc_hostdef ** ret_list,int * ret_nhosts)157 int dcc_get_hostlist(struct dcc_hostdef **ret_list,
158 int *ret_nhosts)
159 {
160 char *env;
161 char *path, *top;
162 int ret;
163
164 *ret_list = NULL;
165 *ret_nhosts = 0;
166
167 if ((env = getenv("DISTCC_HOSTS")) != NULL) {
168 rs_trace("read hosts from environment");
169 return dcc_parse_hosts(env, "$DISTCC_HOSTS", ret_list, ret_nhosts, NULL);
170 }
171
172 /* $DISTCC_DIR or ~/.distcc */
173 if ((ret = dcc_get_top_dir(&top)) == 0) {
174 /* if we failed to get it, just warn */
175
176 checked_asprintf(&path, "%s/hosts", top);
177 if (path != NULL && access(path, R_OK) == 0) {
178 ret = dcc_parse_hosts_file(path, ret_list, ret_nhosts);
179 free(path);
180 return ret;
181 } else {
182 rs_trace("not reading %s: %s", path, strerror(errno));
183 free(path);
184 }
185 }
186
187 checked_asprintf(&path, "%s/distcc/hosts", SYSCONFDIR);
188 if (path != NULL && access(path, R_OK) == 0) {
189 ret = dcc_parse_hosts_file(path, ret_list, ret_nhosts);
190 free(path);
191 return ret;
192 } else {
193 rs_trace("not reading %s: %s", path, strerror(errno));
194 free(path);
195 }
196
197 /* FIXME: Clearer message? */
198 rs_log_warning("no hostlist is set; can't distribute work");
199
200 return EXIT_BAD_HOSTSPEC;
201 }
202
203
204 /**
205 * Parse an optionally present multiplier.
206 *
207 * *psrc is the current parse cursor; it is advanced over what is read.
208 *
209 * If a multiplier is present, *psrc points to a substring starting with '/'.
210 * The host definition is updated to the numeric value following. Otherwise
211 * the hostdef is unchanged.
212 **/
dcc_parse_multiplier(const char ** psrc,struct dcc_hostdef * hostdef)213 static int dcc_parse_multiplier(const char **psrc, struct dcc_hostdef *hostdef)
214 {
215 const char *token = *psrc;
216
217 if ((*psrc)[0] == '/' || (*psrc)[0] == '=') {
218 int val;
219 (*psrc)++;
220 val = atoi(*psrc);
221 if (val == 0) {
222 rs_log_error("bad multiplier \"%s\" in host specification", token);
223 return EXIT_BAD_HOSTSPEC;
224 }
225 while (isdigit((uint8_t)**psrc))
226 (*psrc)++;
227 hostdef->n_slots = val;
228 }
229 return 0;
230 }
231
232
233 /**
234 * Parse an optionally present option string.
235 *
236 * At the moment the only two options we have is "lzo" for compression,
237 * and "cpp" if the server supports doing the preprocessing there, also.
238 **/
dcc_parse_options(const char ** psrc,struct dcc_hostdef * host)239 static int dcc_parse_options(const char **psrc,
240 struct dcc_hostdef *host)
241 {
242 const char *started = *psrc, *p = *psrc;
243
244 host->compr = DCC_COMPRESS_NONE;
245 host->cpp_where = DCC_CPP_ON_CLIENT;
246 #ifdef HAVE_GSSAPI
247 host->authenticate = 0;
248 host->auth_name = NULL;
249 #endif
250
251 while (p[0] == ',') {
252 p++;
253 if (str_startswith("lzo", p)) {
254 rs_trace("got LZO option");
255 host->compr = DCC_COMPRESS_LZO1X;
256 p += 3;
257 } else if (str_startswith("down", p)) {
258 /* if "hostid,down", mark it down, and strip down from hostname */
259 host->is_up = 0;
260 p += 4;
261 } else if (str_startswith("cpp", p)) {
262 rs_trace("got CPP option");
263 host->cpp_where = DCC_CPP_ON_SERVER;
264 p += 3;
265 #ifdef HAVE_GSSAPI
266 } else if (str_startswith("auth", p)) {
267 rs_trace("got GSSAPI option");
268 host->authenticate = 1;
269 p += 4;
270 if (p[0] == '=') {
271 p++;
272 int ret;
273 if ((ret = dcc_dup_part(&p, &host->auth_name, "/: \t\n\r\f,")))
274 return ret;
275
276 if (host->auth_name) {
277 rs_trace("using \"%s\" server name instead of fqdn "
278 "lookup for GSS-API auth", host->auth_name);
279 }
280 }
281 #endif
282 } else {
283 rs_log_error("unrecognized option in host specification: %s",
284 started);
285 return EXIT_BAD_HOSTSPEC;
286 }
287 }
288 if (dcc_get_protover_from_features(host->compr, host->cpp_where,
289 &host->protover) == -1) {
290 rs_log_error("invalid host options: %s", started);
291 return EXIT_BAD_HOSTSPEC;
292 }
293
294 *psrc = p;
295
296 return 0;
297 }
298
dcc_parse_ssh_host(struct dcc_hostdef * hostdef,const char * token_start)299 static int dcc_parse_ssh_host(struct dcc_hostdef *hostdef,
300 const char *token_start)
301 {
302 int ret;
303 const char *token = token_start;
304
305 /* Everything up to '@' is the username */
306 if ((ret = dcc_dup_part(&token, &hostdef->user, "@")) != 0)
307 return ret;
308
309 if (token[0] != '@') {
310 rs_log_error("expected '@' to start ssh token");
311 return EXIT_BAD_HOSTSPEC;
312 }
313
314 token++;
315
316 if ((ret = dcc_dup_part(&token, &hostdef->hostname, "/: \t\n\r\f,")) != 0)
317 return ret;
318
319 if (!hostdef->hostname) {
320 rs_log_error("hostname is required in SSH host specification \"%s\"",
321 token_start);
322 return EXIT_BAD_HOSTSPEC;
323 }
324
325 if ((ret = dcc_parse_multiplier(&token, hostdef)) != 0)
326 return ret;
327
328 if (token[0] == ':') {
329 token++;
330 if ((ret = dcc_dup_part(&token, &hostdef->ssh_command, " \t\n\r\f,")))
331 return ret;
332 }
333
334 if ((ret = dcc_parse_options(&token, hostdef)))
335 return ret;
336
337 hostdef->mode = DCC_MODE_SSH;
338 return 0;
339 }
340
341
dcc_parse_tcp_host(struct dcc_hostdef * hostdef,const char * const token_start)342 static int dcc_parse_tcp_host(struct dcc_hostdef *hostdef,
343 const char * const token_start)
344 {
345 int ret;
346 const char *token = token_start;
347
348 if (token[0] == '[') {
349 /* Skip the leading bracket, which is not part of the address */
350 token++;
351
352 /* We have an IPv6 Address */
353 if ((ret = dcc_dup_part(&token, &hostdef->hostname, "/] \t\n\r\f,")))
354 return ret;
355 if(token[0] != ']') {
356 rs_log_error("IPv6 Hostname requires closing ']'");
357 return EXIT_BAD_HOSTSPEC;
358 }
359 token++;
360
361 } else {
362 /* Parse IPv4 address */
363 if ((ret = dcc_dup_part(&token, &hostdef->hostname, "/: \t\n\r\f,")))
364 return ret;
365 }
366
367 if (!hostdef->hostname) {
368 rs_log_error("hostname is required in tcp host specification \"%s\"",
369 token_start);
370 return EXIT_BAD_HOSTSPEC;
371 }
372
373 if ((ret = dcc_parse_multiplier(&token, hostdef)) != 0)
374 return ret;
375
376 hostdef->port = dcc_default_port;
377 if (token[0] == ':') {
378 char *tail;
379
380 token++;
381
382 hostdef->port = strtol(token, &tail, 10);
383 if (*tail != '\0' && !isspace((uint8_t)*tail) && *tail != '/' && *tail != ',') {
384 rs_log_error("invalid tcp port specification in \"%s\"", token);
385 return EXIT_BAD_HOSTSPEC;
386 } else {
387 token = tail;
388 }
389 }
390
391 if ((ret = dcc_parse_multiplier(&token, hostdef)) != 0)
392 return ret;
393
394 if ((ret = dcc_parse_options(&token, hostdef)))
395 return ret;
396
397 hostdef->mode = DCC_MODE_TCP;
398 return 0;
399 }
400
401
dcc_parse_localhost(struct dcc_hostdef * hostdef,const char * token_start)402 static int dcc_parse_localhost(struct dcc_hostdef *hostdef,
403 const char * token_start)
404 {
405 const char *token = token_start + strlen("localhost");
406
407 hostdef->mode = DCC_MODE_LOCAL;
408 hostdef->hostname = strdup("localhost");
409
410 /* Run only two tasks on localhost by default.
411 *
412 * It might be nice to run more if there are more CPUs, but determining
413 * the number of CPUs on Linux is a bit expensive since it requires
414 * examining mtab and /proc/stat. Anyone lucky enough to have a >2 CPU
415 * machine can specify a number in the host list.
416 */
417 hostdef->n_slots = 2;
418
419 return dcc_parse_multiplier(&token, hostdef);
420 }
421
422 /** Given a host with its protover fields set, set
423 * its feature fields appropriately. Returns 0 if the protocol
424 * is known, non-zero otherwise.
425 */
dcc_get_features_from_protover(enum dcc_protover protover,enum dcc_compress * compr,enum dcc_cpp_where * cpp_where)426 int dcc_get_features_from_protover(enum dcc_protover protover,
427 enum dcc_compress *compr,
428 enum dcc_cpp_where *cpp_where)
429 {
430 if (protover > 1) {
431 *compr = DCC_COMPRESS_LZO1X;
432 } else {
433 *compr = DCC_COMPRESS_NONE;
434 }
435 if (protover > 2) {
436 *cpp_where = DCC_CPP_ON_SERVER;
437 } else {
438 *cpp_where = DCC_CPP_ON_CLIENT;
439 }
440
441 if (protover == 0 || protover > 3) {
442 return 1;
443 } else {
444 return 0;
445 }
446 }
447
448 /** Given a host with its feature fields set, set
449 * its protover appropriately. Return the protover,
450 * or -1 on error.
451 */
dcc_get_protover_from_features(enum dcc_compress compr,enum dcc_cpp_where cpp_where,enum dcc_protover * protover)452 int dcc_get_protover_from_features(enum dcc_compress compr,
453 enum dcc_cpp_where cpp_where,
454 enum dcc_protover *protover)
455 {
456 *protover = -1;
457
458 if (compr == DCC_COMPRESS_NONE && cpp_where == DCC_CPP_ON_CLIENT) {
459 *protover = DCC_VER_1;
460 }
461
462 if (compr == DCC_COMPRESS_LZO1X && cpp_where == DCC_CPP_ON_SERVER) {
463 *protover = DCC_VER_3;
464 }
465
466 if (compr == DCC_COMPRESS_LZO1X && cpp_where == DCC_CPP_ON_CLIENT) {
467 *protover = DCC_VER_2;
468 }
469
470 if (compr == DCC_COMPRESS_NONE && cpp_where == DCC_CPP_ON_SERVER) {
471 rs_log_error("pump mode (',cpp') requires compression (',lzo')");
472 }
473
474 return *protover;
475 }
476
477
478 /**
479 * @p where is the host list, taken either from the environment or file.
480 *
481 * @return 0 if parsed successfully; nonzero if there were any errors,
482 * or if no hosts were defined.
483 **/
dcc_parse_hosts(const char * where,const char * source_name,struct dcc_hostdef ** ret_list,int * ret_nhosts,struct dcc_hostdef ** ret_prev)484 int dcc_parse_hosts(const char *where, const char *source_name,
485 struct dcc_hostdef **ret_list,
486 int *ret_nhosts, struct dcc_hostdef **ret_prev)
487 {
488 int ret, flag_randomize = 0;
489 struct dcc_hostdef *curr, *_prev;
490
491 if (!ret_prev) {
492 ret_prev = &_prev;
493 _prev = NULL;
494 }
495
496 /* TODO: Check for '/' in places where it might cause trouble with
497 * a lock file name. */
498
499 /* A simple, hardcoded scanner. Some of the GNU routines might be
500 * useful here, but they won't work on less capable systems.
501 *
502 * We repeatedly attempt to extract a whitespace-delimited host
503 * definition from the string until none remain. Allocate an
504 * entry; hook to previous entry. We then determine if there is a
505 * '@' in it, which tells us whether it is an SSH or TCP
506 * definition. We then duplicate the relevant subcomponents into
507 * the relevant fields. */
508 while (1) {
509 int token_len;
510 const char *token_start;
511 int has_at;
512
513 if (where[0] == '\0')
514 break; /* end of string */
515
516 /* skip over comments */
517 if (where[0] == '#') {
518 do
519 where++;
520 while (where[0] != '\n' && where[0] != '\r' && where[0] != '\0');
521 continue;
522 }
523
524 if (isspace((uint8_t)where[0])) {
525 where++; /* skip space */
526 continue;
527 }
528
529 token_start = where;
530 token_len = strcspn(where, " #\t\n\f\r");
531
532 /* intercept keywords which are not actually hosts */
533 if (!strncmp(token_start, "--randomize", 11)) {
534 flag_randomize = 1;
535 where = token_start + token_len;
536 continue;
537 }
538
539 if(!strncmp(token_start, "--localslots_cpp", 16)) {
540 const char *ptr;
541 ptr = token_start + 16;
542 if(dcc_parse_multiplier(&ptr, dcc_hostdef_local_cpp) == 0) {
543 where = token_start + token_len;
544 continue;
545 }
546 }
547
548 if(!strncmp(token_start, "--localslots", 12)) {
549 const char *ptr;
550 ptr = token_start + 12;
551 if(dcc_parse_multiplier(&ptr, dcc_hostdef_local) == 0) {
552 where = token_start + token_len;
553 continue;
554 }
555 }
556
557 #ifdef HAVE_AVAHI
558 if (token_len == sizeof(ZEROCONF_MAGIC)-1 &&
559 !strncmp(token_start, ZEROCONF_MAGIC, (unsigned) token_len)) {
560 if ((ret = dcc_zeroconf_add_hosts(ret_list, ret_nhosts, 4, ret_prev) != 0))
561 return ret;
562 goto skip;
563 }
564 #endif
565
566 /* Allocate new list item */
567 curr = calloc(1, sizeof(struct dcc_hostdef));
568 if (!curr) {
569 rs_log_crit("failed to allocate host definition");
570 return EXIT_OUT_OF_MEMORY;
571 }
572
573 /* by default, mark the host up */
574 curr->is_up = 1;
575
576 /* Store verbatim hostname */
577 if (!(curr->hostdef_string = strndup(token_start, (size_t) token_len))) {
578 rs_log_crit("failed to allocate hostdef_string");
579 return EXIT_OUT_OF_MEMORY;
580 }
581
582 /* Link into list */
583 if (*ret_prev) {
584 (*ret_prev)->next = curr;
585 } else {
586 *ret_list = curr; /* first */
587 }
588
589 /* Default task limit. A bit higher than the local limit to allow for
590 * some files in transit. */
591 curr->n_slots = 4;
592
593 curr->protover = DCC_VER_1; /* default */
594 curr->compr = DCC_COMPRESS_NONE;
595
596 has_at = (memchr(token_start, '@', (size_t) token_len) != NULL);
597
598 if (!strncmp(token_start, "localhost", 9)
599 && (token_len == 9 || token_start[9] == '/')) {
600 rs_trace("found localhost token \"%.*s\"", token_len, token_start);
601 if ((ret = dcc_parse_localhost(curr, token_start)) != 0)
602 return ret;
603 } else if (has_at) {
604 rs_trace("found ssh token \"%.*s\"", token_len, token_start);
605 if ((ret = dcc_parse_ssh_host(curr, token_start)) != 0)
606 return ret;
607 } else {
608 rs_trace("found tcp token \"%.*s\"", token_len, token_start);
609 if ((ret = dcc_parse_tcp_host(curr, token_start)) != 0)
610 return ret;
611 }
612
613 if (!curr->is_up) {
614 rs_trace("host %s is down", curr->hostdef_string);
615 }
616
617 (*ret_nhosts)++;
618 *ret_prev = curr;
619
620 #ifdef HAVE_AVAHI
621 skip:
622 #endif
623
624 /* continue to next token if any */
625 where = token_start + token_len;
626 }
627
628 if (*ret_nhosts) {
629 if (flag_randomize)
630 if ((ret = dcc_randomize_host_list(ret_list, *ret_nhosts)) != 0)
631 return ret;
632 return 0;
633 } else {
634 rs_log_warning("%s contained no hosts; can't distribute work", source_name);
635 return EXIT_BAD_HOSTSPEC;
636 }
637 }
638
639
dcc_compare_container(const void * a,const void * b)640 int dcc_compare_container(const void *a, const void *b)
641 {
642 struct rand_container *i, *j;
643 i = (struct rand_container *) a;
644 j = (struct rand_container *) b;
645
646 if (i->rand == j->rand)
647 return 0;
648 else if (i->rand > j->rand)
649 return 1;
650 else
651 return -1;
652 }
653
dcc_randomize_host_list(struct dcc_hostdef ** host_list,int length)654 int dcc_randomize_host_list(struct dcc_hostdef **host_list, int length)
655 {
656 int i;
657 unsigned int rand_val;
658 struct dcc_hostdef *curr;
659 struct rand_container *c;
660
661 c = malloc(length * sizeof(struct rand_container));
662 if (!c) {
663 rs_log_crit("failed to allocate host definition");
664 return EXIT_OUT_OF_MEMORY;
665 }
666 /*
667 {
668 #ifdef HAVE_GETTIMEOFDAY
669 int ret;
670 struct timeval tv;
671 if ((ret = gettimeofday(&tv, NULL)) == 0)
672 rand_val = (unsigned int) tv.tv_usec;
673 else
674 #else
675 rand_val = (unsigned int) time(NULL) ^ (unsigned int) getpid();
676 #endif
677 }
678 */
679 rand_val = (unsigned int) getpid();
680
681 /* create pairs of hosts -> random numbers */
682 srand(rand_val);
683 curr = *host_list;
684 for (i = 0; i < length; i++) {
685 c[i].host = curr;
686 c[i].rand = rand();
687 curr = curr->next;
688 }
689
690 /* sort */
691 qsort(c, length, sizeof(struct rand_container), &dcc_compare_container);
692
693 /* reorder the list */
694 for (i = 0; i < length; i++) {
695 if (i != length - 1)
696 c[i].host->next = c[i+1].host;
697 else
698 c[i].host->next = NULL;
699 }
700
701 /* move the start of the list */
702 *host_list = c[0].host;
703
704 free(c);
705 return 0;
706 }
707
dcc_free_hostdef(struct dcc_hostdef * host)708 int dcc_free_hostdef(struct dcc_hostdef *host)
709 {
710 /* ANSI C requires free() to accept NULL */
711
712 free(host->user);
713 free(host->hostname);
714 free(host->ssh_command);
715 free(host->hostdef_string);
716 memset(host, 0xf1, sizeof *host);
717 free(host);
718
719 return 0;
720 }
721