#include #include #include #ifdef __WIN32__ #include #include #ifndef MSG_NOSIGNAL #define MSG_NOSIGNAL 0 #endif #else #include #include #include #include #include #endif #include "til_str.h" #include "til_stream.h" #include "rkt.h" #include "rkt_scener.h" /* Copyright (C) 2023 - Vito Caputo */ /* This implements a rudimentary BBS-style interface for manipulating the scenes in ctxt->scenes. * * Only a single connection is supported at this time. It's really intended just to get _something_ * cross-platform usable for editing the available scenes "live" at runtime in a minimum of time/effort. * * A more "modern" approach to this would be a HTTP REST API, yikes. */ #define RKT_SCENER_DEFAULT_MODULE "compose" typedef enum rkt_scener_fsm_t { RKT_SCENER_FSM_LISTENING, /* port is listening, waiting for connection */ RKT_SCENER_FSM_SENDING, /* sending output */ RKT_SCENER_FSM_RECVING, /* reading input */ RKT_SCENER_FSM_SEND_SETTINGS, /* send rkt's settings hierarchy, including current scenes state, as args */ RKT_SCENER_FSM_SEND_SCENES, /* send main scenes list -> prompt */ RKT_SCENER_FSM_RECV_SCENES, /* waiting/reading at main scenes prompt */ RKT_SCENER_FSM_SEND_SCENE, /* send per-scene dialog for scene @ scener->scene -> prompt */ RKT_SCENER_FSM_RECV_SCENE, /* waiting/reading at the per-scene prompt */ RKT_SCENER_FSM_RECV_EDITSCENE, /* waiting/reading at the edit scene prompt, creating/setting up replacement scene on input */ RKT_SCENER_FSM_RECV_NEWSCENE, /* waiting/reading at the new scene prompt, creating/setting up new scene on input */ RKT_SCENER_FSM_SEND_NEWSCENE_SETUP, /* send whatever's necessary for next step of new_scene.settings setup */ RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT, RKT_SCENER_FSM_RECV_NEWSCENE_SETUP, /* waiting/reading at new scene setup setting prompt, finalizing and adding when complete */ } rkt_scener_fsm_t; typedef struct rkt_scener_t { rkt_scener_fsm_t state, next_state; unsigned scene; unsigned pin_scene:1; int listener; int client; til_str_t *input; til_str_t *output; size_t output_pos; struct { /* used while constructing a new scene, otherwise NULL */ til_settings_t *settings; til_setting_t *cur_setting; const til_setting_desc_t *cur_desc; til_setting_t *cur_invalid; til_setting_t *cur_edited; unsigned replacement:1; /* set when editing / replacing scener->scene */ unsigned editing:1; /* set when editing */ } new_scene; } rkt_scener_t; static int rkt_nonblocking(int socket) { #ifndef __WIN32__ if (fcntl(socket, F_SETFL, (int)O_NONBLOCK) == -1) return -1; #else if (ioctlsocket(socket, FIONBIO, &(u_long){1}) != 0) return -1; #endif return 0; } int rkt_scener_startup(rkt_context_t *ctxt) { rkt_scener_t *scener; rkt_setup_t *setup; int on = 1; struct sockaddr_in addr; assert(ctxt); setup = ((rkt_setup_t *)ctxt->til_module_context.setup); scener = calloc(1, sizeof(rkt_scener_t)); if (!scener) return -ENOMEM; scener->listener = socket(AF_INET, SOCK_STREAM, 0); if (scener->listener == -1) goto _err_free; if (setsockopt(scener->listener, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on)) == -1) goto _err_close; addr.sin_family = AF_INET; addr.sin_port = htons(setup->scener_port); addr.sin_addr.s_addr = inet_addr(setup->scener_address); if (bind(scener->listener, (struct sockaddr *)&addr, sizeof(addr)) == -1) goto _err_close; if (listen(scener->listener, 1) == -1) goto _err_close; if (rkt_nonblocking(scener->listener) == -1) goto _err_close; scener->client = -1; ctxt->scener = scener; return 0; _err_close: close(scener->listener); _err_free: free(scener); return -errno; } /* helper for sending output, entering next_state once sent */ static int rkt_scener_send(rkt_scener_t *scener, til_str_t *output, rkt_scener_fsm_t next_state) { assert(scener); assert(!scener->output); /* catch entering send mid-send (or leaking output) */ assert(output); assert(next_state != RKT_SCENER_FSM_SENDING); /* we generally send after processing input, so cleaning up for the input handlers here * is ergonomic, enabling such callers to simply 'return rkt_scener_send(...);' */ scener->input = til_str_free(scener->input); scener->output_pos = 0; scener->output = output; scener->next_state = next_state; scener->state = RKT_SCENER_FSM_SENDING; return 0; } /* helper for receiving input, entering next_state once received (a line of text) */ static int rkt_scener_recv(rkt_scener_t *scener, rkt_scener_fsm_t next_state) { assert(scener); assert(!scener->input); assert(next_state != RKT_SCENER_FSM_RECVING); scener->next_state = next_state; scener->state = RKT_SCENER_FSM_RECVING; return 0; } /* helper for reentering the listening state and returning -errno, for hard errors */ static int rkt_scener_err_close(rkt_scener_t *scener, int err) { assert(scener); if (err > 0) err = -err; scener->state = RKT_SCENER_FSM_LISTENING; return err; } /* helper for sending a minimal strerror(errno) style message to the user before entering next_state */ static int rkt_scener_send_error(rkt_scener_t *scener, int error, rkt_scener_fsm_t next_state) { til_str_t *output; assert(scener); /* TODO: this should really use a static allocated output buffer to try work under ENOMEM */ output = til_str_newf("\nError: %s\n", strerror(error)); if (!output) return -ENOMEM; return rkt_scener_send(scener, output, next_state); } /* helper for sending "invalid input" message w/scener->input incorporated */ static int rkt_scener_send_invalid_input(rkt_scener_t *scener, rkt_scener_fsm_t next_state) { til_str_t *output; assert(scener); assert(scener->input); output = til_str_new("\nInvalid input: "); if (!output) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, "\"%s\"\n\n", til_str_buf(scener->input, NULL)) < 0) return rkt_scener_err_close(scener, ENOMEM); scener->input = til_str_free(scener->input); return rkt_scener_send(scener, output, next_state); } /* helper for sending simple messages */ /* TODO: make variadic */ static int rkt_scener_send_message(rkt_scener_t *scener, const char *msg, rkt_scener_fsm_t next_state) { til_str_t *output; output = til_str_new(msg); if (!output) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, next_state); } /* send welcome message */ static int rkt_scener_send_welcome(rkt_scener_t *scener, rkt_scener_fsm_t next_state) { return rkt_scener_send_message(scener, "\n\nWelcome to scener.\n" "\n\n Long live the scene!\n\n", next_state); } /* send goodbye message */ static int rkt_scener_send_goodbye(rkt_scener_t *scener, rkt_scener_fsm_t next_state) { return rkt_scener_send_message(scener, "\n\n The scene is dead.\n\n", next_state); } static int rkt_scener_edit_scene(rkt_context_t *ctxt) { rkt_scener_t *scener; til_settings_t *scenes_settings; til_setting_t *scene_setting; til_str_t *output; char *as_arg; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); assert(!scener->new_scene.settings); /* XXX TODO: should we expect to be called with scene=RKT_EXIT_SCENE_IDX? */ assert(scener->scene < ctxt->n_scenes); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; if (!til_settings_get_value_by_idx(scenes_settings, scener->scene, &scene_setting)) return rkt_scener_err_close(scener, ENOENT); as_arg = til_settings_as_arg(scene_setting->value_as_nested_settings); if (!as_arg ) return rkt_scener_err_close(scener, ENOMEM); output = til_str_newf("\nInput replacement scene \"module[,settings...]\" \n [%s]: ", as_arg); free(as_arg); if (!output) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, RKT_SCENER_FSM_RECV_EDITSCENE); } static int rkt_scener_new_scene(rkt_context_t *ctxt) { rkt_scener_t *scener; til_str_t *output; assert(ctxt); scener = ctxt->scener; assert(scener); output = til_str_new("\nInput new scene \"module[,settings...]\" :\n"); if (!output) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, RKT_SCENER_FSM_RECV_NEWSCENE); } /* handle input from the main scenes prompt */ static int rkt_scener_handle_input_scenes(rkt_context_t *ctxt) { rkt_scener_t *scener; size_t len, i; const char *buf; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); buf = til_str_buf(scener->input, &len); /* skip any leading whitespace XXX maybe move to a helper? */ for (i = 0; i < len; i++) { if (buf[i] != '\t' && buf[i] != ' ') break; } switch (buf[i]) { case '0' ... '9': { /* edit scene, parse uint */ unsigned s = 0; for (;i < len; i++) { if (buf[i] < '0' || buf[i] > '9') break; s *= 10; s += buf[i] - '0'; } /* XXX: maybe skip trailing whitespace too? */ if (i < len) return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENES); if (s >= ctxt->n_scenes) return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENES); scener->scene = s; scener->state = RKT_SCENER_FSM_SEND_SCENE; break; } case 'N': /* Add new scene */ case 'n': return rkt_scener_new_scene(ctxt); case 'S': /* Serialize current settings for the entire rkt scenes state */ case 's': scener->state = RKT_SCENER_FSM_SEND_SETTINGS; break; case 'Q': /* End scener session; disconnects, but leaves rkt/rototiller intact */ case 'q': /* TODO: it might make sense to dump the serialized settings on quit just as a safety-net, * or ask if an export is desired (assuming track data exports from scener becomes a thing) */ return rkt_scener_send_goodbye(scener, RKT_SCENER_FSM_LISTENING); case '!': /* toggle pin_scene */ scener->pin_scene = !scener->pin_scene; scener->state = RKT_SCENER_FSM_SEND_SCENES; break; case '=': /* set scener scene idx to current Rocket scene idx, and go to scene view */ scener->scene = ctxt->scene; scener->state = RKT_SCENER_FSM_SEND_SCENE; break; case '\0': /* if you don't say anything to even quote as "invalid input", just go back to the scenes dialog */ scener->state = RKT_SCENER_FSM_SEND_SCENES; break; default: return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENES); } /* XXX: note it's important the above non-invalid-input cases break and not return, * so we discard the input. */ scener->input = til_str_free(scener->input); return 0; } static int rkt_scener_handle_input_editscene(rkt_context_t *ctxt) { rkt_scener_t *scener; til_settings_t *scenes_settings; til_setting_t *scene_setting; til_settings_t *new_settings; const char *buf, *label; size_t len; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); assert(!scener->new_scene.settings); /* XXX TODO: should we expect to be called with scene=RKT_EXIT_SCENE_IDX? */ assert(scener->scene < ctxt->n_scenes); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; if (!til_settings_get_value_by_idx(scenes_settings, scener->scene, &scene_setting)) return rkt_scener_err_close(scener, ENOENT); buf = til_str_buf(scener->input, &len); if (!len) { buf = til_settings_as_arg(scene_setting->value_as_nested_settings); if (!buf) return rkt_scener_err_close(scener, ENOMEM); } label = til_settings_get_label(scene_setting->value_as_nested_settings); new_settings = til_settings_new(NULL, scenes_settings, label, buf); if (!len) free((void *)buf); if (!new_settings) return rkt_scener_err_close(scener, ENOMEM); scener->input = til_str_free(scener->input); scener->new_scene.settings = new_settings; scener->new_scene.cur_setting = NULL; scener->new_scene.cur_desc = NULL; scener->new_scene.cur_invalid = NULL; scener->new_scene.cur_edited = NULL; scener->new_scene.replacement = 1; scener->new_scene.editing = 1; scener->state = RKT_SCENER_FSM_SEND_NEWSCENE_SETUP; return 0; } static int rkt_scener_handle_input_newscene(rkt_context_t *ctxt) { rkt_scener_t *scener; til_settings_t *scenes_settings; til_settings_t *new_settings; const char *buf; size_t len; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); assert(!scener->new_scene.settings); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; /* XXX: this !len exception is to treat "" exceptionally and not add a "" bare-value @ idx 0, * which will be looked up as the module name. It would be nice to not need this, maybe * if the invalid input handler could just be graceful enough this could be removed and * allow the "" invalid module name to then fall-through into a module re-select with * the empty name in the setting... TODO */ buf = til_str_buf(scener->input, &len); if (!len) buf = NULL; new_settings = til_settings_new(NULL, scenes_settings, "WIP-new-scene", buf); if (!new_settings) return rkt_scener_err_close(scener, ENOMEM); scener->input = til_str_free(scener->input); scener->new_scene.settings = new_settings; scener->new_scene.cur_setting = NULL; scener->new_scene.cur_desc = NULL; scener->new_scene.cur_invalid = NULL; scener->new_scene.cur_edited = NULL; scener->new_scene.replacement = 0; scener->new_scene.editing = 0; scener->state = RKT_SCENER_FSM_SEND_NEWSCENE_SETUP; return 0; } static int rkt_scener_handle_input_newscene_setup(rkt_context_t *ctxt) { rkt_scener_t *scener; til_setting_t *setting; const til_setting_desc_t *desc; const til_setting_t *invalid; int editing; size_t len; const char *buf; const char *value; const char *preferred; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); assert(scener->new_scene.settings); setting = scener->new_scene.cur_setting; desc = scener->new_scene.cur_desc; invalid = scener->new_scene.cur_invalid; editing = scener->new_scene.editing; if (invalid && setting == invalid && !desc) desc = invalid->desc; assert(desc); preferred = desc->spec.preferred; if (scener->new_scene.editing && setting) preferred = til_setting_get_raw_value(setting); assert(preferred); /* we're adding a setting, the input may be a raw string, or * it might be a subscript of an array of values, it all depends on * scener->new_scene.cur_desc * * there should probably be some optional syntax supported for forcing * raw values in the event you want to go outside what's in the values, * bypassing any spec check in the process, relying entirely on the * setup function's robustness to detect invalid input, which is * going to break sometimes no doubt. But the values[] arrays are * quite limited in some cases where we've basically just given a * handful of presets for something where really any old float value * would work. Maybe what's really necessary there is to just have * something in the spec that says "arbitrary_ok" despite having * values[]. There's certainly some strict scenarios where the list has * the only valid options (eg drizzle::style={mask,map}), so blocking * raw input for those seems sensible. TODO */ buf = til_str_buf(scener->input, &len); if (!len) { value = preferred; } else { til_str_t *output; if (desc->spec.values) { /* multiple choice */ if (buf[0] != ':') { /* map numeric input to values entry */ unsigned i, j, found; if (sscanf(buf, "%u", &j) < 1) { output = til_str_newf("Invalid input: \"%s\"\n", buf); if (!output) return -ENOMEM; return rkt_scener_send(scener, output, RKT_SCENER_FSM_SEND_NEWSCENE_SETUP); } for (found = i = 0; desc->spec.values[i]; i++) { if (i == j) { value = desc->spec.values[i]; found = 1; break; } } if (!found) { output = til_str_newf("Invalid option: %u outside of range [0-%u]\n", j, i - 1); if (!output) return -ENOMEM; return rkt_scener_send(scener, output, RKT_SCENER_FSM_SEND_NEWSCENE_SETUP); } } else { /* = prefix overrides the multiple choice options with whatever follows the = */ value = buf; } } else { /* use typed input as setting, TODO: apply regex */ value = buf; } } /* we might be fixing an invalid setting instead of adding, determine that here */ if (invalid && setting == invalid) { til_setting_set_raw_value(setting, value); /* TODO: ENOMEM */ scener->new_scene.cur_invalid = NULL; /* try again */ } else if (editing && setting) { til_setting_set_raw_value(setting, value); /* TODO: ENOMEM */ } else if (!(setting = til_settings_add_value(desc->container, desc->spec.key, value))) return -ENOMEM; if (editing) scener->new_scene.cur_edited = setting; scener->input = til_str_free(scener->input); scener->state = RKT_SCENER_FSM_SEND_NEWSCENE_SETUP; return 0; } /* randomize the settings for ctxt->scenes[scene], keeping its current module */ static int rkt_scener_randomize_scene_settings(rkt_context_t *ctxt, unsigned scene_idx) { const til_module_t *module; rkt_scener_t *scener; rkt_scene_t *scene; til_settings_t *scenes_settings; til_setting_t *scene_setting; til_setting_t *module_name_setting; til_settings_t *new_settings; til_setup_t *setup; char *label; int r; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scene_idx < ctxt->n_scenes); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; scene = &ctxt->scenes[scene_idx]; module = scene->module_ctxt->module, assert(module); til_settings_get_value_by_idx(scenes_settings, scene_idx, &scene_setting); /* FIXME: this is all rather janky TODO clean up the api for these uses, helpers, whatever */ r = til_settings_label_setting(scenes_settings, scene_setting, &label); if (r < 0) return r; if (!til_settings_get_value_by_idx(scene_setting->value_as_nested_settings, 0, &module_name_setting)) { free(label); return -EINVAL; } new_settings = til_settings_new(NULL, scenes_settings, label, til_setting_get_raw_value(module_name_setting)); free(label); if (!new_settings) return -ENOMEM; /* FIXME: seed reproducibility needs to be sorted out, maybe move seed into settings */ til_module_settings_randomize(module, new_settings, rand_r(&ctxt->til_module_context.seed), &setup, NULL); /* TODO: errors! */ scene_setting->value_as_nested_settings = new_settings; #if 0 free((void *)scene_setting->value); scene_setting->value = as_arg; /* XXX should I really overrite the original bare-value? maybe * preserve the ability to go back to what it was by leaving * this alone? printing the settings_as_arg ignores this anyways, * when there's a non-NULL value_as_nested_settings... * Maybe once scene editing gets implemented, this may become * more of an issue with an obvious Right Way. */ #endif scene->module_ctxt = til_module_context_free(scene->module_ctxt); (void) til_module_create_context(module, ctxt->til_module_context.stream, rand_r(&ctxt->til_module_context.seed), ctxt->til_module_context.last_ticks, ctxt->til_module_context.n_cpus, setup, &scene->module_ctxt); /* TODO: errors */ til_setup_free(setup); /* This will probably get more complicated once rkt starts getting more active about * creating and destroying scene contexts only while they're in use. But today all * contexts persist for the duration, so they always have a reference as long as they're * extant in ctxt->scenes[]... */ til_stream_gc_module_contexts(ctxt->til_module_context.stream); return 0; } static int rkt_scener_handle_input_scene(rkt_context_t *ctxt) { rkt_scener_t *scener; size_t len, i; const char *buf; assert(ctxt); scener = ctxt->scener; assert(scener); assert(scener->input); buf = til_str_buf(scener->input, &len); /* skip any leading whitespace XXX maybe move to a helper? */ for (i = 0; i < len; i++) { if (buf[i] != '\t' && buf[i] != ' ') break; } switch (buf[i]) { case '0' ... '9': { /* edit scene */ unsigned s = 0; /* XXX: move to function? */ for (;i < len; i++) { if (buf[i] < '0' || buf[i] > '9') break; s *= 10; s += buf[i] - '0'; } /* XXX: maybe skip trailing whitespace too? */ if (i < len) return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENE); if (s >= ctxt->n_scenes) return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENE); scener->scene = s; scener->state = RKT_SCENER_FSM_SEND_SCENE; break; } case 'E': /* edit scene settings */ case 'e': return rkt_scener_edit_scene(ctxt); case 'R': /* randomize only the settings (keep existing module) */ case 'r': { int r; r = rkt_scener_randomize_scene_settings(ctxt, scener->scene); if (r < 0) return rkt_scener_send_error(scener, r, RKT_SCENER_FSM_SEND_SCENE); scener->state = RKT_SCENER_FSM_SEND_SCENE; break; } case 'N': case 'n': return rkt_scener_new_scene(ctxt); case '!': /* toggle pin_scene */ scener->pin_scene = !scener->pin_scene; scener->state = RKT_SCENER_FSM_SEND_SCENE; break; case '=': /* set scener scene idx to current Rocket scene idx, and go to edit scene view */ scener->scene = ctxt->scene; scener->state = RKT_SCENER_FSM_SEND_SCENE; break; case '\0': /* if you don't say anything to even quote as "invalid input", just go back to the scenes dialog */ scener->state = RKT_SCENER_FSM_SEND_SCENES; break; default: return rkt_scener_send_invalid_input(scener, RKT_SCENER_FSM_SEND_SCENE); } /* XXX: note it's important the above non-invalid-input cases break and not return, * so we discard the input. */ scener->input = til_str_free(scener->input); return 0; } /* The architecture here is kept simple; just one client is supported, the sockets are * all put in non-blocking mode, so we just poll for accepts on the listener when not * connected (listening), or poll for send buffer availability when sending, or poll for * recv bytes when receiving. We're only doing one of those at a time WRT IO, per-update. * * When something is to be sent, it gets buffered entirely and placed in scener->output * before entering a generic sending state which persists until the output is * all sent. Once that happens, the queued "next state" gets entered. Since we're not going * to be sending big binary streams of filesystem data, this is fine; it's basically a BBS UI. * * When something needs to be received, a "next state" is queued and the generic receiving * state entered. The receiving state persists receiving bytes until a newline byte is * received, making this fundamentally line-oriented. The received line is buffered and left * in scener->input, for the queued "next state" to handle, which is transitioned to @ newline. * Yes, this means scener will consume all memory if you just keep sending non-newline data to * it, this isn't a public tcp service, it's a tool just for you to use - the safety guards are * minimal if present at all. * * That's basically all there is... error situations are handled by reentering the listening * state which will first close the client fd if valid, before resuming polling the listener fd * w/accept. * * This update function is expected to be called regularly by rkt, probably every frame. It's * kept in this "dumb polling" single-threaded synchronous fashion deliberately so scener can be * relatively unconcerned about mucking with the scenes state and any other rkt state without * introducing locks or other synchronization complexities. It's not the greatest perf-wise, but * keep in mind this is strictly for the creative process, you wouldn't have this enabled when * running a finished prod in anything resembling release mode. * * The return value is -errno on errors, 0 on "everything's normal". Some attempt * has been made to be resilient to errors in that many -errno paths will just return * the state to "listening" and may just recover. I'm not sure right now what rkt will * do with the errors coming out of this function, for now they'll surely just be silently * ignored... but at some point it might be useful to print something to stderr about their * nature at least, in rkt that is, not here. */ int rkt_scener_update(rkt_context_t *ctxt) { rkt_scener_t *scener; unsigned ctxt_scene; assert(ctxt); if (!ctxt->scener) return 0; scener = ctxt->scener; ctxt_scene = ctxt->scene; if (scener->pin_scene) ctxt->scene = scener->scene; switch (scener->state) { case RKT_SCENER_FSM_LISTENING: { int fd, on = 1; /* any state can just resume listening anytime, which will close and free things */ if (scener->client != -1) { #ifndef __WIN32__ close(scener->client); #else closesocket(scener->client); #endif scener->client = -1; } scener->output = til_str_free(scener->output); scener->input = til_str_free(scener->input); scener->new_scene.settings = til_settings_free(scener->new_scene.settings); fd = accept(scener->listener, NULL, NULL); if (fd == -1) { #ifndef __WIN32__ if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; return -errno; #else if (WSAGetLastError() == WSAEWOULDBLOCK) return 0; return -(WSAGetLastError() - WSABASEERR); #endif } if (rkt_nonblocking(fd) == -1) { close(fd); return -errno; } scener->client = fd; (void) setsockopt(scener->client, IPPROTO_TCP, TCP_NODELAY, (void *)&on, sizeof(on)); return rkt_scener_send_welcome(scener, RKT_SCENER_FSM_SEND_SCENES); } case RKT_SCENER_FSM_SENDING: { const char *buf; size_t len; ssize_t ret; buf = til_str_buf(scener->output, &len); assert(scener->output_pos < len); ret = send(scener->client, &buf[scener->output_pos], len - scener->output_pos, MSG_NOSIGNAL); if (ret == -1) { #ifndef __WIN32__ if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; #else if (WSAGetLastError() == WSAEWOULDBLOCK) return 0; #endif return rkt_scener_err_close(scener, errno); } scener->output_pos += ret; if (scener->output_pos < len) return 0; scener->output = til_str_free(scener->output); scener->state = scener->next_state; return 0; } case RKT_SCENER_FSM_RECVING: { char b[2] = {}; for (;;) { /* keep accumulating input until a newline, then transition to next_state */ switch (recv(scener->client, &b[0], 1, 0)) { case -1: #ifndef __WIN32__ if (errno == EWOULDBLOCK || errno == EAGAIN) return 0; #else if (WSAGetLastError() == WSAEWOULDBLOCK) return 0; #endif return rkt_scener_err_close(scener, errno); case 0: /* client shutdown before finding \n, return to listening (discard partial input) */ return rkt_scener_err_close(scener, 0); case 1: /* add byte to input buffer, transition on newline (TODO: one-byte recv()s are _slow_) */ if (!scener->input) { scener->input = til_str_new(b); if (!scener->input) return rkt_scener_err_close(scener, ENOMEM); } else { if (til_str_appendf(scener->input, "%c", b[0]) < 0) return rkt_scener_err_close(scener, ENOMEM); } if (b[0] == '\n') { /* TODO: maybe support escaping the newline for multiline input? */ /* before transitioning. let's strip off the line delimiter. * it's effectively encapsulation protocol and not part of the input buffer */ scener->input = til_str_chomp(scener->input); scener->state = scener->next_state; return 0; } continue; default: assert(0); } } } case RKT_SCENER_FSM_SEND_SETTINGS: { til_str_t *output; char *as_arg; as_arg = til_settings_as_arg(((rkt_setup_t *)ctxt->til_module_context.setup)->settings); if (!as_arg) return rkt_scener_err_close(scener, ENOMEM); output = til_str_newf("\n--module='%s'\n", as_arg); free(as_arg); if (!output) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, RKT_SCENER_FSM_SEND_SCENES); } case RKT_SCENER_FSM_SEND_SCENES: { til_settings_t *scenes_settings; til_str_t *output; unsigned i; output = til_str_new("\n\n"); if (!output) return rkt_scener_err_close(scener, ENOMEM); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; if (til_settings_strprint_path(scenes_settings, output) < 0) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, ":\n\n") < 0) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, " +- Rocket\n" " |+- Scener\n" " ||+- Pinned by scener\n" " |||\n") < 0) return rkt_scener_err_close(scener, ENOMEM); for (i = 0; i < ctxt->n_scenes; i++) { if (til_str_appendf(output, " %c%c%c%s\n", ctxt_scene == i ? '*' : ' ', scener->scene == i ? '*' : ' ', (scener->scene == i && scener->pin_scene) ? '!' : ' ', ctxt->scenes[i].module_ctxt->setup->path) < 0) return rkt_scener_err_close(scener, ENOMEM); } if (til_str_appendf(output, " ...\n %c%c%cEXITED [%u]\n", ctxt_scene == RKT_EXIT_SCENE_IDX ? '*' : ' ', scener->scene == RKT_EXIT_SCENE_IDX ? '*' : ' ', (scener->scene == RKT_EXIT_SCENE_IDX && scener->pin_scene) ? '!' : ' ', RKT_EXIT_SCENE_IDX) < 0) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, "\n") < 0) return rkt_scener_err_close(scener, ENOMEM); if (i) { if (til_str_appendf(output, " [0-%u,=]", i - 1) < 0) return rkt_scener_err_close(scener, ENOMEM); } if (til_str_appendf(output, " (N)ewScene (S)howSettings %s (Q)uit: ", scener->pin_scene ? "Unpin(!)" : "Pin(!)") < 0) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, RKT_SCENER_FSM_RECV_SCENES); } case RKT_SCENER_FSM_RECV_SCENES: if (!scener->input) return rkt_scener_recv(scener, scener->state); return rkt_scener_handle_input_scenes(ctxt); case RKT_SCENER_FSM_RECV_EDITSCENE: if (!scener->input) return rkt_scener_recv(scener, scener->state); return rkt_scener_handle_input_editscene(ctxt); case RKT_SCENER_FSM_RECV_NEWSCENE: if (!scener->input) return rkt_scener_recv(scener, scener->state); return rkt_scener_handle_input_newscene(ctxt); case RKT_SCENER_FSM_SEND_NEWSCENE_SETUP: { int r; r = rkt_scene_module_setup(scener->new_scene.settings, &scener->new_scene.cur_setting, &scener->new_scene.cur_desc, NULL); /* res_setup deliberately left NULL for two reasons: * 1. prevents finalizing (path is "...WIP-new-scene...") * 2. disambiguates -EINVAL errors from those potentially * returned while finalizing/baking into a til_setup_t. * * This way if such an error is returned, we can expect * res_setting and res_desc to refer to /what's/ invalid, * and correct it before retrying. * * It'd be rather obnoxious with how the setup funcs are * implemented currently to try force returning the relevant * setting+desc for errors found during baking... I still * need to firm up and document setup_func error semantics, and * ideally it would be nice if the finalizing could say what * setting/desc is relevant when say.. doing an atoi(). * That will require a bit of churn in the settings get_value * API and changing all the setup_funcs to be setting-centric * instead of the current char* value-centric mode TODO */ if (r < 0) { /* something went wrong... */ if (r != -EINVAL) /* just give up on everything non-EINVAL */ return rkt_scener_err_close(scener, r); /* Invalid setting! go back to prompting for input */ scener->new_scene.cur_invalid = scener->new_scene.cur_setting; return rkt_scener_send_error(scener, EINVAL, RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT); } else if (r > 0) { til_setting_t *setting = scener->new_scene.cur_setting; const til_setting_desc_t *desc = scener->new_scene.cur_desc; til_setting_t *invalid = scener->new_scene.cur_invalid; til_setting_t *edited = scener->new_scene.cur_edited; int editing = scener->new_scene.editing; assert(desc); if (editing && setting && setting != invalid && !setting->desc && setting != edited) { /* we have an existing setting and haven't made it available for editing yet, * so go back to the setup prompt for it, which will set cur_edited when edited. */ scener->state = RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT; return 0; } if (setting && setting != invalid && !setting->desc) { /* Apply override before, or after the spec_check()? unclear. * TODO This probably also needs to move into a til_settings helper */ if (desc->spec.override) { const char *o; o = desc->spec.override(setting->value); if (!o) return rkt_scener_err_close(scener, ENOMEM); if (o != setting->value) { r = til_setting_set_raw_value(setting, o); free((void *)o); if (r < 0) return -ENOMEM; } } r = til_setting_check_spec(setting, &desc->spec); if (r < 0) { /* setting invalid! go back to prompting for input */ scener->new_scene.cur_invalid = setting; return rkt_scener_send_error(scener, EINVAL, RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT); } if (desc->spec.as_nested_settings && !setting->value_as_nested_settings) { char *label = NULL; if (!desc->spec.key) { /* generate a positional label for bare-value specs */ r = til_settings_label_setting(desc->container, setting, &label); if (r < 0) return rkt_scener_err_close(scener, r); } setting->value_as_nested_settings = til_settings_new(NULL, desc->container, desc->spec.key ? : label, til_setting_get_raw_value(setting)); free(label); if (!setting->value_as_nested_settings) { /* FIXME: til_settings_new() seems like it should return an errno, since it can encounter parse errors too? */ return rkt_scener_err_close(scener, ENOMEM); }; } setting->desc = desc; /* setting OK and described, stay in this state and do more setup */ return 0; } /* More settings needed! go back to prompting for input */ scener->state = RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT; return 0; } /* new_scene.settings appears to be complete, but it still needs to be finalized. * Before that can happen scenes_settings need to be enlarged, then new_settings * can get added under the new entry. At that point a proper path would result, * so finalizing may proceed. */ /* these *may* move into helpers, so scoping them as ad-hoc unnamed functions for now */ if (!scener->new_scene.replacement) { /* expand scenes settings */ til_settings_t *scenes_settings; til_setting_t *scene_setting; char *label, *as_arg; int r; /* now that we know settings is completely valid, expand scenes and get everything wired up */ as_arg = til_settings_as_arg(scener->new_scene.settings); if (!as_arg) return rkt_scener_err_close(scener, ENOMEM); scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; scene_setting = til_settings_add_value(scenes_settings, NULL, as_arg); if (!scene_setting) return rkt_scener_err_close(scener, ENOMEM); r = til_setting_desc_new(scenes_settings, &(til_setting_spec_t){ .as_nested_settings = 1, }, &scene_setting->desc); if (r < 0) /* FIXME TODO we should probably drop the half-added value??? */ return rkt_scener_err_close(scener, r); r = til_settings_label_setting(scenes_settings, scene_setting, &label); if (r < 0) /* FIXME TODO we should probably drop the half-added value??? */ return rkt_scener_err_close(scener, r); r = til_settings_set_label(scener->new_scene.settings, label); free(label); if (r < 0) /* FIXME TODO we should probably drop the half-added value??? */ return rkt_scener_err_close(scener, r); scene_setting->value_as_nested_settings = scener->new_scene.settings; } else { /* simply replace current nested settings */ til_settings_t *scenes_settings; til_setting_t *scene_setting; scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; if (!til_settings_get_value_by_idx(scenes_settings, scener->scene, &scene_setting)) return rkt_scener_err_close(scener, ENOENT); /* FIXME TODO: keep this around until finishing the context replacement, * and put it back in all the failure cases before that point. */ til_settings_free(scene_setting->value_as_nested_settings); scene_setting->value_as_nested_settings = scener->new_scene.settings; } { /* finalize setup, create context, expand context scenes or replace existing */ til_module_context_t *module_ctxt; til_setup_t *setup; rkt_scene_t *new_scenes; int r; /* Still haven't baked the setup. * At this point the new scene's settings are inserted, * and it's time to bake the setup and create the context, * adding the rkt_scene_t instance corresponding to the settings. */ r = rkt_scene_module_setup(scener->new_scene.settings, &scener->new_scene.cur_setting, &scener->new_scene.cur_desc, &setup); if (r < 0) { /* FIXME TODO we should probably un-add the scene from scenes_settings??? */ if (r != -EINVAL) return rkt_scener_err_close(scener, r); /* FIXME TODO: error recovery needs a bunch of work, but let's not just * hard disconnect when finalize finds invalid settings... especially * with the addition of til_setting_t.nocheck / ':" prefixing, this is * much more likely. */ scener->new_scene.settings = NULL; return rkt_scener_send_error(scener, EINVAL, RKT_SCENER_FSM_SEND_SCENES); } /* have baked setup @ setup, create context using it */ r = til_module_create_context(setup->creator, ctxt->til_module_context.stream, rand_r(&ctxt->til_module_context.seed), /* FIXME TODO seeds need work (make reproducible) */ ctxt->til_module_context.last_ticks, ctxt->til_module_context.n_cpus, setup, &module_ctxt); til_setup_free(setup); if (r < 0) return rkt_scener_err_close(scener, r); if (!scener->new_scene.replacement) { /* have context, almost there, enlarge scener->scenes, bump scener->n_scenes */ new_scenes = realloc(ctxt->scenes, (ctxt->n_scenes + 1) * sizeof(*new_scenes)); if (!new_scenes) { til_module_context_free(module_ctxt); return rkt_scener_err_close(scener, r); } new_scenes[ctxt->n_scenes].module_ctxt = module_ctxt; ctxt->scenes = new_scenes; ctxt->n_scenes++; /* added! */ scener->scene = ctxt->n_scenes - 1; } else { /* have context, replace what's there */ assert(scener->scene < ctxt->n_scenes); ctxt->scenes[scener->scene].module_ctxt = til_module_context_free(ctxt->scenes[scener->scene].module_ctxt); ctxt->scenes[scener->scene].module_ctxt = module_ctxt; til_stream_gc_module_contexts(ctxt->til_module_context.stream); } } scener->new_scene.settings = NULL; return rkt_scener_send_message(scener, scener->new_scene.replacement ? "\n\nScene replaced successfully...\n" : "\n\nNew scene added successfully...\n", RKT_SCENER_FSM_SEND_SCENES); } case RKT_SCENER_FSM_SEND_NEWSCENE_SETUP_PROMPT: { til_setting_t *setting = scener->new_scene.cur_setting; const til_setting_desc_t *desc = scener->new_scene.cur_desc; til_setting_t *invalid = scener->new_scene.cur_invalid; til_str_t *output; const char *def; char *label = NULL; if (invalid && setting == invalid && !desc) /* XXX: should we really be checking !desc? */ desc = invalid->desc; assert(desc); def = desc->spec.preferred; if (scener->new_scene.editing && setting) def = til_setting_get_raw_value(setting); if (!desc->spec.key && setting) { /* get the positional label */ /* TODO: this is so when the desc has no key/name context, as editing bare-value settings, * we can show _something_. But this feels very ad-hoc/hacky and seems like it should * be handled by some magic in til, or some helper to get these desc paths in a more * setting-aware way optionally when there is an associated setting onhand. */ if (til_settings_label_setting(desc->container, setting, &label) < 0) return rkt_scener_err_close(scener, ENOMEM); } /* construct a prompt based on cur_desc * * this was derived from setup_interactively(), but til_str_t-centric and * decomposed further for the scener fsm integration. */ output = til_str_new("\n"); if (!output) return rkt_scener_err_close(scener, ENOMEM); if (desc->spec.values) { int width = 0; unsigned i; for (i = 0; desc->spec.values[i]; i++) { int len; len = strlen(desc->spec.values[i]); if (len > width) width = len; } /* multiple choice */ if (til_setting_desc_strprint_path(desc, output) < 0) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, "%s%s:\n%s%s%s", label ? "/" : "", label ? : "", label ? "" : " ", label ? "" : desc->spec.name, label ? "" : ":\n") < 0) return rkt_scener_err_close(scener, ENOMEM); for (i = 0; desc->spec.values[i]; i++) { if (til_str_appendf(output, " %2u: %*s%s%s\n", i, width, desc->spec.values[i], desc->spec.annotations ? ": " : "", desc->spec.annotations ? desc->spec.annotations[i] : "") < 0) return rkt_scener_err_close(scener, ENOMEM); } if (til_str_appendf(output, " Enter a value 0-%u [%s]: ", i - 1, def) < 0) return rkt_scener_err_close(scener, ENOMEM); } else { /* arbitrarily typed input */ if (til_setting_desc_strprint_path(desc, output) < 0) return rkt_scener_err_close(scener, ENOMEM); if (til_str_appendf(output, "%s%s:\n %s%s[%s]: ", label ? "/" : "", label ? : "", label ? "" : desc->spec.name, label ? "" : " ", def) < 0) return rkt_scener_err_close(scener, ENOMEM); } /* FIXME TODO: label and output are leaked in all the ENOMEM returns above! */ free(label); return rkt_scener_send(scener, output, RKT_SCENER_FSM_RECV_NEWSCENE_SETUP); } case RKT_SCENER_FSM_RECV_NEWSCENE_SETUP: if (!scener->input) return rkt_scener_recv(scener, scener->state); return rkt_scener_handle_input_newscene_setup(ctxt); case RKT_SCENER_FSM_SEND_SCENE: { til_settings_t *scenes_settings = ((rkt_setup_t *)ctxt->til_module_context.setup)->scenes_settings; til_setting_t *scene_setting; char *as_arg; til_str_t *output; if (scener->scene == RKT_EXIT_SCENE_IDX) { scener->state = RKT_SCENER_FSM_SEND_SCENES; break; } if (!til_settings_get_value_by_idx(scenes_settings, scener->scene, &scene_setting)) return rkt_scener_err_close(scener, ENOENT); as_arg = til_settings_as_arg(scene_setting->value_as_nested_settings); if (!as_arg) return rkt_scener_err_close(scener, ENOMEM); output = til_str_newf("\n" "%s:\n\n" " Visible: %s\n" " Pinned: %s\n" " Settings: \'%s\'\n" "\n" " (E)dit (R)andomizeSetup (N)ewScene %s: ", ctxt->scenes[scener->scene].module_ctxt->setup->path, scener->pin_scene ? "YES" : (ctxt_scene == scener->scene ? "YES" : "NO, PIN TO FORCE"), scener->pin_scene ? "YES, (!) to UNPIN" : "NO, (!) TO PIN", as_arg, scener->pin_scene ? "Unpin(!)" : "Pin(!)"); free(as_arg); if (!output) return rkt_scener_err_close(scener, ENOMEM); return rkt_scener_send(scener, output, RKT_SCENER_FSM_RECV_SCENE); } case RKT_SCENER_FSM_RECV_SCENE: if (!scener->input) return rkt_scener_recv(scener, scener->state); return rkt_scener_handle_input_scene(ctxt); default: assert(0); } return 0; } int rkt_scener_shutdown(rkt_context_t *ctxt) { assert(ctxt); if (!ctxt->scener) return 0; close(ctxt->scener->listener); if (ctxt->scener->client != -1) close(ctxt->scener->client); til_str_free(ctxt->scener->input); til_str_free(ctxt->scener->output); free(ctxt->scener); ctxt->scener = NULL; return 0; }