Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AP_Scripting: Support REPL over MAVLink #13547

Merged
merged 3 commits into from
Feb 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion libraries/AP_Scripting/AP_Scripting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,57 @@ void AP_Scripting::init(void) {
}
}

MAV_RESULT AP_Scripting::handle_command_int_packet(const mavlink_command_int_t &packet) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: normally brackets are on the following line.. obviously not a blocker just mentioning it

switch ((SCRIPTING_CMD)packet.param1) {
case SCRIPTING_CMD_REPL_START:
return repl_start() ? MAV_RESULT_ACCEPTED : MAV_RESULT_FAILED;
case SCRIPTING_CMD_REPL_STOP:
repl_stop();
return MAV_RESULT_ACCEPTED;
case SCRIPTING_CMD_ENUM_END: // cope with MAVLink generator appending to our enum
break;
}

return MAV_RESULT_UNSUPPORTED;
}

bool AP_Scripting::repl_start(void) {
if (terminal.session) { // it's already running, this is fine
return true;
}

// nuke the old folder and all contents
struct stat st;
if ((AP::FS().stat(REPL_DIRECTORY, &st) == -1) &&
(AP::FS().unlink(REPL_DIRECTORY) == -1) &&
(errno != EEXIST)) {
gcs().send_text(MAV_SEVERITY_INFO, "Unable to delete old REPL %s", strerror(errno));
}

// create a new folder
AP::FS().mkdir(REPL_DIRECTORY);
// delete old files in case we couldn't
AP::FS().unlink(REPL_DIRECTORY "/in");
AP::FS().unlink(REPL_DIRECTORY "/out");

// make the output pointer
terminal.output_fd = AP::FS().open(REPL_OUT, O_WRONLY|O_CREAT|O_TRUNC);
if (terminal.output_fd == -1) {
gcs().send_text(MAV_SEVERITY_INFO, "Unable to make new REPL");
return false;
}

terminal.session = true;
return true;
}

void AP_Scripting::repl_stop(void) {
terminal.session = false;
// can't do any more cleanup here, closing the open FD's is the REPL's responsibility
}

