Make node.output() RTOS thread safe.

Also removed old, very unsafe node.osoutput(). We're now integrating cleanly
with the IDF/newlib way of redirecting stdout.

Added necessary depends in Kconfig to ensure VFS support is enabled, as
otherwise you'd only get a mysterious crash when attempting to enable
output redirection.
This commit is contained in:
Johny Mattsson 2021-11-04 22:17:55 +11:00 committed by Johny Mattsson
parent 9a2fb84512
commit a0c9085cca
6 changed files with 108 additions and 178 deletions

View File

@ -97,23 +97,3 @@ size_t feed_lua_input(const char *buf, size_t n)
return n; // we consumed/buffered all the provided data
}
void output_redirect(const char *str, size_t l) {
lua_State *L = lua_getstate();
int n = lua_gettop(L);
lua_pushliteral(L, "stdout");
lua_rawget(L, LUA_REGISTRYINDEX); /* fetch reg.stdout */
if (lua_istable(L, -1)) { /* reg.stdout is pipe */
lua_rawgeti(L, -1, 1); /* get the pipe_write func from stdout[1] */
lua_insert(L, -2); /* and move above the pipe ref */
lua_pushlstring(L, str, l);
if (lua_pcall(L, 2, 0, 0) != LUA_OK) { /* Reg.stdout:write(str) */
lua_writestringerror("error calling stdout:write(%s)\n", lua_tostring(L, -1));
esp_restart();
}
} else { /* reg.stdout == nil */
printf(str, l);
}
lua_settop(L, n); /* Make sure all code paths leave stack unchanged */
}

View File

@ -305,7 +305,7 @@ extern int readline4lua(const char *prompt, char *buffer, int length);
** They are only used in libraries and the stand-alone program. (The #if
** avoids including 'stdio.h' everywhere.)
*/
#ifdef LUA_USE_ESP
#ifdef LUA_USE_ESP8266
void output_redirect(const char *str, size_t l);
#define lua_writestring(s,l) output_redirect((s),(l))
#else

View File

