AP_Scripting: Run multiple scripts at once

This commit is contained in:
Michael du Breuil 2018-10-31 15:43:23 -07:00 committed by WickedShell
parent b0a84ab7cf
commit 19a8c5a6ed
6 changed files with 342 additions and 87 deletions

View File

@ -16,21 +16,20 @@
#include <AP_Scripting/AP_Scripting.h> #include <AP_Scripting/AP_Scripting.h>
#include <AP_HAL/AP_HAL.h> #include <AP_HAL/AP_HAL.h>
#include <GCS_MAVLink/GCS.h> #include <GCS_MAVLink/GCS.h>
#include <AP_ROMFS/AP_ROMFS.h>
#include "lua_bindings.h" #include "lua_scripts.h"
// ensure that we have a set of stack sizes, and enforce constraints around it // ensure that we have a set of stack sizes, and enforce constraints around it
// except for the minimum size, these are allowed to be defined by the build system // except for the minimum size, these are allowed to be defined by the build system
#undef SCRIPTING_STACK_MIN_SIZE #undef SCRIPTING_STACK_MIN_SIZE
#define SCRIPTING_STACK_MIN_SIZE 2048 #define SCRIPTING_STACK_MIN_SIZE (8 * 1024)
#if !defined(SCRIPTING_STACK_SIZE) #if !defined(SCRIPTING_STACK_SIZE)
#define SCRIPTING_STACK_SIZE 16384 #define SCRIPTING_STACK_SIZE (16 * 1024)
#endif // !defined(SCRIPTING_STACK_SIZE) #endif // !defined(SCRIPTING_STACK_SIZE)
#if !defined(SCRIPTING_STACK_MAX_SIZE) #if !defined(SCRIPTING_STACK_MAX_SIZE)
#define SCRIPTING_STACK_MAX_SIZE 16384 #define SCRIPTING_STACK_MAX_SIZE (64 * 1024)
#endif // !defined(SCRIPTING_STACK_MAX_SIZE) #endif // !defined(SCRIPTING_STACK_MAX_SIZE)
static_assert(SCRIPTING_STACK_SIZE >= SCRIPTING_STACK_MIN_SIZE, "Scripting requires a larger minimum stack size"); static_assert(SCRIPTING_STACK_SIZE >= SCRIPTING_STACK_MIN_SIZE, "Scripting requires a larger minimum stack size");
@ -58,7 +57,7 @@ const AP_Param::GroupInfo AP_Scripting::var_info[] = {
AP_GROUPEND AP_GROUPEND
}; };
AP_Scripting::AP_Scripting() { AP_Scripting::AP_Scripting() {
AP_Param::setup_object_defaults(this, var_info); AP_Param::setup_object_defaults(this, var_info);
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL #if CONFIG_HAL_BOARD == HAL_BOARD_SITL
@ -76,91 +75,23 @@ bool AP_Scripting::init(void) {
if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_Scripting::thread, void), if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_Scripting::thread, void),
"Scripting", SCRIPTING_STACK_SIZE, AP_HAL::Scheduler::PRIORITY_SCRIPTING, 0)) { "Scripting", SCRIPTING_STACK_SIZE, AP_HAL::Scheduler::PRIORITY_SCRIPTING, 0)) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Could not create scripting stack (%d)", SCRIPTING_STACK_SIZE);
return false; return false;
} }
_running = true;
return true; return true;
} }
struct {
bool overtime; // script exceeded it's execution slot, and we are bailing out with pcalls
} current_script;
void hook(lua_State *state, lua_Debug *ar) {
gcs().send_text(MAV_SEVERITY_INFO, "got a debug hook");
current_script.overtime = true;
// we need to aggressively bail out as we are over time
// so we will aggressively trap errors until we clear out
lua_sethook(state, hook, LUA_MASKCOUNT, 1);
luaL_error(state, "Exceeded CPU time");
}
void AP_Scripting::thread(void) { void AP_Scripting::thread(void) {
unsigned int loop = 0; lua_scripts *lua = new lua_scripts(_script_vm_exec_count);
lua_State *state = luaL_newstate(); if (lua == nullptr) {
luaL_openlibs(state); gcs().send_text(MAV_SEVERITY_CRITICAL, "Unable to allocate scripting memory");
load_lua_bindings(state);
// load the sandbox creation function
uint32_t sandbox_size;
char *sandbox_data = (char *)AP_ROMFS::find_decompress("sandbox.lua", sandbox_size);
if (sandbox_data == nullptr) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Could not find sandbox");
return; return;
} }
lua->run();
if (luaL_dostring(state, sandbox_data)) { // only reachable if the lua backend has died for any reason
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Loading sandbox: %s", lua_tostring(state, -1)); gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting has died");
return;
}
free(sandbox_data);
luaL_loadstring(state, "gcs.send_text(string.format(\"math.cos(1 + 2) = %f\", math.cos(1+2)))");
// luaL_loadfile(state, "test.lua");
lua_getglobal(state, "get_sandbox_env"); // find the sandbox creation function
if (lua_pcall(state, 0, LUA_MULTRET, 0)) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Could not create sandbox: %s", lua_tostring(state, -1));
return;
}
lua_setupvalue(state, -2, 1);
while (true) {
hal.scheduler->delay(1000);
gcs().send_text(MAV_SEVERITY_INFO, "Scripting Loop: %u", loop++);
// number of VM instructions to run, enforce a minimum script length, don't cap the upper end
const int32_t vm_steps = MAX(_script_vm_exec_count, 1000);
// reset the current script tracking information
memset(&current_script, 0, sizeof(current_script));
// always reset the hook in case we bailed out
lua_sethook(state, hook, LUA_MASKCOUNT, vm_steps);
const uint32_t startMem = hal.util->available_memory();
const uint32_t loadEnd = AP_HAL::micros();
lua_pushvalue(state, -1);
if(lua_pcall(state, 0, LUA_MULTRET, 0)) {
if (current_script.overtime) {
// script has consumed an excessive amount of CPU time
// FIXME: Find the chunkname for the error message
// FIXME: Remove from future execution/restart?
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: exceeded time limit (%d)", vm_steps);
} else {
gcs().send_text(MAV_SEVERITY_INFO, "Lua: %s", lua_tostring(state, -1));
hal.console->printf("Lua: %s", lua_tostring(state, -1));
}
lua_pop(state, 1);
continue;
}
const uint32_t runEnd = AP_HAL::micros();
const uint32_t endMem = hal.util->available_memory();
gcs().send_text(MAV_SEVERITY_INFO, "Time: %d Memory: %d", runEnd - loadEnd, startMem - endMem);
}
} }
AP_Scripting *AP_Scripting::_singleton = nullptr; AP_Scripting *AP_Scripting::_singleton = nullptr;

