1 /* @(#) $Id: actsync.c 6372 2003-05-31 19:48:28Z rra $ */
2 /* @(#) Under RCS control in /usr/local/news/src/inn/local/RCS/actsync.c,v */
4 * actsync - sync or merge two active files
7 * actsync [-b hostid][-d hostid][-g max][-i ignore_file][-I][-k][-l hostid]
8 * [-m][-n name][-o fmt][-p %][-q hostid][-s size]
9 * [-t hostid][-T][-v verbose_lvl][-z sec]
12 * -A use authentication to server
13 * -b hostid ignore *.bork.bork.bork groups from: (def: -b 0)
17 * 12 from host1 and host2
18 * 21 from host1 and host2
19 * -d hostid ignore groups with all numeric components (def: -d 0)
20 * -g max ignore group >max levels (0=dont ignore) (def: -g 0)
21 * -i ignore_file file with list/types of groups to ignore (def: no file)
22 * -I hostid ignore_file applies only to hostid (def: -I 12)
23 * -k keep host1 groups with errors (def: remove)
24 * -l hostid flag =group problems as errors (def: -l 12)
25 * -m merge, keep group not on host2 (def: sync)
26 * -n name name given to ctlinnd newgroup commands (def: actsync)
27 * -o fmt type of output: (def: -o c)
28 * a output groups in active format
29 * a1 like 'a', but output ignored non-err host1 grps
30 * ak like 'a', keep host2 hi/low values on new groups
31 * aK like 'a', use host2 hi/low values always
32 * c output in ctlinnd change commands
33 * x no output, safely exec ctlinnd commands
34 * xi no output, safely exec commands interactively
35 * -p % min % host1 lines unchanged allowed (def: -p 96)
36 * -q hostid silence errors from a host (see -b) (def: -q 0)
37 * -s size ignore names longer than size (0=no lim) (def: -s 0)
38 * -t hostid ignore bad top level groups from:(see -b) (def: -t 2)
39 * -T no new hierarchies (def: allow)
40 * -v verbose_lvl verbosity level (def: -v 0)
41 * 0 no debug or status reports
42 * 1 summary if work done
43 * 2 summary & actions (if exec output) only if done
44 * 3 summary & actions (if exec output)
45 * 4 debug output plus all -v 3 messages
46 * -z sec sleep sec seconds per exec if -o x (def: -z 4)
47 * host1 host to be changed (def: local server)
48 * host2 reference host used in merge
51 * By: Landon Curt Noll chongo@toad.com (chongo was here /\../\)
53 * Copyright (c) Landon Curt Noll, 1996.
54 * All rights reserved.
56 * Permission to use and modify is hereby granted so long as this
57 * notice remains. Use at your own risk. No warranty is implied.
62 #include "portable/wait.h"
71 #include "inn/innconf.h"
72 #include "inn/messages.h"
77 static const char usage[] = "\
78 Usage: actsync [-A][-b hostid][-d hostid][-i ignore_file][-I hostid][-k]\n\
79 [-l hostid][-m][-n name][-o fmt][-p min_%_unchg][-q hostid]\n\
80 [-s size][-t hostid][-T][-v verbose_lvl][-z sec]\n\
83 -A use authentication to server\n\
84 -b hostid ignore *.bork.bork.bork groups from: (def: -b 0)\n\
85 0 from neither host\n\
88 12 from host1 and host2\n\
89 21 from host1 and host2\n\
90 -d hostid ignore grps with all numeric components (def: -d 0)\n\
91 -g max ignore group >max levels (0=don't) (def: -g 0)\n\
92 -i file file with groups to ignore (def: no file)\n\
93 -I hostid ignore_file applies only to hostid (def: -I 12)\n\
94 -k keep host1 groups with errors (def: remove)\n\
95 -l hostid flag =group problems as errors (def: -l 12)\n\
96 -m merge, keep group not on host2 (def: sync)\n\
97 -n name name given to ctlinnd newgroup cmds (def: actsync)\n\
98 -o fmt type of output: (def: -o c)\n\
99 a output groups in active format\n\
100 a1 like 'a', but output ignored non-err host1 grps\n\
101 ak like 'a', keep host2 hi/low values on new groups\n\
102 aK like 'a', use host2 hi/low values always\n\
103 c output in ctlinnd change commands\n\
104 x no output, safely exec ctlinnd commands\n\
105 xi no output, safely exec commands interactively\n\
106 -p % min % host1 lines unchanged allowed (def: -p 96)\n\
107 -q hostid silence errors from a host (see -b) (def: -q 0)\n\
108 -s size ignore names > than size (0=no lim) (def: -s 0)\n\
109 -t hostid ignore bad top level grps from: (see -b)(def: -t 2)\n\
110 -T no new hierarchies (def: allow)\n\
111 -v level verbosity level (def: -v 0)\n\
112 0 no debug or status reports\n\
113 1 summary if work done\n\
114 2 summary & actions (if exec output) only if done\n\
115 3 summary & actions (if exec output)\n\
116 4 debug output plus all -v 3 messages\n\
117 -z sec sleep sec seconds per exec if -o x (def: -z 4)\n\
119 host1 host to be changed (def: local server)\n\
120 host2 reference host used in merge\n";
124 * pat - internal ignore/check pattern
126 * A pattern, derived from an ignore file, will determine if a group
127 * is will be checked if it is on both hosts or ignored altogether.
129 * The type related to the 4th field of an active file. Types may
130 * currently be one of [ymjnx=]. If '=' is one of the types, an
131 * optional equivalence pattern may be given in the 'epat' element.
133 * For example, to ignore "foo.bar.*", if it is junked or equated to
134 * a group of the form "alt.*.foo.bar.*":
136 * x.pat = "foo.bar.*";
138 * x.epat = "alt.*.foo.bar.*";
141 * To further check "foo.bar.mod" if it is moderated:
143 * x.pat = "foo.bar.mod";
148 * The 'i' value means ignore, 'c' value means 'compare'. The last pattern
149 * that matches a group determines the fate of the group. By default all
150 * groups are included.
153 char *pat; /* newsgroup pattern */
154 int type_match; /* 1 => match only if group type matches */
155 int y_type; /* 1 => match if a 'y' type group */
156 int m_type; /* 1 => match if a 'm' type group */
157 int n_type; /* 1 => match if a 'n' type group */
158 int j_type; /* 1 => match if a 'j' type group */
159 int x_type; /* 1 => match if a 'x' type group */
160 int eq_type; /* 1 => match if a 'eq' type group */
161 char *epat; /* =pattern to match, if non-NULL and = is in type */
162 int ignore; /* 0 => check matching group, 1 => ignore it */
165 /* internal representation of an active line */
167 int ignore; /* ignore reason, 0 => not ignore (see below) */
168 int hostid; /* HOSTID this group is from */
169 int linenum; /* >0 => active line number, <=0 => not a line */
170 int output; /* 1 => output to produce the merged active file */
171 int remove; /* 1 => remove this group */
172 char *name; /* newsgroup name */
173 char *hi; /* high article string */
174 char *low; /* low article string */
175 char *type; /* newsgroup type string */
176 char *outhi; /* output high article string */
177 char *outlow; /* output low article string */
178 char *outtype; /* output newsgroup type string */
181 /* structure used in the process of looking for =group type problems */
183 int skip; /* 1 => skip this entry */
184 struct grp *g; /* =group that is being examined */
185 char *eq; /* current equivalence name */
189 * These ignore reasons are listed in order severity; from mild to severe.
191 #define NOT_IGNORED 0x0000 /* newsgroup has not been ignored */
192 #define CHECK_IGNORE 0x0001 /* ignore file ignores this entry */
193 #define CHECK_TYPE 0x0002 /* group type is ignored */
194 #define CHECK_BORK 0x0004 /* group is a *.bork.bork.bork group */
195 #define CHECK_HIER 0x0008 /* -T && new group's hierarchy does not exist */
196 #define ERROR_LONGLOOP 0x0010 /* =name refers to long =grp chain or cycle */
197 #define ERROR_EQLOOP 0x0020 /* =name refers to itself in some way */
198 #define ERROR_NONEQ 0x0040 /* =name does not refer to a valid group */
199 #define ERROR_DUP 0x0080 /* newsgroup is a duplicate of another */
200 #define ERROR_EQNAME 0x0100 /* =name is a bad group name */
201 #define ERROR_BADTYPE 0x0200 /* newsgroup type is invalid */
202 #define ERROR_BADNAME 0x0400 /* newsgroup name is invalid */
203 #define ERROR_FORMAT 0x0800 /* entry line is malformed */
205 #define IS_IGNORE(ign) ((ign) & (CHECK_IGNORE|CHECK_TYPE|CHECK_BORK|CHECK_HIER))
206 #define IS_ERROR(ign) ((ign) & ~(CHECK_IGNORE|CHECK_TYPE|CHECK_BORK|CHECK_HIER))
208 #define NOHOST 0 /* neither host1 nor host2 */
209 #define HOSTID1 1 /* entry from the first host */
210 #define HOSTID2 2 /* entry from the second host */
212 #define CHUNK 5000 /* number of elements to alloc at a time */
214 #define TYPES "ymjnx=" /* group types (1st char of 4th active fld) */
215 #define TYPECNT (sizeof(TYPES)-1)
217 #define DEF_HI "0000000000" /* default hi string value for new groups */
218 #define DEF_LOW "0000000001" /* default low string value for new groups */
219 #define WATER_LEN 10 /* string length of hi/low water mark */
221 #define DEF_NAME "actsync" /* default name to use for ctlinnd newgroup */
223 #define MIN_UNCHG (double)96.0 /* min % of host1 lines unchanged allowed */
225 #define DEV_NULL "/dev/null" /* path to the bit bucket */
226 #define CTLINND_NAME "ctlinnd" /* basename of ctlinnd command */
227 #define CTLINND_TIME_OUT "-t30" /* seconds to wait before timeout */
229 #define READ_SIDE 0 /* read side of a pipe */
230 #define WRITE_SIDE 1 /* write side of a pipe */
232 #define EQ_LOOP 16 /* give up if =eq loop/chain is this long */
233 #define NOT_REACHED 127 /* exit value if unable to get active files */
235 #define NEWGRP_EMPTY 0 /* no new group dir was found */
236 #define NEWGRP_NOCHG 1 /* new group dir found but no hi/low change */
237 #define NEWGRP_CHG 2 /* new group dir found but no hi/low change */
240 #define BORK_CHECK(hostid) \
241 ((hostid == HOSTID1 && bork_host1_flag) || \
242 (hostid == HOSTID2 && bork_host2_flag))
245 #define NUM_CHECK(hostid) \
246 ((hostid == HOSTID1 && num_host1_flag) || \
247 (hostid == HOSTID2 && num_host2_flag))
250 #define TOP_CHECK(hostid) \
251 ((hostid == HOSTID1 && t_host1_flag) || \
252 (hostid == HOSTID2 && t_host2_flag))
254 /* -o output types */
255 #define OUTPUT_ACTIVE 1 /* output in active file format */
256 #define OUTPUT_CTLINND 2 /* output in ctlinnd change commands */
257 #define OUTPUT_EXEC 3 /* no output, safely exec commands */
258 #define OUTPUT_IEXEC 4 /* no output, exec commands interactively */
261 #define QUIET(hostid) \
262 ((hostid == HOSTID1 && quiet_host1) || (hostid == HOSTID2 && quiet_host2))
264 /* -v verbosity level */
265 #define VER_MIN 0 /* minimum -v level */
266 #define VER_NONE 0 /* no -v output */
267 #define VER_SUMM_IF_WORK 1 /* output summary if actions were performed */
268 #define VER_REPT_IF_WORK 2 /* output summary & actions only if performed */
269 #define VER_REPORT 3 /* output summary & actions performed */
270 #define VER_FULL 4 /* output all summary, actins and debug */
271 #define VER_MAX 4 /* maximum -v level */
272 #define D_IF_SUMM (v_flag >= VER_SUMM_IF_WORK) /* true => give summary always */
273 #define D_REPORT (v_flag >= VER_REPT_IF_WORK) /* true => give reports */
274 #define D_BUG (v_flag == VER_FULL) /* true => debug processing */
275 #define D_SUMMARY (v_flag >= VER_REPORT) /* true => give summary always */
277 /* flag and arg related defaults */
278 int bork_host1_flag = 0; /* 1 => -b 1 or -b 12 or -b 21 given */
279 int bork_host2_flag = 0; /* 1 => -b 2 or -b 12 or -b 21 given */
280 int num_host1_flag = 0; /* 1 => -d 1 or -d 12 or -d 21 given */
281 int num_host2_flag = 0; /* 1 => -d 2 or -d 12 or -d 21 given */
282 char *ign_file = NULL; /* default ignore file */
283 int ign_host1_flag = 1; /* 1 => -i ign_file applies to host1 */
284 int ign_host2_flag = 1; /* 1 => -i ign_file applies to host2 */
285 int g_flag = 0; /* ignore grps deeper than > g_flag, 0=>dont */
286 int k_flag = 0; /* 1 => -k given */
287 int l_host1_flag = HOSTID1; /* HOSTID1 => host1 =group error detection */
288 int l_host2_flag = HOSTID2; /* HOSTID2 => host2 =group error detection */
289 int m_flag = 0; /* 1 => merge active files, don't sync */
290 const char *new_name = DEF_NAME; /* ctlinnd newgroup name */
291 int o_flag = OUTPUT_CTLINND; /* default output type */
292 double p_flag = MIN_UNCHG; /* min % host1 lines allowed to be unchanged */
293 int host1_errs = 0; /* errors found in host1 active file */
294 int host2_errs = 0; /* errors found in host2 active file */
295 int quiet_host1 = 0; /* 1 => -q 1 or -q 12 or -q 21 given */
296 int quiet_host2 = 0; /* 1 => -q 2 or -q 12 or -q 21 given */
297 int s_flag = 0; /* max group size (length), 0 => do not check */
298 int t_host1_flag = 0; /* 1 => -t 1 or -t 12 or -t 21 given */
299 int t_host2_flag = 1; /* 1 => -t 2 or -d 12 or -t 21 given */
300 int no_new_hier = 0; /* 1 => -T; no new hierarchies */
301 int host2_hilow_newgrp = 0; /* 1 => use host2 hi/low on new groups */
302 int host2_hilow_all = 0; /* 1 => use host2 hi/low on all groups */
303 int host1_ign_print = 0; /* 1 => print host1 ignored groups too */
304 int v_flag = 0; /* default verbosity level */
305 int z_flag = 4; /* sleep z_flag sec per exec if -o x */
308 /* forward declarations */
309 static struct grp *get_active(); /* get an active file from a remote host */
310 static int bad_grpname(); /* test if string is a valid group name */
311 static struct pat *get_ignore(); /* read in an ignore file */
312 static void ignore(); /* ignore newsgroups given an ignore list */
313 static int merge_cmp(); /* qsort compare for active file merge */
314 static void merge_grps(); /* merge groups from active files */
315 static int active_cmp(); /* qsort compare for active file output */
316 static void output_grps(); /* output the merged groups */
317 static void process_args(); /* process command line arguments */
318 static void error_mark(); /* mark for removal, error grps from host */
319 static int eq_merge_cmp(); /* qsort compare for =type grp processing */
320 static int mark_eq_probs(); /* mark =type problems from a host */
321 static int exec_cmd(); /* exec a ctlinnd command */
322 static int new_top_hier(); /* see if we have a new top level */
326 int argc; /* arg count */
327 char *argv[]; /* the args */
329 struct grp *grp; /* struct grp array for host1 & host2 */
330 struct pat *ignor; /* ignore list from ignore file */
331 int grplen; /* length of host1/host2 group array */
332 int iglen; /* length of ignore list */
333 char *host1; /* host to change */
334 char *host2; /* comparison host */
336 /* First thing, set up our identity. */
337 message_program_name = "actsync";
339 /* Read in default info from inn.conf. */
340 if (!innconf_read(NULL))
342 process_args(argc, argv, &host1, &host2);
344 /* obtain the active files */
345 grp = get_active(host1, HOSTID1, &grplen, NULL, &host1_errs);
346 grp = get_active(host2, HOSTID2, &grplen, grp, &host2_errs);
348 /* ignore groups from both active files, if -i */
349 if (ign_file != NULL) {
351 /* read in the ignore file */
352 ignor = get_ignore(ign_file, &iglen);
355 ignore(grp, grplen, ignor, iglen);
358 /* compare groups from both hosts */
359 merge_grps(grp, grplen, host1, host2);
361 /* mark for removal, error groups from host1 if -e */
364 /* mark error groups for removal */
365 error_mark(grp, grplen, HOSTID1);
368 /* output result of merge */
369 output_grps(grp, grplen);
376 * process_args - process the command line arguments
381 * host1 name of first host (may be 2nd if -R)
382 * host2 name of second host2 *may be 1st if -R)
385 process_args(argc, argv, host1, host2)
386 int argc; /* arg count */
387 char *argv[]; /* the arg array */
388 char **host1; /* where to place name of host1 */
389 char **host2; /* where to place name of host2 */
391 char *def_serv = NULL; /* name of default server */
395 while ((i = getopt(argc,argv,"Ab:d:g:i:I:kl:mn:o:p:q:s:t:Tv:z:")) != EOF) {
400 case 'b': /* -b {0|1|2|12|21} */
401 switch (atoi(optarg)) {
418 warn("-b option must be 0, 1, 2, 12, or 21");
422 case 'd': /* -d {0|1|2|12|21} */
423 switch (atoi(optarg)) {
440 warn("-d option must be 0, 1, 2, 12, or 21");
444 case 'g': /* -g max */
445 g_flag = atoi(optarg);
447 case 'i': /* -i ignore_file */
450 case 'I': /* -I {0|1|2|12|21} */
451 switch (atoi(optarg)) {
470 warn("-I option must be 0, 1, 2, 12, or 21");
477 case 'l': /* -l {0|1|2|12|21} */
478 switch (atoi(optarg)) {
480 l_host1_flag = NOHOST;
481 l_host2_flag = NOHOST;
484 l_host1_flag = HOSTID1;
485 l_host2_flag = NOHOST;
488 l_host1_flag = NOHOST;
489 l_host2_flag = HOSTID2;
493 l_host1_flag = HOSTID1;
494 l_host2_flag = HOSTID2;
497 warn("-l option must be 0, 1, 2, 12, or 21");
504 case 'n': /* -n name */
507 case 'o': /* -o out_type */
510 o_flag = OUTPUT_ACTIVE;
514 case 'K': /* -o a1K */
517 host2_hilow_newgrp = 1;
519 case 'k': /* -o a1k */
521 host2_hilow_newgrp = 1;
530 case '1': /* -o aK1 */
533 host2_hilow_newgrp = 1;
537 host2_hilow_newgrp = 1;
543 case '1': /* -o ak1 */
545 host2_hilow_newgrp = 1;
548 host2_hilow_newgrp = 1;
552 case '\0': /* -o a */
555 warn("-o type must be a, a1, ak, aK, ak1, or aK1");
560 o_flag = OUTPUT_CTLINND;
563 if (optarg[1] == 'i') {
564 o_flag = OUTPUT_IEXEC;
566 o_flag = OUTPUT_EXEC;
570 warn("-o type must be a, a1, ak, aK, ak1, aK1, c, x, or xi");
574 case 'p': /* -p %_min_host1_change */
575 /* parse % into [0,100] */
576 p_flag = atof(optarg);
577 if (p_flag > (double)100.0) {
578 p_flag = (double)100.0;
579 } else if (p_flag < (double)0.0) {
580 p_flag = (double)0.0;
583 case 'q': /* -q {0|1|2|12|21} */
584 switch (atoi(optarg)) {
601 warn("-q option must be 0, 1, 2, 12, or 21");
605 case 's': /* -s size */
606 s_flag = atoi(optarg);
608 case 't': /* -t {0|1|2|12|21} */
609 switch (atoi(optarg)) {
611 t_host1_flag = NOHOST;
612 t_host2_flag = NOHOST;
615 t_host1_flag = HOSTID1;
616 t_host2_flag = NOHOST;
619 t_host1_flag = NOHOST;
620 t_host2_flag = HOSTID2;
624 t_host1_flag = HOSTID1;
625 t_host2_flag = HOSTID2;
628 warn("-t option must be 0, 1, 2, 12, or 21");
635 case 'v': /* -v verbose_lvl */
636 v_flag = atoi(optarg);
637 if (v_flag < VER_MIN || v_flag > VER_MAX) {
638 warn("-v level must be >= %d and <= %d", VER_MIN, VER_MAX);
642 case 'z': /* -z sec */
643 z_flag = atoi(optarg);
646 warn("unknown flag");
651 /* process the remaining args */
657 /* assume host1 is the local server */
665 warn("expected 1 or 2 host args, found %d", argc);
669 /* determine default host name if needed */
670 if (*host1 == NULL || strcmp(*host1, "-") == 0) {
671 def_serv = innconf->server;
674 if (*host2 == NULL || strcmp(*host2, "-") == 0) {
675 def_serv = innconf->server;
678 if (*host1 == NULL || *host2 == NULL)
679 die("unable to determine default server name");
680 if (D_BUG && def_serv != NULL)
681 warn("STATUS: using default server: %s", def_serv);
683 /* processing done */
688 * get_active - get an active file from a host
691 * host host to contact or file to read, NULL => local server
692 * hostid HOST_ID of host
693 * len pointer to length of grp return array
694 * grp existing host array to add, or NULL
695 * errs count of lines that were found to have some error
698 * Pointer to an array of grp structures describing each active entry.
699 * Does not return on fatal error.
701 * If host starts with a '/' or '.', then it is assumed to be a local file.
702 * In that case, the local file is opened and read.
705 get_active(host, hostid, len, grp, errs)
706 char *host; /* the host to contact */
707 int hostid; /* HOST_ID of host */
708 int *len; /* length of returned grp array in elements */
709 struct grp* grp; /* existing group array or NULL */
710 int *errs; /* line error count */
712 FILE *active; /* stream for fetched active data */
713 FILE *FromServer; /* stream from server */
714 FILE *ToServer; /* stream to server */
715 QIOSTATE *qp; /* QIO active state */
716 char buff[8192+1]; /* QIO buffer */
717 char *line; /* the line just read */
718 struct grp *ret; /* array of groups to return */
719 struct grp *cur; /* current grp entry being formed */
720 int max; /* max length of ret */
721 int cnt; /* number of entries read */
722 int ucnt; /* number of entries to be used */
723 int namelen; /* length of newsgroup name */
724 int is_file; /* 1 => host is actually a filename */
725 int num_check; /* true => check for all numeric components */
733 die("internal error #1: len is NULL");
735 die("internal error #2: errs in NULL");
737 warn("STATUS: obtaining active file from %s", host);
739 /* setup return array if needed */
741 ret = xmalloc(CHUNK * sizeof(struct grp));
745 /* or prep to use the existing array */
748 max = ((*len + CHUNK-1)/CHUNK)*CHUNK;
751 /* check for host being a filename */
752 if (host != NULL && (host[0] == '/' || host[0] == '.')) {
754 /* note that host is actually a file */
757 /* setup to read the local file quickly */
758 if ((qp = QIOopen(host)) == NULL)
759 sysdie("cannot open active file");
761 /* case: host is a hostname or NULL (default server) */
764 /* note that host is actually a hostname or NULL */
767 /* prepare remote host variables */
768 if ((p = strchr(host, ':')) != NULL) {
771 rhost = xstrdup(host);
774 rhost = xstrdup(host);
778 /* open a connection to the server */
780 if (NNTPconnect(rhost, rport, &FromServer, &ToServer, buff) < 0)
781 die("cannot connect to server: %s",
782 buff[0] ? buff : strerror(errno));
784 if (A_flag && NNTPsendpassword(rhost, FromServer, ToServer) < 0)
785 die("cannot authenticate to server");
789 /* get the active data from the server */
790 active = CAlistopen(FromServer, ToServer, NULL);
792 sysdie("cannot retrieve data");
794 /* setup to read the retrieved data quickly */
795 if ((qp = QIOfdopen((int)fileno(active))) == NULL)
796 sysdie("cannot read temp file");
799 /* scan server's output, processing appropriate lines */
800 num_check = NUM_CHECK(hostid);
801 for (cnt=0, ucnt=0; (line = QIOread(qp)) != NULL; ++(*len), ++cnt) {
803 /* expand return array if needed */
806 ret = xrealloc(ret, sizeof(struct grp) * max);
809 /* setup the next return element */
811 cur->ignore = NOT_IGNORED;
812 cur->hostid = hostid;
813 cur->linenum = cnt+1;
824 /* obtain a copy of the current line */
825 cur->name = xstrdup(line);
827 /* get the group name */
828 if ((p = strchr(cur->name, ' ')) == NULL) {
830 warn("line %d from %s is malformed, skipping line", cnt + 1,
833 /* don't form an entry for this group */
838 namelen = p - cur->name;
840 /* find the other 3 fields, ignore if not found */
842 if ((p = strchr(p + 1, ' ')) == NULL) {
844 warn("skipping malformed line %d (field 2) from %s", cnt + 1,
847 /* don't form an entry for this group */
853 if ((p = strchr(p + 1, ' ')) == NULL) {
855 warn("skipping malformed line %d (field 3) from %s", cnt + 1,
858 /* don't form an entry for this group */
864 if ((p = strchr(p + 1, ' ')) != NULL) {
866 warn("skipping line %d from %s, it has more than 4 fields",
869 /* don't form an entry for this group */
874 /* check for bad group name */
875 if (bad_grpname(cur->name, num_check)) {
877 warn("line %d <%s> from %s has a bad newsgroup name",
878 cnt + 1, cur->name, host);
879 cur->ignore |= ERROR_BADNAME;
883 /* check for long name if requested */
884 if (s_flag > 0 && strlen(cur->name) > (size_t)s_flag) {
886 warn("line %d <%s> from %s has a name that is too long",
887 cnt + 1, cur->name, host);
888 cur->ignore |= ERROR_BADNAME;
892 /* look for only a bad top level element if the proper -t was given */
893 if (TOP_CHECK(hostid)) {
895 /* look for a '.' in the name */
896 if (strcmp(cur->name, "junk") != 0 &&
897 strcmp(cur->name, "control") != 0 &&
898 strcmp(cur->name, "to") != 0 &&
899 strcmp(cur->name, "test") != 0 &&
900 strcmp(cur->name, "general") != 0 &&
901 strchr(cur->name, '.') == NULL) {
903 warn("line %d <%s> from %s is an invalid top level name",
904 cnt + 1, cur->name, host);
905 cur->ignore |= ERROR_BADNAME;
910 /* look for *.bork.bork.bork groups if the proper -b was given */
911 if (BORK_CHECK(cur->hostid)) {
912 int elmlen; /* length of element */
913 char *q; /* beyond end of element */
915 /* scan the name backwards */
916 q = &(cur->name[namelen]);
917 for (p = &(cur->name[namelen-1]); p >= cur->name; --p) {
918 /* if '.', see if this is a bork element */
920 /* see if the bork element is short enough */
922 if (3*elmlen <= q-cur->name) {
923 /* look for a triple match */
924 if (strncmp(p,p-elmlen,elmlen) == 0 &&
925 strncmp(p,p-(elmlen*2),elmlen) == 0) {
926 /* found a *.bork.bork.bork group */
927 cur->ignore |= CHECK_BORK;
931 /* note the end of a new element */
938 * check for bad chars in the hi water mark
940 for (p=cur->hi, i=0; *p && isascii(*p) && isdigit((int)*p); ++p, ++i) {
944 warn("line %d <%s> from %s has non-digits in hi water",
945 cnt + 1, cur->name, cur->hi);
946 cur->ignore |= ERROR_FORMAT;
951 * check for excessive hi water length
955 warn("line %d <%s> from %s hi water len: %d < %d",
956 cnt + 1, cur->name, cur->hi, i, WATER_LEN);
957 cur->ignore |= ERROR_FORMAT;
962 * if the hi water length is too small, malloc and resize
964 if (i != WATER_LEN) {
965 p = xmalloc(WATER_LEN + 1);
966 memcpy(p, cur->hi, ((i > WATER_LEN) ? WATER_LEN : i)+1);
970 * check for bad chars in the low water mark
972 for (p=cur->low, i=0; *p && isascii(*p) && isdigit((int)*p); ++p, ++i) {
976 warn("line %d <%s> from %s has non-digits in low water",
977 cnt + 1, cur->name, cur->low);
978 cur->ignore |= ERROR_FORMAT;
983 * check for excessive low water length
987 warn("line %d <%s> from %s low water len: %d < %d",
988 cnt + 1, cur->name, cur->hi, i, WATER_LEN);
989 cur->ignore |= ERROR_FORMAT;
994 * if the low water length is too small, malloc and resize
996 if (i != WATER_LEN) {
997 p = xmalloc(WATER_LEN + 1);
998 memcpy(p, cur->low, ((i > WATER_LEN) ? WATER_LEN : i)+1);
1001 /* check for a bad group type */
1002 switch (cur->type[0]) {
1004 /* of COURSE: collabra has incompatible flags. but it */
1005 /* looks like they can be fixed easily enough. */
1006 if (cur->type[1] == 'g') {
1007 cur->type[1] = '\0';
1013 if (cur->type[1] != '\0') {
1015 warn("line %d <%s> from %s has a bad newsgroup type",
1016 cnt + 1, cur->name, host);
1017 cur->ignore |= ERROR_BADTYPE;
1021 if (cur->type[1] == '\0') {
1023 warn("line %d <%s> from %s has an empty =group name",
1024 cnt + 1, cur->name, host);
1025 cur->ignore |= ERROR_BADTYPE;
1030 warn("line %d <%s> from %s has an unknown newsgroup type",
1031 cnt + 1, cur->name, host);
1032 cur->ignore |= ERROR_BADTYPE;
1035 if (cur->ignore & ERROR_BADTYPE) {
1039 /* if an = type, check for bad = name */
1040 if (cur->type[0] == '=' && bad_grpname(&(cur->type[1]), num_check)) {
1042 warn("line %d <%s> from %s is equivalenced to a bad name:"
1043 " <%s>", cnt+1, cur->name, host,
1044 (cur->type) ? cur->type : "NULL");
1045 cur->ignore |= ERROR_EQNAME;
1049 /* if an = type, check for long = name if requested */
1050 if (cur->type[0] == '=' && s_flag > 0 &&
1051 strlen(&(cur->type[1])) > (size_t)s_flag) {
1053 warn("line %d <%s> from %s is equivalenced to a long name:"
1054 " <%s>", cnt+1, cur->name, host,
1055 (cur->type) ? cur->type : "NULL");
1056 cur->ignore |= ERROR_EQNAME;
1060 /* count this entry which will be used */
1064 warn("STATUS: read %d groups, will merge %d groups from %s",
1067 /* count the errors */
1070 warn("STATUS: found %d line errors from %s", *errs, host);
1072 /* determine why we stopped */
1074 sysdie("cannot read temp file for %s at line %d", host, cnt);
1075 else if (QIOtoolong(qp))
1076 sysdie("line %d from host %s is too long", cnt, host);
1083 fprintf(ToServer, "quit\r\n");
1085 fgets(buff, sizeof buff, FromServer);
1092 * bad_grpname - test if the string is a valid group name
1094 * Newsgroup names must consist of only alphanumeric chars and
1095 * characters from the following regular expression:
1099 * One cannot have two '.'s in a row. The first character must be
1100 * alphanumeric. The character following a '.' must be alphanumeric.
1101 * The name cannot end in a '.' character.
1103 * If we are checking for all numeric compnents, (see num_chk) then
1104 * a component cannot be all numeric. I.e,. there must be a non-numeric
1105 * character in the name, there must be a non-numeric character between
1106 * the start and the first '.', there must be a non-numeric character
1107 * between two '.'s anmd there must be a non-numeric character between
1108 * the last '.' and the end.
1111 * name newsgroup name to check
1112 * num_chk true => all numeric newsgroups components are invalid
1113 * false => do not check for numeric newsgroups
1120 bad_grpname(name, num_chk)
1121 char *name; /* newsgroup name to check */
1122 int num_chk; /* true => check for numeric newsgroup */
1125 int non_num; /* true => found a non-numeric, non-. character */
1126 int level; /* group levels (.'s) */
1133 /* must start with a alpha numeric ascii character */
1134 if (!isascii(name[0])) {
1137 /* set non_num as needed */
1138 if (isalpha((int)name[0])) {
1140 } else if ((int)isdigit((int)name[0])) {
1146 /* scan each char */
1148 for (p=name+1; *p; ++p) {
1150 /* name must contain ASCII chars */
1155 /* alpha chars are ok */
1156 if (isalpha((int)*p)) {
1161 /* numeric chars are ok */
1162 if (isdigit((int)*p)) {
1166 /* +, - and _ are ok */
1167 if (*p == '+' || *p == '-' || *p == '_') {
1172 /* check for the '.' case */
1175 * look for groups that are too deep, if requested by -g
1177 if (g_flag > 0 && ++level > g_flag) {
1178 /* we are too deep */
1183 * A '.' is ok as long as the next character is alphanumeric.
1184 * This imples that '.' cannot before a previous '.' and
1185 * that it cannot be at the end.
1187 * If we are checking for all numeric compnents, then
1188 * '.' is ok if we saw a non-numeric char before the
1189 * last '.', or before the beginning if no previous '.'
1192 if ((!num_chk || non_num) && isascii(*(p+1)) && isalnum((int)*(p+1))) {
1193 ++p; /* '.' is ok, and so is the next char */
1194 if (isdigit((int)*p)) { /* reset non_num as needed */
1203 /* this character must be invalid */
1206 if (num_chk && !non_num) {
1207 /* last component is all numeric */
1211 /* the name must be ok */
1216 * get_ignore - get the ignore list from an ignore file
1219 * filename name of the ignore file to read
1220 * *len pointer to length of ignore return array
1223 * returns a malloced ignore pattern array, changes len
1225 * An ignore file is of the form:
1227 * # this is a comment which is ignored
1228 * # comments begin at the first # character
1229 * # comments may follow text on the same line
1231 * # blank lines are ignored too
1233 * # lines are [ic] <spaces-tabs> pattern [<spaces-tabs> type] ...
1234 * i foo.* # ignore foo.* groups,
1235 * c foo.bar m # but check foo.bar if moderated
1236 * c foo.keep.* # and check foo.keep.*
1237 * i foo.keep.* j =alt.* # except when foo.keep.* is junked
1238 * # or equivalenced to an alt.* group
1240 * The 'i' value means ignore, 'c' value means 'compare'. The last pattern
1241 * that matches a group determines the fate of the group. By default all
1242 * groups are included.
1244 * NOTE: Only one '=name' is allowed per line.
1245 * "=" is considered to be equivalent to "=*".
1248 get_ignore(filename, len)
1249 char *filename; /* name of the ignore file to read */
1250 int *len; /* length of return array */
1252 QIOSTATE *qp; /* QIO ignore file state */
1253 char *line; /* the line just read */
1254 struct pat *ret; /* array of ignore patterns to return */
1255 struct pat *cur; /* current pattern entry being formed */
1256 int max; /* max length (in elements) of ret */
1257 int linenum; /* current line number */
1262 if (filename == NULL)
1263 die("internal error #3: filename is NULL");
1265 die("internal error #4: len is NULL");
1267 warn("STATUS: reading ignore file %s", filename);
1269 /* setup return array */
1270 ret = xmalloc(CHUNK * sizeof(struct grp));
1273 /* setup to read the ignore file data quickly */
1274 if ((qp = QIOopen(filename)) == NULL)
1275 sysdie("cannot read ignore file %s", filename);
1277 /* scan server's output, displaying appropriate lines */
1279 for (linenum = 1; (line = QIOread(qp)) != NULL; ++linenum) {
1281 /* expand return array if needed */
1284 ret = xrealloc(ret, sizeof(struct pat) * max);
1287 /* remove any trailing comments */
1288 p = strchr(line, '#');
1293 /* remove any trailing spaces and tabs */
1294 for (p = &line[strlen(line)-1];
1295 p >= line && (*p == ' ' || *p == '\t');
1300 /* ignore line if the remainder of the line is empty */
1301 if (line[0] == '\0') {
1305 /* ensure that the line starts with an i or c token */
1306 if ((line[0] != 'i' && line[0] != 'c') ||
1307 (line[1] != ' ' && line[1] != '\t'))
1308 die("first token is not i or c in line %d of %s", linenum,
1311 /* ensure that the second newsgroup pattern token follows */
1312 p = strtok(line+2, " \t");
1314 die("did not find 2nd field in line %d of %s", linenum,
1317 /* setup the next return element */
1320 cur->type_match = 0;
1328 cur->ignore = (line[0] == 'i');
1330 /* obtain a copy of the newsgroup pattern token */
1331 cur->pat = xstrdup(p);
1333 /* process any other type tokens */
1334 for (p=strtok(NULL, " \t"), i=3;
1336 p=strtok(NULL, " \t"), ++i) {
1338 /* ensure that this next token is a valid type */
1346 warn("field %d on line %d of %s not a valid type",
1347 i, linenum, filename);
1348 die("valid types are a char from [ymnjx=] or =name");
1354 warn("field %d on line %d of %s is not a valid type",
1355 i, linenum, filename);
1356 die("valid types are a char from [ymnjx=] or =name");
1359 /* note that we have a type specific pattern */
1360 cur->type_match = 1;
1362 /* ensure that type is not a duplicate */
1363 if ((p[0] == 'y' && cur->y_type) ||
1364 (p[0] == 'm' && cur->m_type) ||
1365 (p[0] == 'n' && cur->n_type) ||
1366 (p[0] == 'j' && cur->j_type) ||
1367 (p[0] == 'x' && cur->x_type) ||
1368 (p[0] == '=' && cur->eq_type)) {
1369 warn("only one %c type allowed per line", p[0]);
1370 die("field %d on line %d of %s is a duplicate type",
1371 i, linenum, filename);
1374 /* note what we have seen */
1393 if (p[0] == '=' && p[1] != '\0')
1394 cur->epat = xstrdup(p + 1);
1398 /* object if too many fields */
1400 die("too many fields on line %d of %s", linenum, filename);
1403 /* count another pat element */
1407 /* return the pattern array */
1412 * ignore - ignore newsgroups given an ignore list
1415 * grp array of groups
1416 * grplen length of grp array in elements
1417 * igcl array of ignore
1418 * iglen length of igcl array in elements
1421 ignore(grp, grplen, igcl, iglen)
1422 struct grp *grp; /* array of groups */
1423 int grplen; /* length of grp array in elements */
1424 struct pat *igcl; /* array of ignore patterns */
1425 int iglen; /* length of igcl array in elements */
1427 struct grp *gp; /* current group element being examined */
1428 struct pat *pp; /* current pattern element being examined */
1429 int g; /* current group index number */
1430 int p; /* current pattern index number */
1431 int ign; /* 1 => ignore this group, 0 => check it */
1432 int icnt; /* groups ignored */
1433 int ccnt; /* groups to be checked */
1437 die("internal error #5: grp is NULL");
1439 die("internal error $6: igcl is NULL");
1441 warn("STATUS: determining which groups to ignore");
1443 /* if nothing to do, return quickly */
1444 if (grplen <= 0 || iglen <= 0) {
1448 /* examine each group */
1451 for (g=0; g < grplen; ++g) {
1453 /* check the group to examine */
1456 /* already ignored no need to examine */
1460 /* check group against all patterns */
1462 for (p=0, pp=igcl; p < iglen; ++p, ++pp) {
1464 /* if pattern has a specific type, check it first */
1465 if (pp->type_match) {
1467 /* specific type required, check for match */
1468 switch (gp->type[0]) {
1470 if (! pp->y_type) continue; /* pattern does not apply */
1473 if (! pp->m_type) continue; /* pattern does not apply */
1476 if (! pp->n_type) continue; /* pattern does not apply */
1479 if (! pp->j_type) continue; /* pattern does not apply */
1482 if (! pp->x_type) continue; /* pattern does not apply */
1485 if (! pp->eq_type) continue; /* pattern does not apply */
1486 if (pp->epat != NULL && !uwildmat(&gp->type[1], pp->epat)) {
1487 /* equiv pattern doesn't match, patt does not apply */
1494 /* perform a match on group name */
1495 if (uwildmat(gp->name, pp->pat)) {
1496 /* this pattern fully matches, use the ignore value */
1501 /* if this group is to be ignored, note it */
1503 switch (gp->hostid) {
1505 if (ign_host1_flag) {
1506 gp->ignore |= CHECK_IGNORE;
1511 if (ign_host2_flag) {
1512 gp->ignore |= CHECK_IGNORE;
1517 die("newsgroup %s bad hostid: %d", gp->name, gp->hostid);
1524 warn("STATUS: examined %d groups: %d ignored, %d to be checked",
1525 grplen, icnt, ccnt);
1529 * merge_cmp - qsort compare function for later group merge
1532 * a group a to compare
1533 * b group b to compare
1537 * 0 a == b elements match (fatal error if a and b are different)
1540 * To speed up group comparison, we compare by the following items listed
1541 * in order of sorting:
1544 * hostid (host1 ahead of host2)
1545 * linenum (active file line number)
1548 merge_cmp(arg_a, arg_b)
1549 const void *arg_a; /* first qsort compare arg */
1550 const void *arg_b; /* first qsort compare arg */
1552 const struct grp *a = arg_a; /* group a to compare */
1553 const struct grp *b = arg_b; /* group b to compare */
1558 /* we guess this could happen */
1562 /* compare group names */
1563 i = strcmp(a->name, b->name);
1568 /* compare hostid's */
1569 if (a->hostid != b->hostid) {
1570 if (a->hostid > b->hostid) {
1577 /* compare active line numbers */
1578 if (a->linenum != b->linenum) {
1579 if (a->linenum > b->linenum) {
1586 /* two different elements match, this should not happen! */
1587 die("two internal grp elements match!");
1592 * merge_grps - compare groups from both hosts
1595 * grp array of groups
1596 * grplen length of grp array in elements
1597 * host1 name of host with HOSTID1
1598 * host2 name of host with HOSTID2
1600 * This routine will select which groups to output form a merged active file.
1603 merge_grps(grp, grplen, host1, host2)
1604 struct grp *grp; /* array of groups */
1605 int grplen; /* length of grp array in elements */
1606 char *host1; /* name of host with HOSTID1 */
1607 char *host2; /* name of host with HOSTID2 */
1609 int cur; /* current group index being examined */
1610 int nxt; /* next group index being examined */
1611 int outcnt; /* groups to output */
1612 int rmcnt; /* groups to remove */
1613 int h1_probs; /* =type problem groups from host1 */
1614 int h2_probs; /* =type problem groups from host2 */
1618 die("internal error #7: grp is NULL");
1620 /* sort groups for the merge */
1622 warn("STATUS: sorting groups");
1623 qsort((char *)grp, grplen, sizeof(grp[0]), merge_cmp);
1625 /* mark =type problem groups from host2, if needed */
1626 h2_probs = mark_eq_probs(grp, grplen, l_host2_flag, host1, host2);
1629 * We will walk thru the sorted group array, looking for pairs
1630 * among the groups that we have not already ignored.
1632 * If a host has duplicate groups, then the duplicates will
1633 * be next to each other.
1635 * If both hosts have the name group, they will be next to each other.
1638 warn("STATUS: merging groups");
1641 for (cur=0; cur < grplen; cur=nxt) {
1643 /* determine the next group index */
1646 /* skip if this group is ignored */
1647 if (grp[cur].ignore) {
1650 /* assert: cur is not ignored */
1652 /* check for duplicate groups from the same host */
1653 while (nxt < grplen) {
1655 /* mark the later as a duplicate */
1656 if (grp[cur].hostid == grp[nxt].hostid &&
1657 strcmp(grp[cur].name, grp[nxt].name) == 0) {
1658 grp[nxt].ignore |= ERROR_DUP;
1659 if (!QUIET(grp[cur].hostid))
1660 warn("lines %d and %d from %s refer to the same group",
1661 grp[cur].linenum, grp[nxt].linenum,
1662 ((grp[cur].hostid == HOSTID1) ? host1 : host2));
1668 /* assert: cur is not ignored */
1669 /* assert: cur & nxt are not the same group from the same host */
1671 /* if nxt is ignored, look for the next non-ignored group */
1672 while (nxt < grplen && grp[nxt].ignore) {
1675 /* assert: cur is not ignored */
1676 /* assert: nxt is not ignored or is beyond end */
1677 /* assert: cur & nxt are not the same group from the same host */
1679 /* case: cur and nxt are the same group */
1680 if (nxt < grplen && strcmp(grp[cur].name, grp[nxt].name) == 0) {
1682 /* assert: cur is HOSTID1 */
1683 if (grp[cur].hostid != HOSTID1)
1684 die("internal error #8: grp[%d].hostid: %d != %d",
1685 cur, grp[cur].hostid, HOSTID1);
1688 * Both hosts have the same group. Make host1 group type
1689 * match host2. (it may already)
1691 grp[cur].output = 1;
1692 grp[cur].outhi = (host2_hilow_all ? grp[nxt].hi : grp[cur].hi);
1693 grp[cur].outlow = (host2_hilow_all ? grp[nxt].low : grp[cur].low);
1694 grp[cur].outtype = grp[nxt].type;
1697 /* do not process nxt, skip to the one beyond */
1700 /* case: cur and nxt are different groups */
1704 * if cur is host2, then host1 doesn't have it, so output it
1706 if (grp[cur].hostid == HOSTID2) {
1707 grp[cur].output = 1;
1708 grp[cur].outhi = (host2_hilow_newgrp ? grp[cur].hi : DEF_HI);
1709 grp[cur].outlow = (host2_hilow_newgrp ? grp[cur].low : DEF_LOW);
1710 grp[cur].outtype = grp[cur].type;
1714 * If cur is host1, then host2 doesn't have it.
1715 * Mark for removal if -m was not given.
1718 grp[cur].output = 1;
1719 grp[cur].outhi = grp[cur].hi;
1720 grp[cur].outlow = grp[cur].low;
1721 grp[cur].outtype = grp[cur].type;
1723 grp[cur].remove = 1;
1728 /* if no more groups to examine, we are done */
1729 if (nxt >= grplen) {
1735 /* mark =type problem groups from host1, if needed */
1736 h1_probs = mark_eq_probs(grp, grplen, l_host1_flag, host1, host2);
1740 warn("STATUS: sort-merge passed thru %d groups", outcnt);
1741 warn("STATUS: sort-merge marked %d groups for removal", rmcnt);
1742 warn("STATUS: marked %d =type error groups from host1", h1_probs);
1743 warn("STATUS: marked %d =type error groups from host2", h2_probs);
1749 * active_cmp - qsort compare function for active file style output
1752 * a group a to compare
1753 * b group b to compare
1757 * 0 a == b elements match (fatal error if a and b are different)
1760 * This sort will sort groups so that the lines that will we output
1761 * host1 lines followed by host2 lines. Thus, we will sort by
1762 * the following keys:
1764 * hostid (host1 ahead of host2)
1765 * linenum (active file line number)
1768 active_cmp(arg_a, arg_b)
1769 const void *arg_a; /* first qsort compare arg */
1770 const void *arg_b; /* first qsort compare arg */
1772 const struct grp *a = arg_a; /* group a to compare */
1773 const struct grp *b = arg_b; /* group b to compare */
1777 /* we guess this could happen */
1781 /* compare hostid's */
1782 if (a->hostid != b->hostid) {
1783 if (a->hostid > b->hostid) {
1790 /* compare active line numbers */
1791 if (a->linenum != b->linenum) {
1792 if (a->linenum > b->linenum) {
1799 /* two different elements match, this should not happen! */
1800 die("two internal grp elements match!");
1805 * output_grps - output the result of the merge
1808 * grp array of groups
1809 * grplen length of grp array in elements
1812 output_grps(grp, grplen)
1813 struct grp *grp; /* array of groups */
1814 int grplen; /* length of grp array in elements */
1816 int add; /* number of groups added */
1817 int change; /* number of groups changed */
1818 int remove; /* number of groups removed */
1819 int no_new_dir; /* number of new groups with missing/empty dirs */
1820 int new_dir; /* number of new groupsm, non-empty dir no water chg */
1821 int water_change; /* number of new groups where hi&low water changed */
1822 int work; /* adds + changes + removals */
1823 int same; /* the number of groups the same */
1824 int ignore; /* host1 newsgroups to ignore */
1825 int not_done; /* exec errors and execs not performed */
1826 int rm_cycle; /* 1 => removals only, 0 => adds & changes only */
1827 int sleep_msg; /* 1 => -o x sleep message was given */
1828 int top_ignore; /* number of groups ignored because of no top level */
1829 int restore; /* host1 groups restored due to -o a1 */
1830 double host1_same; /* % of host1 that is the same */
1835 die("internal error #9: grp is NULL");
1838 * If -a1 was given, mark for output any host1 newsgroup that was
1839 * simply ignored due to the -i ign_file.
1841 if (host1_ign_print) {
1843 for (i=0; i < grplen; ++i) {
1844 if (grp[i].hostid == HOSTID1 &&
1845 (grp[i].ignore == CHECK_IGNORE ||
1846 grp[i].ignore == CHECK_TYPE ||
1847 grp[i].ignore == (CHECK_IGNORE|CHECK_TYPE))) {
1848 /* force group to output and not be ignored */
1852 grp[i].outhi = grp[i].hi;
1853 grp[i].outlow = grp[i].low;
1854 grp[i].outtype = grp[i].type;
1859 warn("STATUS: restored %d host1 groups", restore);
1863 * If -T, ignore new top level groups from host2
1867 for (i=0; i < grplen; ++i) {
1868 /* look at new newsgroups */
1869 if (grp[i].hostid == HOSTID2 &&
1870 grp[i].output != 0 &&
1871 new_top_hier(grp[i].name)) {
1872 /* no top level ignore this new group */
1873 grp[i].ignore |= CHECK_HIER;
1876 warn("ignore new newsgroup: %s, new hierarchy",
1882 warn("STATUS: ignored %d new newsgroups due to new hierarchy",
1886 /* sort by active file order if active style output (-a) */
1887 if (o_flag == OUTPUT_ACTIVE) {
1889 warn("STATUS: sorting groups in output order");
1890 qsort((char *)grp, grplen, sizeof(grp[0]), active_cmp);
1894 * Determine the % of lines from host1 active file that remain unchanged
1895 * ignoring any low/high water mark changes.
1897 * Determine the number of old groups that will remain the same
1898 * the number of new groups that will be added.
1908 for (i=0; i < grplen; ++i) {
1909 /* skip non-output ... */
1910 if (grp[i].output == 0) {
1911 if (grp[i].hostid == HOSTID1) {
1916 /* case: group needs removal */
1917 } else if (grp[i].remove) {
1920 /* case: group is from host2, so we need a newgroup */
1921 } else if (grp[i].hostid == HOSTID2) {
1924 /* case: group is from host1, but the type changed */
1925 } else if (grp[i].type != grp[i].outtype &&
1926 strcmp(grp[i].type,grp[i].outtype) != 0) {
1929 /* case: group did not change */
1934 work = add+change+remove;
1935 if (same+work+host1_errs <= 0) {
1936 /* no lines, no work, no errors == nothing changed == 100% the same */
1937 host1_same = (double)100.0;
1939 /* calculate % unchanged */
1940 host1_same = (double)100.0 *
1941 ((double)same / (double)(same+work+host1_errs));
1944 warn("STATUS: same=%d add=%d, change=%d, remove=%d",
1945 same, add, change, remove);
1946 warn("STATUS: ignore=%d, work=%d, err=%d",
1947 ignore, work, host1_errs);
1948 warn("STATUS: same+work+err=%d, host1_same=%.2f%%",
1949 same+work+host1_errs, host1_same);
1953 * Bail out if we too few lines in host1 active file (ignoring
1954 * low/high water mark changes) remaining unchanged.
1956 * We define change as:
1958 * line errors from host1 active file
1959 * newsgroups to be added to host1
1960 * newsgroups to be removed from host1
1961 * newsgroups to be change in host1
1963 if (host1_same < p_flag) {
1964 warn("HALT: lines unchanged: %.2f%% < min change limit: %.2f%%",
1965 host1_same, p_flag);
1966 warn(" No output or commands executed. Determine if the degree");
1967 warn(" of changes is okay and re-execute with a lower -p value");
1968 die(" or with the problem fixed.");
1972 * look at all groups
1974 * If we are not producing active file output, we must do removals
1975 * before we do any adds and changes.
1977 * We recalculate the work stats in finer detail as well as noting how
1978 * many actions were successful.
1988 rm_cycle = ((o_flag == OUTPUT_ACTIVE) ? 0 : 1);
1990 for (i=0; i < grplen; ++i) {
1992 /* if -o Ax, output ignored non-error groups too */
1995 * skip non-output ...
1997 * but if '-a' and active output mode, then don't skip ignored,
1998 * non-error, non-removed groups from host1
2000 if (grp[i].output == 0) {
2001 if (grp[i].hostid == HOSTID1) {
2007 /* case: output active lines */
2008 if (o_flag == OUTPUT_ACTIVE) {
2010 /* case: group needs removal */
2011 if (grp[i].remove) {
2015 /* case: group will be kept */
2018 /* output in active file format */
2019 printf("%s %s %s %s\n",
2020 grp[i].name, grp[i].outhi, grp[i].outlow,
2023 /* if -v level is high enough, do group accounting */
2026 /* case: group is from host2, so we need a newgroup */
2027 if (grp[i].hostid == HOSTID2) {
2031 /* case: group is from host1, but the type changed */
2032 } else if (grp[i].type != grp[i].outtype &&
2033 strcmp(grp[i].type,grp[i].outtype) != 0) {
2037 /* case: group did not change */
2044 /* case: output ctlinnd commands */
2045 } else if (o_flag == OUTPUT_CTLINND) {
2047 /* case: group needs removal */
2048 if (grp[i].remove) {
2050 /* output rmgroup */
2052 printf("ctlinnd rmgroup %s\n", grp[i].name);
2057 /* case: group is from host2, so we need a newgroup */
2058 } else if (grp[i].hostid == HOSTID2) {
2060 /* output newgroup */
2062 printf("ctlinnd newgroup %s %s %s\n",
2063 grp[i].name, grp[i].outtype, new_name);
2068 /* case: group is from host1, but the type changed */
2069 } else if (grp[i].type != grp[i].outtype &&
2070 strcmp(grp[i].type,grp[i].outtype) != 0) {
2072 /* output changegroup */
2074 printf("ctlinnd changegroup %s %s\n",
2075 grp[i].name, grp[i].outtype);
2080 /* case: group did not change */
2087 /* case: exec ctlinnd commands */
2088 } else if (o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) {
2090 /* warn about sleeping if needed and first time */
2091 if (o_flag == OUTPUT_EXEC && z_flag > 0 && sleep_msg == 0) {
2093 warn("will sleep %d seconds before each fork/exec",
2098 /* case: group needs removal */
2099 if (grp[i].remove) {
2103 if (D_REPORT && o_flag == OUTPUT_EXEC)
2104 warn("rmgroup %s", grp[i].name);
2105 if (! exec_cmd(o_flag, "rmgroup",
2106 grp[i].name, NULL, NULL)) {
2114 /* case: group is from host2, so we need a newgroup */
2115 } else if (grp[i].hostid == HOSTID2) {
2119 if (D_REPORT && o_flag == OUTPUT_EXEC)
2120 warn("newgroup %s %s %s",
2121 grp[i].name, grp[i].outtype, new_name);
2122 if (! exec_cmd(o_flag, "newgroup", grp[i].name,
2123 grp[i].outtype, new_name)) {
2131 /* case: group is from host1, but the type changed */
2132 } else if (grp[i].type != grp[i].outtype &&
2133 strcmp(grp[i].type,grp[i].outtype) != 0) {
2135 /* exec changegroup */
2137 if (D_REPORT && o_flag == OUTPUT_EXEC)
2138 warn("changegroup %s %s",
2139 grp[i].name, grp[i].outtype);
2140 if (! exec_cmd(o_flag, "changegroup", grp[i].name,
2141 grp[i].outtype, NULL)) {
2149 /* case: group did not change */
2157 } while (--rm_cycle >= 0);
2159 /* final accounting, if -v */
2160 if (D_SUMMARY || (D_IF_SUMM && (work > 0 || not_done > 0))) {
2161 warn("STATUS: %d group(s)", add+remove+change+same);
2162 warn("STATUS: %d group(s)%s added", add,
2163 ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2165 warn("STATUS: %d group(s)%s removed", remove,
2166 ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2168 warn("STATUS: %d group(s)%s changed", change,
2169 ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2171 warn("STATUS: %d group(s) %s the same", same,
2172 ((o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC) ?
2174 warn("STATUS: %.2f%% of lines unchanged", host1_same);
2175 warn("STATUS: %d group(s) ignored", ignore);
2176 if (o_flag == OUTPUT_EXEC || o_flag == OUTPUT_IEXEC)
2177 warn("STATUS: %d exec(s) not performed", not_done);
2182 * error_mark - mark for removal, error groups from a given host
2185 * grp array of groups
2186 * grplen length of grp array in elements
2187 * hostid host to mark error groups for removal
2190 error_mark(grp, grplen, hostid)
2191 struct grp *grp; /* array of groups */
2192 int grplen; /* length of grp array in elements */
2193 int hostid; /* host to mark error groups for removal */
2200 die("internal error #11: grp is NULL");
2202 /* loop thru groups, looking for error groups from a given host */
2204 for (i=0; i < grplen; ++i) {
2206 /* skip if not from hostid */
2207 if (grp[i].hostid != hostid) {
2211 /* mark for removal if an error group not already removed */
2212 if (IS_ERROR(grp[i].ignore)) {
2214 /* mark for removal */
2215 if (grp[i].output != 1 || grp[i].remove != 1) {
2224 if (D_SUMMARY || (D_IF_SUMM && errcnt > 0))
2225 warn("STATUS: marked %d error groups for removal", errcnt);
2230 * eq_merge_cmp - qsort compare function for =type group processing
2233 * a =group a to compare
2234 * b =group b to compare
2238 * 0 a == b elements match (fatal error if a and b are different)
2241 * To speed up group comparison, we compare by the following items listed
2242 * in order of sorting:
2244 * skip (non-skipped groups after skipped ones)
2247 * hostid (host1 ahead of host2)
2248 * linenum (active file line number)
2251 eq_merge_cmp(arg_a, arg_b)
2252 const void *arg_a; /* first qsort compare arg */
2253 const void *arg_b; /* first qsort compare arg */
2255 const struct eqgrp *a = arg_a; /* group a to compare */
2256 const struct eqgrp *b = arg_b; /* group b to compare */
2261 /* we guess this could happen */
2265 /* compare skip values */
2266 if (a->skip != b->skip) {
2267 if (a->skip > b->skip) {
2268 /* a is skipped, b is not */
2271 /* b is skipped, a is not */
2276 /* compare the names the groups are equivalenced to */
2277 i = strcmp(a->eq, b->eq);
2282 /* compare the group names themselves */
2283 i = strcmp(a->g->name, b->g->name);
2288 /* compare hostid's */
2289 if (a->g->hostid != b->g->hostid) {
2290 if (a->g->hostid > b->g->hostid) {
2297 /* compare active line numbers */
2298 if (a->g->linenum != b->g->linenum) {
2299 if (a->g->linenum > b->g->linenum) {
2306 /* two different elements match, this should not happen! */
2307 die("two internal eqgrp elements match!");
2311 * mark_eq_probs - mark =type groups from a given host that have problems
2314 * grp sorted array of groups
2315 * grplen length of grp array in elements
2316 * hostid host to mark error groups for removal, or NOHOST
2317 * host1 name of host with HOSTID1
2318 * host2 name of host with HOSTID2
2320 * This function assumes that the grp array has been sorted by name.
2323 mark_eq_probs(grp, grplen, hostid, host1, host2)
2324 struct grp *grp; /* array of groups */
2325 int grplen; /* length of grp array in elements */
2326 int hostid; /* host to mark error groups for removal */
2327 char *host1; /* name of host with HOSTID1 */
2328 char *host2; /* name of host with HOSTID2 */
2330 struct eqgrp *eqgrp; /* =type pointer array */
2331 int eq_cnt; /* number of =type groups from host */
2332 int new_eq_cnt; /* number of =type groups remaining */
2333 int missing; /* =type groups equiv to missing groups */
2334 int cycled; /* =type groups equiv to themselves */
2335 int chained; /* =type groups in long chain or loop */
2336 int cmp; /* strcmp of two names */
2337 int step; /* equiv loop step */
2343 die("internal error #12: grp is NULL");
2344 if (hostid == NOHOST) {
2345 /* nothing to detect, nothing else to do */
2349 /* count the =type groups from hostid that are not in error */
2351 for (i=0; i < grplen; ++i) {
2352 if (grp[i].hostid == hostid &&
2353 ! IS_ERROR(grp[i].ignore) &&
2354 grp[i].type != NULL &&
2355 grp[i].type[0] == '=') {
2359 if (D_BUG && hostid != NOHOST)
2360 warn("STATUS: host%d has %d =type groups", hostid, eq_cnt);
2362 /* if no groups, then there is nothing to do */
2367 /* setup the =group record array */
2368 eqgrp = xmalloc(eq_cnt * sizeof(eqgrp[0]));
2369 for (i=0, j=0; i < grplen && j < eq_cnt; ++i) {
2370 if (grp[i].hostid == hostid &&
2371 ! IS_ERROR(grp[i].ignore) &&
2372 grp[i].type != NULL &&
2373 grp[i].type[0] == '=') {
2375 /* initialize record */
2377 eqgrp[j].g = &grp[i];
2378 eqgrp[j].eq = &(grp[i].type[1]);
2384 * try to resolve =type groups in at least EQ_LOOP equiv links
2386 new_eq_cnt = eq_cnt;
2389 for (step=0; step < EQ_LOOP && new_eq_cnt >= 0; ++step) {
2391 /* sort the =group record array */
2392 qsort((char *)eqgrp, eq_cnt, sizeof(eqgrp[0]), eq_merge_cmp);
2394 /* look for the groups to which =type group point at */
2395 eq_cnt = new_eq_cnt;
2396 for (i=0, j=0; i < grplen && j < eq_cnt; ++i) {
2398 /* we will skip any group in error or from the wrong host */
2399 if (grp[i].hostid != hostid || IS_ERROR(grp[i].ignore)) {
2403 /* we will skip any skipped eqgrp's */
2404 if (eqgrp[j].skip) {
2405 /* try the same group against the next eqgrp */
2411 /* compare the =name of the eqgrp with the name of the grp */
2412 cmp = strcmp(grp[i].name, eqgrp[j].eq);
2414 /* case: this group is pointed at by an eqgrp */
2417 /* see if we have looped around to the original group name */
2418 if (strcmp(grp[i].name, eqgrp[j].g->name) == 0) {
2420 /* note the detected loop */
2421 if (! QUIET(hostid))
2422 warn("%s from %s line %d =loops around to itself",
2424 ((eqgrp[j].g->hostid == HOSTID1) ? host1 : host2),
2425 eqgrp[j].g->linenum);
2426 eqgrp[j].g->ignore |= ERROR_EQLOOP;
2428 /* the =group is bad, so we don't need to bother with it */
2437 /* if =group refers to a valid group, we are done with it */
2438 if (grp[i].type != NULL && grp[i].type[0] != '=') {
2441 /* otherwise note the equiv name */
2443 eqgrp[j].eq = &(grp[i].type[1]);
2448 /* case: we missed the =name */
2449 } else if (cmp > 0) {
2451 /* mark the eqgrp in error */
2452 eqgrp[j].g->ignore |= ERROR_NONEQ;
2453 if (! QUIET(hostid))
2454 warn("%s from %s line %d not equiv to a valid group",
2456 ((eqgrp[j].g->hostid == HOSTID1) ? host1 : host2),
2457 eqgrp[j].g->linenum);
2459 /* =group is bad, so we don't need to bother with it anymore */
2467 /* any remaining non-skipped eqgrps are bad */
2468 while (j < eq_cnt) {
2470 /* mark the eqgrp in error */
2471 eqgrp[j].g->ignore |= ERROR_NONEQ;
2472 if (! QUIET(hostid))
2473 warn("%s from %s line %d isn't equiv to a valid group",
2475 ((hostid == HOSTID1) ? host1 : host2),
2476 eqgrp[j].g->linenum);
2478 /* the =group is bad, so we don't need to bother with it anymore */
2486 /* note groups that are in a long chain or loop */
2487 chained = new_eq_cnt;
2488 qsort((char *)eqgrp, eq_cnt, sizeof(eqgrp[0]), eq_merge_cmp);
2489 for (j=0; j < new_eq_cnt; ++j) {
2491 /* skip if already skipped */
2492 if (eqgrp[j].skip == 1) {
2496 /* mark as a long loop group */
2497 eqgrp[j].g->ignore |= ERROR_LONGLOOP;
2498 if (! QUIET(hostid))
2499 warn("%s from %s line %d in a long equiv chain or loop > %d",
2501 ((hostid == HOSTID1) ? host1 : host2),
2502 eqgrp[j].g->linenum, EQ_LOOP);
2507 warn("%d =type groups from %s are not equiv to a valid group",
2508 missing, ((hostid == HOSTID1) ? host1 : host2));
2509 warn("%d =type groups from %s are equiv to themselves",
2510 cycled, ((hostid == HOSTID1) ? host1 : host2));
2511 warn("%d =type groups from %s are in a long chain or loop > %d",
2512 chained, ((hostid == HOSTID1) ? host1 : host2), EQ_LOOP);
2515 return missing+cycled+chained;
2519 * exec_cmd - exec a ctlinnd command in forked process
2522 * mode OUTPUT_EXEC or OUTPUT_IEXEC (interactive mode)
2523 * cmd "changegroup", "newgroup", "rmgroup"
2525 * type type of group or NULL
2526 * who newgroup creator or NULL
2529 * 1 exec was performed
2530 * 0 exec was not performed
2533 exec_cmd(mode, cmd, grp, type, who)
2534 int mode; /* OUTPUT_EXEC or OUTPUT_IEXEC (interactive mode) */
2535 char *cmd; /* changegroup, newgroup or rmgroup */
2536 char *grp; /* name of group to change, add, remove */
2537 char *type; /* type of group or NULL */
2538 char *who; /* newgroup creator or NULL */
2540 FILE *ch_stream = NULL; /* stream from a child process */
2541 char buf[BUFSIZ+1]; /* interactive buffer */
2542 int pid; /* pid of child process */
2543 int io[2]; /* pair of pipe descriptors */
2544 int status; /* wait status */
2545 int exitval; /* exit status of the child */
2549 if (cmd == NULL || grp == NULL)
2550 die("internal error #13, cmd or grp is NULL");
2552 /* if interactive, ask the question */
2553 if (mode == OUTPUT_IEXEC) {
2555 /* ask the question */
2560 printf("%s %s [yn]? ", cmd, grp);
2561 } else if (who == NULL) {
2562 printf("%s %s %s [yn]? ", cmd, grp, type);
2564 printf("%s %s %s %s [yn]? ", cmd, grp, type, who);
2569 p = fgets(buf, BUFSIZ, stdin);
2571 /* EOF/ERROR on interactive input, silently stop processing */
2575 /* if non-empty line doesn't start with 'y' or 'Y', skip command */
2576 if (buf[0] != 'y' && buf[0] != 'Y' && buf[0] != '\n') {
2577 /* indicate nothing was done */
2582 /* build a pipe for output from child interactive mode */
2583 if (mode == OUTPUT_IEXEC) {
2585 sysdie("pipe create failed");
2587 /* setup a fake pipe to /dev/null for non-interactive mode */
2589 io[READ_SIDE] = open(DEV_NULL, 0);
2590 if (io[READ_SIDE] < 0)
2591 sysdie("unable to open %s for reading", DEV_NULL);
2592 io[WRITE_SIDE] = open(DEV_NULL, 1);
2593 if (io[WRITE_SIDE] < 0)
2594 sysdie("unable to open %s for writing", DEV_NULL);
2597 /* pause if in non-interactive mode so as to not busy-out the server */
2598 if (mode == OUTPUT_EXEC && z_flag > 0) {
2600 warn("sleeping %d seconds before fork/exec", z_flag);
2601 /* be sure they know what we are stalling */
2606 /* fork the child process */
2611 sysdie("fork failed");
2613 /* case: child process */
2617 * prep file descriptors
2620 close(io[READ_SIDE]);
2621 if (dup2(io[WRITE_SIDE], 1) < 0)
2622 sysdie("child: dup of write I/O pipe to stdout failed");
2623 if (dup2(io[WRITE_SIDE], 2) < 0)
2624 sysdie("child: dup of write I/O pipe to stderr failed");
2626 /* exec the ctlinnd command */
2627 p = concatpath(innconf->pathbin, _PATH_CTLINND);
2630 CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, (char *) 0);
2631 } else if (who == NULL) {
2633 CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, type, (char *) 0);
2636 CTLINND_NAME, CTLINND_TIME_OUT, cmd, grp, type, who, (char *) 0);
2639 /* child exec failed */
2640 sysdie("child process exec failed");
2642 /* case: parent process */
2645 /* prep file descriptors */
2646 if (mode != OUTPUT_IEXEC) {
2647 close(io[READ_SIDE]);
2649 close(io[WRITE_SIDE]);
2651 /* print a line from the child, if interactive */
2652 if (mode == OUTPUT_IEXEC) {
2654 /* read what the child says */
2657 ch_stream = fdopen(io[READ_SIDE], "r");
2658 if (ch_stream == NULL)
2659 sysdie("fdopen of pipe failed");
2660 p = fgets(buf, BUFSIZ, ch_stream);
2662 /* print what the child said, if anything */
2664 if (buf[strlen(buf)-1] == '\n')
2665 buf[strlen(buf)-1] = '\0';
2670 /* look for abnormal child termination/status */
2672 while (wait(&status) < 0) {
2673 if (errno == EINTR) {
2674 /* just an interrupt, try to wait again */
2677 sysdie("wait returned -1");
2680 if (mode == OUTPUT_IEXEC) {
2681 /* close the pipe now that we are done with reading it */
2684 if (WIFSTOPPED(status)) {
2685 warn(" %s %s %s%s%s%s%s stopped",
2686 CTLINND_NAME, cmd, grp,
2687 (type ? "" : " "), (type ? type : ""),
2688 (who ? "" : " "), (who ? who : ""));
2689 /* assume no work was done */
2692 if (WIFSIGNALED(status)) {
2693 warn(" %s %s %s%s%s%s%s killed by signal %d",
2694 CTLINND_NAME, cmd, grp,
2695 (type ? "" : " "), (type ? type : ""),
2696 (who ? "" : " "), (who ? who : ""), WTERMSIG(status));
2697 /* assume no work was done */
2700 if (!WIFEXITED(status)) {
2701 warn(" %s %s %s%s%s%s%s returned unknown wait status: 0x%x",
2702 CTLINND_NAME, cmd, grp,
2703 (type ? "" : " "), (type ? type : ""),
2704 (who ? "" : " "), (who ? who : ""), status);
2705 /* assume no work was done */
2708 exitval = WEXITSTATUS(status);
2710 warn(" %s %s %s%s%s%s%s exited with status: %d",
2711 CTLINND_NAME, cmd, grp,
2712 (type ? "" : " "), (type ? type : ""),
2713 (who ? "" : " "), (who ? who : ""), exitval);
2714 /* assume no work was done */
2724 * new_top_hier - determine if the newsgroup represents a new hierarchy
2726 * Determine of the newsgroup name is a new hierarchy.
2729 * name name of newsgroup to check
2732 * false hierarchy already exists
2733 * true hierarchy does not exist, name represents a new hierarchy
2735 * NOTE: This function assumes that we are at the top of the news spool.
2741 struct stat statbuf; /* stat of the hierarchy */
2742 int result; /* return result */
2746 * temp change name to just the top level
2748 dot = strchr(name, '.');
2754 * determine if we can find this top level hierarchy directory
2756 result = !(stat(name, &statbuf) >= 0 && S_ISDIR(statbuf.st_mode));