/* SPDX-License-Identifier: GPL-2.0 */ /** \file audiod_command.c Commands for para_audiod. */ #include "para.h" #include #include #include #include #include #include #include #include "audiod.lsg.h" #include "lsu.h" #include "audiod_cmd.lsg.h" #include "list.h" #include "sched.h" #include "buffer_tree.h" #include "filter.h" #include "grab_client.h" #include "error.h" #include "sideband.h" #include "audiod.h" #include "net.h" #include "daemon.h" #include "string.h" #include "write.h" #include "fd.h" /** \cond doxygen_exclude */ #define OPT_GIVEN(_opt, _cmd, _lpr) (lls_opt_given(lls_opt_result( \ LSG_AUDIOD_CMD_ ## _cmd ## _OPT_ ## _opt, _lpr))) extern struct sched *sched; extern char *stat_item_values[NUM_STAT_ITEMS]; /* The maximal number of simultaneous connections. */ #define MAX_STAT_CLIENTS 50 /* Pointer to a command handler function. */ typedef int (*audiod_cmd_handler_t)(int, struct lls_parse_result *); /* The lopsub user_data pointer. Only the command handler at the moment. */ struct audiod_command_info { audiod_cmd_handler_t handler; /**< Implementation of the command. */ }; /* Define the user_data pointer as expected by lopsub. */ #define EXPORT_AUDIOD_CMD_HANDLER(_cmd) \ /** Implementation of _cmd. */ \ const struct audiod_command_info lsg_audiod_cmd_com_ ## _cmd ## _user_data = { \ .handler = com_ ## _cmd \ }; /* Flags used for the stat command of para_audiod. */ enum stat_client_flags { /** Enable parser-friendly output. */ SCF_PARSER_FRIENDLY = 1, }; static INITIALIZED_LIST_HEAD(client_list); static int num_clients; /* The list of all status items used by para_{server,audiod,gui}. */ const char *status_item_list[] = {STATUS_ITEMS}; /* * Describes one status client of para_audiod. * * There is one instance of this structure per audiod client that has sent * the 'stat' command. These instances are organized as a linked list. */ struct stat_client { /* The stat client's file descriptor. */ int fd; /* Bitmask of those status items the client is interested in. */ uint64_t item_mask; /* See stat_client flags. */ unsigned flags; /* Its entry in the list of stat clients. */ struct list_head node; }; /** \endcond */ /* * Add a status client to the client list and increment num_clients. * The mask parameter specifies which status items are sent to the client. */ static int stat_client_add(int fd, uint64_t mask, unsigned flags) { struct stat_client *new_client; int ret; if (num_clients >= MAX_STAT_CLIENTS) { PARA_ERROR_LOG("maximal number of stat clients (%d) exceeded\n", MAX_STAT_CLIENTS); return -E_TOO_MANY_CLIENTS; } ret = dup(fd); if (ret < 0) return -ERRNO_TO_PARA_ERROR(errno); new_client = zalloc(sizeof(*new_client)); new_client->fd = ret; PARA_INFO_LOG("adding client on fd %d\n", new_client->fd); new_client->item_mask = mask; new_client->flags = flags; para_list_add(&new_client->node, &client_list); num_clients++; return 1; } static void close_stat_client(struct stat_client *sc) { PARA_INFO_LOG("closing client fd %d\n", sc->fd); close(sc->fd); list_del(&sc->node); free(sc); num_clients--; } /** * Empty the status clients list. * * This iterates over the list of connected status clients, closes each client * file descriptor and frees the resources. */ void close_stat_clients(void) { struct stat_client *sc, *tmp; list_for_each_entry_safe(sc, tmp, &client_list, node) close_stat_client(sc); assert(num_clients == 0); } /** * Send a sideband packet to a client. * * Create a header and send header and buffer to the client using \ref * xwritev(). * * \param fd Identifies the client. * \param buf May be NULL. * \param len Number of bytes to send, not including header. * \param band Stored as part of the header. * * \return Standard. */ int client_write_bin(int fd, char *buf, size_t len, enum sb_designator band) { struct sb_context *ctx; struct sb_buffer sbb; struct iovec iov[2]; int ret; sbb.iov.iov_base = buf; sbb.iov.iov_len = len; sbb.band = band; ctx = sb_new_send(&sbb, true, NULL, NULL); do { ret = sb_get_send_buffers(ctx, iov); ret = xwritev(fd, iov, ret); if (ret < 0) { sb_free(ctx); return ret; } } while (sb_sent(ctx, ret) == false); return 1; } static int client_write(int fd, char *buf, enum sb_designator band) { return client_write_bin(fd, buf, buf? strlen(buf) : 0, band); } /** * Write the contents of a status item to all connected status clients. * * \param item_num The number of the status item. * * Removes the status client from the client list on write errors. */ void stat_client_write_item(int item_num) { struct stat_client *sc, *tmp; struct para_buffer pb = {.flags = 0}; struct para_buffer pfpb = {.flags = PBF_SIZE_PREFIX}; const uint64_t one = 1; char *msg = stat_item_values[item_num]; struct para_buffer *b; list_for_each_entry_safe(sc, tmp, &client_list, node) { int ret; if (!((one << item_num) & sc->item_mask)) continue; b = (sc->flags & SCF_PARSER_FRIENDLY)? &pfpb : &pb; if (!b->buf) WRITE_STATUS_ITEM(b, item_num, "%s\n", msg? msg : ""); ret = client_write(sc->fd, b->buf, SBD_OUTPUT); if (ret < 0) { PARA_INFO_LOG("%s: closing stat client on fd %d\n", para_strerror(-ret), sc->fd); close_stat_client(sc); } } free(pb.buf); free(pfpb.buf); } /* Check if the given string is a known status item and return its index. */ static int stat_item_valid(const char *item) { int i; if (!item || !*item) { PARA_ERROR_LOG("%s\n", "no item"); return -E_UNKNOWN_STAT_ITEM; } FOR_EACH_STATUS_ITEM(i) if (!strcmp(status_item_list[i], item)) return i; PARA_ERROR_LOG("invalid stat item: %s\n", item); return -E_UNKNOWN_STAT_ITEM; } __malloc static char *audiod_status_string(void) { const char *status = (audiod_status == AUDIOD_ON)? "on" : (audiod_status == AUDIOD_OFF)? "off": "sb"; return para_strdup(status); } static int com_help(int fd, struct lls_parse_result *lpr) { char *buf; int ret, ret2; bool long_help = OPT_GIVEN(LONG, HELP, lpr); ret = lsu_com_help(long_help, lpr, audiod_cmd_suite, NULL, &buf, NULL); ret2 = client_write(fd, buf, ret < 0? SBD_ERROR_LOG : SBD_OUTPUT); free(buf); return ret < 0? ret : ret2; } EXPORT_AUDIOD_CMD_HANDLER(help) static int com_ll(int fd, struct lls_parse_result *lpr) { unsigned ll; char *errctx; const char *sev[] = {SEVERITIES}; const char *arg; int ret = lls(lls_check_arg_count(lpr, 0, 1, &errctx)); if (ret < 0) { char *tmp = make_message("%s\n", errctx); free(errctx); client_write(fd, tmp, SBD_ERROR_LOG); free(tmp); return ret; } if (lls_num_inputs(lpr) == 0) { char *msg; ll = daemon_get_loglevel(); msg = make_message("%s\n", sev[ll]); ret = client_write(fd, msg, SBD_OUTPUT); free(msg); return ret; } arg = lls_input(0, lpr); for (ll = 0; ll < NUM_LOGLEVELS; ll++) { if (!strcmp(arg, sev[ll])) break; } if (ll >= NUM_LOGLEVELS) return -ERRNO_TO_PARA_ERROR(EINVAL); PARA_INFO_LOG("new log level: %s\n", sev[ll]); daemon_set_loglevel(ll); return 1; } EXPORT_AUDIOD_CMD_HANDLER(ll) static int com_tasks(int fd, __a_unused struct lls_parse_result *lpr) { int ret; char *tl = get_task_list(sched); if (!tl) /* no tasks registered yet */ return 0; ret = client_write(fd, tl, SBD_OUTPUT); free(tl); return ret; } EXPORT_AUDIOD_CMD_HANDLER(tasks) static int com_stat(int fd, struct lls_parse_result *lpr) { int i, ret; bool parser_friendly = OPT_GIVEN(PARSER_FRIENDLY, STAT, lpr); bool one_shot = OPT_GIVEN(ONE_SHOT, STAT, lpr); uint64_t mask = 0; const uint64_t one = 1; struct para_buffer b = {.flags = 0}; unsigned num_inputs, client_flags = 0; ret = mark_fd_nonblocking(fd); if (ret < 0) return ret; if (parser_friendly) { b.flags = PBF_SIZE_PREFIX; client_flags |= SCF_PARSER_FRIENDLY; } num_inputs = lls_num_inputs(lpr); if (num_inputs == 0) mask--; /* set all bits */ for (i = 0; i < num_inputs; i++) { ret = stat_item_valid(lls_input(i, lpr)); if (ret < 0) return ret; mask |= (one << ret); } PARA_INFO_LOG("mask: 0x%llx\n", (long long unsigned)mask); FOR_EACH_STATUS_ITEM(i) { char *item = stat_item_values[i]; if (!((one << i) & mask)) continue; WRITE_STATUS_ITEM(&b, i, "%s\n", item? item : ""); } ret = client_write(fd, b.buf, SBD_OUTPUT); free(b.buf); if (ret < 0) return ret; if (one_shot) return 0; ret = stat_client_add(fd, mask, client_flags); return ret < 0? ret : -E_SUBCMD_RUNNING; } EXPORT_AUDIOD_CMD_HANDLER(stat) static int com_grab(int fd, struct lls_parse_result *lpr) { char buf[] = "The grab subcommand is deprecated.\n"; int ret; PARA_WARNING_LOG("running deprecated grab subcommand on fd %d\n", fd); ret = client_write(fd, buf, SBD_WARNING_LOG); if (ret < 0) return ret; ret = grab_client_new(fd, lpr, sched); return ret < 0? ret : -E_SUBCMD_RUNNING; } EXPORT_AUDIOD_CMD_HANDLER(grab) static int com_term(__a_unused int fd, __a_unused struct lls_parse_result *lpr) { return -E_AUDIOD_TERM; } EXPORT_AUDIOD_CMD_HANDLER(term) static int com_on(__a_unused int fd, __a_unused struct lls_parse_result *lpr) { audiod_status = AUDIOD_ON; return 1; } EXPORT_AUDIOD_CMD_HANDLER(on) static int com_off(__a_unused int fd, __a_unused struct lls_parse_result *lpr) { audiod_status = AUDIOD_OFF; return 1; } EXPORT_AUDIOD_CMD_HANDLER(off) static int com_sb(__a_unused int fd, __a_unused struct lls_parse_result *lpr) { audiod_status = AUDIOD_STANDBY; return 1; } EXPORT_AUDIOD_CMD_HANDLER(sb) static int com_cycle(__a_unused int fd, __a_unused struct lls_parse_result *lpr) { switch (audiod_status) { case AUDIOD_ON: audiod_status = AUDIOD_STANDBY; break; case AUDIOD_OFF: audiod_status = AUDIOD_ON; break; case AUDIOD_STANDBY: audiod_status = AUDIOD_OFF; break; } return 1; } EXPORT_AUDIOD_CMD_HANDLER(cycle) static int com_version(int fd, struct lls_parse_result *lpr) { int ret; char *msg; bool verbose = OPT_GIVEN(VERBOSE, VERSION, lpr); if (verbose) msg = make_message("%s", version_text("audiod")); else msg = make_message("%s\n", version_single_line("audiod")); ret = client_write(fd, msg, SBD_OUTPUT); free(msg); return ret < 0? ret : 0; } EXPORT_AUDIOD_CMD_HANDLER(version) #ifndef HAVE_UCRED static int recv_cred_buffer(int fd, struct iovec *iov, uid_t *uid) { *uid = 1; return recv_buffer(fd, iov->iov_base, iov->iov_len); } #else /* HAVE_UCRED */ static void dispose_fds(int *fds, unsigned num) { int i; for (i = 0; i < num; i++) close(fds[i]); } /* * Receive a buffer and the Unix credentials of the sending process. * Returns negative on errors, the number of bytes received on success. The * user id of the sending process is returned via the uid pointer argument. */ static int recv_cred_buffer(int fd, struct iovec *iov, uid_t *uid) { char control[255] __a_aligned(8); struct msghdr msg; struct cmsghdr *cmsg; int ret, yes = 1; struct ucred cred; bool have_uid = false; *uid = -1; setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &yes, sizeof(int)); memset(&msg, 0, sizeof(msg)); memset(iov->iov_base, 0, iov->iov_len); msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_control = control; msg.msg_controllen = sizeof(control); ret = recvmsg(fd, &msg, 0); if (ret < 0) return -ERRNO_TO_PARA_ERROR(errno); cmsg = CMSG_FIRSTHDR(&msg); while (cmsg) { if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_CREDENTIALS) { memcpy(&cred, CMSG_DATA(cmsg), sizeof(struct ucred)); *uid = cred.uid; have_uid = true; } else if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { dispose_fds((int *)CMSG_DATA(cmsg), (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int)); } cmsg = CMSG_NXTHDR(&msg, cmsg); } return have_uid? ret: -E_SCM_CREDENTIALS; } #endif /* HAVE_UCRED */ /** * Handle arriving connections on the local socket. * * \param accept_fd The fd to accept connections on. * * This is called in each iteration of the main loop of the scheduler. If there * is an incoming connection, the function reads the command sent by the peer, * checks the connecting user's permissions by using unix socket credentials * (if supported by the OS) and calls the corresponding command handler if * permissions are OK. * * \return Positive on success, negative on errors, zero if there was no * connection to accept. * * \sa \ref para_accept(). */ int dispatch_local_connection(int accept_fd) { int argc, ret, clifd; char buf[MAXLINE], **argv = NULL; struct iovec iov = {.iov_base = buf, .iov_len = sizeof(buf) - 1}; struct sockaddr_un unix_addr; uid_t uid; const struct lls_command *cmd; struct lls_parse_result *lpr; char *errctx = NULL; const struct audiod_command_info *aci; size_t received; char *p, *end; ret = para_accept(accept_fd, &unix_addr, sizeof(struct sockaddr_un), &clifd); if (ret <= 0) return ret; ret = recv_cred_buffer(clifd, &iov, &uid); if (ret < 0) goto out; received = ret; #ifdef HAVE_UCRED ret = -E_UCRED_PERM; if (!uid_is_whitelisted(uid)) goto close_clifd; #endif p = buf + 1; end = buf + received; argc = 0; while (p < end) { argc++; p = memchr(p, '\0', end - p); ret = -ERRNO_TO_PARA_ERROR(EINVAL); if (!p) goto close_clifd; p++; } argv = alloc((argc + 1) * sizeof(char *)); p = buf + 1; for (unsigned n = 0; n < argc; n++) { argv[n] = para_strdup(p); p += strlen(p) + 1; } argv[argc] = NULL; ret = lls(lls_lookup_subcmd(argv[0], audiod_cmd_suite, &errctx)); if (ret < 0) goto free_argv; cmd = lls_cmd(ret, audiod_cmd_suite); ret = lls(lls_parse(argc, argv, cmd, &lpr, &errctx)); if (ret < 0) goto free_argv; aci = lls_user_data(cmd); ret = aci->handler(clifd, lpr); lls_free_parse_result(lpr, cmd); free_argv: free_argv(argv); close_clifd: if (ret == -E_CLIENT_WRITE) goto out; if (ret == -E_SUBCMD_RUNNING) { ret = 0; goto out; } if (ret < 0) { char *tmp; if (errctx) { tmp = make_message("errctx: %s\n", errctx); free(errctx); client_write(clifd, tmp, SBD_ERROR_LOG); free(tmp); } tmp = make_message("%s\n", para_strerror(-ret)); client_write(clifd, tmp, SBD_ERROR_LOG); free(tmp); client_write(clifd, NULL, ret == -E_AUDIOD_TERM? SBD_EXIT__SUCCESS : SBD_EXIT__FAILURE); goto out; } client_write(clifd, NULL, SBD_EXIT__SUCCESS); out: close(clifd); return ret; } /** * Send the current audiod status to all connected stat clients. * * \param force Whether to write unchanged items. */ void audiod_status_dump(bool force) { char *old, *new; old = stat_item_values[SI_play_time]; new = get_time_string(); if (new) { if (force || !old || strcmp(old, new)) { free(old); stat_item_values[SI_play_time] = new; stat_client_write_item(SI_play_time); } else free(new); } new = daemon_get_uptime_str(now); old = stat_item_values[SI_audiod_uptime]; if (force || !old || strcmp(old, new)) { free(old); stat_item_values[SI_audiod_uptime] = new; stat_client_write_item(SI_audiod_uptime); } else free(new); old = stat_item_values[SI_audiod_status]; new = audiod_status_string(); if (force || !old || strcmp(old, new)) { free(old); stat_item_values[SI_audiod_status] = new; stat_client_write_item(SI_audiod_status); } else free(new); old = stat_item_values[SI_decoder_flags]; new = audiod_get_decoder_flags(); if (force || !old || strcmp(old, new)) { free(old); stat_item_values[SI_decoder_flags] = new; stat_client_write_item(SI_decoder_flags); } else free(new); }