/* RetroArch - A frontend for libretro. * Copyright (C) 2010-2014 - Hans-Kristian Arntzen * Copyright (C) 2011-2017 - Daniel De Matteis * Copyright (C) 2021 - David G.F. * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include "input/input_driver.h" #include #include #include #include #include #include #ifdef HAVE_NETWORKING #include #include #endif #include #include #include #include #include #include #ifdef HAVE_CONFIG_H #include "config.h" #endif #ifdef HAVE_CHEEVOS #include "cheevos/cheevos.h" #endif #ifdef HAVE_GFX_WIDGETS #include "gfx/gfx_widgets.h" #endif #ifdef HAVE_MENU #include "menu/menu_driver.h" #endif #ifdef HAVE_NETWORKING #include "network/netplay/netplay.h" #endif #include "audio/audio_driver.h" #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) #include "gfx/video_shader_parse.h" #endif #include "autosave.h" #include "command.h" #include "core.h" #include "core_info.h" #include "playlist.h" #include #include "cheat_manager.h" #include "content.h" #include "dynamic.h" #include "file_path_special.h" #include "list_special.h" #include "paths.h" #include "record/record_driver.h" #include "retroarch.h" #include "runloop.h" #include "verbosity.h" #include "version.h" #include "version_git.h" #include "tasks/task_content.h" #ifdef HAVE_TRANSLATE #include "accessibility.h" #endif #ifdef HAVE_SCREENSHOTS #include "tasks/tasks_internal.h" #endif #include #ifdef __MACH__ #include #endif #define CMD_BUF_SIZE 4096 /* Replies with a failure and why, and marks the reply as one: a line * client reads the text, a structured one is told outright. The text * opens with @name and ERROR, as a deferred reply's does. */ void command_reply_error(command_t *cmd, const char *name, const char *fmt, ...) { char msg[512]; int n; size_t _len; va_list ap; if (!cmd || !cmd->replier) return; /* Both of these report the length they wanted rather than the one * they wrote, so each is brought back inside the buffer before the * next writes past its end. */ if ((n = snprintf(msg, sizeof(msg), "%s ERROR ", name)) < 0) return; _len = (size_t)n; if (_len >= sizeof(msg)) _len = sizeof(msg) - 1; va_start(ap, fmt); n = vsnprintf(msg + _len, sizeof(msg) - _len, fmt, ap); va_end(ap); if (n > 0) { _len += (size_t)n; if (_len >= sizeof(msg)) _len = sizeof(msg) - 1; } cmd->error = true; cmd->replier(cmd, msg, _len); } #ifdef HAVE_COMMAND /* a queued STEPS was dropped since the reply now sent began */ static bool command_queue_ended; #endif static void command_post_state_loaded(void) { #ifdef HAVE_COMMAND /* steps queued from the state it left would play on from this one */ command_run_frames_forget(); #endif #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { const char *_msg = msg_hash_to_str(MSG_CHEEVOS_HARDCORE_MODE_DISABLED); rcheevos_pause_hardcore(); runloop_msg_queue_push(_msg, strlen(_msg), 0, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); } #endif #ifdef HAVE_NETWORKING netplay_driver_ctl(RARCH_NETPLAY_CTL_LOAD_SAVESTATE, NULL); #endif { video_driver_state_t *video_st = video_state_get_ptr(); bool frame_time_counter_auto_reset = config_get_ptr()->bools.frame_time_counter_auto_reset; if (frame_time_counter_auto_reset) video_st->frame_time_count = 0; } #if defined(HAVE_GFX_WIDGETS) && defined(HAVE_SCREENSHOTS) { gfx_widget_state_slot_show(dispwidget_get_ptr(), NULL, NULL); } #endif } #if defined(HAVE_COMMAND) /* Generic command parse utilities */ static bool command_get_arg(const char *tok, const char **arg, unsigned *index) { unsigned i; for (i = 0; i < ARRAY_SIZE(map); i++) { if (string_is_equal(tok, map[i].str)) { if (arg) *arg = NULL; if (index) *index = i; return true; } } for (i = 0; i < ARRAY_SIZE(action_map); i++) { const char *str = strstr(tok, action_map[i].str); if (str == tok) { const char *argument = str + strlen(action_map[i].str); if (!argument) return false; if (*argument != ' ' && *argument != '\0') return false; /* past the separating space; a bare command gets "", not the * byte after its terminator */ if (arg) *arg = *argument ? argument + 1 : argument; if (index) *index = i; return true; } } return false; } static void command_parse_sub_msg(command_t *handle, const char *tok) { const char *arg = NULL; unsigned index = 0; handle->error = false; if (command_get_arg(tok, &arg, &index)) { if (arg) { if (!action_map[index].action(handle, arg)) RARCH_ERR("[Command] Command \"%s\" failed.\n", arg); } else { /* For MENU_TOGGLE, bypass the press-and-release mechanism * by directly invoking the command event. This avoids * timing issues with runahead (single-instance) where the * 1-frame pulse from network commands can be lost. */ if (map[index].id == RARCH_MENU_TOGGLE) command_event(CMD_EVENT_MENU_TOGGLE, NULL); else handle->state[map[index].id] = true; } } else RARCH_WARN(msg_hash_to_str(MSG_UNRECOGNIZED_COMMAND), tok); } static void command_parse_msg(command_t *handle, char *buf) { char *save = NULL; char *tok = strtok_r(buf, "\n", &save); unsigned gen = input_driver_command_generation(); while (tok) { /* a client ending its lines in CRLF leaves no \r on the last word; * a \r anywhere else is the command's own */ size_t len = strlen(tok); if (len && tok[len - 1] == '\r') tok[len - 1] = '\0'; command_parse_sub_msg(handle, tok); /* The command may have reinitialised the input driver, which * owns and has now freed 'handle'. Nothing further may use it. */ if (input_driver_command_generation() != gen) return; tok = strtok_r(NULL, "\n", &save); } } #if defined(HAVE_NETWORK_CMD) typedef struct { /* Network socket FD */ int net_fd; /* Source address for the command received */ struct sockaddr_storage cmd_source; /* Size of the previous structure in use */ socklen_t cmd_source_len; } command_network_t; static void network_command_reply(command_t *cmd, const char *s, size_t len) { command_network_t *netcmd = (command_network_t*)cmd->userptr; /* Respond (fire and forget since it's UDP) */ sendto(netcmd->net_fd, s, len, 0, (struct sockaddr*)&netcmd->cmd_source, netcmd->cmd_source_len); } typedef struct { struct sockaddr_storage addr; socklen_t addr_len; } command_network_dest_t; static void *network_command_reply_dest(command_t *cmd) { command_network_t *netcmd = (command_network_t*)cmd->userptr; command_network_dest_t *dest = (command_network_dest_t*) malloc(sizeof(*dest)); if (dest) { dest->addr = netcmd->cmd_source; dest->addr_len = netcmd->cmd_source_len; } return dest; } static void network_command_reply_to(command_t *cmd, void *data, const char *s, size_t len) { command_network_t *netcmd = (command_network_t*)cmd->userptr; command_network_dest_t *dest = (command_network_dest_t*)data; sendto(netcmd->net_fd, s, len, 0, (struct sockaddr*)&dest->addr, dest->addr_len); } static void network_command_free(command_t *handle) { command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd >= 0) socket_close(netcmd->net_fd); free(netcmd); free(handle); } static void command_network_poll(command_t *handle) { ssize_t ret; unsigned gen = input_driver_command_generation(); char buf[2048]; command_network_t *netcmd = (command_network_t*)handle->userptr; if (netcmd->net_fd < 0) return; for (;;) { netcmd->cmd_source_len = sizeof(netcmd->cmd_source); if ((ret = recvfrom(netcmd->net_fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&netcmd->cmd_source, &netcmd->cmd_source_len)) <= 0) return; buf[ret] = '\0'; command_parse_msg(handle, buf); /* See command_generation: the command may have freed this * object. The remaining datagrams wait for the next poll. */ if ( input_driver_command_generation() != gen || command_interfaces_held()) return; } } command_t* command_network_new(uint16_t port, const char *bind_address) { struct addrinfo *res = NULL; command_t *cmd = NULL; command_network_t *netcmd = NULL; int fd = -1; /* Allocate-then-check in sequence, matching command_uds_new. * The previous code allocated cmd + netcmd back-to-back and then * assigned netcmd->net_fd / cmd->userptr before NULL-checking * either pointer - an OOM on either calloc would NULL-deref on * line 252/253. It also leaked netcmd on the '!cmd' failure * path via the 'error' label (which now cleanly frees both * because they are initialised to NULL up-front; free(NULL) is * a no-op). */ if (!(cmd = (command_t*)calloc(1, sizeof(*cmd)))) goto error; if (!(netcmd = (command_network_t*)calloc(1, sizeof(command_network_t)))) goto error; /* An empty bind address means every interface (NULL server -> * AI_PASSIVE -> 0.0.0.0), which is what this interface has always * done. Anyone on that network can then send LOAD_CORE or * WRITE_CORE_RAM, so users who only drive RetroArch from the same * machine can set network_cmd_bind_address = "127.0.0.1". */ if (bind_address && !*bind_address) bind_address = NULL; fd = socket_init((void**)&res, port, bind_address, SOCKET_TYPE_DATAGRAM, AF_INET); RARCH_LOG("[NetCMD] %s %hu (%s).\n", msg_hash_to_str(MSG_BRINGING_UP_COMMAND_INTERFACE_ON_PORT), (unsigned short)port, bind_address ? bind_address : "all interfaces"); if (fd < 0) goto error; netcmd->net_fd = fd; cmd->userptr = netcmd; cmd->poll = command_network_poll; cmd->replier = network_command_reply; cmd->reply_dest = network_command_reply_dest; cmd->reply_to = network_command_reply_to; cmd->destroy = network_command_free; if (!socket_nonblock(netcmd->net_fd)) goto error; if (!socket_bind(netcmd->net_fd, (void*)res)) { RARCH_ERR("[NetCMD] %s\n", msg_hash_to_str(MSG_FAILED_TO_BIND_SOCKET)); goto error; } freeaddrinfo_retro(res); return cmd; error: if (res) freeaddrinfo_retro(res); /* The only path that reaches 'error' with fd >= 0 is the * socket_nonblock / socket_bind failure after fd was already * stored in netcmd->net_fd. Close it so we don't leak the * socket file descriptor along with the allocations. */ if (fd >= 0) socket_close(fd); free(netcmd); free(cmd); return NULL; } #endif #if defined(HAVE_STDIN_CMD) typedef struct { /* Buffer and pointer for stdin reads */ size_t stdin_buf_ptr; char stdin_buf[CMD_BUF_SIZE]; } command_stdin_t; static void stdin_command_reply(command_t *cmd, const char *s, size_t len) { /* Just write to stdout! */ fwrite(s, 1, len, stdout); fflush(stdout); } static void stdin_command_free(command_t *handle) { free(handle->userptr); free(handle); } static void command_stdin_poll(command_t *handle) { command_stdin_t *stdincmd = (command_stdin_t*)handle->userptr; ssize_t ret = read_stdin( stdincmd->stdin_buf + stdincmd->stdin_buf_ptr, CMD_BUF_SIZE - stdincmd->stdin_buf_ptr - 1); if (ret != 0) { char *last_newline = NULL; stdincmd->stdin_buf_ptr += ret; /* Ensure we don't write past buffer bounds */ if (stdincmd->stdin_buf_ptr >= CMD_BUF_SIZE) stdincmd->stdin_buf_ptr = CMD_BUF_SIZE - 1; stdincmd->stdin_buf[stdincmd->stdin_buf_ptr] = '\0'; last_newline = strrchr(stdincmd->stdin_buf, '\n'); if (!last_newline) { /* We're receiving bogus data in pipe * (no terminating newline), flush out the buffer. */ if (stdincmd->stdin_buf_ptr + 1 >= CMD_BUF_SIZE) { stdincmd->stdin_buf_ptr = 0; stdincmd->stdin_buf[0] = '\0'; } } else { ptrdiff_t msg_len; size_t rest; char *msg; *last_newline++ = '\0'; msg_len = last_newline - stdincmd->stdin_buf; rest = stdincmd->stdin_buf_ptr - msg_len; /* A command can free this object from under us (see * command_generation): take the message off the buffer, tidy the * buffer, and only then run the message from a private copy, * touching nothing of this object afterwards. */ msg = strdup(stdincmd->stdin_buf); memmove(stdincmd->stdin_buf, last_newline, rest); stdincmd->stdin_buf_ptr = rest; if (msg) { command_parse_msg(handle, msg); free(msg); } return; } } } command_t* command_stdin_new(void) { command_t *cmd; command_stdin_t *stdincmd; #if !(defined(_WIN32) || defined(__EMSCRIPTEN__)) #ifdef HAVE_NETWORKING if (!socket_nonblock(STDIN_FILENO)) return NULL; #endif #endif /* Allocate in order with per-step failure handling. The earlier * form allocated both cmd and stdincmd back-to-back then checked * them, which leaked stdincmd on the '!cmd' failure path (the * '!cmd' return simply dropped the stdincmd pointer). Also * matches the command_uds_new pattern further down. */ if (!(cmd = (command_t*)calloc(1, sizeof(command_t)))) return NULL; if (!(stdincmd = (command_stdin_t*)calloc(1, sizeof(command_stdin_t)))) { free(cmd); return NULL; } cmd->userptr = stdincmd; cmd->poll = command_stdin_poll; cmd->replier = stdin_command_reply; cmd->destroy = stdin_command_free; return cmd; } #endif #if defined(__EMSCRIPTEN__) #include "frontend/drivers/platform_emscripten.h" typedef struct { char command_buf[CMD_BUF_SIZE]; } command_emscripten_t; static void emscripten_command_reply(command_t *_cmd, const char *s, size_t len) { platform_emscripten_command_reply(s, len); } static void emscripten_command_free(command_t *handle) { free(handle->userptr); free(handle); } static void command_emscripten_poll(command_t *handle) { command_emscripten_t *emscriptencmd = (command_emscripten_t*)handle->userptr; ptrdiff_t msg_len = platform_emscripten_command_read((char **)(&emscriptencmd->command_buf), CMD_BUF_SIZE); if (msg_len != 0) command_parse_msg(handle, emscriptencmd->command_buf); } command_t* command_emscripten_new(void) { command_t *cmd; command_emscripten_t *emscriptencmd; /* Same sequential-allocation-with-per-step-cleanup pattern as * command_stdin_new / command_uds_new. Previously allocated both * structs before any NULL check, leaking emscriptencmd on the * '!cmd' failure path. */ if (!(cmd = (command_t*)calloc(1, sizeof(command_t)))) return NULL; if (!(emscriptencmd = (command_emscripten_t*)calloc(1, sizeof(command_emscripten_t)))) { free(cmd); return NULL; } cmd->userptr = emscriptencmd; cmd->poll = command_emscripten_poll; cmd->replier = emscripten_command_reply; cmd->destroy = emscripten_command_free; return cmd; } #endif #if defined(HAVE_LAKKA) #include #define MAX_USER_CONNECTIONS 4 typedef struct { /* File descriptor for the domain socket */ int sfd; /* Client sockets */ int userfd[MAX_USER_CONNECTIONS]; /* Which connection holds each slot: a closed descriptor's number * is handed out again, so the number alone does not say. */ unsigned serial[MAX_USER_CONNECTIONS]; unsigned next_serial; /* Last received user socket */ int last_fd; unsigned last_serial; } command_uds_t; typedef struct { int fd; unsigned serial; } command_uds_dest_t; static void uds_command_reply(command_t *cmd, const char *s, size_t len) { command_uds_t *subcmd = (command_uds_t*)cmd->userptr; write(subcmd->last_fd, s, len); } static void *uds_command_reply_dest(command_t *cmd) { command_uds_t *subcmd = (command_uds_t*)cmd->userptr; command_uds_dest_t *dest = (command_uds_dest_t*)malloc(sizeof(*dest)); if (dest) { dest->fd = subcmd->last_fd; dest->serial = subcmd->last_serial; } return dest; } /* Only to the connection that asked: gone, the reply is dropped. */ static void uds_command_reply_to(command_t *cmd, void *data, const char *s, size_t len) { int i; command_uds_t *subcmd = (command_uds_t*)cmd->userptr; command_uds_dest_t *dest = (command_uds_dest_t*)data; for (i = 0; i < MAX_USER_CONNECTIONS; i++) if ( subcmd->userfd[i] == dest->fd && subcmd->serial[i] == dest->serial) { write(dest->fd, s, len); return; } } static void uds_command_free(command_t *handle) { int i; command_uds_t *udscmd = (command_uds_t*)handle->userptr; for (i = 0; i < MAX_USER_CONNECTIONS; i++) if (udscmd->userfd[i] >= 0) socket_close(udscmd->userfd[i]); socket_close(udscmd->sfd); free(handle->userptr); free(handle); } static void command_uds_poll(command_t *handle) { unsigned gen = input_driver_command_generation(); int i; int fd; ssize_t ret; char buf[2048]; command_uds_t *udscmd = (command_uds_t*)handle->userptr; if (udscmd->sfd < 0) return; /* Read data from clients and process commands */ for (i = 0; i < MAX_USER_CONNECTIONS; i++) { bool err = false; fd = udscmd->userfd[i]; if (fd < 0) continue; ret = socket_receive_all_nonblocking(fd, &err, buf, sizeof(buf) - 1); if (!ret) continue; if (!err) { buf[ret] = '\0'; udscmd->last_fd = fd; udscmd->last_serial = udscmd->serial[i]; command_parse_msg(handle, buf); /* See command_generation: this object may be gone now. */ if ( input_driver_command_generation() != gen || command_interfaces_held()) return; } else { socket_close(fd); udscmd->userfd[i] = -1; } } /* Accepts new connections from clients */ fd = accept(udscmd->sfd, NULL, NULL); if (fd >= 0) { if (socket_nonblock(fd)) { for (i = 0; i < MAX_USER_CONNECTIONS; i++) { if (udscmd->userfd[i] < 0) { udscmd->userfd[i] = fd; udscmd->serial[i] = ++udscmd->next_serial; return; } } } socket_close(fd); } } command_t* command_uds_new(void) { int i; command_t *cmd; command_uds_t *subcmd; struct sockaddr_un addr; socklen_t addrsz = offsetof(struct sockaddr_un, sun_path) + STRLEN_CONST("retroarch/cmd") + 1; int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return NULL; /* use an abstract socket for simplicity */ memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strlcpy_lit(&addr.sun_path[1], "retroarch/cmd", sizeof(addr.sun_path) - 1); if ( bind(fd, (struct sockaddr*)&addr, addrsz) < 0 || listen(fd, MAX_USER_CONNECTIONS) < 0 || !socket_nonblock(fd)) { socket_close(fd); return NULL; } cmd = (command_t*)calloc(1, sizeof(command_t)); if (!cmd) { socket_close(fd); return NULL; } subcmd = (command_uds_t*)calloc(1, sizeof(command_uds_t)); if (!subcmd) { free(cmd); socket_close(fd); return NULL; } subcmd->sfd = fd; subcmd->last_fd = -1; for (i = 0; i < MAX_USER_CONNECTIONS; i++) subcmd->userfd[i] = -1; cmd->userptr = subcmd; cmd->poll = command_uds_poll; cmd->replier = uds_command_reply; cmd->reply_dest = uds_command_reply_dest; cmd->reply_to = uds_command_reply_to; cmd->destroy = uds_command_free; return cmd; } #endif /* Routines used to invoke retroarch command ... */ /* The family tool a command runs for, so its answers carry the tool's * name: STATE save 1 rather than SAVE_STATE_SLOT 1. */ struct command_family { const char *family; const char *word; const char *name; command_replier_t replier; /* the command answered, or will */ bool answered; }; static struct command_family command_family_now; /* @msg under @f's name, allocated, its length in @out_len; NULL where * it could not be. */ static char *command_family_rename(const struct command_family *f, const char *msg, size_t len, size_t *out_len) { size_t n = strlen(f->name); size_t size = len + strlen(f->family) + strlen(f->word) + 3; char *s = (char*)malloc(size); int _len; if (!s) return NULL; if ( len > n && !strncmp(msg, f->name, n) && (msg[n] == ' ' || msg[n] == '\n' || msg[n] == ',')) { msg += n; len -= n; if (!strncmp(msg, " ERROR", 6)) _len = snprintf(s, size, "%s%.*s", f->family, (int)len, msg); else _len = snprintf(s, size, "%s %s%.*s", f->family, f->word, (int)len, msg); } else if (len == n && !strncmp(msg, f->name, n)) _len = snprintf(s, size, "%s %s", f->family, f->word); else _len = snprintf(s, size, "%s %s %.*s", f->family, f->word, (int)len, msg); if (_len <= 0 || (size_t)_len >= size) { free(s); return NULL; } *out_len = (size_t)_len; return s; } static void command_family_replier(command_t *cmd, const char *data, size_t len) { size_t _len = 0; char *msg = command_family_rename(&command_family_now, data, len, &_len); command_family_now.answered = true; if (msg) command_family_now.replier(cmd, msg, _len); else command_family_now.replier(cmd, data, len); free(msg); } /* A reply sent after its command has returned: where it goes is taken * while the command runs, as afterwards the interface may be answering * someone else. */ typedef struct { command_t *cmd; void *dest; struct command_family family; unsigned gen; } command_deferred_t; static void command_deferred_take(command_deferred_t *d, command_t *cmd) { d->cmd = cmd; d->dest = cmd->reply_dest ? cmd->reply_dest(cmd) : NULL; d->gen = input_driver_command_generation(); d->family = command_family_now; if (command_family_now.family) command_family_now.answered = true; } /* Sends @msg (none when NULL) and releases @d. Dropped, unsent, if the * interface was torn down meanwhile (the command generation moved). * @is_error says the reply reports a failure, so the interface sending * it is told rather than left to read the text. */ static void command_deferred_send(command_deferred_t *d, const char *msg, size_t len, bool is_error) { char *renamed = NULL; if (msg && input_driver_command_generation() == d->gen) { size_t _len = 0; if (d->family.family && (renamed = command_family_rename(&d->family, msg, len, &_len))) { msg = renamed; len = _len; } d->cmd->error = is_error; if (d->cmd->reply_to && d->dest) d->cmd->reply_to(d->cmd, d->dest, msg, len); else if (!d->cmd->reply_dest) d->cmd->replier(d->cmd, msg, len); } free(renamed); free(d->dest); d->dest = NULL; } #ifdef HAVE_SCREENSHOTS static void command_screenshot_done(retro_task_t *task, void *task_data, void *user_data, const char *error) { command_deferred_t *d = (command_deferred_t*)user_data; const struct screenshot_result *r = (const struct screenshot_result*)task_data; char msg[PATH_MAX_LENGTH]; if (error || !r) { snprintf(msg, sizeof(msg), "SCREENSHOT ERROR %s", error ? error : "no result"); command_deferred_send(d, msg, strlen(msg), true); } else if (r->png_base64 && d->cmd->reply_image_to && d->dest && input_driver_command_generation() == d->gen) { /* the picture itself, with its path, where the interface takes one */ d->cmd->reply_image_to(d->cmd, d->dest, r->path, strlen(r->path), "image/png", r->png_base64, r->png_base64_len); command_deferred_send(d, NULL, 0, false); } else { strlcpy(msg, r->path, sizeof(msg)); command_deferred_send(d, msg, strlen(msg), false); } free(d); } /* SCREENSHOT run by command_run(): answered with the file's path once * the screenshot task has written it. */ static bool command_screenshot(command_t *cmd) { settings_t *settings = config_get_ptr(); runloop_state_t *runloop_st = runloop_state_get_ptr(); command_deferred_t *d = (command_deferred_t*)calloc(1, sizeof(*d)); if (!d) return false; /* Threaded (use_thread true), so the callback, which reads @d, runs * from the task queue on a later frame, after it is taken below. * Unthreaded, the callback would run inside this call and free @d * before it was taken. It is taken only once the screenshot is * queued, as taking it marks the asking MCP request as waiting. */ if (!take_screenshot_notify(settings->paths.directory_screenshot, runloop_st->runtime_content_path_basename, false, video_driver_cached_frame_is_hw_render(), false, true, command_screenshot_done, d)) { free(d); return false; } /* the task's callback, which uses this, runs on a later frame */ command_deferred_take(d, cmd); return true; } #endif /* A command answered once the work it starts is through: by @ok, else * by @name and the task's result, or by @name, ERROR and the reason. */ struct command_reply { command_deferred_t deferred; enum event_command event; /* Run instead of the event, when set; false has the failure * reported for it. */ bool (*start)(struct command_reply *r); const char *name; char ok[64]; char path[PATH_MAX_LENGTH]; /* a load that ends on the dummy core failed */ bool want_core; /* LOAD_CONTENT ... paused: pause once it has loaded */ bool pause; /* what succeeded loaded a state */ bool state_loaded; bool reset; }; static struct command_reply *command_reply_new(const char *name) { struct command_reply *r = (struct command_reply*)calloc(1, sizeof(*r)); if (r) r->name = name; return r; } static void command_reply_done(retro_task_t *task, void *task_data, void *user_data, const char *error) { struct command_reply *r = (struct command_reply*)user_data; char msg[PATH_MAX_LENGTH + 128]; /* a core that fails to start is replaced by the dummy core, and the * load goes on to succeed */ if ( !error && r->want_core && runloop_state_get_ptr()->current_core_type == CORE_TYPE_DUMMY) error = msg_hash_to_str(MSG_FAILED_TO_LOAD_CONTENT); if (error) snprintf(msg, sizeof(msg), "%s ERROR %s", r->name, error); else { if (*r->ok) strlcpy(msg, r->ok, sizeof(msg)); else if (task_data) snprintf(msg, sizeof(msg), "%s %s", r->name, (const char*)task_data); else strlcpy(msg, r->name, sizeof(msg)); if (r->state_loaded) command_post_state_loaded(); #ifdef HAVE_COMMAND if ( command_queue_ended && (r->state_loaded || r->reset)) strlcat(msg, ", queue ended", sizeof(msg)); #endif /* a pause is dropped with no core running yet, as after a cold * start: then it is the first frame's to do */ if (r->pause) { if (runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING) command_event(CMD_EVENT_PAUSE, NULL); else runloop_frame_work_set(RUNLOOP_WORK_PAUSE_LOADED, true); } } command_deferred_send(&r->deferred, msg, strlen(msg), error != NULL); free(r); } /* Runs the work from a main-thread task, outside the core's run: a * command can arrive from the core's own input poll. */ static void command_reply_handler(retro_task_t *task) { struct command_reply *r = (struct command_reply*)task->state; task_notify_t notify; task->state = NULL; task_set_flags(task, RETRO_TASK_FLG_FINISHED, true); notify.cb = command_reply_done; notify.user_data = r; #ifdef HAVE_COMMAND command_queue_ended = false; #endif if (!(r->start ? r->start(r) : command_event(r->event, ¬ify))) command_reply_done(NULL, NULL, r, "The command failed."); } /* Takes @r, NULL included. */ static bool command_reply_push(command_t *cmd, struct command_reply *r) { retro_task_t *task; if (!r) return false; if (!(task = task_init())) { free(r); return false; } task->handler = command_reply_handler; task->state = r; task->flags |= RETRO_TASK_FLG_MAIN_THREAD | RETRO_TASK_FLG_MUTE; if (!task_queue_push(task)) { free(task); free(r); return false; } command_deferred_take(&r->deferred, cmd); return true; } #ifdef HAVE_BSV_MOVIE /* Pushes @event as the work of command @name. */ static bool command_reply_event(command_t *cmd, enum event_command event, const char *name) { struct command_reply *r = command_reply_new(name); if (r) r->event = event; return command_reply_push(cmd, r); } #endif #ifdef HAVE_TRANSLATE /* AI_SERVICE run by command_run(): one translation of the screen, * answered with the service's text. */ static bool command_ai_service_start(struct command_reply *r) { settings_t *settings = config_get_ptr(); if (!settings->bools.ai_service_enable) return false; return run_translation_service_notify(settings, (runloop_get_flags() & RUNLOOP_FLAG_PAUSED) != 0, command_reply_done, r); } #endif /* Answers @cmd once the content load in progress, or with @next the * one the command leads to, is through. */ static bool command_content_reply(command_t *cmd, const char *name, bool next, bool want_core, bool pause) { struct command_reply *r; /* a line client is answered too where it asked for a pause, which * happens here */ if ( (!cmd->structured && !pause) || (!next && !task_content_load_pending()) || !(r = command_reply_new(name))) return true; r->want_core = want_core; r->pause = pause; task_notify_set(&content_state_get_ptr()->load_notify, command_reply_done, r); command_deferred_take(&r->deferred, cmd); return true; } /* Runs @name with @arg (NULL or "" for none) as if it had arrived as a * command line on @handle: replies go to @handle's replier, a hotkey * presses on @handle for one frame. For interfaces that receive * commands as structured requests rather than lines. * * Returns: false for an unknown command or one whose handler failed. */ bool command_run(command_t *handle, const char *name, const char *arg) { unsigned i; handle->error = false; for (i = 0; i < ARRAY_SIZE(action_map); i++) if (string_is_equal(name, action_map[i].str)) return action_map[i].action(handle, arg ? arg : ""); for (i = 0; i < ARRAY_SIZE(map); i++) if (string_is_equal(name, map[i].str)) { if (map[i].id == RARCH_MENU_TOGGLE) command_event(CMD_EVENT_MENU_TOGGLE, NULL); #ifdef HAVE_SCREENSHOTS else if (map[i].id == RARCH_SCREENSHOT) return command_screenshot(handle); #endif #ifdef HAVE_BSV_MOVIE else if (map[i].id == RARCH_RECORD_REPLAY_KEY) return command_reply_event(handle, CMD_EVENT_RECORD_REPLAY, map[i].str); else if (map[i].id == RARCH_SAVE_REPLAY_CHECKPOINT_KEY) return command_reply_event(handle, CMD_EVENT_SAVE_REPLAY_CHECKPOINT, map[i].str); else if (map[i].id == RARCH_PREV_REPLAY_CHECKPOINT_KEY) return command_reply_event(handle, CMD_EVENT_PREV_REPLAY_CHECKPOINT, map[i].str); else if (map[i].id == RARCH_NEXT_REPLAY_CHECKPOINT_KEY) return command_reply_event(handle, CMD_EVENT_NEXT_REPLAY_CHECKPOINT, map[i].str); #endif #ifdef HAVE_TRANSLATE else if (map[i].id == RARCH_AI_SERVICE) { struct command_reply *r = command_reply_new(map[i].str); if (r) r->start = command_ai_service_start; return command_reply_push(handle, r); } #endif else handle->state[map[i].id] = true; /* The press goes out on the next poll and the hotkey acts a * frame later, so there is nothing yet to report but that it * was sent. A structured client is told that much rather than * a bare "done": what the hotkey changed is read back with * GET_OPTION. MENU_TOGGLE is the one that has already acted, * above, and reports the menu instead. */ if (handle->structured) { char reply[NAME_MAX_LENGTH + 16]; size_t _len; #ifdef HAVE_MENU if (map[i].id == RARCH_MENU_TOGGLE) { struct menu_state *menu_st = menu_state_get_ptr(); _len = (size_t)snprintf(reply, sizeof(reply), "%s %s\n", map[i].str, (menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE)) ? "open" : "closed"); } else #endif _len = (size_t)snprintf(reply, sizeof(reply), "%s pressed\n", map[i].str); handle->replier(handle, reply, _len); } return true; } return false; } /* MENU, DISK, REINIT, STATE, REPLAY and CORE: a family of commands as one, * so a structured client is offered one tool with a choice rather than * a tool apiece. Each word runs as the command it stands for, which * line clients can still send by name; @rest says it takes what * follows the word, a slot or a frame. */ struct cmd_choice { const char *word; const char *name; bool rest; }; static bool command_run_choice(command_t *cmd, const char *family, const struct cmd_choice *choices, size_t count, const char *values, const char *arg) { size_t i; size_t n; const char *rest; while (arg && *arg == ' ') arg++; if (!arg) arg = ""; for (rest = arg; *rest && *rest != ' '; rest++) ; n = (size_t)(rest - arg); while (*rest == ' ') rest++; for (i = 0; i < count; i++) { if (strncmp(arg, choices[i].word, n) || choices[i].word[n]) continue; if (!choices[i].rest && *rest) { command_reply_error(cmd, family, "nothing after %s", choices[i].word); return false; } { bool ret; command_family_now.family = family; command_family_now.word = choices[i].word; command_family_now.name = choices[i].name; command_family_now.replier = cmd->replier; cmd->replier = command_family_replier; ret = command_run(cmd, choices[i].name, rest); cmd->replier = command_family_now.replier; /* what answers nothing of its own still answers as the tool */ if (ret && !command_family_now.answered && cmd->structured) { char msg[64]; int _len = snprintf(msg, sizeof(msg), "%s %s\n", family, choices[i].word); if (_len > 0 && (size_t)_len < sizeof(msg)) cmd->replier(cmd, msg, (size_t)_len); } memset(&command_family_now, 0, sizeof(command_family_now)); return ret; } } command_reply_error(cmd, family, "one of %s", values); return false; } bool command_menu(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "up", "MENU_UP", false }, { "down", "MENU_DOWN", false }, { "left", "MENU_LEFT", false }, { "right", "MENU_RIGHT", false }, { "a", "MENU_A", false }, { "b", "MENU_B", false } }; return command_run_choice(cmd, "MENU", choices, ARRAY_SIZE(choices), COMMAND_MENU_BUTTONS, arg); } bool command_disk(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "eject", "DISK_EJECT_TOGGLE", false }, { "next", "DISK_NEXT", false }, { "prev", "DISK_PREV", false } }; /* the hotkeys are pressed regardless; the tool says when they can * do nothing */ if (!disk_control_enabled(&runloop_state_get_ptr()->system.disk_control)) { command_reply_error(cmd, "DISK", "this core has no disk control"); return false; } return command_run_choice(cmd, "DISK", choices, ARRAY_SIZE(choices), COMMAND_DISK_ACTIONS, arg); } bool command_reinit(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "video", "VIDEO_REINIT", false }, { "audio", "AUDIO_REINIT", false }, { "all", "DRIVERS_REINIT", false } }; return command_run_choice(cmd, "REINIT", choices, ARRAY_SIZE(choices), COMMAND_REINIT_DRIVERS, arg); } bool command_state(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "save", "SAVE_STATE_SLOT", true }, { "load", "LOAD_STATE_SLOT", true }, { "checkpoint", "CHECKPOINT_STATE", false }, { "rollback", "ROLLBACK_STATE", false } }; return command_run_choice(cmd, "STATE", choices, ARRAY_SIZE(choices), COMMAND_STATE_ACTIONS, arg); } bool command_core(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "load", "LOAD_CORE", true }, { "start", "START_CORE", false }, { "unload", "UNLOAD_CORE", false } }; return command_run_choice(cmd, "CORE", choices, ARRAY_SIZE(choices), COMMAND_CORE_ACTIONS, arg); } bool command_replay(command_t *cmd, const char *arg) { static const struct cmd_choice choices[] = { { "play", "PLAY_REPLAY_SLOT", true }, { "seek", "SEEK_REPLAY", true }, { "record", "RECORD_REPLAY", false }, { "halt", "HALT_REPLAY", false }, { "checkpoint", "SAVE_REPLAY_CHECKPOINT", false } }; return command_run_choice(cmd, "REPLAY", choices, ARRAY_SIZE(choices), COMMAND_REPLAY_ACTIONS, arg); } bool command_interfaces_held(void) { return runloop_is_content_switching(); } const struct cmd_action_map *command_action_list(size_t *count) { *count = ARRAY_SIZE(action_map); return action_map; } const struct cmd_map *command_hotkey_list(size_t *count) { *count = ARRAY_SIZE(map); return map; } #ifdef HAVE_NETWORK_CMD static bool command_verify(const char *cmd) { size_t i; if (command_get_arg(cmd, NULL, NULL)) return true; RARCH_ERR("[NetCMD] Command \"%s\" is not recognized by the program.\n", cmd); RARCH_ERR("[NetCMD] \tValid commands:\n"); for (i = 0; i < ARRAY_SIZE(map); i++) RARCH_ERR("[NetCMD] \t\t%s\n", map[i].str); for (i = 0; i < ARRAY_SIZE(action_map); i++) RARCH_ERR("[NetCMD] \t\t%s %s\n", action_map[i].str, action_map[i].arg_desc); return false; } static bool udp_send_packet(const char *host, uint16_t port, const char *msg) { char port_buf[6]; const struct addrinfo *tmp_info; struct addrinfo *addr = NULL; struct addrinfo hints = {0}; size_t len = strlen(msg); bool ret = false; snprintf(port_buf, sizeof(port_buf), "%hu", (unsigned short)port); hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_NUMERICSERV; if (getaddrinfo_retro(host, port_buf, &hints, &addr)) return false; if (!addr) return false; /* Send to all possible targets. */ tmp_info = addr; do { int fd = socket(tmp_info->ai_family, tmp_info->ai_socktype, tmp_info->ai_protocol); if (fd < 0) continue; if (sendto(fd, msg, len, 0, tmp_info->ai_addr, tmp_info->ai_addrlen) == (ssize_t)len) ret = true; socket_close(fd); } while ((tmp_info = tmp_info->ai_next)); freeaddrinfo_retro(addr); return ret; } /* Whether @s, after a ";", can be a host or a port rather than a STEPS * step: no space, no "|" (a list's "|on"), and not "checkpoint". Empty * is the default, as in "CMD;;PORT". */ static bool command_network_host_like(const char *s) { return !strchr(s, ' ') && !strchr(s, '|') && !string_is_equal(s, "checkpoint"); } bool command_network_send(const char *cmd_) { char buf[4096]; char *ptr; const char *cmd = NULL; const char *host = NULL; const char *port_str = NULL; uint16_t port = DEFAULT_NETWORK_CMD_PORT; size_t len; if (!cmd_ || !*cmd_) return false; len = strlen(cmd_); if (len >= sizeof(buf)) return false; if (!network_init()) return false; memcpy(buf, cmd_, len + 1); /* ";host;port" ends it, the two read from the end and only where they * can be no STEPS step: a list separates its steps with ";" too */ cmd = buf; if ((ptr = strrchr(buf, ';')) && command_network_host_like(ptr + 1)) { char *prev; *ptr = '\0'; host = ptr + 1; if ((prev = strrchr(buf, ';')) && command_network_host_like(prev + 1)) { *prev = '\0'; port_str = host; host = prev + 1; } } if (!cmd || !*cmd) return false; if (!host || !*host) { #ifdef _WIN32 host = "127.0.0.1"; #else host = "localhost"; #endif } if (port_str && *port_str) { unsigned long val = strtoul(port_str, NULL, 0); if (val > 0 && val <= 0xFFFF) port = (uint16_t)val; } RARCH_LOG("[NetCMD] %s: \"%s\" to %s:%hu.\n", msg_hash_to_str(MSG_SENDING_COMMAND), cmd, host, (unsigned short)port); if (!command_verify(cmd)) return false; return udp_send_packet(host, port, cmd); } #endif bool command_show_osd_msg(command_t *cmd, const char* arg) { runloop_msg_queue_push(arg, strlen(arg), 1, 180, false, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO); return true; } /* A whole number and nothing after it but spaces, into @out. "3xyz" is * not one: it used to come out as 3, and a save then overwrote that * slot on an argument its sender had got wrong. */ static bool command_arg_uint(const char *arg, unsigned *out) { char *end = NULL; unsigned long v; while (arg && *arg == ' ') arg++; if (!arg || *arg < '0' || *arg > '9') return false; errno = 0; v = strtoul(arg, &end, 10); while (*end == ' ') end++; if (*end) return false; *out = (unsigned)v; /* range-checked before narrowing, so 4294967297 is not slot 1, and * where long is 32 bits strtoul clamps it and says so */ return errno != ERANGE && (unsigned long)*out == v; } /* A state or replay slot: past INT_MAX one turns negative, which is the * auto slot's file. */ static bool command_arg_slot(const char *arg, unsigned *out) { return command_arg_uint(arg, out) && *out <= INT_MAX; } static bool command_load_state_start(struct command_reply *r) { return content_load_state_notify(r->path, false, false, command_reply_done, r); } /* Serializing re-enters the core, so this must not run from a command's * own poll. */ static bool command_save_state_start(struct command_reply *r) { if (!content_save_state_notify(r->path, true, command_reply_done, r)) { /* on the heap: this runs from a task, whose thread may have a * small stack */ const char *what = msg_hash_to_str(MSG_FAILED_TO_SAVE_STATE_TO); size_t len = strlen(what) + strlen(r->path) + 8; char *msg = (char*)malloc(len); if (msg) snprintf(msg, len, "%s \"%s\".", what, r->path); command_reply_done(NULL, NULL, r, msg ? msg : what); free(msg); } return true; } /* LOAD_STATE_SLOT and SAVE_STATE_SLOT, answered once their task is * through. */ static bool command_state_slot(command_t *cmd, const char *arg, bool load) { const char *name = load ? "LOAD_STATE_SLOT" : "SAVE_STATE_SLOT"; unsigned int slot = 0; bool savestates_enabled = core_info_current_supports_savestate(); struct command_reply *r; if (!command_arg_slot(arg, &slot)) { command_reply_error(cmd, name, "a slot number, not %s", (arg && *arg) ? arg : "nothing"); return false; } if ((r = command_reply_new(name))) { runloop_get_savestate_path(r->path, sizeof(r->path), slot); /* For LOADING, an existing state file outranks metadata and * save-capability probes: core_serialize_size() measures whether * the core can SAVE right now (0 at e.g. a game's own main menu), * which says nothing about whether it can restore. Let the load * task and retro_unserialize() arbitrate. */ if (load && !savestates_enabled) savestates_enabled = path_is_valid(r->path); if (savestates_enabled) { snprintf(r->ok, sizeof(r->ok), "%s %u", name, slot); r->state_loaded = load; r->start = load ? command_load_state_start : command_save_state_start; return command_reply_push(cmd, r); } free(r); command_reply_error(cmd, name, load ? "no state in slot %u, and this core cannot restore one" : "this core cannot save a state to slot %u right now", slot); return false; } command_reply_error(cmd, name, "out of memory"); return false; } /* The state STATE checkpoint keeps, in memory rather than a slot's * file, for STATE rollback; dropped when the content closes. */ static struct { void *data; size_t size; } command_checkpoint; void command_checkpoint_clear(void) { free(command_checkpoint.data); command_checkpoint.data = NULL; command_checkpoint.size = 0; } /* Keeps the core's state as the checkpoint. Serializing re-enters the * core, so only with the core off the stack. */ static bool command_checkpoint_take(void) { retro_ctx_serialize_info_t info; size_t len = core_serialize_size(); void *buf = len ? malloc(len) : NULL; info.data_const = NULL; info.data = buf; info.size = len; if (!buf || !core_serialize(&info)) { free(buf); return false; } free(command_checkpoint.data); command_checkpoint.data = buf; command_checkpoint.size = len; return true; } /* Serializing re-enters the core, so this runs from the reply's main * thread task, as a save to a slot does. */ static bool command_checkpoint_start(struct command_reply *r) { const char *error = NULL; if (r->state_loaded) { bool ramped; if (!command_checkpoint.data) error = "no checkpoint to roll back to; STATE checkpoint keeps one"; /* as a load from a slot is refused: the recording would go on * from a state it never saw */ else if (input_driver_replay_recording()) error = "a replay is recording, which a rollback would break"; else { /* through the loader of states without RetroArch's wrapper, * which resets the achievements and stops a replay playing, * as a load does */ ramped = audio_driver_jump_fade_begin(); if (!content_deserialize_state(command_checkpoint.data, command_checkpoint.size)) error = "the core could not restore the checkpoint"; audio_driver_jump_fade_end(ramped); } } else if (!command_checkpoint_take()) error = "this core cannot save a state right now"; command_reply_done(NULL, NULL, r, error); return true; } static bool command_checkpoint_state(command_t *cmd, const char *arg, bool rollback) { const char *name = rollback ? "ROLLBACK_STATE" : "CHECKPOINT_STATE"; struct command_reply *r; while (arg && *arg == ' ') arg++; if (arg && *arg) { command_reply_error(cmd, name, "nothing after it"); return false; } if (!core_info_current_supports_savestate()) { command_reply_error(cmd, name, "this core cannot save a state"); return false; } #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { command_reply_error(cmd, name, "achievements hardcore mode forbids states"); return false; } #endif if (!(r = command_reply_new(name))) { command_reply_error(cmd, name, "out of memory"); return false; } r->state_loaded = rollback; r->start = command_checkpoint_start; return command_reply_push(cmd, r); } bool command_checkpoint_save(command_t *cmd, const char *arg) { return command_checkpoint_state(cmd, arg, false); } bool command_checkpoint_rollback(command_t *cmd, const char *arg) { return command_checkpoint_state(cmd, arg, true); } /* RESET [on|off]: from the reply's main thread task, since a reset * re-enters the core; with on, paused after it, as LOAD_CONTENT ... * paused is, so nothing runs before the first step. */ static bool command_reset_start(struct command_reply *r) { command_event(CMD_EVENT_RESET, NULL); command_reply_done(NULL, NULL, r, NULL); return true; } bool command_reset(command_t *cmd, const char *arg) { struct command_reply *r; bool pause = false; while (arg && *arg == ' ') arg++; if (arg && *arg) { if (string_is_equal(arg, "on")) pause = true; else if (!string_is_equal(arg, "off")) { command_reply_error(cmd, "RESET", "on or off, not %s", arg); return false; } } if (!(runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING)) { command_reply_error(cmd, "RESET", "no content is running"); return false; } if (!(r = command_reply_new("RESET"))) { command_reply_error(cmd, "RESET", "out of memory"); return false; } r->pause = pause; r->reset = true; r->start = command_reset_start; return command_reply_push(cmd, r); } bool command_load_state_slot(command_t *cmd, const char *arg) { return command_state_slot(cmd, arg, true); } bool command_save_state_slot(command_t *cmd, const char *arg) { return command_state_slot(cmd, arg, false); } bool command_play_replay_slot(command_t *cmd, const char *arg) { #ifdef HAVE_BSV_MOVIE unsigned int slot = 0; struct command_reply *r = NULL; if (!command_arg_slot(arg, &slot)) { command_reply_error(cmd, "PLAY_REPLAY_SLOT", "a slot number, not %s", (arg && *arg) ? arg : "nothing"); return false; } if ( core_info_current_supports_savestate() && (r = command_reply_new("PLAY_REPLAY_SLOT"))) { runloop_get_replay_path(r->path, sizeof(r->path), slot); r->state_loaded = true; /* answered from the movie task's callback, on a later frame */ if (movie_start_playback_notify(input_state_get_ptr(), r->path, command_reply_done, r)) { command_deferred_take(&r->deferred, cmd); return true; } free(r); command_reply_error(cmd, "PLAY_REPLAY_SLOT", "no replay in slot %u", slot); return false; } command_reply_error(cmd, "PLAY_REPLAY_SLOT", "this core cannot play a replay"); return false; #else command_reply_error(cmd, "PLAY_REPLAY_SLOT", "this build has no replay support"); return false; #endif } bool command_seek_replay(command_t *cmd, const char *arg) { #ifdef HAVE_BSV_MOVIE char reply[32]; char *endptr = NULL; size_t _len; int64_t frame = arg ? (int64_t)strtoll(arg, &endptr, 10) : 0; input_driver_state_t *input_st = input_state_get_ptr(); struct command_reply *r; /* strtoll always writes a valid pointer, so an empty or non-numeric * argument shows up as no characters consumed; anything but spaces * after the number means it was not one. */ if (arg && endptr != arg) while (*endptr == ' ') endptr++; if (!arg || endptr == arg || *endptr) { command_reply_error(cmd, "SEEK_REPLAY", "a frame number, not %s", (arg && *arg) ? arg : "nothing"); return false; } if (!(input_st->bsv_movie_state.flags & (BSV_FLAG_MOVIE_PLAYBACK | BSV_FLAG_MOVIE_RECORDING))) { command_reply_error(cmd, "SEEK_REPLAY", "no replay is playing or recording"); return false; } #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) { command_reply_error(cmd, "SEEK_REPLAY", "achievements hardcore mode forbids seeking"); return false; } #endif if (!movie_seek_to_frame(input_st, frame)) { command_reply_error(cmd, "SEEK_REPLAY", "the replay could not be seeked to frame %" PRId64, frame); return false; } /* A structured request is answered once the seek has run, on a * later frame; a line-based one at once, as it always was. */ if (cmd->structured && (r = command_reply_new("SEEK_REPLAY"))) { snprintf(r->ok, sizeof(r->ok), "OK %" PRId64, input_st->bsv_movie_state.seek_target_frame); task_notify_set(&input_st->bsv_movie_op, command_reply_done, r); command_deferred_take(&r->deferred, cmd); return true; } _len = (size_t)snprintf(reply, sizeof(reply), "OK %" PRId64 "\n", input_st->bsv_movie_state.seek_target_frame); cmd->replier(cmd, reply, _len); return true; #else command_reply_error(cmd, "SEEK_REPLAY", "this build has no replay support"); return false; #endif } bool command_save_savefiles(command_t *cmd, const char* arg) { /* In the future, this should probably send each saved file path to the replier. */ if (!command_event(CMD_EVENT_SAVE_FILES, NULL)) { command_reply_error(cmd, "SAVE_FILES", "nothing to save: no content is loaded, or the core has no " "save files"); return false; } cmd->replier(cmd, "OK\n", 3); return true; } bool command_load_savefiles(command_t *cmd, const char* arg) { if (!command_event(CMD_EVENT_LOAD_FILES, NULL)) { command_reply_error(cmd, "LOAD_FILES", "nothing to load: no content is loaded, or the core has no " "save files"); return false; } cmd->replier(cmd, "OK\n", 3); return true; } /* Largest byte count READ_CORE_RAM / READ_CORE_MEMORY will serve. The * reply carries 3 characters per byte and goes out as one UDP datagram, * so anything much bigger could not be delivered anyway; the cap also * keeps `nbytes * 3` far from wrapping in unsigned int, which a request * of ~1431655766 bytes did, allocating a few bytes and writing 4 GiB. */ #define COMMAND_READ_NBYTES_MAX 16384u /* A system or achievement address in hexadecimal, range-checked before * it is narrowed, so 100001001 is refused rather than read as 1001. */ static bool command_parse_address(const char *s, unsigned *address, char **end) { unsigned long num; while (s && *s == ' ') s++; if (!s || (unsigned char)*s <= ' ' || *s == '-' || *s == '+') return false; errno = 0; num = strtoul(s, end, 16); *address = (unsigned)num; return *end != s && errno != ERANGE && (unsigned long)*address == num; } /* The next of a run of hexadecimal bytes separated by spaces: 1 with * it in @out, 0 at the end, -1 at anything that is not a byte. A * write checks every byte before it makes the first. */ static int command_next_byte(const char **s, uint8_t *out) { char *end = NULL; unsigned long num; while (**s == ' ') (*s)++; if (!**s) return 0; if (**s == '-' || **s == '+') return -1; num = strtoul(*s, &end, 16); if (end == *s || num > 0xff || (*end && *end != ' ')) return -1; *s = end; *out = (uint8_t)num; return 1; } static bool command_bytes_valid(const char *s) { uint8_t b; int r; while ((r = command_next_byte(&s, &b)) > 0) ; return r == 0; } /* @n bytes of @data as " XX" apiece; gives back the end of what it * wrote. */ static char *command_hex_bytes(char *at, const uint8_t *data, size_t n) { static const char hex[] = "0123456789ABCDEF"; size_t i; for (i = 0; i < n; i++) { *at++ = ' '; *at++ = hex[data[i] >> 4]; *at++ = hex[data[i] & 0x0f]; } return at; } #if defined(HAVE_CHEEVOS) bool command_read_ram(command_t *cmd, const char *arg) { unsigned long num = 0; unsigned int nbytes = 0; unsigned int addr = 0; char *end = NULL; if (command_parse_address(arg, &addr, &end) && *end == ' ') num = strtoul(end + 1, NULL, 10); nbytes = num <= COMMAND_READ_NBYTES_MAX ? (unsigned)num : 0; if (!nbytes) { command_reply_error(cmd, "READ_CORE_RAM", "an achievement address in hexadecimal, a space, and a byte " "count from 1 to %u", COMMAND_READ_NBYTES_MAX); return false; } { size_t _len = 0; char *reply_at = NULL; unsigned int avail = 0; const uint8_t *data = rcheevos_patch_address_avail(addr, &avail); unsigned int alloc_size; char *reply; /* Never read past the end of the region the address lives in. */ if (data && nbytes > avail) nbytes = avail; /* We allocate more than needed, saving 20 bytes is not really relevant */ alloc_size = 40 + nbytes * 3; reply = (char*)malloc(alloc_size); if (!reply) { command_reply_error(cmd, "READ_CORE_RAM", "out of memory"); return false; } reply[0] = '\0'; reply_at = reply + snprintf( reply, alloc_size - 1, "READ_CORE_RAM" " %x", addr); if (data && nbytes > 0) { char *end = command_hex_bytes(reply_at, data, nbytes); *end++ = '\n'; _len = (size_t)(end - reply); } else { /* no achievement memory at this address; " -1" is what line * clients have always read */ strlcpy_lit(reply_at, " -1\n", alloc_size - (size_t)(reply_at - reply)); _len = reply_at + STRLEN_CONST(" -1\n") - reply; cmd->error = true; } cmd->replier(cmd, reply, _len); free(reply); } return !cmd->error; } bool command_write_ram(command_t *cmd, const char *arg) { unsigned int addr = 0; unsigned int avail = 0; char *end = NULL; uint8_t *data = NULL; uint8_t more; const uint8_t *start; if (!command_parse_address(arg, &addr, &end) || !command_bytes_valid(end)) { command_reply_error(cmd, "WRITE_CORE_RAM", "an achievement address " "in hexadecimal, then bytes from 00 to ff separated by " "spaces"); return false; } arg = end; data = (uint8_t *)rcheevos_patch_address_avail(addr, &avail); start = data; if (!data) { command_reply_error(cmd, "WRITE_CORE_RAM", "no achievement memory at %x; a game with achievements must " "be running", addr); return false; } if (rcheevos_hardcore_active()) { RARCH_LOG("[Command] Achievements hardcore mode disabled by WRITE_CORE_RAM.\n"); rcheevos_pause_hardcore(); } /* Stop at the end of the region: the payload length is whatever the * sender put in the datagram. */ while (avail && command_next_byte(&arg, data) > 0) { data++; avail--; } /* read into a byte of its own: @data is past the region here */ if (command_next_byte(&arg, &more) > 0) RARCH_WARN("[Command] WRITE_CORE_RAM at %x reached the end of the " "memory region; remainder of the payload ignored.\n", addr); { char reply[64]; size_t _len = (size_t)snprintf(reply, sizeof(reply), "WRITE_CORE_RAM %x %u\n", addr, (unsigned)(data - start)); cmd->replier(cmd, reply, _len); } return true; } #endif /* ---- Queries for clients that browse and launch content --------- */ /* A reply larger than one UDP datagram cannot go back over the network * command port; lists stop short of it and say where to go on. */ #define COMMAND_LIST_REPLY_MAX 60000 #define COMMAND_PLAYLIST_PAGE 200 /* ------------------------------------------------------------------ */ /* Options: what GET_OPTION reports and SET_OPTION writes */ /* Which option a row of the table below is. Every one has a case in * command_option_read(), and no default, so that one added here without * a case is a warning rather than an option that reports nothing. */ enum cmd_option_id { /* on or off */ CMD_OPT_PAUSE = 0, CMD_OPT_FAST_FORWARD, CMD_OPT_SLOW_MOTION, CMD_OPT_MUTE, CMD_OPT_FPS, CMD_OPT_STATISTICS, CMD_OPT_FULLSCREEN, CMD_OPT_MENU, CMD_OPT_SHADER, CMD_OPT_VIDEO_FILTER, CMD_OPT_REWIND, CMD_OPT_RUNAHEAD, CMD_OPT_PREEMPT, CMD_OPT_VRR_RUNLOOP, CMD_OPT_TURBO_FIRE, CMD_OPT_GRAB_MOUSE, CMD_OPT_GAME_FOCUS, CMD_OPT_RECORDING, CMD_OPT_STREAMING, CMD_OPT_CHEEVOS_HARDCORE, CMD_OPT_CHEEVOS_ENABLE, CMD_OPT_LOADING, /* a value they report */ CMD_OPT_FRAME_COUNT, CMD_OPT_STATE_SLOT, CMD_OPT_REPLAY_SLOT, CMD_OPT_ACTIVE_REPLAY, CMD_OPT_SYSTEM, CMD_OPT_CONTENT, CMD_OPT_DIR_SAVEFILE, CMD_OPT_DIR_SAVESTATE, CMD_OPT_DIR_RUNTIME_LOG, CMD_OPT_DIR_LOG, CMD_OPT_DIR_CACHE, CMD_OPT_DIR_SYSTEM, CMD_OPT_NICKNAME }; /* An option GET_OPTION reports and SET_OPTION writes. * * @alias is the name GET_CONFIG_PARAM knew the option by, kept so that * interface answers what it always did; NULL where there was none. * * SET_OPTION is asked for a state, not a change, and writes only when * the option is not already in it, so sending the same value twice is * one change and then nothing. That is why @event, the single event the * hotkey toggles the option with, is enough for most of them; the few * the frontend does differently are @settable with no event, and * command_option_set() has them by @id. */ struct cmd_option_map { const char *str; const char *alias; enum cmd_option_id id; bool settable; enum event_command event; const char *desc; }; /* Whether @o is on, and its value into @s - @yes or @no for one that is * on or off, else what it reports - unless @s is NULL. * * Every option is read where its state lives rather than from the * setting behind it: a setting says what was configured, and this says * what is true now. An option a build does not have never reaches here, * having no row in the table, but its case is still compiled: what the * state would be read from may not exist. */ static bool command_option_read(const struct cmd_option_map *o, char *s, size_t len, const char *yes, const char *no) { settings_t *settings = config_get_ptr(); const char *text = NULL; bool on = false; char num[64]; switch (o->id) { case CMD_OPT_PAUSE: on = (runloop_get_flags() & RUNLOOP_FLAG_PAUSED) != 0; break; case CMD_OPT_FAST_FORWARD: on = (runloop_get_flags() & RUNLOOP_FLAG_FASTMOTION) != 0; break; case CMD_OPT_SLOW_MOTION: on = (runloop_get_flags() & RUNLOOP_FLAG_SLOWMOTION) != 0; break; case CMD_OPT_MUTE: on = audio_state_get_ptr()->mute_enable; break; case CMD_OPT_FPS: on = settings->bools.video_fps_show; break; case CMD_OPT_STATISTICS: on = settings->bools.video_statistics_show; break; case CMD_OPT_FULLSCREEN: on = settings->bools.video_fullscreen; break; case CMD_OPT_MENU: #ifdef HAVE_MENU { struct menu_state *menu_st = menu_state_get_ptr(); on = menu_st && (menu_st->flags & MENU_ST_FLAG_ALIVE); } #endif break; case CMD_OPT_SHADER: #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) on = settings->bools.video_shader_enable; #endif break; case CMD_OPT_VIDEO_FILTER: #ifdef HAVE_VIDEO_FILTER on = settings->bools.video_filter_enable; #endif break; case CMD_OPT_REWIND: #ifdef HAVE_REWIND on = settings->bools.rewind_enable; #endif break; case CMD_OPT_RUNAHEAD: #ifdef HAVE_RUNAHEAD on = settings->bools.run_ahead_enabled; #endif break; case CMD_OPT_PREEMPT: #ifdef HAVE_RUNAHEAD on = settings->bools.preemptive_frames_enable; #endif break; case CMD_OPT_VRR_RUNLOOP: on = settings->bools.vrr_runloop_enable; break; case CMD_OPT_TURBO_FIRE: on = settings->bools.input_turbo_enable; break; case CMD_OPT_GRAB_MOUSE: on = input_driver_mouse_grabbed(); break; case CMD_OPT_GAME_FOCUS: on = input_driver_game_focus_enabled(); break; /* Both are the recorder running; the stream is the one that has * somewhere to send it. */ case CMD_OPT_RECORDING: case CMD_OPT_STREAMING: { const recording_state_t *rec_st = recording_state_get_ptr(); on = rec_st && rec_st->enable && (rec_st->streaming_enable == (o->id == CMD_OPT_STREAMING)); } break; case CMD_OPT_CHEEVOS_HARDCORE: #ifdef HAVE_CHEEVOS on = rcheevos_hardcore_active(); #endif break; case CMD_OPT_CHEEVOS_ENABLE: #ifdef HAVE_CHEEVOS on = settings->bools.cheevos_enable; #endif break; case CMD_OPT_LOADING: on = command_interfaces_held(); break; case CMD_OPT_FRAME_COUNT: snprintf(num, sizeof(num), "%" PRIu64, video_driver_get_frame_count()); text = num; break; case CMD_OPT_STATE_SLOT: snprintf(num, sizeof(num), "%d", settings->ints.state_slot); text = num; break; case CMD_OPT_REPLAY_SLOT: snprintf(num, sizeof(num), "%d", settings->ints.replay_slot); text = num; break; /* the replay's identifier, its state flags and the frame it is * on, as GET_CONFIG_PARAM has always given them */ case CMD_OPT_ACTIVE_REPLAY: text = "0 0 0"; #ifdef HAVE_BSV_MOVIE { input_driver_state_t *input_st = input_state_get_ptr(); if (input_st->bsv_movie_state_handle) { bsv_movie_t *movie = input_st->bsv_movie_state_handle; snprintf(num, sizeof(num), "%lld %u %lld", (long long)movie->identifier, input_st->bsv_movie_state.flags, (long long)movie->frame_counter); text = num; } } #endif break; case CMD_OPT_SYSTEM: { core_info_t *core_info = NULL; runloop_state_t *runloop_st = runloop_state_get_ptr(); core_info_get_current_core(&core_info); if (core_info && core_info->system_id) text = core_info->system_id; else text = runloop_st->system.info.library_name; } break; case CMD_OPT_CONTENT: text = path_get(RARCH_PATH_BASENAME); break; case CMD_OPT_DIR_SAVEFILE: text = dir_get_ptr(RARCH_DIR_SAVEFILE); break; case CMD_OPT_DIR_SAVESTATE: text = dir_get_ptr(RARCH_DIR_SAVESTATE); break; case CMD_OPT_DIR_RUNTIME_LOG: text = settings->paths.directory_runtime_log; break; case CMD_OPT_DIR_LOG: text = settings->paths.log_dir; break; case CMD_OPT_DIR_CACHE: text = settings->paths.directory_cache; break; case CMD_OPT_DIR_SYSTEM: text = settings->paths.directory_system; break; case CMD_OPT_NICKNAME: text = settings->paths.username; break; } if (s) strlcpy(s, text ? text : (on ? yes : no), len); return on; } /* Puts @o in state @on, which it is not already in. */ static bool command_option_set(const struct cmd_option_map *o, command_t *cmd, bool on) { switch (o->id) { case CMD_OPT_PAUSE: command_event(on ? CMD_EVENT_PAUSE : CMD_EVENT_UNPAUSE, NULL); break; case CMD_OPT_RECORDING: case CMD_OPT_STREAMING: if (on && recording_state_get_ptr()->enable) { command_reply_error(cmd, "SET_OPTION", "stop the active recording or stream before starting %s", o->str); return false; } if (o->id == CMD_OPT_STREAMING) command_event(CMD_EVENT_STREAMING_TOGGLE, NULL); else command_event(on ? CMD_EVENT_RECORD_INIT : CMD_EVENT_RECORD_DEINIT, NULL); break; case CMD_OPT_GAME_FOCUS: { enum input_game_focus_cmd_type type = on ? GAME_FOCUS_CMD_ON : GAME_FOCUS_CMD_OFF; command_event(CMD_EVENT_GAME_FOCUS_TOGGLE, &type); } break; case CMD_OPT_FAST_FORWARD: return runloop_set_fastmotion(on); case CMD_OPT_SLOW_MOTION: return runloop_set_slowmotion(on); default: command_event(o->event, NULL); break; } /* An event that cannot act now - pausing with no core running, the * video filter likewise - does nothing and reports success all the * same, so what it reached is read back. */ return command_option_read(o, NULL, 0, NULL, NULL) == on; } static const struct cmd_option_map option_map[] = { { "pause", NULL, CMD_OPT_PAUSE, true, CMD_EVENT_NONE, "The content is paused." }, { "fast_forward", NULL, CMD_OPT_FAST_FORWARD, true, CMD_EVENT_NONE, "Fast-forward is running." }, { "slow_motion", NULL, CMD_OPT_SLOW_MOTION, true, CMD_EVENT_NONE, "Slow motion is running." }, { "mute", NULL, CMD_OPT_MUTE, true, CMD_EVENT_AUDIO_MUTE_TOGGLE, "Audio is muted." }, { "fps", NULL, CMD_OPT_FPS, true, CMD_EVENT_FPS_TOGGLE, "The framerate is shown on screen." }, { "statistics", NULL, CMD_OPT_STATISTICS, true, CMD_EVENT_STATISTICS_TOGGLE, "The technical statistics are shown on screen." }, { "fullscreen", "video_fullscreen", CMD_OPT_FULLSCREEN, true, CMD_EVENT_FULLSCREEN_TOGGLE, "The window is fullscreen." }, #ifdef HAVE_MENU { "menu", "menu_active", CMD_OPT_MENU, true, CMD_EVENT_MENU_TOGGLE, "The menu is open." }, #endif #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) { "shader", NULL, CMD_OPT_SHADER, true, CMD_EVENT_SHADER_TOGGLE, "The shader preset is applied." }, #endif #ifdef HAVE_VIDEO_FILTER { "video_filter", NULL, CMD_OPT_VIDEO_FILTER, true, CMD_VIDEO_FILTER_TOGGLE, "The video filter is applied." }, #endif #ifdef HAVE_REWIND { "rewind", NULL, CMD_OPT_REWIND, true, CMD_EVENT_REWIND_TOGGLE, "Rewind is available." }, #endif #ifdef HAVE_RUNAHEAD { "runahead", NULL, CMD_OPT_RUNAHEAD, true, CMD_EVENT_RUNAHEAD_TOGGLE, "Run-ahead is on." }, { "preempt", NULL, CMD_OPT_PREEMPT, true, CMD_EVENT_PREEMPT_TOGGLE, "Preemptive frames are on." }, #endif { "vrr_runloop", NULL, CMD_OPT_VRR_RUNLOOP, true, CMD_EVENT_VRR_RUNLOOP_TOGGLE, "The runloop syncs to the exact content framerate." }, { "turbo_fire", NULL, CMD_OPT_TURBO_FIRE, true, CMD_EVENT_TURBO_FIRE_TOGGLE, "Turbo fire is on." }, { "grab_mouse", NULL, CMD_OPT_GRAB_MOUSE, true, CMD_EVENT_GRAB_MOUSE_TOGGLE, "The mouse is grabbed." }, { "game_focus", NULL, CMD_OPT_GAME_FOCUS, true, CMD_EVENT_NONE, "The game has all keyboard input, rather than the hotkeys." }, { "recording", NULL, CMD_OPT_RECORDING, true, CMD_EVENT_NONE, "Video is being recorded to a file." }, { "streaming", NULL, CMD_OPT_STREAMING, true, CMD_EVENT_NONE, "The session is being streamed." }, #ifdef HAVE_CHEEVOS { "cheevos_hardcore", NULL, CMD_OPT_CHEEVOS_HARDCORE, true, CMD_EVENT_CHEEVOS_HARDCORE_MODE_TOGGLE, "Achievements hardcore mode is on: it forbids states, rewind and " "frame advance." }, { "cheevos_enable", "cheevos_enable", CMD_OPT_CHEEVOS_ENABLE, false, CMD_EVENT_NONE, "Achievements are enabled." }, #endif /* Reported, never set. */ { "loading", NULL, CMD_OPT_LOADING, false, CMD_EVENT_NONE, "Content is loading: a command that needs the core waits for it." }, { "frame_count", NULL, CMD_OPT_FRAME_COUNT, false, CMD_EVENT_NONE, "Frames shown since the video driver started." }, { "state_slot", NULL, CMD_OPT_STATE_SLOT, false, CMD_EVENT_NONE, "The save state slot LOAD_STATE and SAVE_STATE use." }, { "replay_slot", NULL, CMD_OPT_REPLAY_SLOT, false, CMD_EVENT_NONE, "The replay slot PLAY_REPLAY and RECORD_REPLAY use." }, { "active_replay", "active_replay", CMD_OPT_ACTIVE_REPLAY, false, CMD_EVENT_NONE, "The open replay: its identifier, state flags and frame, or zeroes." }, { "system", NULL, CMD_OPT_SYSTEM, false, CMD_EVENT_NONE, "The running core's system, or empty when none is loaded." }, { "content", NULL, CMD_OPT_CONTENT, false, CMD_EVENT_NONE, "The running content's path, or empty when none is loaded." }, { "savefile_directory", "savefile_directory", CMD_OPT_DIR_SAVEFILE, false, CMD_EVENT_NONE, "Where save files are written." }, { "savestate_directory", "savestate_directory", CMD_OPT_DIR_SAVESTATE, false, CMD_EVENT_NONE, "Where save states are written." }, { "runtime_log_directory", "runtime_log_directory", CMD_OPT_DIR_RUNTIME_LOG, false, CMD_EVENT_NONE, "Where runtime logs are written." }, { "log_dir", "log_dir", CMD_OPT_DIR_LOG, false, CMD_EVENT_NONE, "Where the log is written." }, { "cache_directory", "cache_directory", CMD_OPT_DIR_CACHE, false, CMD_EVENT_NONE, "Where extracted content is cached." }, { "system_directory", "system_directory", CMD_OPT_DIR_SYSTEM, false, CMD_EVENT_NONE, "Where cores look for system files." }, { "netplay_nickname", "netplay_nickname", CMD_OPT_NICKNAME, false, CMD_EVENT_NONE, "The name shown to netplay peers." } }; /* The option @name, by its own name or the one GET_CONFIG_PARAM knows * it by when @alias, or NULL. */ static const struct cmd_option_map *command_option_find(const char *name, bool alias) { size_t i; for (i = 0; i < ARRAY_SIZE(option_map); i++) { if (string_is_equal(name, option_map[i].str)) return &option_map[i]; if ( alias && option_map[i].alias && string_is_equal(name, option_map[i].alias)) return &option_map[i]; } return NULL; } /* GET_OPTION [option] * * Every option and its value, name and value tab separated, one per * line; with a name, that option's line alone. */ bool command_get_option(command_t *cmd, const char *arg) { char value[PATH_MAX_LENGTH]; size_t i; size_t _len = 0; bool found = false; bool one; size_t cap; char *reply; while (arg && *arg == ' ') arg++; one = arg && *arg; /* one line needs room for one; the whole table stops short of what a * reply can carry, as the other lists do */ cap = one ? NAME_MAX_LENGTH + PATH_MAX_LENGTH + 2 : COMMAND_LIST_REPLY_MAX; if (!(reply = (char*)malloc(cap + 1))) { command_reply_error(cmd, "GET_OPTION", "out of memory"); return false; } reply[0] = '\0'; for (i = 0; i < ARRAY_SIZE(option_map); i++) { int n; if (one && !string_is_equal(arg, option_map[i].str)) continue; found = true; command_option_read(&option_map[i], value, sizeof(value), "on", "off"); n = snprintf(reply + _len, cap - _len, "%s\t%s\n", option_map[i].str, value); /* Short of a reply holding every option, half the list would be * read as the whole of it, so say so instead. */ if (n < 0 || (size_t)n >= cap - _len) { free(reply); command_reply_error(cmd, "GET_OPTION", "the options did not fit one reply, stopping at %s", option_map[i].str); return false; } _len += (size_t)n; } if (!found) { free(reply); command_reply_error(cmd, "GET_OPTION", "no such option %s; GET_OPTION with no argument lists them", arg); return false; } cmd->replier(cmd, reply, _len); free(reply); return true; } /* SET_OPTION