xref: /openbsd-src/usr.sbin/apmd/apmd.c (revision 1a8dbaac879b9f3335ad7fb25429ce63ac1d6bac)
1 /*	$OpenBSD: apmd.c,v 1.99 2020/09/28 21:35:14 jca Exp $	*/
2 
3 /*
4  *  Copyright (c) 1995, 1996 John T. Kohl
5  *  All rights reserved.
6  *
7  *  Redistribution and use in source and binary forms, with or without
8  *  modification, are permitted provided that the following conditions
9  *  are met:
10  *  1. Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer.
12  *  2. Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution.
15  *  3. The name of the author may not be used to endorse or promote products
16  *     derived from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS OR
19  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
22  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  */
31 
32 #include <sys/stat.h>
33 #include <sys/ioctl.h>
34 #include <sys/socket.h>
35 #include <sys/un.h>
36 #include <sys/wait.h>
37 #include <sys/event.h>
38 #include <sys/time.h>
39 #include <sys/sysctl.h>
40 #include <assert.h>
41 #include <stdarg.h>
42 #include <stdio.h>
43 #include <syslog.h>
44 #include <fcntl.h>
45 #include <unistd.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #include <signal.h>
49 #include <errno.h>
50 #include <err.h>
51 #include <limits.h>
52 #include <machine/apmvar.h>
53 
54 #include "pathnames.h"
55 #include "apm-proto.h"
56 
57 #define AUTO_SUSPEND 1
58 #define AUTO_HIBERNATE 2
59 
60 int debug = 0;
61 
62 extern char *__progname;
63 
64 void usage(void);
65 int power_status(int fd, int force, struct apm_power_info *pinfo);
66 int bind_socket(const char *sn);
67 enum apm_state handle_client(int sock_fd, int ctl_fd);
68 void suspend(int ctl_fd);
69 void stand_by(int ctl_fd);
70 void hibernate(int ctl_fd);
71 void resumed(int ctl_fd);
72 void setperfpolicy(char *policy);
73 void sigexit(int signo);
74 void do_etc_file(const char *file);
75 void sockunlink(void);
76 void error(const char *fmt, const char *arg);
77 void set_driver_messages(int fd, int mode);
78 
79 /* ARGSUSED */
80 void
81 sigexit(int signo)
82 {
83 	sockunlink();
84 	_exit(1);
85 }
86 
87 void
88 logmsg(int prio, const char *msg, ...)
89 {
90 	va_list ap;
91 
92 	va_start(ap, msg);
93 	if (debug) {
94 		vfprintf(stderr, msg, ap);
95 		fprintf(stderr, "\n");
96 	} else {
97 		vsyslog(prio, msg, ap);
98 	}
99 	va_end(ap);
100 }
101 
102 void
103 usage(void)
104 {
105 	fprintf(stderr,
106 	    "usage: %s [-AadHLs] [-f devname] [-S sockname] [-t seconds] "
107 		"[-Z percent] [-z percent]\n", __progname);
108 	exit(1);
109 }
110 
111 void
112 error(const char *fmt, const char *arg)
113 {
114 	char buf[128];
115 
116 	if (debug)
117 		err(1, fmt, arg);
118 	else {
119 		strlcpy(buf, fmt, sizeof(buf));
120 		strlcat(buf, ": %m", sizeof(buf));
121 		syslog(LOG_ERR, buf, arg);
122 		exit(1);
123 	}
124 }
125 
126 
127 /*
128  * tell the driver if it should display messages or not.
129  */
130 void
131 set_driver_messages(int fd, int mode)
132 {
133 	if (ioctl(fd, APM_IOC_PRN_CTL, &mode) == -1)
134 		logmsg(LOG_DEBUG, "can't disable driver messages, error: %s",
135 		    strerror(errno));
136 }
137 
138 int
139 power_status(int fd, int force, struct apm_power_info *pinfo)
140 {
141 	struct apm_power_info bstate;
142 	static struct apm_power_info last;
143 	int acon = 0, priority = LOG_NOTICE;
144 
145 	if (fd == -1) {
146 		if (pinfo) {
147 			bstate.battery_state = 255;
148 			bstate.ac_state = 255;
149 			bstate.battery_life = 0;
150 			bstate.minutes_left = -1;
151 			*pinfo = bstate;
152 		}
153 
154 		return 0;
155 	}
156 
157 	if (ioctl(fd, APM_IOC_GETPOWER, &bstate) == 0) {
158 	/* various conditions under which we report status:  something changed
159 	 * enough since last report, or asked to force a print */
160 		if (bstate.ac_state == APM_AC_ON)
161 			acon = 1;
162 		if (bstate.battery_state == APM_BATT_CRITICAL &&
163 		    bstate.battery_state != last.battery_state)
164 			priority = LOG_EMERG;
165 		if (force ||
166 		    bstate.ac_state != last.ac_state ||
167 		    bstate.battery_state != last.battery_state ||
168 		    (bstate.minutes_left && bstate.minutes_left < 15) ||
169 		    abs(bstate.battery_life - last.battery_life) >= 10) {
170 #ifdef __powerpc__
171 			/*
172 			 * When the battery is charging, the estimated life
173 			 * time is in fact the estimated remaining charge time
174 			 * on Apple machines, so lie in the stats.
175 			 * We still want an useful message if the battery or
176 			 * ac status changes, however.
177 			 */
178 			if (bstate.minutes_left != 0 &&
179 			    bstate.battery_state != APM_BATT_CHARGING)
180 #else
181 			if ((int)bstate.minutes_left > 0)
182 #endif
183 				logmsg(priority, "battery status: %s. "
184 				    "external power status: %s. "
185 				    "estimated battery life %d%% (%u minutes)",
186 				    battstate(bstate.battery_state),
187 				    ac_state(bstate.ac_state),
188 				    bstate.battery_life,
189 				    bstate.minutes_left);
190 			else
191 				logmsg(priority, "battery status: %s. "
192 				    "external power status: %s. "
193 				    "estimated battery life %d%%",
194 				    battstate(bstate.battery_state),
195 				    ac_state(bstate.ac_state),
196 				    bstate.battery_life);
197 			last = bstate;
198 		}
199 		if (pinfo)
200 			*pinfo = bstate;
201 	} else
202 		logmsg(LOG_ERR, "cannot fetch power status: %s", strerror(errno));
203 
204 	return acon;
205 }
206 
207 char socketname[PATH_MAX];
208 
209 void
210 sockunlink(void)
211 {
212 	if (socketname[0])
213 		remove(socketname);
214 }
215 
216 int
217 bind_socket(const char *sockname)
218 {
219 	struct sockaddr_un s_un;
220 	mode_t old_umask;
221 	int sock;
222 
223 	sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
224 	if (sock == -1)
225 		error("cannot create local socket", NULL);
226 
227 	s_un.sun_family = AF_UNIX;
228 	strlcpy(s_un.sun_path, sockname, sizeof(s_un.sun_path));
229 
230 	/* remove it if present, we're moving in */
231 	(void) remove(sockname);
232 
233 	old_umask = umask(077);
234 	if (bind(sock, (struct sockaddr *)&s_un, sizeof(s_un)) == -1)
235 		error("cannot bind on APM socket", NULL);
236 	umask(old_umask);
237 	if (chmod(sockname, 0660) == -1 || chown(sockname, 0, 0) == -1)
238 		error("cannot set socket mode/owner/group to 660/0/0", NULL);
239 
240 	listen(sock, 1);
241 	strlcpy(socketname, sockname, sizeof socketname);
242 	atexit(sockunlink);
243 
244 	return sock;
245 }
246 
247 enum apm_state
248 handle_client(int sock_fd, int ctl_fd)
249 {
250 	/* accept a handle from the client, process it, then clean up */
251 	int cli_fd;
252 	struct sockaddr_un from;
253 	socklen_t fromlen;
254 	struct apm_command cmd;
255 	struct apm_reply reply;
256 	int perfpol_mib[] = { CTL_HW, HW_PERFPOLICY };
257 	char perfpol[32];
258 	size_t perfpol_sz = sizeof(perfpol);
259 	int cpuspeed_mib[] = { CTL_HW, HW_CPUSPEED };
260 	int cpuspeed = 0;
261 	size_t cpuspeed_sz = sizeof(cpuspeed);
262 
263 	fromlen = sizeof(from);
264 	cli_fd = accept(sock_fd, (struct sockaddr *)&from, &fromlen);
265 	if (cli_fd == -1) {
266 		logmsg(LOG_INFO, "client accept failure: %s", strerror(errno));
267 		return NORMAL;
268 	}
269 
270 	if (recv(cli_fd, &cmd, sizeof(cmd), 0) != sizeof(cmd)) {
271 		(void) close(cli_fd);
272 		logmsg(LOG_INFO, "client size botch");
273 		return NORMAL;
274 	}
275 
276 	if (cmd.vno != APMD_VNO) {
277 		close(cli_fd);			/* terminate client */
278 		/* no error message, just drop it. */
279 		return NORMAL;
280 	}
281 
282 	power_status(ctl_fd, 0, &reply.batterystate);
283 	switch (cmd.action) {
284 	case SUSPEND:
285 		reply.newstate = SUSPENDING;
286 		break;
287 	case STANDBY:
288 		reply.newstate = STANDING_BY;
289 		break;
290 	case HIBERNATE:
291 		reply.newstate = HIBERNATING;
292 		break;
293 	case SETPERF_LOW:
294 		reply.newstate = NORMAL;
295 		logmsg(LOG_NOTICE, "setting hw.perfpolicy to low");
296 		setperfpolicy("low");
297 		break;
298 	case SETPERF_HIGH:
299 		reply.newstate = NORMAL;
300 		logmsg(LOG_NOTICE, "setting hw.perfpolicy to high");
301 		setperfpolicy("high");
302 		break;
303 	case SETPERF_AUTO:
304 		reply.newstate = NORMAL;
305 		logmsg(LOG_NOTICE, "setting hw.perfpolicy to auto");
306 		setperfpolicy("auto");
307 		break;
308 	default:
309 		reply.newstate = NORMAL;
310 		break;
311 	}
312 
313 	reply.perfmode = PERF_NONE;
314 	if (sysctl(perfpol_mib, 2, perfpol, &perfpol_sz, NULL, 0) == -1)
315 		logmsg(LOG_INFO, "cannot read hw.perfpolicy");
316 	else {
317 		if (strcmp(perfpol, "manual") == 0 ||
318 		    strcmp(perfpol, "high") == 0) {
319 			reply.perfmode = PERF_MANUAL;
320 		} else if (strcmp(perfpol, "auto") == 0)
321 			reply.perfmode = PERF_AUTO;
322 	}
323 
324 	if (sysctl(cpuspeed_mib, 2, &cpuspeed, &cpuspeed_sz, NULL, 0) == -1) {
325 		logmsg(LOG_INFO, "cannot read hw.cpuspeed");
326 		cpuspeed = 0;
327 	}
328 	reply.cpuspeed = cpuspeed;
329 	reply.vno = APMD_VNO;
330 	if (send(cli_fd, &reply, sizeof(reply), 0) != sizeof(reply))
331 		logmsg(LOG_INFO, "reply to client botched");
332 	close(cli_fd);
333 
334 	return reply.newstate;
335 }
336 
337 void
338 suspend(int ctl_fd)
339 {
340 	logmsg(LOG_NOTICE, "system suspending");
341 	power_status(ctl_fd, 1, NULL);
342 	do_etc_file(_PATH_APM_ETC_SUSPEND);
343 	sync();
344 	sleep(1);
345 	ioctl(ctl_fd, APM_IOC_SUSPEND, 0);
346 }
347 
348 void
349 stand_by(int ctl_fd)
350 {
351 	logmsg(LOG_NOTICE, "system entering standby");
352 	power_status(ctl_fd, 1, NULL);
353 	do_etc_file(_PATH_APM_ETC_STANDBY);
354 	sync();
355 	sleep(1);
356 	ioctl(ctl_fd, APM_IOC_STANDBY, 0);
357 }
358 
359 void
360 hibernate(int ctl_fd)
361 {
362 	logmsg(LOG_NOTICE, "system hibernating");
363 	power_status(ctl_fd, 1, NULL);
364 	do_etc_file(_PATH_APM_ETC_HIBERNATE);
365 	sync();
366 	sleep(1);
367 	ioctl(ctl_fd, APM_IOC_HIBERNATE, 0);
368 }
369 
370 void
371 resumed(int ctl_fd)
372 {
373 	do_etc_file(_PATH_APM_ETC_RESUME);
374 	logmsg(LOG_NOTICE, "system resumed from sleep");
375 	power_status(ctl_fd, 1, NULL);
376 }
377 
378 #define TIMO (10*60)			/* 10 minutes */
379 #define AUTOACTION_GRACE_PERIOD (60)	/* 1mn after resume */
380 
381 int
382 main(int argc, char *argv[])
383 {
384 	const char *fname = _PATH_APM_CTLDEV;
385 	int ctl_fd, sock_fd, ch, suspends, standbys, hibernates, resumes;
386 	int autoaction = 0, autoaction_inflight = 0;
387 	int autolimit = 0;
388 	int statonly = 0;
389 	int powerstatus = 0, powerbak = 0, powerchange = 0;
390 	int noacsleep = 0;
391 	struct timespec ts = {TIMO, 0}, sts = {0, 0};
392 	struct timespec last_resume = { 0, 0 };
393 	struct apm_power_info pinfo;
394 	const char *sockname = _PATH_APM_SOCKET;
395 	const char *errstr;
396 	int kq, nchanges;
397 	struct kevent ev[2];
398 	int doperf = PERF_NONE;
399 
400 	while ((ch = getopt(argc, argv, "aACdHLsf:t:S:z:Z:")) != -1)
401 		switch(ch) {
402 		case 'a':
403 			noacsleep = 1;
404 			break;
405 		case 'd':
406 			debug = 1;
407 			break;
408 		case 'f':
409 			fname = optarg;
410 			break;
411 		case 'S':
412 			sockname = optarg;
413 			break;
414 		case 't':
415 			ts.tv_sec = strtonum(optarg, 1, LLONG_MAX, &errstr);
416 			if (errstr != NULL)
417 				errx(1, "number of seconds is %s: %s", errstr,
418 				    optarg);
419 			break;
420 		case 's':	/* status only */
421 			statonly = 1;
422 			break;
423 		case 'A':
424 		case 'C':
425 			if (doperf != PERF_NONE)
426 				usage();
427 			doperf = PERF_AUTO;
428 			setperfpolicy("auto");
429 			break;
430 		case 'L':
431 			if (doperf != PERF_NONE)
432 				usage();
433 			doperf = PERF_MANUAL;
434 			setperfpolicy("low");
435 			break;
436 		case 'H':
437 			if (doperf != PERF_NONE)
438 				usage();
439 			doperf = PERF_MANUAL;
440 			setperfpolicy("high");
441 			break;
442 		case 'Z':
443 			autoaction = AUTO_HIBERNATE;
444 			autolimit = strtonum(optarg, 1, 100, &errstr);
445 			if (errstr != NULL)
446 				errx(1, "battery percentage is %s: %s", errstr,
447 				    optarg);
448 			break;
449 		case 'z':
450 			autoaction = AUTO_SUSPEND;
451 			autolimit = strtonum(optarg, 1, 100, &errstr);
452 			if (errstr != NULL)
453 				errx(1, "battery percentage is %s: %s", errstr,
454 				    optarg);
455 			break;
456 		case '?':
457 		default:
458 			usage();
459 		}
460 
461 	argc -= optind;
462 	argv += optind;
463 
464 	if (argc != 0)
465 		usage();
466 
467 	if (doperf == PERF_NONE)
468 		doperf = PERF_MANUAL;
469 
470 	if (debug == 0) {
471 		if (daemon(0, 0) == -1)
472 			error("failed to daemonize", NULL);
473 		openlog(__progname, LOG_CONS, LOG_DAEMON);
474 		setlogmask(LOG_UPTO(LOG_NOTICE));
475 	}
476 
477 	(void) signal(SIGTERM, sigexit);
478 	(void) signal(SIGHUP, sigexit);
479 	(void) signal(SIGINT, sigexit);
480 
481 	if ((ctl_fd = open(fname, O_RDWR | O_CLOEXEC)) == -1) {
482 		if (errno != ENXIO && errno != ENOENT)
483 			error("cannot open device file `%s'", fname);
484 	}
485 
486 	sock_fd = bind_socket(sockname);
487 
488 	power_status(ctl_fd, 1, &pinfo);
489 
490 	if (statonly)
491 		exit(0);
492 
493 	set_driver_messages(ctl_fd, APM_PRINT_OFF);
494 
495 	kq = kqueue();
496 	if (kq <= 0)
497 		error("kqueue", NULL);
498 
499 	EV_SET(&ev[0], sock_fd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR,
500 	    0, 0, NULL);
501 	if (ctl_fd == -1)
502 		nchanges = 1;
503 	else {
504 		EV_SET(&ev[1], ctl_fd, EVFILT_READ, EV_ADD | EV_ENABLE |
505 		    EV_CLEAR, 0, 0, NULL);
506 		nchanges = 2;
507 	}
508 	if (kevent(kq, ev, nchanges, NULL, 0, &sts) == -1)
509 		error("kevent", NULL);
510 
511 	for (;;) {
512 		int rv, event, index;
513 
514 		sts = ts;
515 
516 		if ((rv = kevent(kq, NULL, 0, ev, 1, &sts)) == -1)
517 			break;
518 
519 		if (rv == 1 && ev->ident == sock_fd) {
520 			switch (handle_client(sock_fd, ctl_fd)) {
521 			case NORMAL:
522 				break;
523 			case SUSPENDING:
524 				suspend(ctl_fd);
525 				break;
526 			case STANDING_BY:
527 				stand_by(ctl_fd);
528 				break;
529 			case HIBERNATING:
530 				hibernate(ctl_fd);
531 				break;
532 			}
533 			continue;
534 		}
535 
536 		suspends = standbys = hibernates = resumes = 0;
537 
538 		if (rv == 0 && ctl_fd == -1) {
539 			/* timeout and no way to query status */
540 			continue;
541 		} else if (rv == 0) {
542 			/* wakeup for timeout: take status */
543 			event = APM_POWER_CHANGE;
544 			index = -1;
545 		} else {
546 			assert(rv == 1 && ev->ident == ctl_fd);
547 			event = APM_EVENT_TYPE(ev->data);
548 			index = APM_EVENT_INDEX(ev->data);
549 		}
550 
551 		logmsg(LOG_DEBUG, "apmevent %04x index %d", event, index);
552 
553 		switch (event) {
554 		case APM_SUSPEND_REQ:
555 		case APM_USER_SUSPEND_REQ:
556 		case APM_CRIT_SUSPEND_REQ:
557 		case APM_BATTERY_LOW:
558 			suspends++;
559 			break;
560 		case APM_USER_STANDBY_REQ:
561 		case APM_STANDBY_REQ:
562 			standbys++;
563 			break;
564 		case APM_USER_HIBERNATE_REQ:
565 			hibernates++;
566 			break;
567 #if 0
568 		case APM_CANCEL:
569 			suspends = standbys = 0;
570 			break;
571 #endif
572 		case APM_NORMAL_RESUME:
573 		case APM_CRIT_RESUME:
574 		case APM_SYS_STANDBY_RESUME:
575 			powerbak = power_status(ctl_fd, 0, &pinfo);
576 			if (powerstatus != powerbak) {
577 				powerstatus = powerbak;
578 				powerchange = 1;
579 			}
580 			clock_gettime(CLOCK_MONOTONIC, &last_resume);
581 			autoaction_inflight = 0;
582 			resumes++;
583 			break;
584 		case APM_POWER_CHANGE:
585 			powerbak = power_status(ctl_fd, 0, &pinfo);
586 			if (powerstatus != powerbak) {
587 				powerstatus = powerbak;
588 				powerchange = 1;
589 			}
590 
591 			if (!powerstatus && autoaction &&
592 			    autolimit > (int)pinfo.battery_life) {
593 				struct timespec graceperiod, now;
594 
595 				graceperiod = last_resume;
596 				graceperiod.tv_sec += AUTOACTION_GRACE_PERIOD;
597 				clock_gettime(CLOCK_MONOTONIC, &now);
598 
599 				logmsg(LOG_NOTICE,
600 				    "estimated battery life %d%%"
601 				    " below configured limit %d%%%s%s",
602 				    pinfo.battery_life, autolimit,
603 				    !autoaction_inflight ? "" : ", in flight",
604 				    timespeccmp(&now, &graceperiod, >) ?
605 				        "" : ", grace period"
606 				);
607 
608 				if (!autoaction_inflight &&
609 				    timespeccmp(&now, &graceperiod, >)) {
610 					if (autoaction == AUTO_SUSPEND)
611 						suspends++;
612 					else
613 						hibernates++;
614 					/* Block autoaction until next resume */
615 					autoaction_inflight = 1;
616 				}
617 			}
618 			break;
619 		default:
620 			;
621 		}
622 
623 		if ((standbys || suspends) && noacsleep &&
624 		    power_status(ctl_fd, 0, &pinfo))
625 			logmsg(LOG_DEBUG, "no! sleep! till brooklyn!");
626 		else if (suspends)
627 			suspend(ctl_fd);
628 		else if (standbys)
629 			stand_by(ctl_fd);
630 		else if (hibernates)
631 			hibernate(ctl_fd);
632 		else if (resumes) {
633 			resumed(ctl_fd);
634 		}
635 
636 		if (powerchange) {
637 			if (powerstatus)
638 				do_etc_file(_PATH_APM_ETC_POWERUP);
639 			else
640 				do_etc_file(_PATH_APM_ETC_POWERDOWN);
641 			powerchange = 0;
642 		}
643 	}
644 	error("kevent loop", NULL);
645 
646 	return 1;
647 }
648 
649 void
650 setperfpolicy(char *policy)
651 {
652 	int hw_perfpol_mib[] = { CTL_HW, HW_PERFPOLICY };
653 	int hw_perf_mib[] = { CTL_HW, HW_SETPERF };
654 	int new_perf = -1;
655 
656 	if (strcmp(policy, "low") == 0) {
657 		policy = "manual";
658 		new_perf = 0;
659 	} else if (strcmp(policy, "high") == 0) {
660 		policy = "manual";
661 		new_perf = 100;
662 	}
663 
664 	if (sysctl(hw_perfpol_mib, 2, NULL, NULL,
665 	    policy, strlen(policy) + 1) == -1)
666 		logmsg(LOG_INFO, "cannot set hw.perfpolicy");
667 
668 	if (new_perf == -1)
669 		return;
670 
671 	if (sysctl(hw_perf_mib, 2, NULL, NULL,
672 	    &new_perf, sizeof(new_perf)) == -1)
673 		logmsg(LOG_INFO, "cannot set hw.setperf");
674 }
675 
676 void
677 do_etc_file(const char *file)
678 {
679 	pid_t pid;
680 	int status;
681 	const char *prog;
682 
683 	/* If file doesn't exist, do nothing. */
684 	if (access(file, X_OK|R_OK)) {
685 		logmsg(LOG_DEBUG, "do_etc_file(): cannot access file %s", file);
686 		return;
687 	}
688 
689 	prog = strrchr(file, '/');
690 	if (prog)
691 		prog++;
692 	else
693 		prog = file;
694 
695 	pid = fork();
696 	switch (pid) {
697 	case -1:
698 		logmsg(LOG_ERR, "failed to fork(): %s", strerror(errno));
699 		return;
700 	case 0:
701 		/* We are the child. */
702 		execl(file, prog, (char *)NULL);
703 		logmsg(LOG_ERR, "failed to exec %s: %s", file, strerror(errno));
704 		_exit(1);
705 		/* NOTREACHED */
706 	default:
707 		/* We are the parent. */
708 		wait4(pid, &status, 0, 0);
709 		if (WIFEXITED(status))
710 			logmsg(LOG_DEBUG, "%s exited with status %d", file,
711 			    WEXITSTATUS(status));
712 		else
713 			logmsg(LOG_ERR, "%s exited abnormally.", file);
714 	}
715 }
716