View File

@ -30,16 +30,14 @@ public:
bool init(void); bool init(void);
bool is_running(void) const { return _running; }
static AP_Scripting * get_singleton(void) { return _singleton; } static AP_Scripting * get_singleton(void) { return _singleton; }
static const struct AP_Param::GroupInfo var_info[]; static const struct AP_Param::GroupInfo var_info[];
private: private:
void thread(void); // main script execution thread void load_script(const char *filename); // load a script from a file
bool _running; void thread(void); // main script execution thread
AP_Int8 _enable; AP_Int8 _enable;
AP_Int32 _script_vm_exec_count; AP_Int32 _script_vm_exec_count;

View File

@ -6,4 +6,5 @@ extern "C" {
#include "lua.h" #include "lua.h"
#include "lualib.h" #include "lualib.h"
#include "lauxlib.h" #include "lauxlib.h"
#include "lmem.h"
} }

View File

@ -17,8 +17,8 @@ static const luaL_Reg gcs_functions[] =
}; };
int lua_servo_set_output_pwm(lua_State *state) { int lua_servo_set_output_pwm(lua_State *state) {
int servo_function = luaL_checkinteger(state, 1); int servo_function = luaL_checkinteger(state, -2);
int output_value = luaL_checknumber(state, 2); int output_value = luaL_checknumber(state, -1);
// range check the output function // range check the output function
if ((servo_function < SRV_Channel::Aux_servo_function_t::k_scripting1) || if ((servo_function < SRV_Channel::Aux_servo_function_t::k_scripting1) ||

View File

@ -0,0 +1,264 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "lua_scripts.h"
#include <AP_HAL/AP_HAL.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_ROMFS/AP_ROMFS.h>
extern const AP_HAL::HAL& hal;
bool lua_scripts::overtime;
lua_scripts::lua_scripts(const AP_Int32 &vm_steps)
: _vm_steps(vm_steps) {
scripts = nullptr;
}
void lua_scripts::hook(lua_State *L, lua_Debug *ar) {
lua_scripts::overtime = true;
// we need to aggressively bail out as we are over time
// so we will aggressively trap errors until we clear out
lua_sethook(L, hook, LUA_MASKCOUNT, 1);
luaL_error(L, "Exceeded CPU time");
}
lua_scripts::script_info *lua_scripts::load_script(lua_State *L, const char *filename) {
if (int error = luaL_loadfile(L, filename)) {
switch (error) {
case LUA_ERRSYNTAX:
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: Syntax error in %s", filename);
return nullptr;
case LUA_ERRMEM:
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: Insufficent memory loading %s", filename);
return nullptr;
default:
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: Unknown error (%d) loading %s", error, filename);
return nullptr;
}
}
script_info *new_script = (script_info *)luaM_malloc(L, sizeof(script_info));
if (new_script == nullptr) {
// No memory, shouldn't happen, we even attempted to do a GC
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: Insufficent memory loading %s", filename);
lua_pop(L, 1); // we can't use the function we just loaded, so ditch it
return nullptr;
}
new_script->name = filename;
new_script->next = nullptr;
// find and create a sandbox for the new chunk
lua_getglobal(L, "get_sandbox_env");
if (lua_pcall(L, 0, LUA_MULTRET, 0)) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Could not create sandbox: %s", lua_tostring(L, -1));
return nullptr;
}
lua_setupvalue(L, -2, 1);
new_script->lua_ref = luaL_ref(L, LUA_REGISTRYINDEX); // cache the reference
new_script->next_run_ms = AP_HAL::millis64() - 1; // force the script to be stale
return new_script;
}
void lua_scripts::run_next_script(lua_State *L) {
if (scripts == nullptr) {
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
AP_HAL::panic("Lua: Attempted to run a script without any scripts queued");
#endif // #if CONFIG_HAL_BOARD == HAL_BOARD_SITL
return;
}
// reset the current script tracking information
overtime = false;
// strip the selected script out of the list
script_info *script = scripts;
scripts = script->next;
// reset the hook to clear the counter
const int32_t vm_steps = MAX(_vm_steps, 1000);
lua_sethook(L, hook, LUA_MASKCOUNT, vm_steps);
// store top of stack so we can calculate the number of return values
int stack_top = lua_gettop(L);
// pop the function to the top of the stack
lua_rawgeti(L, LUA_REGISTRYINDEX, script->lua_ref);
if(lua_pcall(L, 0, LUA_MULTRET, 0)) {
if (overtime) {
// script has consumed an excessive amount of CPU time
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: %s exceeded time limit (%d)", script->name, vm_steps);
remove_script(L, script);
} else {
gcs().send_text(MAV_SEVERITY_INFO, "Lua: %s", lua_tostring(L, -1));
hal.console->printf("Lua: Error: %s\n", lua_tostring(L, -1));
remove_script(L, script);
}
lua_pop(L, 1);
return;
} else {
int returned = lua_gettop(L) - stack_top;
switch (returned) {
case 0:
// no time to reschedule so bail out
remove_script(L, script);
break;
case 2:
{
// sanity check the return types
if (lua_type(L, -2) != LUA_TNUMBER) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: %s did not return a delay (0x%d)", script->name, lua_type(L, -2));
lua_pop(L, 2);
remove_script(L, script);
return;
}
if (lua_type(L, -1) != LUA_TFUNCTION) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: %s did not return a function (0x%d)", script->name, lua_type(L, -1));
lua_pop(L, 2);
remove_script(L, script);
return;
}
// types match the expectations, go ahead and reschedule
int old_ref = script->lua_ref;
script->lua_ref = luaL_ref(L, LUA_REGISTRYINDEX);
luaL_unref(L, LUA_REGISTRYINDEX, old_ref);
script->next_run_ms = AP_HAL::millis64() + (uint64_t)luaL_checknumber(L, -1);
lua_pop(L, 1);
reschedule_script(script);
break;
}
default:
{
gcs().send_text(MAV_SEVERITY_CRITICAL, "Lua: %s returned bad result count (%d)", script->name, returned);
remove_script(L, script);
// pop all the results we got that we didn't expect
lua_pop(L, returned);
break;
}
}
}
}
void lua_scripts::remove_script(lua_State *L, script_info *script) {
// ensure that the script isn't in the loaded list for any reason
if (scripts == nullptr) {
// nothing to do, already not in the list
} else if (scripts == script) {
scripts = script->next;
} else {
for(script_info * current = scripts; current->next != nullptr; current = current->next) {
if (current->next == script) {
current->next = script->next;
break;
}
}
}
luaL_unref(L, LUA_REGISTRYINDEX, script->lua_ref);
luaM_free(L, script);
}
void lua_scripts::reschedule_script(script_info *script) {
if (script == nullptr) {
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
AP_HAL::panic("Lua: Attempted to schedule a null pointer");
#endif // #if CONFIG_HAL_BOARD == HAL_BOARD_SITL
return;
}
script->next = nullptr;
if (scripts == nullptr) {
scripts = script;
return;
}
uint64_t next_run_ms = script->next_run_ms;
if (scripts->next_run_ms > next_run_ms) {
script->next = scripts;
scripts = script;
return;
}
script_info *previous = scripts;
while (previous->next != nullptr) {
if (previous->next->next_run_ms > next_run_ms) {
script->next = previous->next;
previous->next = script;
return;
}
previous = previous->next;
}
previous->next = script;
}
void lua_scripts::run(void) {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
load_lua_bindings(L);
// load the sandbox creation function
uint32_t sandbox_size;
char *sandbox_data = (char *)AP_ROMFS::find_decompress("sandbox.lua", sandbox_size);
if (sandbox_data == nullptr) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Could not find sandbox");
return;
}
if (luaL_dostring(L, sandbox_data)) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Scripting: Loading sandbox: %s", lua_tostring(L, -1));
return;
}
free(sandbox_data);
// FIXME: Scan the filesystem in an appropriate manner and autostart scripts
// FIXME: This panic's SITL if the file is not found
reschedule_script(load_script(L, "test.lua"));
while (true) {
if (scripts != nullptr) {
// compute delay time
uint64_t now_ms = AP_HAL::millis64();
if (now_ms < scripts->next_run_ms) {
hal.scheduler->delay(scripts->next_run_ms - now_ms);
}
gcs().send_text(MAV_SEVERITY_DEBUG, "Lua: Running %s", scripts->name);
const uint32_t startMem = hal.util->available_memory();
const uint32_t loadEnd = AP_HAL::micros();
run_next_script(L);
const uint32_t runEnd = AP_HAL::micros();
const uint32_t endMem = hal.util->available_memory();
gcs().send_text(MAV_SEVERITY_DEBUG, "Lua: Time: %d Mem: %d", runEnd - loadEnd, startMem - endMem);
} else {
gcs().send_text(MAV_SEVERITY_DEBUG, "Lua: No scripts to run");
hal.scheduler->delay(100);
}
}
}