@ -230,7 +230,7 @@ LUALIB_API void (luaL_openlib) (lua_State *L, const char *libname,
/* print a string */
#if !defined(lua_writestring)
#ifdef LUA_USE_ESP
#ifdef LUA_USE_ESP8266
void output_redirect(const char *str, size_t l);
#define lua_writestring(s,l) output_redirect((s),(l))
#else

View File

@ -65,6 +65,8 @@ menu "NodeMCU modules"
config NODEMCU_CMODULE_FILE
bool "File module"
default "y"
select VFS_SUPPORT_IO
select VFS_SUPPORT_DIR
help
Includes the file module (recommended).
@ -117,6 +119,7 @@ menu "NodeMCU modules"
config NODEMCU_CMODULE_NET
bool "Net module"
default "y"
select VFS_SUPPORT_IO
help
Includes the net module (recommended).

View File

@ -399,26 +399,90 @@ static int node_input( lua_State* L )
// When there is any write to the replaced stdout, our function redir_write will be called.
// we can then invoke the lua callback.
static FILE *oldstdout; // keep the old stdout, e.g., the uart0
lua_ref_t output_redir = LUA_NOREF; // this will hold the Lua callback
int serial_debug = 0; // whether or not to write also to uart
const char *VFS_REDIR = "/redir"; // virtual filesystem mount point
// A buffer size that should be sufficient for most cases, yet not so large
// as to present an issue.
# define OUTPUT_CHUNK_SIZE 127
typedef struct {
uint8_t used;
uint8_t bytes[OUTPUT_CHUNK_SIZE];
} output_chunk_t;
static task_handle_t output_task; // for getting output into the LVM thread
static lua_ref_t output_redir = LUA_NOREF; // this will hold the Lua callback
static FILE *serial_debug; // the console uart, if wanted
static const char *VFS_REDIR = "/redir"; // virtual filesystem mount point
// redir_write will be called everytime any code writes to stdout when
// redirection is active
// redirection is active, from ANY RTOS thread
ssize_t redir_write(int fd, const void *data, size_t size) {
if (serial_debug) // if serial_debug is nonzero, write to uart
fwrite(data, sizeof(char), size, oldstdout);
if (output_redir != LUA_NOREF) { // prepare lua call
lua_State *L = lua_getstate();
lua_rawgeti(L, LUA_REGISTRYINDEX, output_redir); // push function reference
lua_pushlstring(L, (char *)data, size); // push data
luaL_pcallx(L, 1, 0); // invoke callback
UNUSED(fd);
if (size)
{
size_t n = (size > OUTPUT_CHUNK_SIZE) ? OUTPUT_CHUNK_SIZE : size;
output_chunk_t *chunk = malloc(sizeof(output_chunk_t));
chunk->used = (uint8_t)n;
memcpy(chunk->bytes, data, n);
_Static_assert(sizeof(task_param_t) >= sizeof(chunk), "cast error below");
if (!task_post_high(output_task, (task_param_t)chunk))
{
static const char overflow[] = "E: output overflow\n";
fwrite(overflow, sizeof(overflow) -1, sizeof(char), serial_debug);
free(chunk);
return -1;
}
return size;
if (serial_debug)
{
size_t written = 0;
while (written < n)
{
size_t w = fwrite(
data + written, sizeof(char), n - written, serial_debug);
if (w > 0)
written += w;
else break;
}
}
return n;
}
else
return 0;
}
void redir_output(task_param_t param, task_prio_t prio)
{
UNUSED(prio);
output_chunk_t *chunk = (output_chunk_t *)param;
bool redir_active = (output_redir != LUA_NOREF);
if (redir_active)
{
lua_State *L = lua_getstate();
lua_rawgeti(L, LUA_REGISTRYINDEX, output_redir);
lua_pushlstring(L, (char *)chunk->bytes, chunk->used);
luaL_pcallx(L, 1, 0);
}
free(chunk);
}
#if !defined(CONFIG_ESP_CONSOLE_NONE)
static const char *default_console_name(void)
{
return
#if defined(CONFIG_ESP_CONSOLE_UART)
# define STRINGIFY(x) STRINGIFY2(x)
# define STRINGIFY2(x) #x
"/dev/uart/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM);
#undef STRINGIFY2
#undef STRINGIFY
#elif defined(CONFIG_ESP_CONSOLE_USB_CDC)
"/dev/cdcacm";
#elif defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG)
"/dev/usbserjtag";
#endif
}
#endif
// redir_open is called when fopen() is called on /redir/xxx
int redir_open(const char *path, int flags, int mode) {
return 79; // since we only have one "file", just return some fd number to make the VFS system happy
@ -426,6 +490,11 @@ int redir_open(const char *path, int flags, int mode) {
// Lua: node.output(func, serial_debug)
static int node_output(lua_State *L) {
if (serial_debug)
{
fclose(serial_debug);
serial_debug = NULL;
}
if (lua_isfunction(L, 1)) {
if (output_redir == LUA_NOREF) {
// create an instance of a virtual filesystem so we can use fopen
@ -439,69 +508,25 @@ static int node_output(lua_State *L) {
};
// register this filesystem under the `/redir` namespace
ESP_ERROR_CHECK(esp_vfs_register(VFS_REDIR, &redir_fs, NULL));
oldstdout = stdout; // save the previous stdout
stdout = fopen(VFS_REDIR, "w"); // open the new one for writing
freopen(VFS_REDIR, "w", stdout);
if (lua_isnoneornil(L, 2) ||
(lua_isnumber(L, 2) && lua_tonumber(L, 2)))
serial_debug = fopen(default_console_name(), "w");
} else {
luaX_unset_ref(L, &output_redir); // dereference previous callback
}
luaX_set_ref(L, 1, &output_redir); // set the callback
} else {
if (output_redir != LUA_NOREF) {
fclose(stdout); // close the redirected stdout
stdout = oldstdout; // restore original stdout
ESP_ERROR_CHECK(esp_vfs_unregister(VFS_REDIR)); // unregister redir filesystem
luaX_unset_ref(L, &output_redir); // forget callback
}
serial_debug = 1;
return 0;
}
// second parameter indicates whether output will also be sent to old stdout
if (lua_isnumber(L, 2)) {
serial_debug = lua_tointeger(L, 2);
if (serial_debug != 0)
serial_debug = 1;
} else {
serial_debug = 1; // default to 1
}
return 0;
}
// The implementation of node.osoutput redirect all OS logging to Lua space
lua_ref_t os_output_redir = LUA_NOREF; // this will hold the Lua callback
static vprintf_like_t oldvprintf; // keep the old vprintf
// redir_vprintf will be called everytime the OS attempts to print a trace statement
int redir_vprintf(const char *fmt, va_list ap)
{
static char data[128];
int size = vsnprintf(data, 128, fmt, ap);
if (os_output_redir != LUA_NOREF) { // prepare lua call
lua_State *L = lua_getstate();
lua_rawgeti(L, LUA_REGISTRYINDEX, os_output_redir); // push function reference
lua_pushlstring(L, (char *)data, size); // push data
luaL_pcallx(L, 1, 0); // invoke callback
}
return size;
}
// Lua: node.output(func, serial_debug)
static int node_osoutput(lua_State *L) {
if (lua_isfunction(L, 1)) {
if (os_output_redir == LUA_NOREF) {
// register our log redirect first time this is invoked
oldvprintf = esp_log_set_vprintf(redir_vprintf);
} else {
luaX_unset_ref(L, &os_output_redir); // dereference previous callback
}
luaX_set_ref(L, 1, &os_output_redir); // set the callback
} else {
if (os_output_redir != LUA_NOREF) {
esp_log_set_vprintf(oldvprintf);
luaX_unset_ref(L, &os_output_redir); // forget callback
#if defined(CONFIG_ESP_CONSOLE_NONE)
fclose(stdout);
#else
// reopen the console device onto the stdout stream
freopen(default_console_name(), "w", stdout);
#endif
ESP_ERROR_CHECK(esp_vfs_unregister(VFS_REDIR));
luaX_unset_ref(L, &output_redir);
}
}
@ -836,7 +861,6 @@ LROT_BEGIN(node, NULL, 0)
LROT_FUNCENTRY( heap, node_heap )
LROT_FUNCENTRY( input, node_input )
LROT_FUNCENTRY( output, node_output )
LROT_FUNCENTRY( osoutput, node_osoutput )
LROT_FUNCENTRY( osprint, node_osprint )
LROT_FUNCENTRY( restart, node_restart )
LROT_FUNCENTRY( setonerror, node_setonerror )
@ -849,6 +873,8 @@ LROT_END(node, NULL, 0)
int luaopen_node(lua_State *L)
{
output_task = task_get_id(redir_output);
lua_settop(L, 0);
return node_setonerror(L); /* set default onerror action */
}

View File

@ -301,68 +301,20 @@ Reload LFS with the flash image provided. Flash images can be generated on the h
- The reload process internally makes multiple passes through the LFS image file. The first pass validates the file and header formats and detects many errors. If any is detected then an error string is returned.
## node.key() --deprecated
Defines action to take on button press (on the old devkit 0.9), button connected to GPIO 16.
This function is only available if the firmware was compiled with DEVKIT_VERSION_0_9 defined.
#### Syntax
`node.key(type, function())`
#### Parameters
- `type`: type is either string "long" or "short". long: press the key for 3 seconds, short: press shortly(less than 3 seconds)
- `function`: user defined function which is called when key is pressed. If nil, remove the user defined function. Default function: long: change LED blinking rate, short: reset chip
#### Returns
`nil`
#### Example
```lua
node.key("long", function() print('hello world') end)
```
#### See also
[`node.led()`](#nodeled-deprecated)
## node.led() --deprecated
Sets the on/off time for the LED (on the old devkit 0.9), with the LED connected to GPIO16, multiplexed with [`node.key()`](#nodekey-deprecated).
This function is only available if the firmware was compiled with DEVKIT_VERSION_0_9 defined.
#### Syntax
`node.led(low, high)`
#### Parameters
- `low` LED off time, LED keeps on when low=0. Unit: milliseconds, time resolution: 80~100ms
- `high` LED on time. Unit: milliseconds, time resolution: 80~100ms
#### Returns
`nil`
#### Example
```lua
-- turn led on forever.
node.led(0)
```
#### See also
[`node.key()`](#nodekey-deprecated)
## node.output()
Redirects the Lua interpreter output to a callback function. Optionally also prints it to the serial console.
Redirects all standard output (`stdout`) to a callback function. Optionally also prints it to the console device, as specified in Kconfig under the "ESP System Settings" section.
!!! note "Note:"
Do **not** attempt to `print()` or otherwise induce the Lua interpreter to produce output from within the callback function. Doing so results in infinite recursion, and leads to a watchdog-triggered restart.
Do **not** attempt to `print()` or otherwise induce the Lua interpreter to produce output from within the callback function. Doing so results in infinite recursion, and leads to a crash or watchdog-triggered restart.
#### Syntax
`node.output(function(str), serial_output)`
`node.output(function(str), console_output)`
#### Parameters
- `output_fn(str)` a function accept every output as str, and can send the output to a socket (or maybe a file). `nil` to unregister the previous function.
- `serial_output` 1 output also sent out the serial port. 0: no serial output. Defaults to 1 if not specified.
- `console_output` 1 output also sent out the console port. 0: no console output. Defaults to 1 if not specified.
#### Returns
`nil`
@ -372,7 +324,7 @@ Redirects the Lua interpreter output to a callback function. Optionally also pri
function tonet(str)
sk:send(str)
end
node.output(tonet, 1) -- serial also get the lua output.
node.output(tonet, 1) -- console also get the lua output.
```
```lua
@ -403,37 +355,6 @@ node.output(nil, 0)
#### See also
[`node.input()`](#nodeinput)
## node.osoutput()
Redirects the debugging output from the Espressif SDK to a callback function allowing it to be captured or processed in Lua.
####Syntax
`node.osoutput(function(str))`
#### Parameters
- `function(str)` a function accepts debugging output as str, and can send the output to a socket (or maybe a file). `nil` to unregister the previous function.
#### Returns
Nothing
#### Example
```lua
function luaprint(str)
print("lua space: "str)
end
node.osoutput(luaprint)
```
```lua
-- disable all output completely
node.osoutput(nil)
```
## node.readvdd33() --deprecated
Moved to [`adc.readvdd33()`](adc/#adcreadvdd33).
## node.restart()