swayaway
a drop-in replacement for swayidle(1)
main.c
8792 bytes raw
1#include "swayaway.h"
2
3#include <errno.h>
4#include <fcntl.h>
5#include <getopt.h>
6#include <poll.h>
7#include <signal.h>
8#include <stdio.h>
9#include <stdlib.h>
10#include <string.h>
11#include <sys/signalfd.h>
12#include <unistd.h>
13#include <wordexp.h>
14#include <wayland-client.h>
15
16struct swayaway state;
17
18static void usage(const char *prog)
19{
20 printf("Usage: %s [options] [events...]\n", prog);
21 printf(" -C <path>\tpath to config file\n");
22 printf(" -h\t\tthis help menu\n");
23 printf(" -d\t\tdebug\n");
24 printf(" -w\t\twait for command to finish\n");
25 printf(" -S\t\tpick the seat to work with\n");
26}
27
28static int parse_timeout(int argc, char **argv)
29{
30 if (argc < 3) {
31 sway_log("too few parameters to timeout: "
32 "timeout <seconds> <command> [resume <command>]");
33 exit(-1);
34 }
35 errno = 0;
36 char *endptr = NULL;
37 long seconds = strtol(argv[1], &endptr, 10);
38 if (errno != 0 || endptr == argv[1] || *endptr != '\0') {
39 sway_log("invalid timeout '%s': must be a number of seconds", argv[1]);
40 exit(-1);
41 }
42
43 char *idle_cmd = NULL;
44 char *resume_cmd = NULL;
45 int consumed = 3;
46
47 if (argc >= 4 && strcmp(argv[2], "resume") == 0) {
48 /* legacy mode syntax: `timeout N resume CMD` runs on timeout AND on
49 * resume, which was the behavior before swayidle 1.8 */
50 idle_cmd = strdup(argv[3]);
51 resume_cmd = strdup(argv[3]);
52 consumed = 4;
53 } else {
54 idle_cmd = strdup(argv[2]);
55 if (argc >= 5 && strcmp(argv[3], "resume") == 0) {
56 resume_cmd = strdup(argv[4]);
57 consumed = 5;
58 }
59 }
60
61 idle_add_timeout(seconds > 0 ? (int)seconds : -1, idle_cmd, resume_cmd);
62 return consumed;
63}
64
65static int parse_resume(int argc, char **argv)
66{
67 // legacy top-level event: `resume CMD` runs on every activity
68 if (argc < 2) {
69 sway_log("too few parameters to resume: resume <command>");
70 exit(-1);
71 }
72 idle_add_resume_cmd(strdup(argv[1]));
73 return 2;
74}
75
76static int parse_before_sleep(int argc, char **argv)
77{
78 if (argc < 2) {
79 sway_log("too few parameters to before-sleep: before-sleep <command>");
80 exit(-1);
81 }
82 state.before_sleep_cmd = strdup(argv[1]);
83 sway_log_debug("before-sleep: %s", state.before_sleep_cmd);
84 return 2;
85}
86
87static int parse_after_resume(int argc, char **argv)
88{
89 if (argc < 2) {
90 sway_log("too few parameters to after-resume: after-resume <command>");
91 exit(-1);
92 }
93 state.after_resume_cmd = strdup(argv[1]);
94 sway_log_debug("after-resume: %s", state.after_resume_cmd);
95 return 2;
96}
97
98static int dispatch_event(int argc, char **argv)
99{
100 const char *name = argv[0];
101 if (strcmp(name, "timeout") == 0) {
102 return parse_timeout(argc, argv);
103 }
104 if (strcmp(name, "resume") == 0) {
105 return parse_resume(argc, argv);
106 }
107 if (strcmp(name, "before-sleep") == 0) {
108 return parse_before_sleep(argc, argv);
109 }
110 if (strcmp(name, "after-resume") == 0) {
111 return parse_after_resume(argc, argv);
112 }
113 if (strcmp(name, "lock") == 0 || strcmp(name, "unlock") == 0 ||
114 strcmp(name, "idlehint") == 0) {
115 /* swayidle implements these through the logind Session interface;
116 * swayaway intentionally has no logind integration */
117 sway_log("'%s' is not supported: swayaway has no logind session "
118 "integration", name);
119 exit(-1);
120 }
121 sway_log("unsupported event '%s'", name);
122 return -1;
123}
124
125static int parse_args(int argc, char *argv[], char **config_path)
126{
127 int c;
128 while ((c = getopt(argc, argv, "C:hdwS:")) != -1) {
129 switch (c) {
130 case 'C':
131 free(*config_path);
132 *config_path = strdup(optarg);
133 break;
134 case 'd':
135 state.debug = true;
136 break;
137 case 'w':
138 state.wait = true;
139 break;
140 case 'S':
141 free(state.seat_name);
142 state.seat_name = strdup(optarg);
143 break;
144 case 'h':
145 case '?':
146 usage(argv[0]);
147 return 1;
148 }
149 }
150
151 for (int i = optind; i < argc;) {
152 int consumed = dispatch_event(argc - i, &argv[i]);
153 if (consumed <= 0) {
154 return 1;
155 }
156 i += consumed;
157 }
158 return 0;
159}
160
161static char *get_config_path(void)
162{
163 /* try swayaway's own paths first, then swayidle's, so an existing
164 * swayidle config works as a drop-in */
165 const char *paths[] = {
166 "$XDG_CONFIG_HOME/swayaway/config",
167 "$HOME/.config/swayaway/config",
168 "$HOME/.swayaway/config",
169 "$XDG_CONFIG_HOME/swayidle/config",
170 "$HOME/.config/swayidle/config",
171 "$HOME/.swayidle/config",
172 };
173 for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
174 wordexp_t p;
175 if (wordexp(paths[i], &p, 0) != 0) {
176 continue;
177 }
178 char *path = strdup(p.we_wordv[0]);
179 wordfree(&p);
180 if (path && access(path, R_OK) == 0) {
181 return path;
182 }
183 free(path);
184 }
185 return NULL;
186}
187
188static int load_config(const char *path)
189{
190 FILE *f = fopen(path, "r");
191 if (!f) {
192 return -ENOENT;
193 }
194
195 char *line = NULL;
196 size_t n = 0;
197 ssize_t nread;
198 size_t lineno = 0;
199 while ((nread = getline(&line, &n, f)) != -1) {
200 lineno++;
201 if (nread > 0 && line[nread - 1] == '\n') {
202 line[nread - 1] = '\0';
203 }
204 if (strlen(line) == 0 || line[0] == '#') {
205 continue;
206 }
207
208 wordexp_t p;
209 if (wordexp(line, &p, 0) != 0) {
210 sway_log("shell expansion error on line %zu", lineno);
211 free(line);
212 fclose(f);
213 return -EINVAL;
214 }
215 if (p.we_wordc < 1) {
216 wordfree(&p);
217 continue;
218 }
219 if (dispatch_event((int)p.we_wordc, p.we_wordv) <= 0) {
220 sway_log("error on line %zu of %s", lineno, path);
221 wordfree(&p);
222 free(line);
223 fclose(f);
224 return -EINVAL;
225 }
226 wordfree(&p);
227 }
228 free(line);
229 fclose(f);
230 return 0;
231}
232
233int main(int argc, char *argv[])
234{
235 state.sleep_lock_fd = -1;
236 state.running = true;
237
238 char *config_path = NULL;
239 if (parse_args(argc, argv, &config_path) != 0) {
240 free(config_path);
241 return 1;
242 }
243 log_init(state.debug);
244
245 if (!config_path) {
246 config_path = get_config_path();
247 }
248 if (config_path) {
249 int r = load_config(config_path);
250 if (r == -EINVAL) {
251 sway_log("config file %s has errors, exiting", config_path);
252 free(config_path);
253 return 1;
254 } else if (r == 0) {
255 sway_log("loaded config from %s", config_path);
256 }
257 }
258 free(config_path);
259
260 // block the signals handled via signalfd; these turn into poll events
261 sigset_t mask;
262 sigemptyset(&mask);
263 sigaddset(&mask, SIGINT);
264 sigaddset(&mask, SIGTERM);
265 sigaddset(&mask, SIGUSR1);
266 sigprocmask(SIG_BLOCK, &mask, NULL);
267 int sigfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
268 if (sigfd < 0) {
269 sway_log("signalfd failed: %s", strerror(errno));
270 return 1;
271 }
272
273 if (wayland_init() != 0) {
274 return 1;
275 }
276
277 screensaver_init();
278
279 if (state.before_sleep_cmd || state.after_resume_cmd) {
280 logind_init();
281 }
282
283 if (!idle_has_work()) {
284 sway_log("no events configured; nothing to do, exiting");
285 return 0;
286 }
287
288 wayland_create_tracking_notification();
289
290 // the wayland fd, the two optional bus fds, and the signalfd
291 struct pollfd fds[4];
292 int wl_idx = 0;
293 int session_idx = -1;
294 int system_idx = -1;
295 int sig_idx;
296
297 while (state.running) {
298 int timeout = idle_next_timeout_ms();
299 int nfds = 0;
300
301 fds[nfds].fd = wl_display_get_fd(state.display);
302 fds[nfds].events = POLLIN;
303 wl_idx = nfds++;
304
305 if (state.session_conn) {
306 fds[nfds].fd = state.session_fd;
307 fds[nfds].events = POLLIN;
308 session_idx = nfds++;
309 } else {
310 session_idx = -1;
311 }
312 if (state.system_conn) {
313 fds[nfds].fd = state.system_fd;
314 fds[nfds].events = POLLIN;
315 system_idx = nfds++;
316 } else {
317 system_idx = -1;
318 }
319
320 fds[nfds].fd = sigfd;
321 fds[nfds].events = POLLIN;
322 sig_idx = nfds++;
323
324 int r = poll(fds, nfds, timeout);
325 if (r < 0) {
326 if (errno == EINTR) {
327 continue;
328 }
329 sway_log("poll failed: %s", strerror(errno));
330 break;
331 }
332
333 if (fds[wl_idx].revents) {
334 if (fds[wl_idx].revents & (POLLERR | POLLHUP)) {
335 sway_log("wayland connection closed");
336 break;
337 }
338 if (fds[wl_idx].revents & POLLIN) {
339 /* non-blocking read: avoid wl_display_dispatch's internal
340 * poll on spurious wakeups */
341 if (wl_display_prepare_read(state.display) == -1) {
342 wl_display_dispatch_pending(state.display);
343 } else {
344 wl_display_read_events(state.display);
345 wl_display_dispatch_pending(state.display);
346 }
347 }
348 }
349
350 if (session_idx >= 0 && fds[session_idx].revents) {
351 if (fds[session_idx].revents & (POLLERR | POLLHUP)) {
352 sway_log("session bus closed");
353 break;
354 }
355 dbus_connection_read_write_dispatch(state.session_conn, 0);
356 }
357
358 if (system_idx >= 0 && fds[system_idx].revents) {
359 if (fds[system_idx].revents & (POLLERR | POLLHUP)) {
360 sway_log("system bus closed");
361 break;
362 }
363 dbus_connection_read_write_dispatch(state.system_conn, 0);
364 }
365
366 if (fds[sig_idx].revents & POLLIN) {
367 struct signalfd_siginfo si;
368 while (read(sigfd, &si, sizeof(si)) == sizeof(si)) {
369 if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) {
370 idle_terminate();
371 } else if (si.ssi_signo == SIGUSR1) {
372 idle_force_idle();
373 }
374 }
375 }
376
377 idle_check_timeouts();
378 }
379
380 logind_release_sleep_lock();
381 return 0;
382}