void AP_Scripting::thread(void) {
lua_scripts *lua = new lua_scripts(_script_vm_exec_count, _script_heap_size, _debug_level);
lua_scripts *lua = new lua_scripts(_script_vm_exec_count, _script_heap_size, _debug_level, terminal);
if (lua == nullptr || !lua->heap_allocated()) {
gcs().send_text(MAV_SEVERITY_CRITICAL, "Unable to allocate scripting memory");
_init_failed = true;
Expand Down
14 changes: 14 additions & 0 deletions libraries/AP_Scripting/AP_Scripting.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#include <AP_Common/AP_Common.h>
#include <AP_Param/AP_Param.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_Filesystem/AP_Filesystem.h>

class AP_Scripting
{
Expand All @@ -37,7 +39,19 @@ class AP_Scripting

static const struct AP_Param::GroupInfo var_info[];

MAV_RESULT handle_command_int_packet(const mavlink_command_int_t &packet);

struct terminal_s {
int output_fd;
off_t input_offset;
bool session;
} terminal;

private:

bool repl_start(void);
void repl_stop(void);

void load_script(const char *filename); // load a script from a file

void thread(void); // main script execution thread
Expand Down
261 changes: 261 additions & 0 deletions libraries/AP_Scripting/lua_repl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// this implements a Lua REPL, and is based off of a cut down version of
// lua/src/lua.c. It overall modified the functions to the minimum amount
// required, with the exception of fixing whitespace/indentation on if's


#include "lua_scripts.h"
#include "lua_generated_bindings.h"

#include "lua/src/lua.h"
#include "lua/src/lauxlib.h"
#include "lua/src/lualib.h"

#if !defined(LUA_MAXINPUT)
#define LUA_MAXINPUT 256
#endif

#if !defined(LUA_PROMPT)
#define LUA_PROMPT "> "
#define LUA_PROMPT2 ">> "
#endif

extern const AP_HAL::HAL& hal;

/*
** Message handler used to run all chunks
*/
static int msghandler(lua_State *L) {
const char *msg = lua_tostring(L, 1);
if (msg == NULL) { /* is error object not a string? */
if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */
lua_type(L, -1) == LUA_TSTRING) { /* that produces a string? */
return 1; /* that is the message */
} else {
msg = lua_pushfstring(L, "(error object is a %s value)",
luaL_typename(L, 1));
}
}
luaL_traceback(L, L, msg, 1); /* append a standard traceback */
return 1; /* return the traceback */
}


/*
** Interface to 'lua_pcall', which sets appropriate message function
** and C-signal handler. Used to run all chunks.
*/
int lua_scripts::docall(lua_State *L, int narg, int nres) {
int status;
int base = lua_gettop(L) - narg; /* function index */
lua_rawgeti(L, LUA_REGISTRYINDEX, sandbox_ref);
lua_setupvalue(L, -2, 1);
lua_pushcfunction(L, msghandler); /* push message handler */
lua_insert(L, base); /* put it under function and args */
status = lua_pcall(L, narg, nres, base);
lua_remove(L, base); /* remove message handler from the stack */
return status;
}


/*
** Returns the string to be used as a prompt by the interpreter.
*/
const char * lua_scripts::get_prompt(lua_State *L, int firstline) {
const char *p;
lua_getglobal(L, firstline ? "_PROMPT" : "_PROMPT2");
p = lua_tostring(L, -1);
if (p == NULL) {
p = (firstline ? LUA_PROMPT : LUA_PROMPT2);
}
return p;
}

/* mark in error messages for incomplete statements */
#define EOFMARK "<eof>"
#define marklen (sizeof(EOFMARK)/sizeof(char) - 1)


/*
** Check whether 'status' signals a syntax error and the error
** message at the top of the stack ends with the above mark for
** incomplete statements.
*/
int lua_scripts::incomplete(lua_State *L, int status) {
if (status == LUA_ERRSYNTAX) {
size_t lmsg;
const char *msg = lua_tolstring(L, -1, &lmsg);
if (lmsg >= marklen && strcmp(msg + lmsg - marklen, EOFMARK) == 0) {
lua_pop(L, 1);
return 1;
}
}
return 0; /* else... */
}


/*
** Prompt the user, read a line, and push it into the Lua stack.
*/
int lua_scripts::pushline(lua_State *L, int firstline) {
char buffer[LUA_MAXINPUT + 1] = {};
ssize_t read_bytes;
size_t l = 0;

// send prompt to the user
terminal_print(get_prompt(L, firstline));

while (terminal.session) {
// reseek to where we need input from, as invalid reads could have done weird stuff, and we want to start from the last valid input
int input_fd = AP::FS().open(REPL_IN, O_RDONLY);
if (input_fd != -1) {
AP::FS().lseek(input_fd, terminal.input_offset, SEEK_SET);
read_bytes = AP::FS().read(input_fd, buffer, ARRAY_SIZE(buffer) - 1);
AP::FS().close(input_fd);
if (read_bytes > 0) {
// locate the first newline
char * newline_chr = strchr(buffer, '\n');
if (newline_chr == NULL) {
// we don't have something that looks like a newline, just keep reading till it's longer
read_bytes = 0;
} else {
newline_chr[0] = '\0';
// only advance to the newline
l = strlen(buffer);
terminal.input_offset += l + 1;
break;
}
}
}
// wait for any input
hal.scheduler->delay(100);
}

lua_pop(L, 1); /* remove prompt */
lua_pushlstring(L, buffer, l);
return 1;
}


/*
** Try to compile line on the stack as 'return <line>;'; on return, stack
** has either compiled chunk or original line (if compilation failed).
*/
int lua_scripts::addreturn(lua_State *L) {
const char *line = lua_tostring(L, -1); /* original line */
const char *retline = lua_pushfstring(L, "return %s;", line);
int status = luaL_loadbuffer(L, retline, strlen(retline), "=stdin");
if (status == LUA_OK) {
lua_remove(L, -2); /* remove modified line */
} else {
lua_pop(L, 2); /* pop result from 'luaL_loadbuffer' and modified line */
}
return status;
}


/*
** Read multiple lines until a complete Lua statement
*/
int lua_scripts::multiline (lua_State *L) {
for (;;) { /* repeat until gets a complete statement */
size_t len;
const char *line = lua_tolstring(L, 1, &len); /* get what it has */
int status = luaL_loadbuffer(L, line, len, "=stdin"); /* try it */
if (!incomplete(L, status) || !pushline(L, 0)) {
return status; /* cannot or should not try to add continuation line */
}
lua_pushliteral(L, "\n"); /* add newline... */
lua_insert(L, -2); /* ...between the two lines */
lua_concat(L, 3); /* join them */
}
}


/*
** Read a line and try to load (compile) it first as an expression (by
** adding "return " in front of it) and second as a statement. Return
** the final status of load/call with the resulting function (if any)
** in the top of the stack.
*/
int lua_scripts::loadline(lua_State *L) {
int status;
lua_settop(L, 0);
if (!pushline(L, 1)) {
return -1; /* no input */
}
if ((status = addreturn(L)) != LUA_OK) { /* 'return ...' did not work? */
status = multiline(L); /* try as command, maybe with continuation lines */
} else {
}
lua_remove(L, 1); /* remove line from the stack */
lua_assert(lua_gettop(L) == 1);
return status;
}

// push the tring into the terminal, blocks until it's queued
void lua_scripts::terminal_print(const char *str) {
if ((AP::FS().write(terminal.output_fd, str, strlen(str)) == -1) ||
(AP::FS().fsync(terminal.output_fd) != 0)) {
terminal.session = false;
}
}

/*
** Prints (calling the Lua 'print' function) any values on the stack
*/
void lua_scripts::l_print(lua_State *L) {
int n = lua_gettop(L);
if (n > 0) { /* any result to be printed? */
luaL_checkstack(L, LUA_MINSTACK, "too many results to print");
// grab all the internal functions via the sandbox
lua_rawgeti(L, LUA_REGISTRYINDEX, sandbox_ref);
lua_getfield(L, -1, "string");
lua_getfield(L, -1, "format");
lua_insert(L, 1);
lua_remove(L, -2);
lua_getfield(L, -1, "rep");
lua_remove(L, -2);
lua_pushliteral(L, "%s");
lua_pushinteger(L, n);
lua_pushliteral(L, "\t");
if (lua_pcall(L, 3, 1, 0) != LUA_OK) {
// should never happen
lua_error(L);
}
lua_insert(L, 2);
if (lua_pcall(L, n + 1, 1, 0) != LUA_OK) {
terminal_print(lua_pushfstring(L, "error calling 'print' (%s)\n", lua_tostring(L, -1)));
} else {
terminal_print(lua_pushfstring(L, "%s\n", lua_tostring(L, -1)));
}
}
}

/*
** Do the REPL: repeatedly read (load) a line, evaluate (call) it, and
** print any results.
*/
void lua_scripts::doREPL(lua_State *L) {
int status;
// clear out any old script results
reset_loop_overtime(L);
// prep the sandbox
create_sandbox(L);
sandbox_ref = luaL_ref(L, LUA_REGISTRYINDEX);
terminal.input_offset = 0;
while (((status = loadline(L)) != -1) && terminal.session) {
if (status == LUA_OK) {
status = docall(L, 0, LUA_MULTRET);
}
if (status == LUA_OK) {
l_print(L);
} else {
terminal_print(lua_pushfstring(L, "%s\n", lua_tostring(L, -1)));
}
reset_loop_overtime(L);
}
lua_settop(L, 0); /* clear stack */
luaL_unref(L, LUA_REGISTRYINDEX, sandbox_ref);
repl_cleanup();
}

Loading