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