[BACK]Return to catman.c CVS log [TXT][DIR] Up to [cvsweb.bsd.lv] / mandoc

File: [cvsweb.bsd.lv] / mandoc / catman.c (download)

Revision 1.29, Mon Jun 30 12:23:42 2025 UTC (4 days, 12 hours ago) by schwarze
Branch: MAIN
CVS Tags: HEAD
Changes since 1.28: +4 -2 lines

Minor improvement to error handling:
Once recv(2) suffered a hard failure, there is no need to try again.

/* $Id: catman.c,v 1.29 2025/06/30 12:23:42 schwarze Exp $ */
/*
 * Copyright (c) 2017, 2025 Ingo Schwarze <schwarze@openbsd.org>
 * Copyright (c) 2017 Michael Stapelberg <stapelberg@debian.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include "config.h"

#if NEED_XPG4_2
#define _XPG4_2
#endif

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>

#include <assert.h>
#if HAVE_ERR
#include <err.h>
#endif
#include <errno.h>
#include <fcntl.h>
#if HAVE_FTS
#include <fts.h>
#else
#include "compat_fts.h"
#endif
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

int		verbose_flag = 0;
sig_atomic_t	got_signal = 0;

int	 process_manpage(int, int, const char *);
int	 process_tree(int, int);
void	 run_mandocd(int, const char *, const char *)
		__attribute__((__noreturn__));
void	 signal_handler(int);
ssize_t	 sock_fd_write(int, int, int, int);
void	 usage(void) __attribute__((__noreturn__));


void
signal_handler(int signum)
{
	got_signal = signum;
}

void
run_mandocd(int sockfd, const char *outtype, const char* defos)
{
	char	 sockfdstr[10];
	int	 len;

	len = snprintf(sockfdstr, sizeof(sockfdstr), "%d", sockfd);
	if (len >= (int)sizeof(sockfdstr)) {
		errno = EOVERFLOW;
		len = -1;
	}
	if (len < 0)
		err(1, "snprintf");
	if (defos == NULL)
		execlp("mandocd", "mandocd", "-T", outtype,
		    sockfdstr, (char *)NULL);
	else
		execlp("mandocd", "mandocd", "-T", outtype,
		    "-I", defos, sockfdstr, (char *)NULL);
	err(1, "exec(mandocd)");
}

ssize_t
sock_fd_write(int fd, int fd0, int fd1, int fd2)
{
	const struct timespec timeout = { 0, 10000000 };  /* 0.01 s */
	struct msghdr	 msg;
	struct iovec	 iov;
	union {
		struct cmsghdr	 cmsghdr;
		char		 control[CMSG_SPACE(3 * sizeof(int))];
	} cmsgu;
	struct cmsghdr	*cmsg;
	int		*walk;
	ssize_t		 sz;
	unsigned char	 dummy[1] = {'\0'};

	iov.iov_base = dummy;
	iov.iov_len = sizeof(dummy);

	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	msg.msg_control = cmsgu.control;
	msg.msg_controllen = sizeof(cmsgu.control);

	cmsg = CMSG_FIRSTHDR(&msg);
	cmsg->cmsg_len = CMSG_LEN(3 * sizeof(int));
	cmsg->cmsg_level = SOL_SOCKET;
	cmsg->cmsg_type = SCM_RIGHTS;

	walk = (int *)CMSG_DATA(cmsg);
	*(walk++) = fd0;
	*(walk++) = fd1;
	*(walk++) = fd2;

	/*
	 * It appears that on some systems, sendmsg(3)
	 * may return EAGAIN even in blocking mode.
	 * Seen for example on Oracle Solaris 11.2.
	 * The sleeping time was chosen by experimentation,
	 * to neither cause more than a handful of retries
	 * in normal operation nor unnecessary delays.
	 */
	while ((sz = sendmsg(fd, &msg, 0)) == -1) {
		if (errno != EAGAIN) {
			warn("FATAL: sendmsg");
			break;
		}
		nanosleep(&timeout, NULL);
	}
	return sz;
}

