From 1ae48f3a5259750ae3804d4c5d23131daecfd7f7 Mon Sep 17 00:00:00 2001 From: Starbeamrainbowlabs Date: Thu, 17 Oct 2024 01:54:32 +0100 Subject: [PATCH] run_command: implement support for async functions. Also update //for to make use of this new functionality. //subdivide is still on the todo list. The new functionality works by adding the new property `async = false` to command definition tables registered via wea_core.register_command()`. When `true`, if and only if the command's MAIN FUNC ONLY returns no values at all then it will consider an async operation to be in progress. This delays `run_command` from emitting the `post-execute` event on `wea_core`. Additionally, all async commands have a callback function injected as the first argument to their main `func` (ref main cmd definition table). This callback function -- if no arguments are returned by the main `func` -- must be called once the async operation is complete with same args you would normally return from `func` -- that is `success, result_message`. These will then be handled as normal and sent to the player as appropriate, as well as finally emitting the `post-execute` event. BUG: There is a potential issue in this implementation, in that if i.e. `minetest.after()` is used to delay async execution then this will break out of the `xpcall()` protection in place to prevent crashes. To this end, if you implement an async function you need to be very careful, and do a manual `wea_core.safe_function()` call yourself! --- .../commands/meta/for.lua | 20 +++-- .../core/register_command.lua | 9 ++ worldeditadditions_core/core/run_command.lua | 82 ++++++++++++++----- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/worldeditadditions_commands/commands/meta/for.lua b/worldeditadditions_commands/commands/meta/for.lua index 03e76d3..7de5725 100644 --- a/worldeditadditions_commands/commands/meta/for.lua +++ b/worldeditadditions_commands/commands/meta/for.lua @@ -15,7 +15,7 @@ local wea_c = worldeditadditions_core -- ?Basename support for values -- ?Comma deliniation support for values -local function step(params) +local function step(params, __callback) -- Initialize additional params on first call if not params.first then params.i = 1 -- Iteration number @@ -37,12 +37,9 @@ local function step(params) if params.i <= #params.values then -- If we haven't run out of values call function again - minetest.after(0, step, params) + minetest.after(0, step, params, __callback) else - wea_c.notify.ok(params.player_name, "For ".. - table.concat(params.values,", ").. - ", /"..params.cmd_name.." completed in " .. - wea_c.format.human_time(params.time)) + __callback(true, "//for completed mapping values ["..table.concat(params.values, ", ").."] over /"..params.cmd_name.." "..tostring(params.args).." in "..wea_c.format.human_time(params.time)) end end @@ -50,6 +47,7 @@ worldeditadditions_core.register_command("for", { params = " ... do // %% ", description = "Executes a chat command for each value before \" do \" replacing any instances of \"%%\" with those values. The forward slashes at the beginning of the chat command must be the same as if you were executing it normally.", privs = { worldedit = true }, + async = true, parse = function(params_text) if not params_text:match("%sdo%s") then return false, "Error: \"do\" argument is not present." @@ -68,7 +66,9 @@ worldeditadditions_core.register_command("for", { return true, values, command, args end, - func = function(name, values, command, args) + func = function(__callback, name, values, command, args) + print("DEBUG://for __callback", wea_c.inspect(__callback), "name", name) + local cmd = minetest.registered_chatcommands[command] if not cmd then return false, "Error: "..command.." isn't a valid command." @@ -77,13 +77,17 @@ worldeditadditions_core.register_command("for", { return false, "Your privileges are insufficient to run /\""..command.."\"." end + step({ player_name = name, cmd_name = command, values = values, cmd = cmd, args = args - }) + }, __callback) + -- Returning nothing + async = true means we're going async and we'll get back to `run_command` later. Until then, I'm going to have to put you on hold :P + -- * cue hold music * + -- ref https://youtu.be/3to4vaWl2dY end }) diff --git a/worldeditadditions_core/core/register_command.lua b/worldeditadditions_core/core/register_command.lua index 2c07293..17adb35 100644 --- a/worldeditadditions_core/core/register_command.lua +++ b/worldeditadditions_core/core/register_command.lua @@ -15,12 +15,20 @@ end --- Registers a new WorldEditAdditions chat command. +-- +-- **Async commands:** Set `async = true` in the definition table you pass to this function. THEN, to indicate you are doing an async thing return NO VALUES AT ALL from your main `func` that is passed the parsed arguments. When `async = true`, your main `func` will be passed a callback function as the 1st argument BEFORE all other arguments. +-- +-- Call this function when you are done with all async work with the same variables you would return: success: bool, result_message: string. +-- +-- **IMPORTANT:** You MUST NOT return `success, result_message` from the main function AND call the callback function in a single call of a command! +-- An example of this in action can be seen in the implementation of `//for`. -- @param cmdname string The name of the command to register. -- @param options table A table of options for the command: -- - `params` (string) A textual description of the parameters the command takes. -- - `description` (string) A description of the command. -- - `privs` (`{someprivilege=true, ....}`) The privileges required to use the command. -- - `require_pos` (number) The number of positions required for the command. +-- - `async=false` (bool) Whether this function is async. See the note in the description of this function for more information. -- - `parse` (function) A function that parses the raw param_text into proper input arguments to be passed to `nodes_needed` and `func`. -- - `nodes_needed` (function) A function that returns the number of nodes the command could potential change given the parsed input arguments. -- - `func` (function) The function to execute when the command is run. @@ -56,6 +64,7 @@ local function register_command(cmdname, options) --- -- 2: Normalisation --- + if options.async == nil then options.async = false end if not options.privs then options.privs = {} end if not options.require_pos then options.require_pos = 0 end if not options.nodes_needed then options.nodes_needed = function() return 0 end end diff --git a/worldeditadditions_core/core/run_command.lua b/worldeditadditions_core/core/run_command.lua index b696a15..5a85c7e 100644 --- a/worldeditadditions_core/core/run_command.lua +++ b/worldeditadditions_core/core/run_command.lua @@ -7,6 +7,26 @@ local safe_region = dofile(weac.modpath.."/core/safe_region.lua") local human_size = weac.format.human_size local safe_function = weac.safe_function +--- Handles the success bool and result message string that a command's main `func` returns. +-- @param success bool Whether the command executed successfully or not. +-- @param result_message string The message (as a string) to send to the player. +local function handle_success_resultmsg(player_name, cmdname, success, result_message) + if success then + if not result_message then + result_message = "//" .. tostring(cmdname) .. " successful" + end + weac.notify.ok(player_name, result_message) + else + if not result_message then + result_message = + "An unspecified (likely user) error was returned by the command. It is a bug that a specific error message is not returned here. It is not necessarily a bug that an error was thrown: your command invocation could have contained invalid syntax, for example." + end + weac.notify.error(player_name, "[//" .. tostring(cmdname) .. "] " .. result_message) + end + + return success, result_message +end + --- Actually runs the command in question. [HIDDEN] -- Unfortunately needed to keep the codebase clena because Lua sucks. -- @internal @@ -17,29 +37,52 @@ local safe_function = weac.safe_function -- @returns nil local function run_command_stage2(player_name, func, parse_result, tbl_event) weac:emit("pre-execute", tbl_event) - local success_safefn, retvals = safe_function(func, { player_name, weac.table.unpack(parse_result) }, player_name, "The function crashed during execution.", tbl_event.cmdname) + + -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~# + + local args = {} + -- If we're async, add the callback function as the 1st argument before everything else + if tbl_event.cmddef.async then + table.insert(args, function(success, result_message) + success, result_message = handle_success_resultmsg(player_name, tbl_event.cmdname, success, result_message) + + tbl_event.success = success + tbl_event.result = result_message + weac:emit("post-execute", tbl_event) + end) + end + -- Add player_name, unpack(args_from_parse_func).... afterwards + table.insert(args, player_name) + for _,value in ipairs(parse_result) do + table.insert(args, value) + end + + -- Run the cmd itself and catch errors + local success_safefn, retvals = safe_function(func, args, player_name, "The function crashed during execution.", tbl_event.cmdname) if not success_safefn then return false end - if #retvals ~= 2 then - weac.notify.error(player_name, "[//"..tostring(tbl_event.cmdname).."] The main execution function for this chat command returned "..tostring(#retvals).." arguments instead of the expected 2 (success, message), so it is unclear whether it succeeded or not. This is a bug!") + -- BELOW: We handle the IMMEDIATE RETURN VALUE. For async commands this requires special handling as the actual exit of async commands is above. + + -- If a function is async (pass `async = true` in the table passed to weac.register_command()`), then if there are no return values then we assume it was successful. + if #retvals ~= 2 and not tbl_event.cmddef.async then + weac.notify.error(player_name, "[//"..tostring(tbl_event.cmdname).."] This command is not async and the main execution function for it returned "..tostring(#retvals).." arguments instead of the expected 2 (success, message), so it is unclear whether it succeeded or not. This is a bug!") + end + + if #retvals == 2 then + local success, result_message = retvals[1], retvals[2] + success, result_message = handle_success_resultmsg(player_name, tbl_event.cmdname, success, result_message) + + tbl_event.success = success + tbl_event.result = result_message + end - local success, result_message = retvals[1], retvals[2] - print("DEBUG:run_command_stage2 SUCCESS", success, "RESULT_MESSAGE", result_message) - if success then - if not result_message then - result_message = "//"..tostring(tbl_event.cmdname).." successful" - end - weac.notify.ok(player_name, result_message) - else - if not result_messasge then - result_message = "An unspecified (likely user) error was returned by the command. It is a bug that a specific error message is not returned here. It is not necessarily a bug that an error was thrown: your command invocation could have contained invalid syntax, for example." - end - weac.notify.error(player_name, "[//"..tostring(tbl_event.cmdname).."] "..result_message) + -- This is outside the above before we need to fire post-execute even if `#retvals ~= 2` or something else happened + -- + -- Don't fire the post-execute event if async = true unless we were explicitly told its fine. If async = false then just go right ahead anyway + if not tbl_event.cmddef.async or (tbl_event.cmddef.async and success) then + weac:emit("post-execute", tbl_event) end - tbl_event.success = success - tbl_event.result = result_message - weac:emit("post-execute", tbl_event) end @@ -93,7 +136,8 @@ end -- - `paramargs` (table): The parsed arguments returned by the parsing function. Available in `post-parse` and later. -- - `potential_changes` (number): The number of potential nodes that could be changed as a result of running the command. `post-nodesneeded` and later: remember not all commands have an associated `nodesneeded` function. -- - `success` (boolean): Whether the command executed successfully or not. Available only in `post-execute`. --- - `result` (any): The `result` value returned by the command function. Value depends on the command executed. Available only in `post-execute`. +-- - `result` (any): The `result` value returned by the command function. Value depends on the command executed. Available only in `post-execute`. SHOULD be a string but don't count on it. +-- -- @param cmdname string The name of the command to run. -- @param options table The table of options associated with the command. See worldeditadditions_core.register_command for more information. -- @param player_name string The name of the player to execute the command for.