main.c revision 1.5
1/* $NetBSD: main.c,v 1.5 2020/01/12 21:31:03 christos Exp $ */ 2 3#ifdef HAVE_NBTOOL_CONFIG_H 4#include "nbtool_config.h" 5#else 6#if HAVE_CONFIG_H 7#include "config.h" 8#endif 9#include <nbcompat.h> 10#if HAVE_SYS_CDEFS_H 11#include <sys/cdefs.h> 12#endif 13#endif 14__RCSID("$NetBSD: main.c,v 1.5 2020/01/12 21:31:03 christos Exp $"); 15 16/*- 17 * Copyright (c) 1999-2019 The NetBSD Foundation, Inc. 18 * All rights reserved. 19 * 20 * This code is derived from software contributed to The NetBSD Foundation 21 * by Hubert Feyrer <hubert@feyrer.de> and 22 * by Joerg Sonnenberger <joerg@NetBSD.org>. 23 * 24 * Redistribution and use in source and binary forms, with or without 25 * modification, are permitted provided that the following conditions 26 * are met: 27 * 1. Redistributions of source code must retain the above copyright 28 * notice, this list of conditions and the following disclaimer. 29 * 2. Redistributions in binary form must reproduce the above copyright 30 * notice, this list of conditions and the following disclaimer in the 31 * documentation and/or other materials provided with the distribution. 32 * 33 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS 34 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 35 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 36 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS 37 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 38 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 39 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 40 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 41 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 42 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 43 * POSSIBILITY OF SUCH DAMAGE. 44 */ 45 46#if HAVE_SYS_TYPES_H 47#include <sys/types.h> 48#endif 49#if HAVE_SYS_STAT_H 50#include <sys/stat.h> 51#endif 52#if HAVE_DIRENT_H 53#include <dirent.h> 54#endif 55#if HAVE_ERR_H 56#include <err.h> 57#endif 58#if HAVE_ERRNO_H 59#include <errno.h> 60#endif 61#if HAVE_FCNTL_H 62#include <fcntl.h> 63#endif 64#ifndef NETBSD 65#include <nbcompat/md5.h> 66#include <nbcompat/sha2.h> 67#else 68#include <md5.h> 69#include <sha2.h> 70#endif 71#if HAVE_LIMITS_H 72#include <limits.h> 73#endif 74#if HAVE_STDIO_H 75#include <stdio.h> 76#endif 77#if HAVE_STRING_H 78#include <string.h> 79#endif 80 81#ifndef BOOTSTRAP 82#include <archive.h> 83#include <fetch.h> 84#endif 85 86#include "admin.h" 87#include "lib.h" 88 89#define DEFAULT_SFX ".t[bg]z" /* default suffix for ls{all,best} */ 90 91struct pkgdb_count { 92 size_t files; 93 size_t directories; 94 size_t packages; 95}; 96 97static const char Options[] = "C:K:SVbd:qs:v"; 98 99int quiet, verbose; 100 101static void set_unset_variable(char **, Boolean); 102static void digest_input(char **); 103 104/* print usage message and exit */ 105void 106usage(void) 107{ 108 (void) fprintf(stderr, "usage: %s [-bqSVv] [-C config] [-d lsdir] [-K pkg_dbdir] [-s sfx] command [args ...]\n" 109 "Where 'commands' and 'args' are:\n" 110 " rebuild - rebuild pkgdb from +CONTENTS files\n" 111 " rebuild-tree - rebuild +REQUIRED_BY files from forward deps\n" 112 " check [pkg ...] - check md5 checksum of installed files\n" 113 " add pkg ... - add pkg files to database\n" 114 " set variable=value pkg ... - set installation variable for package\n" 115 " unset variable pkg ... - unset installation variable for package\n" 116 " lsall /path/to/pkgpattern - list all pkgs matching the pattern\n" 117 " lsbest /path/to/pkgpattern - list pkgs matching the pattern best\n" 118 " dump - dump database\n" 119 " pmatch pattern pkg - returns true if pkg matches pattern, otherwise false\n" 120 " fetch-pkg-vulnerabilities [-s] - fetch new vulnerability file\n" 121 " check-pkg-vulnerabilities [-s] <file> - check syntax and checksums of the vulnerability file\n" 122 " audit [-eis] [-t type] ... - check installed packages for vulnerabilities\n" 123 " audit-pkg [-eis] [-t type] ... - check listed packages for vulnerabilities\n" 124 " audit-batch [-eis] [-t type] ... - check packages in listed files for vulnerabilities\n" 125 " audit-history [-t type] ... - print all advisories for package names\n" 126 " check-license <condition> - check if condition is acceptable\n" 127 " check-single-license <license> - check if license is acceptable\n" 128 " config-var name - print current value of the configuration variable\n" 129 " check-signature ... - verify the signature of packages\n" 130 " x509-sign-package pkg spkg key cert - create X509 signature\n" 131 " gpg-sign-package pkg spkg - create GPG signature\n", 132 getprogname()); 133 exit(EXIT_FAILURE); 134} 135 136/* 137 * add1pkg(<pkg>) 138 * adds the files listed in the +CONTENTS of <pkg> into the 139 * pkgdb.byfile.db database file in the current package dbdir. It 140 * returns the number of files added to the database file. 141 */ 142static int 143add_pkg(const char *pkgdir, void *vp) 144{ 145 FILE *f; 146 plist_t *p; 147 package_t Plist; 148 char *contents; 149 char *PkgName, *dirp; 150 char file[MaxPathSize]; 151 struct pkgdb_count *count; 152 153 if (!pkgdb_open(ReadWrite)) 154 err(EXIT_FAILURE, "cannot open pkgdb"); 155 156 count = vp; 157 ++count->packages; 158 159 contents = pkgdb_pkg_file(pkgdir, CONTENTS_FNAME); 160 if ((f = fopen(contents, "r")) == NULL) 161 errx(EXIT_FAILURE, "%s: can't open `%s'", pkgdir, CONTENTS_FNAME); 162 free(contents); 163 164 read_plist(&Plist, f); 165 if ((p = find_plist(&Plist, PLIST_NAME)) == NULL) { 166 errx(EXIT_FAILURE, "Package `%s' has no @name, aborting.", pkgdir); 167 } 168 169 PkgName = p->name; 170 dirp = NULL; 171 for (p = Plist.head; p; p = p->next) { 172 switch(p->type) { 173 case PLIST_FILE: 174 if (dirp == NULL) { 175 errx(EXIT_FAILURE, "@cwd not yet found, please send-pr!"); 176 } 177 (void) snprintf(file, sizeof(file), "%s/%s", dirp, p->name); 178 if (!(isfile(file) || islinktodir(file))) { 179 if (isbrokenlink(file)) { 180 warnx("%s: Symlink `%s' exists and is in %s but target does not exist!", 181 PkgName, file, CONTENTS_FNAME); 182 } else { 183 warnx("%s: File `%s' is in %s but not on filesystem!", 184 PkgName, file, CONTENTS_FNAME); 185 } 186 } else { 187 pkgdb_store(file, PkgName); 188 ++count->files; 189 } 190 break; 191 case PLIST_PKGDIR: 192 add_pkgdir(PkgName, dirp, p->name); 193 ++count->directories; 194 break; 195 case PLIST_CWD: 196 if (strcmp(p->name, ".") != 0) 197 dirp = p->name; 198 else 199 dirp = pkgdb_pkg_dir(pkgdir); 200 break; 201 case PLIST_IGNORE: 202 p = p->next; 203 break; 204 case PLIST_SHOW_ALL: 205 case PLIST_SRC: 206 case PLIST_CMD: 207 case PLIST_CHMOD: 208 case PLIST_CHOWN: 209 case PLIST_CHGRP: 210 case PLIST_COMMENT: 211 case PLIST_NAME: 212 case PLIST_UNEXEC: 213 case PLIST_DISPLAY: 214 case PLIST_PKGDEP: 215 case PLIST_DIR_RM: 216 case PLIST_OPTION: 217 case PLIST_PKGCFL: 218 case PLIST_BLDDEP: 219 break; 220 } 221 } 222 free_plist(&Plist); 223 fclose(f); 224 pkgdb_close(); 225 226 return 0; 227} 228 229static void 230rebuild(void) 231{ 232 char *cachename; 233 struct pkgdb_count count; 234 235 count.files = 0; 236 count.directories = 0; 237 count.packages = 0; 238 239 cachename = pkgdb_get_database(); 240 if (unlink(cachename) != 0 && errno != ENOENT) 241 err(EXIT_FAILURE, "unlink %s", cachename); 242 243 setbuf(stdout, NULL); 244 245 iterate_pkg_db(add_pkg, &count); 246 247 printf("\n"); 248 printf("Stored %" PRIzu " file%s and %" PRIzu " explicit director%s" 249 " from %"PRIzu " package%s in %s.\n", 250 count.files, count.files == 1 ? "" : "s", 251 count.directories, count.directories == 1 ? "y" : "ies", 252 count.packages, count.packages == 1 ? "" : "s", 253 cachename); 254} 255 256static int 257lspattern(const char *pkg, void *vp) 258{ 259 const char *dir = vp; 260 printf("%s/%s\n", dir, pkg); 261 return 0; 262} 263 264static int 265lsbasepattern(const char *pkg, void *vp) 266{ 267 puts(pkg); 268 return 0; 269} 270 271static int 272remove_required_by(const char *pkgname, void *cookie) 273{ 274 char *path; 275 276 path = pkgdb_pkg_file(pkgname, REQUIRED_BY_FNAME); 277 278 if (unlink(path) == -1 && errno != ENOENT) 279 err(EXIT_FAILURE, "Cannot remove %s", path); 280 281 free(path); 282 283 return 0; 284} 285 286static void 287add_required_by(const char *pattern, const char *required_by) 288{ 289 char *best_installed, *path; 290 int fd; 291 size_t len; 292 293 best_installed = find_best_matching_installed_pkg(pattern); 294 if (best_installed == NULL) { 295 warnx("Dependency %s of %s unresolved", pattern, required_by); 296 return; 297 } 298 299 path = pkgdb_pkg_file(best_installed, REQUIRED_BY_FNAME); 300 free(best_installed); 301 302 if ((fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0644)) == -1) 303 errx(EXIT_FAILURE, "Cannot write to %s", path); 304 free(path); 305 306 len = strlen(required_by); 307 if (write(fd, required_by, len) != (ssize_t)len || 308 write(fd, "\n", 1) != 1 || 309 close(fd) == -1) 310 errx(EXIT_FAILURE, "Cannot write to %s", path); 311} 312 313 314static int 315add_depends_of(const char *pkgname, void *cookie) 316{ 317 FILE *fp; 318 plist_t *p; 319 package_t plist; 320 char *path; 321 322 path = pkgdb_pkg_file(pkgname, CONTENTS_FNAME); 323 if ((fp = fopen(path, "r")) == NULL) 324 errx(EXIT_FAILURE, "Cannot read %s of package %s", 325 CONTENTS_FNAME, pkgname); 326 free(path); 327 read_plist(&plist, fp); 328 fclose(fp); 329 330 for (p = plist.head; p; p = p->next) { 331 if (p->type == PLIST_PKGDEP) 332 add_required_by(p->name, pkgname); 333 } 334 335 free_plist(&plist); 336 337 return 0; 338} 339 340static void 341rebuild_tree(void) 342{ 343 if (iterate_pkg_db(remove_required_by, NULL) == -1) 344 errx(EXIT_FAILURE, "cannot iterate pkgdb"); 345 if (iterate_pkg_db(add_depends_of, NULL) == -1) 346 errx(EXIT_FAILURE, "cannot iterate pkgdb"); 347} 348 349int 350main(int argc, char *argv[]) 351{ 352 Boolean use_default_sfx = TRUE; 353 Boolean show_basename_only = FALSE; 354 char lsdir[MaxPathSize]; 355 char sfx[MaxPathSize]; 356 char *lsdirp = NULL; 357 int ch; 358 359 setprogname(argv[0]); 360 361 if (argc < 2) 362 usage(); 363 364 while ((ch = getopt(argc, argv, Options)) != -1) 365 switch (ch) { 366 case 'C': 367 config_file = optarg; 368 break; 369 370 case 'K': 371 pkgdb_set_dir(optarg, 3); 372 break; 373 374 case 'S': 375 sfx[0] = 0x0; 376 use_default_sfx = FALSE; 377 break; 378 379 case 'V': 380 show_version(); 381 /* NOTREACHED */ 382 383 case 'b': 384 show_basename_only = TRUE; 385 break; 386 387 case 'd': 388 (void) strlcpy(lsdir, optarg, sizeof(lsdir)); 389 lsdirp = lsdir; 390 break; 391 392 case 'q': 393 quiet = 1; 394 break; 395 396 case 's': 397 (void) strlcpy(sfx, optarg, sizeof(sfx)); 398 use_default_sfx = FALSE; 399 break; 400 401 case 'v': 402 ++verbose; 403 break; 404 405 default: 406 usage(); 407 /* NOTREACHED */ 408 } 409 410 argc -= optind; 411 argv += optind; 412 413 if (argc <= 0) { 414 usage(); 415 } 416 417 /* 418 * config-var is reading the config file implicitly, 419 * so skip it here. 420 */ 421 if (strcasecmp(argv[0], "config-var") != 0) 422 pkg_install_config(); 423 424 if (use_default_sfx) 425 (void) strlcpy(sfx, DEFAULT_SFX, sizeof(sfx)); 426 427 if (strcasecmp(argv[0], "pmatch") == 0) { 428 429 char *pattern, *pkg; 430 431 argv++; /* "pmatch" */ 432 433 if (argv[0] == NULL || argv[1] == NULL) { 434 usage(); 435 } 436 437 pattern = argv[0]; 438 pkg = argv[1]; 439 440 if (pkg_match(pattern, pkg)){ 441 return 0; 442 } else { 443 return 1; 444 } 445 446 } else if (strcasecmp(argv[0], "rebuild") == 0) { 447 448 rebuild(); 449 printf("Done.\n"); 450 451 452 } else if (strcasecmp(argv[0], "rebuild-tree") == 0) { 453 454 rebuild_tree(); 455 printf("Done.\n"); 456 457 } else if (strcasecmp(argv[0], "check") == 0) { 458 argv++; /* "check" */ 459 460 check(argv); 461 462 if (!quiet) { 463 printf("Done.\n"); 464 } 465 466 } else if (strcasecmp(argv[0], "lsall") == 0) { 467 argv++; /* "lsall" */ 468 469 while (*argv != NULL) { 470 /* args specified */ 471 int rc; 472 const char *basep, *dir; 473 474 dir = lsdirp ? lsdirp : dirname_of(*argv); 475 basep = basename_of(*argv); 476 477 if (show_basename_only) 478 rc = match_local_files(dir, use_default_sfx, 1, basep, lsbasepattern, NULL); 479 else 480 rc = match_local_files(dir, use_default_sfx, 1, basep, lspattern, __UNCONST(dir)); 481 if (rc == -1) 482 errx(EXIT_FAILURE, "Error from match_local_files(\"%s\", \"%s\", ...)", 483 dir, basep); 484 485 argv++; 486 } 487 488 } else if (strcasecmp(argv[0], "lsbest") == 0) { 489 argv++; /* "lsbest" */ 490 491 while (*argv != NULL) { 492 /* args specified */ 493 const char *basep, *dir; 494 char *p; 495 496 dir = lsdirp ? lsdirp : dirname_of(*argv); 497 basep = basename_of(*argv); 498 499 p = find_best_matching_file(dir, basep, use_default_sfx, 1); 500 501 if (p) { 502 if (show_basename_only) 503 printf("%s\n", p); 504 else 505 printf("%s/%s\n", dir, p); 506 free(p); 507 } 508 509 argv++; 510 } 511 } else if (strcasecmp(argv[0], "list") == 0 || 512 strcasecmp(argv[0], "dump") == 0) { 513 514 pkgdb_dump(); 515 516 } else if (strcasecmp(argv[0], "add") == 0) { 517 struct pkgdb_count count; 518 519 count.files = 0; 520 count.directories = 0; 521 count.packages = 0; 522 523 for (++argv; *argv != NULL; ++argv) 524 add_pkg(*argv, &count); 525 } else if (strcasecmp(argv[0], "set") == 0) { 526 argv++; /* "set" */ 527 set_unset_variable(argv, FALSE); 528 } else if (strcasecmp(argv[0], "unset") == 0) { 529 argv++; /* "unset" */ 530 set_unset_variable(argv, TRUE); 531 } else if (strcasecmp(argv[0], "digest") == 0) { 532 argv++; /* "digest" */ 533 digest_input(argv); 534 } else if (strcasecmp(argv[0], "config-var") == 0) { 535 argv++; 536 if (argv == NULL || argv[1] != NULL) 537 errx(EXIT_FAILURE, "config-var takes exactly one argument"); 538 pkg_install_show_variable(argv[0]); 539 } else if (strcasecmp(argv[0], "check-license") == 0) { 540 if (argv[1] == NULL) 541 errx(EXIT_FAILURE, "check-license takes exactly one argument"); 542 543 load_license_lists(); 544 545 switch (acceptable_pkg_license(argv[1])) { 546 case 0: 547 puts("no"); 548 return 0; 549 case 1: 550 puts("yes"); 551 return 0; 552 case -1: 553 errx(EXIT_FAILURE, "invalid license condition"); 554 } 555 } else if (strcasecmp(argv[0], "check-single-license") == 0) { 556 if (argv[1] == NULL) 557 errx(EXIT_FAILURE, "check-license takes exactly one argument"); 558 load_license_lists(); 559 560 switch (acceptable_license(argv[1])) { 561 case 0: 562 puts("no"); 563 return 0; 564 case 1: 565 puts("yes"); 566 return 0; 567 case -1: 568 errx(EXIT_FAILURE, "invalid license"); 569 } 570 } 571#ifndef BOOTSTRAP 572 else if (strcasecmp(argv[0], "findbest") == 0) { 573 struct url *url; 574 char *output; 575 int rc; 576 577 process_pkg_path(); 578 579 rc = 0; 580 for (++argv; *argv != NULL; ++argv) { 581 url = find_best_package(NULL, *argv, 1); 582 if (url == NULL) { 583 rc = 1; 584 continue; 585 } 586 output = fetchStringifyURL(url); 587 puts(output); 588 fetchFreeURL(url); 589 free(output); 590 } 591 592 return rc; 593 } else if (strcasecmp(argv[0], "fetch-pkg-vulnerabilities") == 0) { 594 fetch_pkg_vulnerabilities(--argc, ++argv); 595 } else if (strcasecmp(argv[0], "check-pkg-vulnerabilities") == 0) { 596 check_pkg_vulnerabilities(--argc, ++argv); 597 } else if (strcasecmp(argv[0], "audit") == 0) { 598 audit_pkgdb(--argc, ++argv); 599 } else if (strcasecmp(argv[0], "audit-pkg") == 0) { 600 audit_pkg(--argc, ++argv); 601 } else if (strcasecmp(argv[0], "audit-batch") == 0) { 602 audit_batch(--argc, ++argv); 603 } else if (strcasecmp(argv[0], "audit-history") == 0) { 604 audit_history(--argc, ++argv); 605 } else if (strcasecmp(argv[0], "check-signature") == 0) { 606 struct archive *pkg; 607 int rc; 608 609 rc = 0; 610 for (--argc, ++argv; argc > 0; --argc, ++argv) { 611 char *archive_name; 612 613 pkg = open_archive(*argv, &archive_name); 614 if (pkg == NULL) { 615 warnx("%s could not be opened", *argv); 616 continue; 617 } 618 if (pkg_full_signature_check(archive_name, &pkg)) 619 rc = 1; 620 free(archive_name); 621 if (pkg != NULL) 622 archive_read_free(pkg); 623 } 624 return rc; 625 } else if (strcasecmp(argv[0], "x509-sign-package") == 0) { 626#ifdef HAVE_SSL 627 --argc; 628 ++argv; 629 if (argc != 4) 630 errx(EXIT_FAILURE, "x509-sign-package takes exactly four arguments"); 631 pkg_sign_x509(argv[0], argv[1], argv[2], argv[3]); 632#else 633 errx(EXIT_FAILURE, "OpenSSL support is not included"); 634#endif 635 } else if (strcasecmp(argv[0], "gpg-sign-package") == 0) { 636 --argc; 637 ++argv; 638 if (argc != 2) 639 errx(EXIT_FAILURE, "gpg-sign-package takes exactly two arguments"); 640 pkg_sign_gpg(argv[0], argv[1]); 641 } 642#endif 643 else { 644 usage(); 645 } 646 647 return 0; 648} 649 650struct set_installed_info_arg { 651 char *variable; 652 char *value; 653 int got_match; 654}; 655 656static int 657set_installed_info_var(const char *name, void *cookie) 658{ 659 struct set_installed_info_arg *arg = cookie; 660 char *filename; 661 int retval; 662 663 filename = pkgdb_pkg_file(name, INSTALLED_INFO_FNAME); 664 665 retval = var_set(filename, arg->variable, arg->value); 666 667 free(filename); 668 arg->got_match = 1; 669 670 return retval; 671} 672 673static void 674set_unset_variable(char **argv, Boolean unset) 675{ 676 struct set_installed_info_arg arg; 677 char *eq; 678 char *variable; 679 int ret = 0; 680 681 if (argv[0] == NULL || argv[1] == NULL) 682 usage(); 683 684 variable = NULL; 685 686 if (unset) { 687 arg.variable = argv[0]; 688 arg.value = NULL; 689 } else { 690 eq = NULL; 691 if ((eq=strchr(argv[0], '=')) == NULL) 692 usage(); 693 694 variable = xmalloc(eq-argv[0]+1); 695 strlcpy(variable, argv[0], eq-argv[0]+1); 696 697 arg.variable = variable; 698 arg.value = eq+1; 699 700 if (strcmp(variable, AUTOMATIC_VARNAME) == 0 && 701 strcasecmp(arg.value, "yes") != 0 && 702 strcasecmp(arg.value, "no") != 0) { 703 errx(EXIT_FAILURE, 704 "unknown value `%s' for " AUTOMATIC_VARNAME, 705 arg.value); 706 } 707 } 708 if (strpbrk(arg.variable, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") != NULL) { 709 free(variable); 710 errx(EXIT_FAILURE, 711 "variable name must not contain uppercase letters"); 712 } 713 714 argv++; 715 while (*argv != NULL) { 716 arg.got_match = 0; 717 if (match_installed_pkgs(*argv, set_installed_info_var, &arg) == -1) 718 errx(EXIT_FAILURE, "Cannot process pkdbdb"); 719 if (arg.got_match == 0) { 720 char *pattern; 721 722 if (ispkgpattern(*argv)) { 723 warnx("no matching pkg for `%s'", *argv); 724 ret++; 725 } else { 726 pattern = xasprintf("%s-[0-9]*", *argv); 727 728 if (match_installed_pkgs(pattern, set_installed_info_var, &arg) == -1) 729 errx(EXIT_FAILURE, "Cannot process pkdbdb"); 730 731 if (arg.got_match == 0) { 732 warnx("cannot find package %s", *argv); 733 ++ret; 734 } 735 free(pattern); 736 } 737 } 738 739 argv++; 740 } 741 742 if (ret > 0) 743 exit(EXIT_FAILURE); 744 745 free(variable); 746 747 return; 748} 749 750static void 751digest_input(char **argv) 752{ 753 char digest[SHA256_DIGEST_STRING_LENGTH]; 754 int failures = 0; 755 756 while (*argv != NULL) { 757 if (SHA256_File(*argv, digest)) { 758 puts(digest); 759 } else { 760 warn("cannot process %s", *argv); 761 ++failures; 762 } 763 argv++; 764 } 765 if (failures) 766 exit(EXIT_FAILURE); 767} 768