Blob


1 /*
2 * Copyright (c) 2022 Omar Polo <op@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include <sys/types.h>
18 #include <sys/socket.h>
19 #include <sys/queue.h>
20 #include <sys/uio.h>
21 #include <sys/un.h>
23 #include <errno.h>
24 #include <event.h>
25 #include <fcntl.h>
26 #include <limits.h>
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <stdint.h>
30 #include <string.h>
31 #include <syslog.h>
32 #include <unistd.h>
33 #include <imsg.h>
35 #include "amused.h"
36 #include "log.h"
37 #include "playlist.h"
38 #include "xmalloc.h"
40 static struct imsgbuf *ibuf;
42 int ctl_noarg(struct parse_result *, int, char **);
43 int ctl_add(struct parse_result *, int, char **);
44 int ctl_show(struct parse_result *, int, char **);
45 int ctl_load(struct parse_result *, int, char **);
46 int ctl_jump(struct parse_result *, int, char **);
47 int ctl_repeat(struct parse_result *, int, char **);
48 int ctl_monitor(struct parse_result *, int, char **);
50 struct ctl_command ctl_commands[] = {
51 { "play", PLAY, ctl_noarg, "" },
52 { "pause", PAUSE, ctl_noarg, "" },
53 { "toggle", TOGGLE, ctl_noarg, "" },
54 { "stop", STOP, ctl_noarg, "" },
55 { "restart", RESTART, ctl_noarg, "" },
56 { "add", ADD, ctl_add, "files...", 1 },
57 { "flush", FLUSH, ctl_noarg, "" },
58 { "show", SHOW, ctl_show, "[-p]" },
59 { "status", STATUS, ctl_noarg, "" },
60 { "next", NEXT, ctl_noarg, "" },
61 { "prev", PREV, ctl_noarg, "" },
62 { "load", LOAD, ctl_load, "[file]", 1 },
63 { "jump", JUMP, ctl_jump, "pattern" },
64 { "repeat", REPEAT, ctl_repeat, "one|all on|off" },
65 { "monitor", MONITOR, ctl_monitor, "[events]" },
66 { NULL },
67 };
69 __dead void
70 usage(void)
71 {
72 fprintf(stderr, "usage: %s [-dv] [-s socket]\n", getprogname());
73 exit(1);
74 }
76 static __dead void
77 ctl_usage(struct ctl_command *ctl)
78 {
79 fprintf(stderr, "usage: %s [-v] [-s socket] %s %s\n", getprogname(),
80 ctl->name, ctl->usage);
81 exit(1);
82 }
84 static int
85 parse(int argc, char **argv)
86 {
87 struct ctl_command *ctl = NULL;
88 struct parse_result res;
89 const char *argv0;
90 int i, status;
92 memset(&res, 0, sizeof(res));
94 if ((argv0 = argv[0]) == NULL)
95 argv0 = "status";
97 for (i = 0; ctl_commands[i].name != NULL; ++i) {
98 if (strncmp(ctl_commands[i].name, argv0, strlen(argv0))
99 == 0) {
100 if (ctl != NULL) {
101 fprintf(stderr,
102 "ambiguous argument: %s\n", argv0);
103 usage();
105 ctl = &ctl_commands[i];
109 if (ctl == NULL) {
110 fprintf(stderr, "unknown argument: %s\n", argv[0]);
111 usage();
114 res.action = ctl->action;
115 res.ctl = ctl;
117 if (!ctl->has_pledge) {
118 /* pledge(2) default if command doesn't have its own */
119 if (pledge("stdio", NULL) == -1)
120 fatal("pledge");
123 status = ctl->main(&res, argc, argv);
124 close(ibuf->fd);
125 free(ibuf);
126 return status;
129 static int
130 load_files(struct parse_result *res, int *ret)
132 FILE *f;
133 const char *file;
134 char *line = NULL;
135 char path[PATH_MAX], cwd[PATH_MAX];
136 size_t linesize = 0, i = 0, n;
137 ssize_t linelen, curr = -1;
139 if (res->file == NULL)
140 f = stdin;
141 else if ((f = fopen(res->file, "r")) == NULL) {
142 log_warn("can't open %s", res->file);
143 *ret = 1;
144 return 1;
147 if (getcwd(cwd, sizeof(cwd)) == NULL)
148 fatal("getcwd");
150 while ((linelen = getline(&line, &linesize, f)) != -1) {
151 if (linelen == 0)
152 continue;
153 line[linelen-1] = '\0';
154 file = line;
155 if (!strncmp(file, "> ", 2)) {
156 file += 2;
157 curr = i;
158 } else if (!strncmp(file, " ", 2))
159 file += 2;
161 if (!strncmp(file, "./", 2))
162 file += 2;
164 memset(path, 0, sizeof(path));
165 if (*file == '/')
166 n = strlcpy(path, file, sizeof(path));
167 else
168 n = snprintf(path, sizeof(path), "%s/%s", cwd, file);
170 if (n >= sizeof(path)) {
171 log_warnx("path too long: %s", file);
172 continue;
175 i++;
176 imsg_compose(ibuf, IMSG_CTL_ADD, 0, 0, -1,
177 path, sizeof(path));
180 free(line);
181 if (ferror(f))
182 fatal("getline");
183 fclose(f);
185 if (i == 0) {
186 *ret = 1;
187 return 1;
190 imsg_compose(ibuf, IMSG_CTL_COMMIT, 0, 0, -1,
191 &curr, sizeof(curr));
192 imsg_flush(ibuf);
193 return 0;
196 static const char *
197 imsg_strerror(struct imsg *imsg)
199 size_t datalen;
200 const char *msg;
202 datalen = IMSG_DATA_SIZE(*imsg);
203 msg = imsg->data;
204 if (datalen == 0 || msg[datalen-1] != '\0')
205 fatalx("malformed error message");
207 return msg;
210 static const char *
211 imsg_name(int type)
213 switch (type) {
214 case IMSG_CTL_PLAY:
215 return "play";
216 case IMSG_CTL_TOGGLE_PLAY:
217 return "toggle";
218 case IMSG_CTL_PAUSE:
219 return "pause";
220 case IMSG_CTL_STOP:
221 return "stop";
222 case IMSG_CTL_RESTART:
223 return "restart";
224 case IMSG_CTL_FLUSH:
225 return "flush";
226 case IMSG_CTL_NEXT:
227 return "next";
228 case IMSG_CTL_PREV:
229 return "prev";
230 case IMSG_CTL_JUMP:
231 return "jump";
232 case IMSG_CTL_REPEAT:
233 return "repeat";
234 case IMSG_CTL_ADD:
235 return "add";
236 case IMSG_CTL_COMMIT:
237 return "load";
238 default:
239 return "unknown";
243 static int
244 ctlaction(struct parse_result *res)
246 char path[PATH_MAX];
247 struct imsg imsg;
248 struct player_status ps;
249 size_t datalen;
250 ssize_t n;
251 int i, type, ret = 0, done = 1;
253 switch (res->action) {
254 case PLAY:
255 imsg_compose(ibuf, IMSG_CTL_PLAY, 0, 0, -1, NULL, 0);
256 if (verbose) {
257 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1,
258 NULL, 0);
259 done = 0;
261 break;
262 case PAUSE:
263 imsg_compose(ibuf, IMSG_CTL_PAUSE, 0, 0, -1, NULL, 0);
264 break;
265 case TOGGLE:
266 imsg_compose(ibuf, IMSG_CTL_TOGGLE_PLAY, 0, 0, -1, NULL, 0);
267 if (verbose) {
268 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1,
269 NULL, 0);
270 done = 0;
272 break;
273 case STOP:
274 imsg_compose(ibuf, IMSG_CTL_STOP, 0, 0, -1, NULL, 0);
275 break;
276 case RESTART:
277 imsg_compose(ibuf, IMSG_CTL_RESTART, 0, 0, -1, NULL, 0);
278 if (verbose) {
279 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1,
280 NULL, 0);
281 done = 0;
283 break;
284 case ADD:
285 done = 0;
286 for (i = 0; res->files[i] != NULL; ++i) {
287 memset(&path, 0, sizeof(path));
288 if (realpath(res->files[i], path) == NULL) {
289 log_warn("realpath %s", res->files[i]);
290 continue;
293 imsg_compose(ibuf, IMSG_CTL_ADD, 0, 0, -1,
294 path, sizeof(path));
296 ret = i == 0;
297 break;
298 case FLUSH:
299 imsg_compose(ibuf, IMSG_CTL_FLUSH, 0, 0, -1, NULL, 0);
300 break;
301 case SHOW:
302 done = 0;
303 imsg_compose(ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
304 break;
305 case STATUS:
306 done = 0;
307 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1, NULL, 0);
308 break;
309 case NEXT:
310 imsg_compose(ibuf, IMSG_CTL_NEXT, 0, 0, -1, NULL, 0);
311 if (verbose) {
312 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1,
313 NULL, 0);
314 done = 0;
316 break;
317 case PREV:
318 imsg_compose(ibuf, IMSG_CTL_PREV, 0, 0, -1, NULL, 0);
319 if (verbose) {
320 imsg_compose(ibuf, IMSG_CTL_STATUS, 0, 0, -1,
321 NULL, 0);
322 done = 0;
324 break;
325 case LOAD:
326 done = 0;
327 imsg_compose(ibuf, IMSG_CTL_BEGIN, 0, 0, -1, NULL, 0);
328 break;
329 case JUMP:
330 done = 0;
331 memset(path, 0, sizeof(path));
332 strlcpy(path, res->file, sizeof(path));
333 imsg_compose(ibuf, IMSG_CTL_JUMP, 0, 0, -1,
334 path, sizeof(path));
335 break;
336 case REPEAT:
337 imsg_compose(ibuf, IMSG_CTL_REPEAT, 0, 0, -1,
338 &res->rep, sizeof(res->rep));
339 break;
340 case MONITOR:
341 done = 0;
342 imsg_compose(ibuf, IMSG_CTL_MONITOR, 0, 0, -1,
343 NULL, 0);
344 break;
345 case NONE:
346 /* action not expected */
347 fatalx("invalid action %u", res->action);
348 break;
351 if (ret != 0)
352 goto end;
354 imsg_flush(ibuf);
356 i = 0;
357 while (!done) {
358 if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
359 fatalx("imsg_read error");
360 if (n == 0)
361 fatalx("pipe closed");
363 while (!done) {
364 if ((n = imsg_get(ibuf, &imsg)) == -1)
365 fatalx("imsg_get error");
366 if (n == 0)
367 break;
369 if (imsg.hdr.type == IMSG_CTL_ERR) {
370 log_warnx("%s: %s", res->ctl->name,
371 imsg_strerror(&imsg));
372 ret = 1;
373 done = 1;
374 break;
377 datalen = IMSG_DATA_SIZE(imsg);
379 switch (res->action) {
380 case ADD:
381 if (res->files[i] == NULL)
382 fatalx("received more replies than "
383 "files enqueued.");
385 if (imsg.hdr.type == IMSG_CTL_ADD)
386 log_debug("enqueued %s", res->files[i]);
387 else
388 fatalx("invalid message %d",
389 imsg.hdr.type);
390 i++;
391 done = res->files[i] == NULL;
392 break;
393 case SHOW:
394 if (datalen == 0) {
395 done = 1;
396 break;
398 if (datalen != sizeof(ps))
399 fatalx("data size mismatch");
400 memcpy(&ps, imsg.data, sizeof(ps));
401 if (ps.path[sizeof(ps.path) - 1] != '\0')
402 fatalx("received corrupted data");
403 if (res->pretty) {
404 char c = ' ';
405 if (ps.status == STATE_PLAYING)
406 c = '>';
407 printf("%c ", c);
409 puts(ps.path);
410 break;
411 case PLAY:
412 case TOGGLE:
413 case RESTART:
414 case STATUS:
415 case NEXT:
416 case PREV:
417 case JUMP:
418 if (imsg.hdr.type != IMSG_CTL_STATUS)
419 fatalx("invalid message %d",
420 imsg.hdr.type);
422 if (datalen != sizeof(ps))
423 fatalx("data size mismatch");
424 memcpy(&ps, imsg.data, sizeof(ps));
425 if (ps.path[sizeof(ps.path) - 1] != '\0')
426 fatalx("received corrupted data");
428 if (ps.status == STATE_STOPPED)
429 printf("stopped ");
430 else if (ps.status == STATE_PLAYING)
431 printf("playing ");
432 else if (ps.status == STATE_PAUSED)
433 printf("paused ");
434 else
435 printf("unknown ");
437 puts(ps.path);
438 printf("repat one %s\nrepeat all %s\n",
439 ps.rp.repeat_one ? "on" : "off",
440 ps.rp.repeat_all ? "on" : "off");
442 done = 1;
443 break;
444 case LOAD:
445 if (imsg.hdr.type == IMSG_CTL_ADD)
446 break;
447 if (imsg.hdr.type == IMSG_CTL_COMMIT) {
448 done = 1;
449 break;
452 if (imsg.hdr.type != IMSG_CTL_BEGIN)
453 fatalx("invalid message %d",
454 imsg.hdr.type);
456 load_files(res, &ret);
457 break;
458 case MONITOR:
459 if (imsg.hdr.type != IMSG_CTL_MONITOR)
460 fatalx("invalid message %d",
461 imsg.hdr.type);
463 if (datalen != sizeof(type))
464 fatalx("data size mismatch");
466 memcpy(&type, imsg.data, sizeof(type));
467 if (type < 0 || type > IMSG__LAST)
468 fatalx("received corrupted data");
470 if (!res->monitor[type])
471 break;
473 puts(imsg_name(type));
474 fflush(stdout);
475 break;
476 default:
477 done = 1;
478 break;
481 imsg_free(&imsg);
485 end:
486 return ret;
489 int
490 ctl_noarg(struct parse_result *res, int argc, char **argv)
492 int ch;
494 while ((ch = getopt(argc, argv, "")) != -1)
495 ctl_usage(res->ctl);
496 argc -= optind;
497 argv += optind;
499 if (argc > 0)
500 ctl_usage(res->ctl);
502 return ctlaction(res);
505 int
506 ctl_add(struct parse_result *res, int argc, char **argv)
508 int ch;
510 while ((ch = getopt(argc, argv, "")) != -1)
511 ctl_usage(res->ctl);
512 argc -= optind;
513 argv += optind;
515 if (argc == 0)
516 ctl_usage(res->ctl);
517 res->files = argv;
519 if (pledge("stdio rpath", NULL) == -1)
520 fatal("pledge");
522 return ctlaction(res);
525 int
526 ctl_show(struct parse_result *res, int argc, char **argv)
528 int ch;
530 while ((ch = getopt(argc, argv, "p")) != -1) {
531 switch (ch) {
532 case 'p':
533 res->pretty = 1;
534 break;
535 default:
536 ctl_usage(res->ctl);
540 return ctlaction(res);
543 int
544 ctl_load(struct parse_result *res, int argc, char **argv)
546 int ch;
548 while ((ch = getopt(argc, argv, "")) != -1)
549 ctl_usage(res->ctl);
550 argc -= optind;
551 argv += optind;
553 if (argc == 0)
554 res->file = NULL;
555 else if (argc == 1)
556 res->file = argv[0];
557 else
558 ctl_usage(res->ctl);
560 if (pledge("stdio rpath", NULL) == -1)
561 fatal("pledge");
563 return ctlaction(res);
566 int
567 ctl_jump(struct parse_result *res, int argc, char **argv)
569 int ch;
571 while ((ch = getopt(argc, argv, "")) != -1)
572 ctl_usage(res->ctl);
573 argc -= optind;
574 argv += optind;
576 if (argc != 1)
577 ctl_usage(res->ctl);
579 res->file = argv[0];
580 return ctlaction(res);
583 int
584 ctl_repeat(struct parse_result *res, int argc, char **argv)
586 int ch, b;
588 while ((ch = getopt(argc, argv, "")) != -1)
589 ctl_usage(res->ctl);
590 argc -= optind;
591 argv += optind;
593 if (argc != 2)
594 ctl_usage(res->ctl);
596 if (!strcmp(argv[1], "on"))
597 b = 1;
598 else if (!strcmp(argv[1], "off"))
599 b = 0;
600 else
601 ctl_usage(res->ctl);
603 res->rep.repeat_one = -1;
604 res->rep.repeat_all = -1;
605 if (!strcmp(argv[0], "one"))
606 res->rep.repeat_one = b;
607 else if (!strcmp(argv[0], "all"))
608 res->rep.repeat_all = b;
609 else
610 ctl_usage(res->ctl);
612 return ctlaction(res);
615 int
616 ctl_monitor(struct parse_result *res, int argc, char **argv)
618 int ch;
619 const char *events;
620 char *dup, *tmp, *tok;
622 while ((ch = getopt(argc, argv, "")) != -1)
623 ctl_usage(res->ctl);
624 argc -= optind;
625 argv += optind;
627 if (argc > 1)
628 ctl_usage(res->ctl);
630 if (argc == 1)
631 events = *argv;
632 else
633 events = "play,toggle,pause,stop,restart,flush,next,prev,"
634 "jump,repeat,add,load";
636 tmp = dup = xstrdup(events);
637 while ((tok = strsep(&tmp, ",")) != NULL) {
638 if (*tok == '\0')
639 continue;
641 if (!strcmp(tok, "play"))
642 res->monitor[IMSG_CTL_PLAY] = 1;
643 else if (!strcmp(tok, "toggle"))
644 res->monitor[IMSG_CTL_TOGGLE_PLAY] = 1;
645 else if (!strcmp(tok, "pause"))
646 res->monitor[IMSG_CTL_PAUSE] = 1;
647 else if (!strcmp(tok, "stop"))
648 res->monitor[IMSG_CTL_STOP] = 1;
649 else if (!strcmp(tok, "restart"))
650 res->monitor[IMSG_CTL_RESTART] = 1;
651 else if (!strcmp(tok, "flush"))
652 res->monitor[IMSG_CTL_FLUSH] = 1;
653 else if (!strcmp(tok, "next"))
654 res->monitor[IMSG_CTL_NEXT] = 1;
655 else if (!strcmp(tok, "prev"))
656 res->monitor[IMSG_CTL_PREV] = 1;
657 else if (!strcmp(tok, "jump"))
658 res->monitor[IMSG_CTL_JUMP] = 1;
659 else if (!strcmp(tok, "repeat"))
660 res->monitor[IMSG_CTL_REPEAT] = 1;
661 else if (!strcmp(tok, "add"))
662 res->monitor[IMSG_CTL_ADD] = 1;
663 else if (!strcmp(tok, "load"))
664 res->monitor[IMSG_CTL_COMMIT] = 1;
665 else
666 fatalx("unknown event \"%s\"", tok);
669 free(dup);
670 return ctlaction(res);
673 static int
674 ctl_get_lock(const char *lockfile)
676 int lockfd;
678 if ((lockfd = open(lockfile, O_WRONLY|O_CREAT, 0600)) == -1) {
679 log_debug("open failed: %s", strerror(errno));
680 return -1;
683 if (flock(lockfd, LOCK_EX|LOCK_NB) == -1) {
684 log_debug("flock failed: %s", strerror(errno));
685 if (errno != EAGAIN)
686 return -1;
687 while (flock(lockfd, LOCK_EX) == -1 && errno == EINTR)
688 /* nop */;
689 close(lockfd);
690 return -2;
692 log_debug("flock succeeded");
694 return lockfd;
697 static int
698 ctl_connect(void)
700 struct timespec ts = { 0, 50000000 }; /* 0.05 seconds */
701 struct sockaddr_un sa;
702 size_t size;
703 int fd, lockfd = -1, locked = 0, spawned = 0;
704 char *lockfile = NULL;
706 memset(&sa, 0, sizeof(sa));
707 sa.sun_family = AF_UNIX;
708 size = strlcpy(sa.sun_path, csock, sizeof(sa.sun_path));
709 if (size >= sizeof(sa.sun_path)) {
710 errno = ENAMETOOLONG;
711 return -1;
714 retry:
715 if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
716 return -1;
718 if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
719 log_debug("connection failed: %s", strerror(errno));
720 if (errno != ECONNREFUSED && errno != ENOENT)
721 goto failed;
722 close(fd);
724 if (!locked) {
725 xasprintf(&lockfile, "%s.lock", csock);
726 if ((lockfd = ctl_get_lock(lockfile)) < 0) {
727 log_debug("didn't get the lock (%d)", lockfd);
729 free(lockfile);
730 lockfile = NULL;
732 if (lockfd == -1)
733 goto retry;
736 /*
737 * Always retry at least once, even if we got
738 * the lock, because another client could have
739 * taken the lock, started the server and released
740 * the lock between our connect() and flock()
741 */
742 locked = 1;
743 goto retry;
746 if (!spawned) {
747 log_debug("spawning the daemon");
748 spawn_daemon();
749 spawned = 1;
752 nanosleep(&ts, NULL);
753 goto retry;
756 if (locked && lockfd >= 0) {
757 unlink(lockfile);
758 free(lockfile);
759 close(lockfd);
761 return fd;
763 failed:
764 if (locked) {
765 free(lockfile);
766 close(lockfd);
768 close(fd);
769 return -1;
772 __dead void
773 ctl(int argc, char **argv)
775 int ctl_sock;
777 log_init(1, LOG_DAEMON);
778 log_setverbose(verbose);
780 if ((ctl_sock = ctl_connect()) == -1)
781 fatal("can't connect");
783 if (ctl_sock == -1)
784 fatalx("failed to connect to the daemon");
786 ibuf = xmalloc(sizeof(*ibuf));
787 imsg_init(ibuf, ctl_sock);
789 optreset = 1;
790 optind = 1;
792 exit(parse(argc, argv));