int
process_manpage(int srv_fd, int dstdir_fd, const char *path)
{
	int	 in_fd, out_fd;
	int	 irc;

	if ((in_fd = open(path, O_RDONLY)) == -1) {
		warn("open %s for reading", path);
		fflush(stderr);
		return 0;
	}

	if ((out_fd = openat(dstdir_fd, path,
	    O_WRONLY | O_NOFOLLOW | O_CREAT | O_TRUNC,
	    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
		warn("openat %s for writing", path);
		fflush(stderr);
		close(in_fd);
		return 0;
	}

	irc = sock_fd_write(srv_fd, in_fd, out_fd, STDERR_FILENO);

	close(in_fd);
	close(out_fd);

	return irc;
}

int
process_tree(int srv_fd, int dstdir_fd)
{
	const struct timespec timeout = { 0, 10000000 };  /* 0.01 s */
	const int	 max_inflight = 16;

	FTS		*ftsp;
	FTSENT		*entry;
	const char	*argv[2];
	const char	*path;
	int		 inflight, irc, decr, fatal;
	int		 gooddirs, baddirs, goodfiles, badfiles;
	char		 dummy[1];

	argv[0] = ".";
	argv[1] = (char *)NULL;

	if ((ftsp = fts_open((char * const *)argv,
	    FTS_PHYSICAL | FTS_NOCHDIR, NULL)) == NULL) {
		warn("fts_open");
		return -1;
	}

	if (verbose_flag >= 2) {
		warnx("allowing up to %d files in flight", max_inflight);
		fflush(stderr);
	}
	inflight = fatal = gooddirs = baddirs = goodfiles = badfiles = 0;
	while (fatal == 0 && got_signal == 0 &&
	    (entry = fts_read(ftsp)) != NULL) {
		if (inflight >= max_inflight) {
			while (recv(srv_fd, dummy, sizeof(dummy), 0) == -1) {
				if (errno != EAGAIN) {
					warn("FATAL: recv");
					fatal = errno;
					break;
				}
				nanosleep(&timeout, NULL);
			}
			if (fatal != 0)
				break;
			decr = 1;
			while ((irc = recv(srv_fd, dummy, sizeof(dummy),
			    MSG_DONTWAIT)) > 0)
				decr++;
			assert(inflight >= decr);
			if (verbose_flag >= 2 && decr > 1) {
				warnx("files in flight: %d - %d = %d",
				    inflight, decr, inflight - decr);
				fflush(stderr);
			}
			inflight -= decr;
			if (irc == 0) {
				errno = ECONNRESET;
				inflight = -1;
			}
			if (errno != EAGAIN) {
				warn("FATAL: recv");
				fatal = errno;
				break;
			}
		}
		path = entry->fts_path + 2;
		switch (entry->fts_info) {
		case FTS_F:
			switch (process_manpage(srv_fd, dstdir_fd, path)) {
			case -1:
				fatal = errno;
				break;
			case 0:
				badfiles++;
				break;
			default:
				goodfiles++;
				inflight++;
				break;
			}
			break;
		case FTS_D:
			if (*path != '\0' &&
			    mkdirat(dstdir_fd, path, S_IRWXU | S_IRGRP |
			      S_IXGRP | S_IROTH | S_IXOTH) == -1 &&
			    errno != EEXIST) {
				warn("mkdirat %s", path);
				fflush(stderr);
				(void)fts_set(ftsp, entry, FTS_SKIP);
				baddirs++;
			} else
				gooddirs++;
			break;
		case FTS_DP:
			break;
		case FTS_DNR:
			warnx("directory %s unreadable: %s",
			    path, strerror(entry->fts_errno));
			fflush(stderr);
			baddirs++;
			break;
		case FTS_DC:
			warnx("directory %s causes cycle", path);
			fflush(stderr);
			baddirs++;
			break;
		case FTS_ERR:
		case FTS_NS:
			warnx("file %s: %s",
			    path, strerror(entry->fts_errno));
			fflush(stderr);
			badfiles++;
			break;
		default:
			warnx("file %s: not a regular file", path);
			fflush(stderr);
			badfiles++;
			break;
		}
	}
	if (got_signal != 0) {
		switch (got_signal) {
		case SIGCHLD:
			warnx("FATAL: mandocd child died: got SIGCHLD");
			break;
		case SIGPIPE:
			warnx("FATAL: mandocd child died: got SIGPIPE");
			break;
		default:
			warnx("FATAL: signal SIG%s", sys_signame[got_signal]);
			break;
		}
		inflight = -1;
		fatal = 1;
	} else if (fatal == 0 && (fatal = errno) != 0)
		warn("FATAL: fts_read");

	fts_close(ftsp);
	if (verbose_flag >= 2 && inflight > 0) {
		warnx("waiting for %d files in flight", inflight);
		fflush(stderr);
	}
	while (inflight > 0) {
		irc = recv(srv_fd, dummy, sizeof(dummy), 0);
		if (irc > 0)
			inflight--;
		else if (irc == -1 && errno == EAGAIN)
			nanosleep(&timeout, NULL);
		else {
			if (irc == 0)
				errno = ECONNRESET;
			warn("recv");
			inflight = -1;
		}
	}
	if (verbose_flag)
		warnx("processed %d files in %d directories",
		    goodfiles, gooddirs);
	if (baddirs > 0)
		warnx("skipped %d %s due to errors", baddirs,
		    baddirs == 1 ? "directory" : "directories");
	if (badfiles > 0)
		warnx("skipped %d %s due to errors", badfiles,
		    badfiles == 1 ? "file" : "files");
	if (fatal != 0) {
		warnx("processing aborted due to fatal error, "
		    "results are probably incomplete");
		inflight = -1;
	}
	return inflight;
}

int
main(int argc, char **argv)
{
	struct sigaction sa;
	const char	*defos, *outtype;
	int		 srv_fds[2];
	int		 dstdir_fd;
	int		 opt;
	pid_t		 pid;

	defos = NULL;
	outtype = "ascii";
	while ((opt = getopt(argc, argv, "I:T:v")) != -1) {
		switch (opt) {
		case 'I':
			defos = optarg;
			break;
		case 'T':
			outtype = optarg;
			break;
		case 'v':
			verbose_flag += 1;
			break;
		default:
			usage();
		}
	}

	if (argc > 0) {
		argc -= optind;
		argv += optind;
	}
	if (argc != 2) {
		switch (argc) {
		case 0:
			warnx("missing arguments: srcdir and dstdir");
			break;
		case 1:
			warnx("missing argument: dstdir");
			break;
		default:
			warnx("too many arguments: %s", argv[2]);
			break;
		}
		usage();
	}

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = &signal_handler;
	sa.sa_flags = SA_NOCLDWAIT;
	if (sigfillset(&sa.sa_mask) == -1)
		err(1, "sigfillset");
	if (sigaction(SIGHUP, &sa, NULL) == -1)
		err(1, "sigaction(SIGHUP)");
	if (sigaction(SIGINT, &sa, NULL) == -1)
		err(1, "sigaction(SIGINT)");
	if (sigaction(SIGPIPE, &sa, NULL) == -1)
		err(1, "sigaction(SIGPIPE)");
	if (sigaction(SIGTERM, &sa, NULL) == -1)
		err(1, "sigaction(SIGTERM)");
	if (sigaction(SIGCHLD, &sa, NULL) == -1)
		err(1, "sigaction(SIGCHLD)");

	if (socketpair(AF_LOCAL, SOCK_STREAM, AF_UNSPEC, srv_fds) == -1)
		err(1, "socketpair");

	pid = fork();
	switch (pid) {
	case -1:
		err(1, "fork");
	case 0:
		close(srv_fds[0]);
		run_mandocd(srv_fds[1], outtype, defos);
	default:
		break;
	}
	close(srv_fds[1]);

	if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1)
		err(1, "open destination %s", argv[1]);

	if (chdir(argv[0]) == -1)
		err(1, "chdir to source %s", argv[0]);

	return process_tree(srv_fds[0], dstdir_fd) == -1 ? 1 : 0;
}

void
usage(void)
{
	fprintf(stderr, "usage: %s [-I os=name] [-T output] "
	    "srcdir dstdir\n", BINM_CATMAN);
	exit(1);
}