View File

@ -0,0 +1,61 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <AP_Common/AP_Common.h>
#include <AP_Param/AP_Param.h>
#include "lua_bindings.h"
class lua_scripts
{
public:
lua_scripts(const AP_Int32 &vm_steps);
/* Do not allow copies */
lua_scripts(const lua_scripts &other) = delete;
lua_scripts &operator=(const lua_scripts&) = delete;
// run scripts, does not return unless an error occured
void run(void);
static bool overtime; // script exceeded it's execution slot, and we are bailing out
private:
typedef struct script_info {
int lua_ref; // reference to the loaded script object
uint64_t next_run_ms; // time (in milliseconds) the script should next be run at
const char *name; // filename for the script // FIXME: This information should be available from Lua
script_info *next;
} script_info;
script_info *load_script(lua_State *L, const char *filename);
void run_next_script(lua_State *L);
void remove_script(lua_State *L, script_info *script);
// reschedule the script for execution. It is assumed the script is not in the list already
void reschedule_script(script_info *script);
script_info *scripts; // linked list of scripts to be run, sorted by next run time (soonest first)
// hook will be run when CPU time for a script is exceeded
// it must be static to be passed to the C API
static void hook(lua_State *L, lua_Debug *ar);
const AP_Int32 & _vm_steps;
};