1 /* $Id: catman.c,v 1.30 2025/07/13 14:15:26 schwarze Exp $ */
2 /*
3 * Copyright (c) 2017, 2025 Ingo Schwarze <schwarze@openbsd.org>
4 * Copyright (c) 2017 Michael Stapelberg <stapelberg@debian.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18 #include "config.h"
19
20 #if NEED_XPG4_2
21 #define _XPG4_2
22 #endif
23
24 #include <sys/types.h>
25 #include <sys/socket.h>
26 #include <sys/stat.h>
27
28 #include <assert.h>
29 #if HAVE_ERR
30 #include <err.h>
31 #endif
32 #include <errno.h>
33 #include <fcntl.h>
34 #if HAVE_FTS
35 #include <fts.h>
36 #else
37 #include "compat_fts.h"
38 #endif
39 #include <signal.h>
40 #include <stdint.h>
41 #include <stdio.h>
42 #include <stdlib.h>
43 #include <string.h>
44 #include <time.h>
45 #include <unistd.h>
46
47 int verbose_flag = 0;
48 sig_atomic_t got_signal = 0;
49
50 int process_manpage(int, int, const char *);
51 int process_tree(int, int);
52 void run_mandocd(int, const char *, const char *)
53 __attribute__((__noreturn__));
54 void signal_handler(int);
55 ssize_t sock_fd_write(int, int, int, int);
56 void usage(void) __attribute__((__noreturn__));
57
58
59 void
signal_handler(int signum)60 signal_handler(int signum)
61 {
62 got_signal = signum;
63 }
64
65 void
run_mandocd(int sockfd,const char * outtype,const char * defos)66 run_mandocd(int sockfd, const char *outtype, const char* defos)
67 {
68 char sockfdstr[10];
69 int len;
70
71 len = snprintf(sockfdstr, sizeof(sockfdstr), "%d", sockfd);
72 if (len >= (int)sizeof(sockfdstr)) {
73 errno = EOVERFLOW;
74 len = -1;
75 }
76 if (len < 0)
77 err(1, "snprintf");
78 if (defos == NULL)
79 execlp("mandocd", "mandocd", "-T", outtype,
80 sockfdstr, (char *)NULL);
81 else
82 execlp("mandocd", "mandocd", "-T", outtype,
83 "-I", defos, sockfdstr, (char *)NULL);
84 err(1, "exec(mandocd)");
85 }
86
87 ssize_t
sock_fd_write(int fd,int fd0,int fd1,int fd2)88 sock_fd_write(int fd, int fd0, int fd1, int fd2)
89 {
90 const struct timespec timeout = { 0, 10000000 }; /* 0.01 s */
91 struct msghdr msg;
92 struct iovec iov;
93 union {
94 struct cmsghdr cmsghdr;
95 char control[CMSG_SPACE(3 * sizeof(int))];
96 } cmsgu;
97 struct cmsghdr *cmsg;
98 int *walk;
99 ssize_t sz;
100 unsigned char dummy[1] = {'\0'};
101
102 iov.iov_base = dummy;
103 iov.iov_len = sizeof(dummy);
104
105 msg.msg_name = NULL;
106 msg.msg_namelen = 0;
107 msg.msg_iov = &iov;
108 msg.msg_iovlen = 1;
109
110 msg.msg_control = cmsgu.control;
111 msg.msg_controllen = sizeof(cmsgu.control);
112
113 cmsg = CMSG_FIRSTHDR(&msg);
114 cmsg->cmsg_len = CMSG_LEN(3 * sizeof(int));
115 cmsg->cmsg_level = SOL_SOCKET;
116 cmsg->cmsg_type = SCM_RIGHTS;
117
118 walk = (int *)CMSG_DATA(cmsg);
119 *(walk++) = fd0;
120 *(walk++) = fd1;
121 *(walk++) = fd2;
122
123 /*
124 * It appears that on some systems, sendmsg(3)
125 * may return EAGAIN even in blocking mode.
126 * Seen for example on Oracle Solaris 11.2.
127 * The sleeping time was chosen by experimentation,
128 * to neither cause more than a handful of retries
129 * in normal operation nor unnecessary delays.
130 */
131 while ((sz = sendmsg(fd, &msg, 0)) == -1) {
132 if (errno != EAGAIN) {
133 warn("FATAL: sendmsg");
134 break;
135 }
136 nanosleep(&timeout, NULL);
137 }
138 return sz;
139 }
140
141 int
process_manpage(int srv_fd,int dstdir_fd,const char * path)142 process_manpage(int srv_fd, int dstdir_fd, const char *path)
143 {
144 int in_fd, out_fd;
145 int irc;
146
147 if ((in_fd = open(path, O_RDONLY)) == -1) {
148 warn("open %s for reading", path);
149 fflush(stderr);
150 return 0;
151 }
152
153 if ((out_fd = openat(dstdir_fd, path,
154 O_WRONLY | O_NOFOLLOW | O_CREAT | O_TRUNC,
155 S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
156 warn("openat %s for writing", path);
157 fflush(stderr);
158 close(in_fd);
159 return 0;
160 }
161
162 irc = sock_fd_write(srv_fd, in_fd, out_fd, STDERR_FILENO);
163
164 close(in_fd);
165 close(out_fd);
166
167 return irc;
168 }
169
170 int
process_tree(int srv_fd,int dstdir_fd)171 process_tree(int srv_fd, int dstdir_fd)
172 {
173 const struct timespec timeout = { 0, 10000000 }; /* 0.01 s */
174 const int max_inflight = 16;
175
176 FTS *ftsp;
177 FTSENT *entry;
178 const char *argv[2];
179 const char *path;
180 int inflight, irc, decr, fatal;
181 int gooddirs, baddirs, goodfiles, badfiles;
182 char dummy[1];
183
184 argv[0] = ".";
185 argv[1] = (char *)NULL;
186
187 if ((ftsp = fts_open((char * const *)argv,
188 FTS_PHYSICAL | FTS_NOCHDIR, NULL)) == NULL) {
189 warn("fts_open");
190 return -1;
191 }
192
193 if (verbose_flag >= 2) {
194 warnx("allowing up to %d files in flight", max_inflight);
195 fflush(stderr);
196 }
197 inflight = fatal = gooddirs = baddirs = goodfiles = badfiles = 0;
198 while (fatal == 0 && got_signal == 0 &&
199 (entry = fts_read(ftsp)) != NULL) {
200 if (inflight >= max_inflight) {
201 while (recv(srv_fd, dummy, sizeof(dummy), 0) == -1) {
202 if (errno != EAGAIN) {
203 warn("FATAL: recv");
204 fatal = errno;
205 break;
206 }
207 nanosleep(&timeout, NULL);
208 }
209 if (fatal != 0)
210 break;
211 decr = 1;
212 while ((irc = recv(srv_fd, dummy, sizeof(dummy),
213 MSG_DONTWAIT)) > 0)
214 decr++;
215 assert(inflight >= decr);
216 if (verbose_flag >= 2 && decr > 1) {
217 warnx("files in flight: %d - %d = %d",
218 inflight, decr, inflight - decr);
219 fflush(stderr);
220 }
221 inflight -= decr;
222 if (irc == 0) {
223 errno = ECONNRESET;
224 inflight = -1;
225 }
226 if (errno != EAGAIN) {
227 warn("FATAL: recv");
228 fatal = errno;
229 break;
230 }
231 }
232 path = entry->fts_path + 2;
233 switch (entry->fts_info) {
234 case FTS_F:
235 switch (process_manpage(srv_fd, dstdir_fd, path)) {
236 case -1:
237 fatal = errno;
238 break;
239 case 0:
240 badfiles++;
241 break;
242 default:
243 goodfiles++;
244 inflight++;
245 break;
246 }
247 break;
248 case FTS_D:
249 if (*path != '\0' &&
250 mkdirat(dstdir_fd, path, S_IRWXU | S_IRGRP |
251 S_IXGRP | S_IROTH | S_IXOTH) == -1 &&
252 errno != EEXIST) {
253 warn("mkdirat %s", path);
254 fflush(stderr);
255 (void)fts_set(ftsp, entry, FTS_SKIP);
256 baddirs++;
257 } else
258 gooddirs++;
259 break;
260 case FTS_DP:
261 break;
262 case FTS_DNR:
263 warnx("directory %s unreadable: %s",
264 path, strerror(entry->fts_errno));
265 fflush(stderr);
266 baddirs++;
267 break;
268 case FTS_DC:
269 warnx("directory %s causes cycle", path);
270 fflush(stderr);
271 baddirs++;
272 break;
273 case FTS_ERR:
274 case FTS_NS:
275 warnx("file %s: %s",
276 path, strerror(entry->fts_errno));
277 fflush(stderr);
278 badfiles++;
279 break;
280 default:
281 warnx("file %s: not a regular file", path);
282 fflush(stderr);
283 badfiles++;
284 break;
285 }
286 }
287 if (got_signal != 0) {
288 switch (got_signal) {
289 case SIGCHLD:
290 warnx("FATAL: mandocd child died: got SIGCHLD");
291 break;
292 case SIGPIPE:
293 warnx("FATAL: mandocd child died: got SIGPIPE");
294 break;
295 default:
296 warnx("FATAL: signal SIG%s", sys_signame[got_signal]);
297 break;
298 }
299 inflight = -1;
300 fatal = 1;
301 } else if (fatal == 0 && (fatal = errno) != 0)
302 warn("FATAL: fts_read");
303
304 fts_close(ftsp);
305 if (verbose_flag >= 2 && inflight > 0) {
306 warnx("waiting for %d files in flight", inflight);
307 fflush(stderr);
308 }
309 while (inflight > 0) {
310 irc = recv(srv_fd, dummy, sizeof(dummy), 0);
311 if (irc > 0)
312 inflight--;
313 else if (irc == -1 && errno == EAGAIN)
314 nanosleep(&timeout, NULL);
315 else {
316 if (irc == 0)
317 errno = ECONNRESET;
318 warn("recv");
319 inflight = -1;
320 }
321 }
322 if (verbose_flag)
323 warnx("processed %d files in %d directories",
324 goodfiles, gooddirs);
325 if (baddirs > 0)
326 warnx("skipped %d %s due to errors", baddirs,
327 baddirs == 1 ? "directory" : "directories");
328 if (badfiles > 0)
329 warnx("skipped %d %s due to errors", badfiles,
330 badfiles == 1 ? "file" : "files");
331 if (fatal != 0) {
332 warnx("processing aborted due to fatal error, "
333 "results are probably incomplete");
334 inflight = -1;
335 }
336 return inflight;
337 }
338
339 int
main(int argc,char ** argv)340 main(int argc, char **argv)
341 {
342 struct sigaction sa;
343 const char *defos, *outtype;
344 int srv_fds[2];
345 int dstdir_fd;
346 int opt;
347 pid_t pid;
348
349 defos = NULL;
350 outtype = "ascii";
351 while ((opt = getopt(argc, argv, "I:T:v")) != -1) {
352 switch (opt) {
353 case 'I':
354 defos = optarg;
355 break;
356 case 'T':
357 outtype = optarg;
358 break;
359 case 'v':
360 verbose_flag += 1;
361 break;
362 default:
363 usage();
364 }
365 }
366
367 if (argc > 0) {
368 argc -= optind;
369 argv += optind;
370 }
371 if (argc != 2) {
372 switch (argc) {
373 case 0:
374 warnx("missing arguments: srcdir and dstdir");
375 break;
376 case 1:
377 warnx("missing argument: dstdir");
378 break;
379 default:
380 warnx("too many arguments: %s", argv[2]);
381 break;
382 }
383 usage();
384 }
385
386 memset(&sa, 0, sizeof(sa));
387 sa.sa_handler = &signal_handler;
388 sa.sa_flags = SA_NOCLDWAIT;
389 if (sigfillset(&sa.sa_mask) == -1)
390 err(1, "sigfillset");
391 if (sigaction(SIGHUP, &sa, NULL) == -1)
392 err(1, "sigaction(SIGHUP)");
393 if (sigaction(SIGINT, &sa, NULL) == -1)
394 err(1, "sigaction(SIGINT)");
395 if (sigaction(SIGPIPE, &sa, NULL) == -1)
396 err(1, "sigaction(SIGPIPE)");
397 if (sigaction(SIGTERM, &sa, NULL) == -1)
398 err(1, "sigaction(SIGTERM)");
399 if (sigaction(SIGCHLD, &sa, NULL) == -1)
400 err(1, "sigaction(SIGCHLD)");
401
402 if (socketpair(AF_LOCAL, SOCK_STREAM, AF_UNSPEC, srv_fds) == -1)
403 err(1, "socketpair");
404
405 pid = fork();
406 switch (pid) {
407 case -1:
408 err(1, "fork");
409 case 0:
410 close(srv_fds[0]);
411 run_mandocd(srv_fds[1], outtype, defos);
412 default:
413 break;
414 }
415 close(srv_fds[1]);
416
417 if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1) {
418 if (errno != ENOENT)
419 err(1, "open destination %s", argv[1]);
420 if (mkdir(argv[1], S_IRWXU |
421 S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == -1)
422 err(1, "mkdir destination %s", argv[1]);
423 if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1)
424 err(1, "open destination %s", argv[1]);
425 }
426
427 if (chdir(argv[0]) == -1)
428 err(1, "chdir to source %s", argv[0]);
429
430 return process_tree(srv_fds[0], dstdir_fd) == -1 ? 1 : 0;
431 }
432
433 void
usage(void)434 usage(void)
435 {
436 fprintf(stderr, "usage: %s [-I os=name] [-T output] "
437 "srcdir dstdir\n", BINM_CATMAN);
438 exit(1);
439 }
440