From 7f3042d75730621ba8ba29662167c8ba90cc95b8 Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Tue, 29 Apr 2025 19:15:59 -0400 Subject: [PATCH] Move to synchronous stack unwinding on cancellation Don't set break and return points in stack frames, represent them as intrinsic properties of operations. Add "op" flags for brackets, set rcode, return point and break point. --- src/lib/unlang/action.h | 1 - src/lib/unlang/call.c | 3 +- src/lib/unlang/caller.c | 2 +- src/lib/unlang/catch.c | 2 +- src/lib/unlang/compile.c | 9 +- src/lib/unlang/condition.c | 8 +- src/lib/unlang/foreach.c | 17 ++- src/lib/unlang/group.c | 11 +- src/lib/unlang/interpret.c | 232 ++++++++++++------------------ src/lib/unlang/limit.c | 2 +- src/lib/unlang/load_balance.c | 6 +- src/lib/unlang/map.c | 4 +- src/lib/unlang/module.c | 14 +- src/lib/unlang/parallel.c | 7 +- src/lib/unlang/return.c | 2 +- src/lib/unlang/subrequest.c | 3 +- src/lib/unlang/subrequest_child.c | 2 - src/lib/unlang/switch.c | 6 +- src/lib/unlang/timeout.c | 2 +- src/lib/unlang/transaction.c | 2 +- src/lib/unlang/try.c | 2 +- src/lib/unlang/unlang_priv.h | 179 ++++++++++++++++++----- 22 files changed, 271 insertions(+), 245 deletions(-) diff --git a/src/lib/unlang/action.h b/src/lib/unlang/action.h index 6bacd0ccd0..6212cfd017 100644 --- a/src/lib/unlang/action.h +++ b/src/lib/unlang/action.h @@ -38,7 +38,6 @@ typedef enum { UNLANG_ACTION_EXECUTE_NEXT, //!< Execute the next #unlang_t. UNLANG_ACTION_PUSHED_CHILD, //!< #unlang_t pushed a new child onto the stack, //!< execute it instead of continuing. - UNLANG_ACTION_UNWIND, //!< Break out of the current group. UNLANG_ACTION_YIELD, //!< Temporarily pause execution until an event occurs. UNLANG_ACTION_STOP_PROCESSING //!< Break out of processing the current request (unwind). } unlang_action_t; diff --git a/src/lib/unlang/call.c b/src/lib/unlang/call.c index 549a66db2f..2a3915e37e 100644 --- a/src/lib/unlang/call.c +++ b/src/lib/unlang/call.c @@ -252,7 +252,6 @@ void unlang_call_init(void) &(unlang_op_t){ .name = "call", .interpret = unlang_call_frame_init, - .rcode_set = true, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/caller.c b/src/lib/unlang/caller.c index ab04924205..a8fda52ea7 100644 --- a/src/lib/unlang/caller.c +++ b/src/lib/unlang/caller.c @@ -58,6 +58,6 @@ void unlang_caller_init(void) &(unlang_op_t){ .name = "caller", .interpret = unlang_caller, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/catch.c b/src/lib/unlang/catch.c index 48ead63597..78dd113ea1 100644 --- a/src/lib/unlang/catch.c +++ b/src/lib/unlang/catch.c @@ -102,6 +102,6 @@ void unlang_catch_init(void) &(unlang_op_t){ .name = "catch", .interpret = unlang_catch, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/compile.c b/src/lib/unlang/compile.c index d135984998..f31010c4df 100644 --- a/src/lib/unlang/compile.c +++ b/src/lib/unlang/compile.c @@ -1545,6 +1545,7 @@ static unlang_t *compile_edit_pair(unlang_t *parent, unlang_compile_t *unlang_ct static int define_local_variable(CONF_ITEM *ci, unlang_variable_t *var, tmpl_rules_t *t_rules, fr_type_t type, char const *name, fr_dict_attr_t const *ref); +#define debug_braces(_type) (unlang_ops[_type].flag & UNLANG_OP_FLAG_DEBUG_BRACES) /** Compile a variable definition. * @@ -1558,7 +1559,7 @@ static int compile_variable(unlang_t *parent, unlang_compile_t *unlang_ctx, CONF char const *attr, *value; unlang_group_t *group; - fr_assert(unlang_ops[parent->type].debug_braces); + fr_assert(debug_braces(parent->type)); /* * Enforce locations for local variables. @@ -1949,7 +1950,7 @@ static bool compile_action_subsection(unlang_t *c, CONF_SECTION *cs, CONF_SECTIO /* * Over-riding the actions can be done in certain limited - * situations. In other situations (e.g. "redundant", + * situations. In other situations (e.g. "redundant", * "load-balance"), it doesn't make sense. * * Note that this limitation also applies to "retry" @@ -5318,7 +5319,7 @@ static void unlang_perf_dump(fr_log_t *log, unlang_t const *instruction, int dep fr_log(log, L_DBG, file, line, "%.*s", depth, unlang_spaces); } - if (unlang_ops[instruction->type].debug_braces) { + if (debug_braces(instruction->type)) { fr_log(log, L_DBG, file, line, "%s { #", instruction->debug_name); } else { fr_log(log, L_DBG, file, line, "%s #", instruction->debug_name); @@ -5337,7 +5338,7 @@ static void unlang_perf_dump(fr_log_t *log, unlang_t const *instruction, int dep } } - if (unlang_ops[instruction->type].debug_braces) { + if (debug_braces(instruction->type)) { if (depth) { fr_log(log, L_DBG, file, line, "%.*s", depth, unlang_spaces); } diff --git a/src/lib/unlang/condition.c b/src/lib/unlang/condition.c index 26325f3661..f711a30c21 100644 --- a/src/lib/unlang/condition.c +++ b/src/lib/unlang/condition.c @@ -106,7 +106,7 @@ void unlang_condition_init(void) &(unlang_op_t){ .name = "if", .interpret = unlang_if, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES, .frame_state_size = sizeof(unlang_frame_state_cond_t), .frame_state_type = "unlang_frame_state_cond_t", }); @@ -114,15 +114,15 @@ void unlang_condition_init(void) unlang_register(UNLANG_TYPE_ELSE, &(unlang_op_t){ .name = "else", - .interpret = unlang_group, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES, + .interpret = unlang_group }); unlang_register(UNLANG_TYPE_ELSIF, &(unlang_op_t){ .name = "elseif", .interpret = unlang_if, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES, .frame_state_size = sizeof(unlang_frame_state_cond_t), .frame_state_type = "unlang_frame_state_cond_t", }); diff --git a/src/lib/unlang/foreach.c b/src/lib/unlang/foreach.c index b3d4b597b8..2f1e360a44 100644 --- a/src/lib/unlang/foreach.c +++ b/src/lib/unlang/foreach.c @@ -295,8 +295,6 @@ static unlang_action_t unlang_foreach_attr_next(rlm_rcode_t *p_result, request_t unlang_frame_state_foreach_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_foreach_t); fr_pair_t *vp; - if (is_stack_unwinding_to_break(request->stack)) return UNLANG_ACTION_CALCULATE_RESULT; - vp = fr_dcursor_current(&state->cursor); fr_assert(vp != NULL); @@ -478,11 +476,6 @@ static unlang_action_t unlang_foreach(rlm_rcode_t *p_result, request_t *request, unlang_foreach_t *gext = unlang_group_to_foreach(g); unlang_frame_state_foreach_t *state; - /* - * Ensure any breaks terminate here... - */ - break_point_set(frame); - MEM(frame->state = state = talloc_zero(request->stack, unlang_frame_state_foreach_t)); talloc_set_destructor(state, _free_unlang_frame_state_foreach); @@ -533,6 +526,10 @@ static unlang_action_t unlang_foreach(rlm_rcode_t *p_result, request_t *request, static unlang_action_t unlang_break(rlm_rcode_t *p_result, request_t *request, unlang_stack_frame_t *frame) { + unlang_action_t ua; + unlang_stack_t *stack = request->stack; + unsigned int break_depth; + RDEBUG2("%s", unlang_ops[frame->instruction->type].name); *p_result = frame->result; @@ -541,7 +538,9 @@ static unlang_action_t unlang_break(rlm_rcode_t *p_result, request_t *request, u * Stop at the next break point, or if we hit * the a top frame. */ - return unwind_to_break(request->stack); + ua = unwind_to_break(&break_depth, request->stack); + repeatable_clear(&stack->frame[break_depth]); + return ua; } void unlang_foreach_init(void) @@ -550,7 +549,7 @@ void unlang_foreach_init(void) &(unlang_op_t){ .name = "foreach", .interpret = unlang_foreach, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_BREAK_POINT }); unlang_register(UNLANG_TYPE_BREAK, diff --git a/src/lib/unlang/group.c b/src/lib/unlang/group.c index d94120dd86..2ce631989a 100644 --- a/src/lib/unlang/group.c +++ b/src/lib/unlang/group.c @@ -34,11 +34,6 @@ unlang_action_t unlang_group(rlm_rcode_t *p_result, request_t *request, unlang_s static unlang_action_t unlang_policy(rlm_rcode_t *result, request_t *request, unlang_stack_frame_t *frame) { - /* - * Ensure returns stop at the enclosing policy - */ - return_point_set(frame); - return unlang_group(result, request, frame); } @@ -49,20 +44,20 @@ void unlang_group_init(void) &(unlang_op_t){ .name = "group", .interpret = unlang_group, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); unlang_register(UNLANG_TYPE_REDUNDANT, &(unlang_op_t){ .name = "redundant", .interpret = unlang_group, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); unlang_register(UNLANG_TYPE_POLICY, &(unlang_op_t){ .name = "policy", .interpret = unlang_policy, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RETURN_POINT }); } diff --git a/src/lib/unlang/interpret.c b/src/lib/unlang/interpret.c index ec70a8e9f4..2c5bba0511 100644 --- a/src/lib/unlang/interpret.c +++ b/src/lib/unlang/interpret.c @@ -38,7 +38,6 @@ RCSID("$Id$") static _Thread_local unlang_interpret_t *intp_thread_default; static fr_table_num_ordered_t const unlang_action_table[] = { - { L("unwind"), UNLANG_ACTION_UNWIND }, { L("calculate-result"), UNLANG_ACTION_CALCULATE_RESULT }, { L("next"), UNLANG_ACTION_EXECUTE_NEXT }, { L("pushed-child"), UNLANG_ACTION_PUSHED_CHILD }, @@ -89,10 +88,13 @@ static void frame_dump(request_t *request, unlang_stack_frame_t *frame) RDEBUG2("priority %d", frame->priority); RDEBUG2("top_frame %s", is_top_frame(frame) ? "yes" : "no"); RDEBUG2("repeat %s", is_repeatable(frame) ? "yes" : "no"); - RDEBUG2("break_point %s", is_break_point(frame) ? "yes" : "no"); - RDEBUG2("return_point %s", is_return_point(frame) ? "yes" : "no"); RDEBUG2("resumable %s", is_yielded(frame) ? "yes" : "no"); + RDEBUG2("cancelled %s", is_cancelled(frame) ? "yes" : "no"); + if (frame->instruction) { + RDEBUG2("break_point %s", is_break_point(frame) ? "yes" : "no"); + RDEBUG2("return_point %s", is_return_point(frame) ? "yes" : "no"); + } /* * Call the custom frame dump function */ @@ -101,42 +103,19 @@ static void frame_dump(request_t *request, unlang_stack_frame_t *frame) REXDENT(); } -static char *stack_unwind_flag_dump(uint8_t unwind) -{ - static __thread char buf[256]; - size_t len; - -#define UNWIND_FRAME_FLAG_DUMP(attrib) \ - if (unwind & attrib) strcat(buf, #attrib" ") - - snprintf(buf, sizeof(buf), "unwind=0x%x (", unwind); - - UNWIND_FRAME_FLAG_DUMP(UNWIND_FRAME_FLAG_TOP_FRAME); - UNWIND_FRAME_FLAG_DUMP(UNWIND_FRAME_FLAG_BREAK_POINT); - UNWIND_FRAME_FLAG_DUMP(UNWIND_FRAME_FLAG_RETURN_POINT); - - len = strlen(buf); - if (buf[len - 1] == ' ') buf[len - 1] = '\0'; /* Trim trailing space */ - strcat(buf, ")"); - -#undef UNWIND_FRAME_FLAG_DUMP - - return buf; -} - static void stack_dump(request_t *request) { int i; unlang_stack_t *stack = request->stack; - RDEBUG2("----- Begin stack debug [depth %i, %s] -----", stack->depth, stack_unwind_flag_dump(stack->unwind)); + RDEBUG2("----- Begin stack debug [depth %i] -----", stack->depth); for (i = stack->depth; i >= 0; i--) { unlang_stack_frame_t *frame = &stack->frame[i]; RDEBUG2("[%d] Frame contents", i); frame_dump(request, frame); } - RDEBUG2("----- End stack debug [depth %i, %s] -------", stack->depth, stack_unwind_flag_dump(stack->unwind)); + RDEBUG2("----- End stack debug [depth %i] -------", stack->depth); } #define DUMP_STACK if (DEBUG_ENABLED5) stack_dump(request) #else @@ -260,7 +239,7 @@ unlang_action_t unlang_interpret_push_children(rlm_rcode_t *p_result, request_t unlang_group_t *g; unlang_variable_ref_t *ref; - fr_assert(unlang_ops[frame->instruction->type].debug_braces); + fr_assert(has_debug_braces(frame)); g = unlang_generic_to_group(frame->instruction); @@ -330,11 +309,19 @@ unlang_frame_action_t result_calculate(request_t *request, unlang_stack_frame_t fr_table_str_by_value(mod_rcode_table, *result, ""), *priority); + if (is_cancelled(frame)) { + RDEBUG4("** [%i] %s - frame is cancelled", + stack->depth, __FUNCTION__); + frame->result = *result; + frame->priority = *priority; + return UNLANG_FRAME_ACTION_POP; + } + /* * Update request->rcode if the instruction says we should * We don't care about priorities for this. */ - if (unlang_ops[instruction->type].rcode_set) { + if (is_rcode_set(frame)) { RDEBUG3("Setting rcode to '%s'", fr_table_str_by_value(rcode_table, *result, "")); request->rcode = *result; @@ -484,46 +471,15 @@ finalize: *priority); } - /* - * Not allowed in frame uflags... - */ - fr_assert(!(frame->uflags & UNWIND_FRAME_FLAG_NO_CLEAR)); - - /* - * If we are unwinding the stack due to a break / return, - * then handle it now. - */ - if (stack->unwind) { - /* - * Continue unwinding... - */ - if (!(stack->unwind & frame->uflags) || (stack->unwind & UNWIND_FRAME_FLAG_NO_CLEAR)) { - RDEBUG4("** [%i] %s - unwinding current frame with (%s %d) - flags - stack (%i), frame (%i)", - stack->depth, __FUNCTION__, - fr_table_str_by_value(mod_rcode_table, frame->result, ""), - frame->priority, stack->unwind, frame->uflags); - - return UNLANG_FRAME_ACTION_POP; - } - - /* - * If we've been told to unwind, and we've hit - * the frame we should be unwinding to, - * and the "NO_CLEAR" flag hasn't been set, then - * clear the unwind field so we stop unwinding. - */ - stack->unwind = UNWIND_FRAME_FLAG_NONE; - - RDEBUG4("** [%i] %s - unwind stop (%s %d) - flags - stack unwind (%i), frame uflags (%i)", - stack->depth, __FUNCTION__, - fr_table_str_by_value(mod_rcode_table, frame->result, ""), - frame->priority, stack->unwind, frame->uflags); - } - return frame->next ? UNLANG_FRAME_ACTION_NEXT : UNLANG_FRAME_ACTION_POP; } /** Evaluates all the unlang nodes in a section + * + * This function interprets a list of unlang instructions at a given level using the same + * stack frame, and pushes additional frames onto the stack as needed. + * + * This function can be seen as moving horizontally. * * @param[in] request The current request. * @param[in] frame The current stack frame. @@ -545,7 +501,7 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame */ while (frame->instruction) { unlang_t const *instruction = frame->instruction; - unlang_action_t ua = UNLANG_ACTION_UNWIND; + unlang_action_t ua; unlang_frame_action_t fa; DUMP_STACK; @@ -598,7 +554,7 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame return UNLANG_FRAME_ACTION_POP; } - if (!is_repeatable(frame) && (unlang_ops[instruction->type].debug_braces)) { + if (!is_repeatable(frame) && has_debug_braces(frame)) { RDEBUG2("%s {", instruction->debug_name); RINDENT(); } @@ -622,25 +578,15 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame */ repeatable_clear(frame); unlang_frame_perf_resume(frame); - ua = frame->process(result, request, frame); /* - * If this frame is breaking or returning - * frame then clear that unwind flag, - * it's been consumed by this call. - * - * We leave the unwind flags for the eval - * call so that the process function knows - * that the stack is being unwound. + * catch plays games with the frame so we skip + * to the next catch section at a given depth, + * it's not safe to access frame->instruction + * after this point, and the cached instruction + * should be used instead. */ - if (is_break_point(frame)) { - stack_unwind_break_clear(stack); - stack_unwind_top_frame_clear(stack); - } - if (is_return_point(frame)) { - stack_unwind_return_clear(stack); - stack_unwind_top_frame_clear(stack); - } + ua = frame->process(result, request, frame); RDEBUG4("** [%i] %s << %s (%d)", stack->depth, __FUNCTION__, fr_table_str_by_value(unlang_action_table, ua, ""), *priority); @@ -648,6 +594,17 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame fr_assert(*priority >= -1); fr_assert(*priority <= MOD_PRIORITY_MAX); + /* + * If the frame is cancelled we ignore the + * return code of the process function and + * jump to calculate result (which tells us + * to start popping frames). This is because + * the cancellation can be signalled + * asynchronously, and the process function + * may not be aware that it's happened. + */ + if (is_cancelled(frame)) goto calculate_result; + switch (ua) { /* * The request is now defunct, and we should not @@ -670,18 +627,6 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame *result = frame->result; return UNLANG_FRAME_ACTION_NEXT; - /* - * We're in a looping construct and need to stop - * execution of the current section. - */ - case UNLANG_ACTION_UNWIND: - if (*priority < 0) *priority = 0; - frame->result = *result; - frame->priority = *priority; - frame->next = NULL; - fr_assert(stack->unwind != UNWIND_FRAME_FLAG_NONE); - return UNLANG_FRAME_ACTION_POP; - /* * Yield control back to the scheduler, or whatever * called the interpreter. @@ -713,7 +658,8 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame * the section rcode and priority. */ case UNLANG_ACTION_CALCULATE_RESULT: - if (unlang_ops[instruction->type].debug_braces) { + calculate_result: + if (has_debug_braces(instruction)) { REXDENT(); /* @@ -741,7 +687,7 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame return UNLANG_FRAME_ACTION_POP; case UNLANG_FRAME_ACTION_RETRY: - if (unlang_ops[instruction->type].debug_braces) { + if (has_debug_braces(instruction)) { REXDENT(); RDEBUG2("} # retrying the same section"); } @@ -756,7 +702,7 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame * Execute the next instruction in this frame */ case UNLANG_ACTION_EXECUTE_NEXT: - if (unlang_ops[instruction->type].debug_braces) { + if (has_debug_braces(instruction)) { REXDENT(); RDEBUG2("}"); } @@ -775,6 +721,9 @@ unlang_frame_action_t frame_eval(request_t *request, unlang_stack_frame_t *frame } /** Run the interpreter for a current request + * + * This function runs the interpreter for a request. It deals with popping + * stack frames, and calaculating the final result for the frame. * * @param[in] request to run. If this is an internal request * the request may be freed by the interpreter. @@ -873,7 +822,7 @@ CC_HINT(hot) rlm_rcode_t unlang_interpret(request_t *request, bool running) /* * Close out the section we entered earlier */ - if (unlang_ops[frame->instruction->type].debug_braces) { + if (has_debug_braces(frame)) { REXDENT(); /* @@ -894,6 +843,15 @@ CC_HINT(hot) rlm_rcode_t unlang_interpret(request_t *request, bool running) */ if (is_top_frame(frame)) break; /* stop */ + /* + * Carry on popping until we find something that shouldn't + * be cancelled. + */ + if (is_cancelled(frame)) { + fa = UNLANG_FRAME_ACTION_POP; + continue; + } + fa = result_calculate(request, frame, &stack->result, &stack->priority); /* @@ -1140,17 +1098,24 @@ void unlang_interpret_request_detach(request_t *request) intp->funcs.detach(request, intp->uctx); } -/** Send a signal (usually stop) to a request +/** Delivers a frame to one or more frames in the stack * - * This is typically called via an "async" action, i.e. an action - * outside of the normal processing of the request. + * This is typically called via an "async" action, i.e. an action outside + * of the normal processing of the request. + * + * For FR_SIGNAL_CANCEL all frames are marked up for cancellation, but the + * cancellation is handled by the interpret. * - * If there is no #unlang_module_signal_t callback defined, the action is ignored. + * Other signal types are delivered immediately, inrrespecitve of whether + * the request is currently being processed or not. * - * The signaling stops at the "limit" frame. This is so that keywords + * Signaling stops at the "limit" frame. This is so that keywords * such as "timeout" and "limit" can signal frames *lower* than theirs * to stop, but then continue with their own work. * + * @note It's better (clearer) to use one of the unwind_* functions + * unless the entire request is being cancelled. + * * @param[in] request The current request. * @param[in] action to signal. * @param[in] limit the frame at which to stop signaling. @@ -1166,21 +1131,12 @@ void unlang_stack_signal(request_t *request, fr_signal_t action, int limit) fr_assert(stack->depth > 0); /* - * Destructive signal where we clean each of the - * stack frames up in turn. - * - * We do this to avoid possible free ordering - * issues where memory allocated by modules higher - * in the stack is used by modules lower in the - * stack. + * Does not complete the unwinding here, just marks + * up the frames for unwinding. The request must + * be marked as runnable to complete the cancellation. */ if (action == FR_SIGNAL_CANCEL) { - for (i = depth; i > limit; i--) { - frame = &stack->frame[i]; - if (frame->signal) frame->signal(request, frame, action); - frame_cleanup(frame); - } - stack->depth = i; + unwind_to_depth(stack, limit); return; } @@ -1204,7 +1160,10 @@ void unlang_stack_signal(request_t *request, fr_signal_t action, int limit) * This is typically called via an "async" action, i.e. an action * outside of the normal processing of the request. * - * If there is no #unlang_module_signal_t callback defined, the action is ignored. + * @note This does NOT immediately stop the request, it just deliveres + * signals, and in the case of a cancel, marks up frames for unwinding. + * The request must be marked as runnable, and executed by the + * interpreter to complete the cancellation. * * @param[in] request The current request. * @param[in] action to signal. @@ -1500,15 +1459,7 @@ static void unlang_cancel_event(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now */ talloc_free(request_data_get(request, (void *)unlang_cancel_xlat, 0)); - unlang_interpret_signal(request, FR_SIGNAL_CANCEL); -} - -static xlat_action_t unlang_cancel_never_run(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, - UNUSED xlat_ctx_t const *xctx, - UNUSED request_t *request, UNUSED fr_value_box_list_t *in) -{ - fr_assert_msg(0, "Should never be run"); - return XLAT_ACTION_FAIL; + unwind_all(request->stack); } /** Allows a request to dynamically alter its own lifetime @@ -1529,6 +1480,19 @@ static xlat_action_t unlang_cancel_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, XLAT_ARGS(args, &timeout); + /* + * No timeout means cancel immediately, so yield allowing + * the interpreter to run the event we added to cancel + * the request. + * + * We call unlang_xlat_yield to keep the interpreter happy + * as it expects to see a resume function set. + */ + if (!timeout || fr_time_delta_eq(timeout->vb_time_delta, fr_time_delta_from_sec(0))) { + unwind_all(request->stack); + return XLAT_ACTION_DONE; + } + /* * First see if we already have a timeout event * that was previously added by this xlat. @@ -1558,18 +1522,6 @@ static xlat_action_t unlang_cancel_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, return XLAT_ACTION_FAIL; } - /* - * No timeout means cancel immediately, so yield allowing - * the interpreter to run the event we added to cancel - * the request. - * - * We call unlang_xlat_yield to keep the interpreter happy - * as it expects to see a resume function set. - */ - if (!timeout || fr_time_delta_eq(timeout->vb_time_delta, fr_time_delta_from_sec(0))) { - return unlang_xlat_yield(request, unlang_cancel_never_run, NULL, 0, NULL); - } - if (ev_p_og) { MEM(vb = fr_value_box_alloc(ctx, FR_TYPE_TIME_DELTA, NULL)); diff --git a/src/lib/unlang/limit.c b/src/lib/unlang/limit.c index 670851dd2f..877c9c8dc5 100644 --- a/src/lib/unlang/limit.c +++ b/src/lib/unlang/limit.c @@ -128,7 +128,7 @@ void unlang_limit_init(void) .name = "limit", .interpret = unlang_limit, .signal = unlang_limit_signal, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES, .frame_state_size = sizeof(unlang_frame_state_limit_t), .frame_state_type = "unlang_frame_state_limit_t", diff --git a/src/lib/unlang/load_balance.c b/src/lib/unlang/load_balance.c index 0e907dba4f..52305ede8a 100644 --- a/src/lib/unlang/load_balance.c +++ b/src/lib/unlang/load_balance.c @@ -253,8 +253,7 @@ void unlang_load_balance_init(void) &(unlang_op_t){ .name = "load-balance group", .interpret = unlang_load_balance, - .rcode_set = true, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET, .frame_state_size = sizeof(unlang_frame_state_redundant_t), .frame_state_type = "unlang_frame_state_redundant_t", }); @@ -263,8 +262,7 @@ void unlang_load_balance_init(void) &(unlang_op_t){ .name = "redundant-load-balance group", .interpret = unlang_redundant_load_balance, - .rcode_set = true, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET, .frame_state_size = sizeof(unlang_frame_state_redundant_t), .frame_state_type = "unlang_frame_state_redundant_t", }); diff --git a/src/lib/unlang/map.c b/src/lib/unlang/map.c index cdd9fe9420..c2e7a9c603 100644 --- a/src/lib/unlang/map.c +++ b/src/lib/unlang/map.c @@ -383,13 +383,13 @@ void unlang_map_init(void) &(unlang_op_t){ .name = "update", .interpret = unlang_update_state_init, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); unlang_register(UNLANG_TYPE_MAP, &(unlang_op_t){ .name = "map", - .rcode_set = true, + .flag = UNLANG_OP_FLAG_RCODE_SET, .interpret = unlang_map_state_init, .frame_state_size = sizeof(unlang_frame_state_map_proc_t), .frame_state_type = "unlang_frame_state_map_proc_t", diff --git a/src/lib/unlang/module.c b/src/lib/unlang/module.c index 135a9a8661..1013f1e0ea 100644 --- a/src/lib/unlang/module.c +++ b/src/lib/unlang/module.c @@ -661,10 +661,6 @@ static unlang_action_t unlang_module_resume(rlm_rcode_t *p_result, request_t *re request->module = state->previous_module; break; - case UNLANG_ACTION_UNWIND: - request->module = state->previous_module; - break; - case UNLANG_ACTION_FAIL: *p_result = RLM_MODULE_FAIL; request->module = state->previous_module; @@ -826,11 +822,6 @@ static unlang_action_t unlang_module(rlm_rcode_t *p_result, request_t *request, state->thread = module_thread(m->mmc.mi); fr_assert(state->thread != NULL); - /* - * Don't allow returning _through_ modules - */ - return_point_set(frame_current(request)); - /* * For logging unresponsive children. */ @@ -934,9 +925,6 @@ static unlang_action_t unlang_module(rlm_rcode_t *p_result, request_t *request, } break; - case UNLANG_ACTION_UNWIND: - break; - case UNLANG_ACTION_FAIL: fail: *p_result = RLM_MODULE_FAIL; @@ -960,7 +948,7 @@ void unlang_module_init(void) &(unlang_op_t){ .name = "module", .interpret = unlang_module, - .rcode_set = true, + .flag = UNLANG_OP_FLAG_RCODE_SET | UNLANG_OP_FLAG_RETURN_POINT, .signal = unlang_module_signal, .frame_state_size = sizeof(unlang_frame_state_module_t), .frame_state_type = "unlang_frame_state_module_t", diff --git a/src/lib/unlang/parallel.c b/src/lib/unlang/parallel.c index 7b6a6cfacb..7c5c891109 100644 --- a/src/lib/unlang/parallel.c +++ b/src/lib/unlang/parallel.c @@ -360,8 +360,6 @@ static unlang_action_t unlang_parallel_process(rlm_rcode_t *p_result, request_t * done. */ } else { - unlang_stack_frame_t *child_frame; - if (unlang_function_push(child, NULL, unlang_parallel_child_done, @@ -369,8 +367,6 @@ static unlang_action_t unlang_parallel_process(rlm_rcode_t *p_result, request_t ~FR_SIGNAL_DETACH, UNLANG_TOP_FRAME, &state->children[i]) < 0) goto error; - child_frame = frame_current(child); - return_point_set(child_frame); /* Don't unwind this frame */ state->children[i].num = i; state->children[i].name = talloc_bstrdup(state, child->name); @@ -497,7 +493,6 @@ void unlang_parallel_init(void) .name = "parallel", .interpret = unlang_parallel, .signal = unlang_parallel_signal, - .rcode_set = true, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET }); } diff --git a/src/lib/unlang/return.c b/src/lib/unlang/return.c index 355147a2dd..fdebfaf422 100644 --- a/src/lib/unlang/return.c +++ b/src/lib/unlang/return.c @@ -37,7 +37,7 @@ unlang_action_t unlang_return(rlm_rcode_t *p_result, request_t *request, unlang_ * Stop at the next return point, or if we hit * the a top frame. */ - return unwind_to_return(request->stack); + return unwind_to_return(NULL, request->stack); } void unlang_return_init(void) diff --git a/src/lib/unlang/subrequest.c b/src/lib/unlang/subrequest.c index 35c8e18c71..d537340ff8 100644 --- a/src/lib/unlang/subrequest.c +++ b/src/lib/unlang/subrequest.c @@ -299,8 +299,7 @@ int unlang_subrequest_op_init(void) .name = "subrequest", .interpret = unlang_subrequest_parent_init, .signal = unlang_subrequest_signal_child, - .rcode_set = true, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES | UNLANG_OP_FLAG_RCODE_SET, .frame_state_size = sizeof(unlang_frame_state_subrequest_t), .frame_state_type = "unlang_frame_state_subrequest_t", }); diff --git a/src/lib/unlang/subrequest_child.c b/src/lib/unlang/subrequest_child.c index 9c5c00cc85..8efb21cb49 100644 --- a/src/lib/unlang/subrequest_child.c +++ b/src/lib/unlang/subrequest_child.c @@ -233,8 +233,6 @@ int unlang_subrequest_child_push_resume(request_t *child, unlang_frame_state_sub UNLANG_TOP_FRAME, state) < 0) return -1; - return_point_set(frame_current(child)); /* Stop return going through the resumption frame */ - return 0; } diff --git a/src/lib/unlang/switch.c b/src/lib/unlang/switch.c index f1056e18a0..bfe95db560 100644 --- a/src/lib/unlang/switch.c +++ b/src/lib/unlang/switch.c @@ -27,7 +27,7 @@ RCSID("$Id$") #include "group_priv.h" #include "switch_priv.h" -static unlang_action_t unlang_switch(rlm_rcode_t *p_result, request_t *request, unlang_stack_frame_t *frame) +static unlang_action_t unlang_switch(UNUSED rlm_rcode_t *p_result, request_t *request, unlang_stack_frame_t *frame) { unlang_t *found; @@ -141,13 +141,13 @@ void unlang_switch_init(void) &(unlang_op_t){ .name = "switch", .interpret = unlang_switch, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); unlang_register(UNLANG_TYPE_CASE, &(unlang_op_t){ .name = "case", .interpret = unlang_case, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/timeout.c b/src/lib/unlang/timeout.c index a8da67fa09..cca9e1e7ad 100644 --- a/src/lib/unlang/timeout.c +++ b/src/lib/unlang/timeout.c @@ -261,7 +261,7 @@ void unlang_timeout_init(void) &(unlang_op_t){ .name = "timeout", .interpret = unlang_timeout, - .debug_braces = true, + .flag = UNLANG_OP_FLAG_DEBUG_BRACES, .frame_state_size = sizeof(unlang_frame_state_timeout_t), .frame_state_type = "unlang_frame_state_timeout_t", }); diff --git a/src/lib/unlang/transaction.c b/src/lib/unlang/transaction.c index a30536fe66..6b2fe61966 100644 --- a/src/lib/unlang/transaction.c +++ b/src/lib/unlang/transaction.c @@ -129,6 +129,6 @@ void unlang_transaction_init(void) .signal = unlang_transaction_signal, .frame_state_size = sizeof(unlang_frame_state_transaction_t), .frame_state_type = "unlang_frame_state_transaction_t", - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/try.c b/src/lib/unlang/try.c index 434fc14ffc..69d2ee5f85 100644 --- a/src/lib/unlang/try.c +++ b/src/lib/unlang/try.c @@ -45,6 +45,6 @@ void unlang_try_init(void) &(unlang_op_t){ .name = "try", .interpret = unlang_try, - .debug_braces = true + .flag = UNLANG_OP_FLAG_DEBUG_BRACES }); } diff --git a/src/lib/unlang/unlang_priv.h b/src/lib/unlang/unlang_priv.h index 1e8f345d27..adf72fa841 100644 --- a/src/lib/unlang/unlang_priv.h +++ b/src/lib/unlang/unlang_priv.h @@ -209,6 +209,14 @@ typedef void (*unlang_dump_t)(request_t *request, unlang_stack_frame_t *frame); typedef int (*unlang_thread_instantiate_t)(unlang_t const *instruction, void *thread_inst); +typedef enum CC_HINT(flag_enum) { + UNLANG_OP_FLAG_NONE = 0x00, //!< No flags. + UNLANG_OP_FLAG_DEBUG_BRACES = 0x01, //!< Print debug braces. + UNLANG_OP_FLAG_RCODE_SET = 0x02, //!< Set request->rcode to the result of this operation. + UNLANG_OP_FLAG_BREAK_POINT = 0x04, //!< Break point. + UNLANG_OP_FLAG_RETURN_POINT = 0x08, //!< Return point. +} unlang_op_flag_t; + /** An unlang operation * * These are like the opcodes in other interpreters. Each operation, when executed @@ -227,11 +235,7 @@ typedef struct { size_t thread_inst_size; char const *thread_inst_type; - - bool debug_braces; //!< Whether the operation needs to print braces - ///< in debug mode. - - bool rcode_set; //!< Set request->rcode to the result of this operation. + unlang_op_flag_t flag; //!< Flags for this operation. size_t frame_state_size; //!< size of instance data in the stack frame @@ -348,47 +352,148 @@ extern unlang_op_t unlang_ops[]; extern fr_table_num_sorted_t const mod_rcode_table[]; extern size_t mod_rcode_table_len; +static inline void repeatable_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_REPEAT; } +static inline void top_frame_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_TOP_FRAME; } +static inline void yielded_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_YIELDED; } +static inline void cancel_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_CANCEL; } + +static inline void repeatable_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_REPEAT; } +static inline void top_frame_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_TOP_FRAME; } +static inline void yielded_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_YIELDED; } +static inline void cancel_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_CANCEL; } + +static inline bool is_repeatable(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_REPEAT; } +static inline bool is_top_frame(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_TOP_FRAME; } +static inline bool is_yielded(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_YIELDED; } +static inline bool is_cancelled(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_CANCEL; } + +static inline bool _instruction_has_debug_braces(unlang_t const *instruction) { return unlang_ops[instruction->type].flag & UNLANG_OP_FLAG_DEBUG_BRACES; } +static inline bool _frame_has_debug_braces(unlang_stack_frame_t const *frame) { return unlang_ops[frame->instruction->type].flag & UNLANG_OP_FLAG_DEBUG_BRACES; } +#define has_debug_braces(_thing) \ + _Generic((_thing), \ + unlang_t *: _instruction_has_debug_braces((unlang_t const *)(_thing)), \ + unlang_t const *: _instruction_has_debug_braces((unlang_t const *)(_thing)), \ + unlang_stack_frame_t *: _frame_has_debug_braces((unlang_stack_frame_t const *)(_thing)), \ + unlang_stack_frame_t const *: _frame_has_debug_braces((unlang_stack_frame_t const *)(_thing)) \ + ) +static inline bool is_rcode_set(unlang_stack_frame_t const *frame) { return unlang_ops[frame->instruction->type].flag & UNLANG_OP_FLAG_RCODE_SET; } +static inline bool is_break_point(unlang_stack_frame_t const *frame) { return unlang_ops[frame->instruction->type].flag & UNLANG_OP_FLAG_BREAK_POINT; } +static inline bool is_return_point(unlang_stack_frame_t const *frame) { return unlang_ops[frame->instruction->type].flag & UNLANG_OP_FLAG_RETURN_POINT; } + +/** Find the first frame with a given flag + * + * @return + * - 0 if no frame has the flag. + * - The index of the first frame with the flag. + */ +static inline unsigned int unlang_frame_by_flag(unlang_stack_t *stack, unlang_frame_flag_t flag) +{ + unsigned int i; -static inline void repeatable_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_REPEAT; } -static inline void top_frame_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_TOP_FRAME; } -static inline void break_point_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_BREAK_POINT; } -static inline void return_point_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_RETURN_POINT; } -static inline void yielded_set(unlang_stack_frame_t *frame) { frame->uflags |= UNWIND_FRAME_FLAG_YIELDED; } + for (i = stack->depth; i > 0; i--) { + unlang_stack_frame_t *frame = &stack->frame[i]; + + if (frame->uflags & flag) return i; + } + return 0; +} + +/** Find the first frame with a given flag + * + * @return + * - 0 if no frame has the flag. + * - The index of the first frame with the flag. + */ +static inline unsigned int unlang_frame_by_op_flag(unlang_stack_t *stack, unlang_op_flag_t flag) +{ + unsigned int i; -static inline void repeatable_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_REPEAT; } -static inline void top_frame_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_TOP_FRAME; } -static inline void break_point_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_BREAK_POINT; } -static inline void return_point_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_RETURN_POINT; } -static inline void yielded_clear(unlang_stack_frame_t *frame) { frame->uflags &= ~UNWIND_FRAME_FLAG_YIELDED; } + for (i = stack->depth; i > 0; i--) { + unlang_stack_frame_t *frame = &stack->frame[i]; -static inline bool is_repeatable(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_REPEAT; } -static inline bool is_top_frame(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_TOP_FRAME; } -static inline bool is_break_point(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_BREAK_POINT; } -static inline bool is_return_point(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_RETURN_POINT; } -static inline bool is_yielded(unlang_stack_frame_t const *frame) { return frame->uflags & UNWIND_FRAME_FLAG_YIELDED; } + if (unlang_ops[frame->instruction->type].flag & flag) return i; + } + return 0; +} -static inline unlang_action_t unwind_to_break(unlang_stack_t *stack) +/** Mark up frames as cancelled so they're immediately popped by the interpreter + * + * @note We used to do this asynchronously, but now we may need to execute timeout sections + * which means it's not enough to pop and cleanup the stack, we need continue executing + * the request. + * + * @param[in] stack The current stack. + * @param[in] to_depth mark all frames below this depth as cancelled. + */ +static inline unlang_action_t unwind_to_depth(unlang_stack_t *stack, unsigned int to_depth) { - stack->unwind = UNWIND_FRAME_FLAG_BREAK_POINT | UNWIND_FRAME_FLAG_TOP_FRAME; - return UNLANG_ACTION_UNWIND; + unlang_stack_frame_t *frame; + unsigned int i, depth = stack->depth; /* must be signed to avoid underflow */ + + if (!fr_cond_assert(to_depth >= 1)) return UNLANG_ACTION_FAIL; + + for (i = depth; i >= to_depth; i--) { + frame = &stack->frame[i]; + frame->uflags |= UNWIND_FRAME_FLAG_CANCEL; + } + + return UNLANG_ACTION_CALCULATE_RESULT; } -static inline unlang_action_t unwind_to_return(unlang_stack_t *stack) + +/** Mark the entire stack as cancelled + * + * This cancels all frames up to the next "break" frame. + * + * @param[out] break_depth Depth of the break point. + * @param[in] stack The current stack. + * @return UNLANG_ACTION_CALCULATE_RESULT + */ +static inline unlang_action_t unwind_to_break(unsigned int *break_depth, unlang_stack_t *stack) { - stack->unwind = UNWIND_FRAME_FLAG_RETURN_POINT | UNWIND_FRAME_FLAG_TOP_FRAME; - return UNLANG_ACTION_UNWIND; + unsigned int depth; + + depth = unlang_frame_by_op_flag(stack, UNLANG_OP_FLAG_BREAK_POINT); + if (depth == 0) return UNLANG_ACTION_CALCULATE_RESULT; + + unwind_to_depth(stack, depth + 1); /* cancel UP TO the break point */ + + if (break_depth) *break_depth = depth; + + return UNLANG_ACTION_CALCULATE_RESULT; } -static inline unlang_action_t unwind_all(unlang_stack_t *stack) + +/** Mark the entire stack as cancelled + * + * This cancels all frames up to the next "return" frame. + * + * @param[out] return_depth Depth of the break point. + * @param[in] stack The current stack. + * @return UNLANG_ACTION_CALCULATE_RESULT + */ +static inline unlang_action_t unwind_to_return(unsigned int *return_depth, unlang_stack_t *stack) { - stack->unwind = UNWIND_FRAME_FLAG_TOP_FRAME | UNWIND_FRAME_FLAG_NO_CLEAR; - return UNLANG_ACTION_UNWIND; + unsigned int depth; + + depth = unlang_frame_by_op_flag(stack, UNLANG_OP_FLAG_RETURN_POINT); + if (depth == 0) return UNLANG_ACTION_CALCULATE_RESULT; + + unwind_to_depth(stack, depth + 1); /* cancel UP TO the break point */ + + if (return_depth) *return_depth = depth; + + return UNLANG_ACTION_CALCULATE_RESULT; } -static inline bool is_stack_unwinding_to_top_frame(unlang_stack_t *stack) { return stack->unwind & UNWIND_FRAME_FLAG_TOP_FRAME; } -static inline bool is_stack_unwinding_to_break(unlang_stack_t *stack) { return stack->unwind & UNWIND_FRAME_FLAG_BREAK_POINT; } -static inline bool is_stack_unwinding_to_return(unlang_stack_t *stack) { return stack->unwind & UNWIND_FRAME_FLAG_RETURN_POINT; } -static inline void stack_unwind_top_frame_clear(unlang_stack_t *stack) { stack->unwind &= ~UNWIND_FRAME_FLAG_TOP_FRAME; } -static inline void stack_unwind_break_clear(unlang_stack_t *stack) { stack->unwind &= ~UNWIND_FRAME_FLAG_BREAK_POINT; } -static inline void stack_unwind_return_clear(unlang_stack_t *stack) { stack->unwind &= ~UNWIND_FRAME_FLAG_RETURN_POINT; } +/** Mark the entire stack as cancelled + * + * This cancels all frames in the stack ignoring the top frames. + * + * @param[in] stack The current stack. + */ +static inline void unwind_all(unlang_stack_t *stack) +{ + unwind_to_depth(stack, 1); +} static inline unlang_stack_frame_t *frame_current(request_t *request) { @@ -518,9 +623,7 @@ static inline void frame_pop(request_t *request, unlang_stack_t *stack) * Signal the frame to get it back into a consistent state * as we won't be calling the resume function. */ - if (stack->unwind && is_repeatable(frame) && - ((is_stack_unwinding_to_break(stack) && !is_break_point(frame)) || - (is_stack_unwinding_to_return(stack) && !is_return_point(frame)))) { + if (is_cancelled(frame)) { if (frame->signal) frame->signal(request, frame, FR_SIGNAL_CANCEL); repeatable_clear(frame); } -- 2.47.2