Files
ESPEasy/src/_C011.cpp
T
TD-er 06e458ea04 [Controller] Fail gracefully when almost out of memory
One often reported issue is that the ESPEasy node (especially ESP8266 ones) will crash when WiFi is unstable.

This could be due to memory suddenly filling up by the controller queue.
When creating a new item for the controller queue, there was no `(std::nothrow)` included on the `new` calls, so if running out of memory this might have thrown a `std::bad_alloc` exception, which are not caught and thus ESPEasy might crash on it.
2024-05-23 15:20:14 +02:00

371 lines
13 KiB
C++

#include "src/Helpers/_CPlugin_Helper.h"
#ifdef USES_C011
// #######################################################################################################
// ########################### Controller Plugin 011: Generic HTTP Advanced ##############################
// #######################################################################################################
# define CPLUGIN_011
# define CPLUGIN_ID_011 11
# define CPLUGIN_NAME_011 "Generic HTTP Advanced"
# define C011_HTTP_METHOD_MAX_LEN 16
# define C011_HTTP_URI_MAX_LEN 240
# define C011_HTTP_HEADER_MAX_LEN 256
# define C011_HTTP_BODY_MAX_LEN 512
bool C011_sendBinary = false;
struct C011_ConfigStruct
{
void zero_last() {
HttpMethod[C011_HTTP_METHOD_MAX_LEN - 1] = 0;
HttpUri[C011_HTTP_URI_MAX_LEN - 1] = 0;
HttpHeader[C011_HTTP_HEADER_MAX_LEN - 1] = 0;
HttpBody[C011_HTTP_BODY_MAX_LEN - 1] = 0;
}
char HttpMethod[C011_HTTP_METHOD_MAX_LEN] = { 0 };
char HttpUri[C011_HTTP_URI_MAX_LEN] = { 0 };
char HttpHeader[C011_HTTP_HEADER_MAX_LEN] = { 0 };
char HttpBody[C011_HTTP_BODY_MAX_LEN] = { 0 };
};
// Forward declarations
bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody);
boolean Create_schedule_HTTP_C011(struct EventStruct *event);
void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted);
void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary);
bool CPlugin_011(CPlugin::Function function, struct EventStruct *event, String& string)
{
bool success = false;
switch (function)
{
case CPlugin::Function::CPLUGIN_PROTOCOL_ADD:
{
ProtocolStruct& proto = getProtocolStruct(event->idx); // = CPLUGIN_ID_011;
proto.usesMQTT = false;
proto.usesAccount = true;
proto.usesPassword = true;
proto.usesExtCreds = true;
proto.defaultPort = 80;
proto.usesID = false;
break;
}
case CPlugin::Function::CPLUGIN_GET_DEVICENAME:
{
string = F(CPLUGIN_NAME_011);
break;
}
case CPlugin::Function::CPLUGIN_INIT:
{
{
MakeControllerSettings(ControllerSettings); //-V522
if (AllocatedControllerSettings()) {
LoadControllerSettings(event->ControllerIndex, *ControllerSettings);
C011_sendBinary = ControllerSettings->sendBinary();
}
}
success = init_c011_delay_queue(event->ControllerIndex);
break;
}
case CPlugin::Function::CPLUGIN_EXIT:
{
exit_c011_delay_queue();
break;
}
case CPlugin::Function::CPLUGIN_WEBFORM_LOAD:
{
{
String HttpMethod;
String HttpUri;
String HttpHeader;
String HttpBody;
if (!load_C011_ConfigStruct(event->ControllerIndex, HttpMethod, HttpUri, HttpHeader, HttpBody))
{
return false;
}
addTableSeparator(F("HTTP Config"), 2, 3);
{
uint8_t choice = 0;
const __FlashStringHelper * methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") };
for (uint8_t i = 0; i < 5; i++)
{
if (HttpMethod.equals(methods[i])) {
choice = i;
}
}
addFormSelector(F("Method"), F("P011httpmethod"), 5, methods, nullptr, choice);
}
addFormTextBox(F("URI"), F("P011httpuri"), HttpUri, C011_HTTP_URI_MAX_LEN - 1);
{
htmlEscape(HttpHeader);
addFormTextArea(F("Header"), F("P011httpheader"), HttpHeader, C011_HTTP_HEADER_MAX_LEN - 1, 4, 50);
}
{
htmlEscape(HttpBody);
addFormTextArea(F("Body"), F("P011httpbody"), HttpBody, C011_HTTP_BODY_MAX_LEN - 1, 8, 50);
}
}
{
// Place in scope to delete ControllerSettings as soon as it is no longer needed
MakeControllerSettings(ControllerSettings); //-V522
if (!AllocatedControllerSettings()) {
addHtmlError(F("Out of memory, cannot load page"));
} else {
LoadControllerSettings(event->ControllerIndex, *ControllerSettings);
addControllerParameterForm(*ControllerSettings, event->ControllerIndex, ControllerSettingsStruct::CONTROLLER_SEND_BINARY);
addFormNote(F("Do not 'percent escape' body when send binary checked"));
}
}
break;
}
case CPlugin::Function::CPLUGIN_WEBFORM_SAVE:
{
std::shared_ptr<C011_ConfigStruct> customConfig(new (std::nothrow) C011_ConfigStruct);
if (customConfig) {
uint8_t choice = 0;
String methods[] = { F("GET"), F("POST"), F("PUT"), F("HEAD"), F("PATCH") };
for (uint8_t i = 0; i < 5; i++)
{
if (methods[i].equals(customConfig->HttpMethod)) {
choice = i;
}
}
int httpmethod = getFormItemInt(F("P011httpmethod"), choice);
String httpuri = webArg(F("P011httpuri"));
String httpheader = webArg(F("P011httpheader"));
String httpbody = webArg(F("P011httpbody"));
strlcpy(customConfig->HttpMethod, methods[httpmethod].c_str(), sizeof(customConfig->HttpMethod));
strlcpy(customConfig->HttpUri, httpuri.c_str(), sizeof(customConfig->HttpUri));
strlcpy(customConfig->HttpHeader, httpheader.c_str(), sizeof(customConfig->HttpHeader));
strlcpy(customConfig->HttpBody, httpbody.c_str(), sizeof(customConfig->HttpBody));
customConfig->zero_last();
SaveCustomControllerSettings(event->ControllerIndex, reinterpret_cast<const uint8_t *>(customConfig.get()), sizeof(C011_ConfigStruct));
}
break;
}
case CPlugin::Function::CPLUGIN_PROTOCOL_SEND:
{
if (C011_DelayHandler->queueFull(event->ControllerIndex)) {
break;
}
success = Create_schedule_HTTP_C011(event);
break;
}
case CPlugin::Function::CPLUGIN_FLUSH:
{
process_c011_delay_queue();
delay(0);
break;
}
default:
break;
}
return success;
}
// ********************************************************************************
// Generic HTTP request
// ********************************************************************************
// Uncrustify may change this into multi line, which will result in failed builds
// *INDENT-OFF*
bool do_process_c011_delay_queue(int controller_number, const Queue_element_base& element_base, ControllerSettingsStruct& ControllerSettings) {
const C011_queue_element& element = static_cast<const C011_queue_element&>(element_base);
// *INDENT-ON*
if (!NetworkConnected()) { return false; }
int httpCode = -1;
send_via_http(
controller_number,
ControllerSettings,
element._controller_idx,
element.uri,
element.HttpMethod,
element.header,
element.postStr,
httpCode);
// HTTP codes:
// 1xx Informational response
// 2xx Success
return httpCode >= 100 && httpCode < 300;
}
bool load_C011_ConfigStruct(controllerIndex_t ControllerIndex, String& HttpMethod, String& HttpUri, String& HttpHeader, String& HttpBody) {
// Just copy the needed strings and destruct the C011_ConfigStruct as soon as possible
std::shared_ptr<C011_ConfigStruct> customConfig(new (std::nothrow) C011_ConfigStruct);
if (!customConfig) {
return false;
}
LoadCustomControllerSettings(ControllerIndex, reinterpret_cast<uint8_t *>(customConfig.get()), sizeof(C011_ConfigStruct));
customConfig->zero_last();
move_special(HttpMethod, String(customConfig->HttpMethod));
move_special(HttpUri , String(customConfig->HttpUri));
move_special(HttpHeader, String(customConfig->HttpHeader));
move_special(HttpBody , String(customConfig->HttpBody));
return true;
}
// ********************************************************************************
// Create request
// ********************************************************************************
boolean Create_schedule_HTTP_C011(struct EventStruct *event)
{
if (C011_DelayHandler == nullptr) {
addLog(LOG_LEVEL_ERROR, F("No C011_DelayHandler"));
return false;
}
//LoadTaskSettings(event->TaskIndex); // FIXME TD-er: This can probably be removed
// Add a new element to the queue with the minimal payload
std::unique_ptr<C011_queue_element> element(new (std::nothrow) C011_queue_element(event));
bool success = C011_DelayHandler->addToQueue(std::move(element));
if (success) {
// Element was added.
// Now we try to append to the existing element
// and thus preventing the need to create a long string only to copy it to a queue element.
C011_queue_element& element = static_cast<C011_queue_element&>(*(C011_DelayHandler->sendQueue.back()));
if (!load_C011_ConfigStruct(event->ControllerIndex, element.HttpMethod, element.uri, element.header, element.postStr))
{
if (loglevelActiveFor(LOG_LEVEL_ERROR)) {
addLogMove(LOG_LEVEL_ERROR, strformat(
F("C011 : %s %s %s %s"),
element.HttpMethod.c_str(),
element.uri.c_str(),
element.header.c_str(),
element.postStr.c_str()));
}
C011_DelayHandler->sendQueue.pop_back();
return false;
}
ReplaceTokenByValue(element.uri, event, false);
ReplaceTokenByValue(element.header, event, false);
if (element.postStr.length() > 0)
{
ReplaceTokenByValue(element.postStr, event, C011_sendBinary);
}
} else {
addLog(LOG_LEVEL_ERROR, F("C011 : Could not add to delay handler"));
}
Scheduler.scheduleNextDelayQueue(SchedulerIntervalTimer_e::TIMER_C011_DELAY_QUEUE, C011_DelayHandler->getNextScheduleTime());
return success;
}
// parses the string and returns only the the number of name/values we want
// according to the parameter numberOfValuesWanted
void DeleteNotNeededValues(String& s, uint8_t numberOfValuesWanted)
{
numberOfValuesWanted++;
for (uint8_t i = 1; i < 5; i++)
{
const String startToken(strformat(F("%%%d%%"), i));
const String endToken(strformat(F("%%/%d%%"), i));
// do we want to keep this one?
if (i < numberOfValuesWanted)
{
// yes, so just remove the tokens
s.replace(startToken, EMPTY_STRING);
s.replace(endToken, EMPTY_STRING);
}
else
{
// remove all the whole strings including tokes
int startIndex = s.indexOf(startToken);
int endIndex = s.indexOf(endToken);
while (startIndex != -1 && endIndex != -1 && endIndex > startIndex)
{
String p = s.substring(startIndex, endIndex + 4);
// remove the whole string including tokens
s.replace(p, EMPTY_STRING);
// find next ones
startIndex = s.indexOf(startToken);
endIndex = s.indexOf(endToken);
}
}
}
}
// ********************************************************************************
// Replace the token in a string by real value.
//
// Example:
// %1%%vname1%____%tskname%____%val1%%/1%%2%%__%%vname2%____%tskname%____%val2%%/2%
// will become in case of a sensor with 1 value:
// SENSORVALUENAME1____TASKNAME1____VALUE1 <- everything not between %1% and %/1% will be discarded
// in case of a sensor with 2 values:
// SENSORVALUENAME1____TASKNAME1____VALUE1__SENSORVALUENAME2____TASKNAME2____VALUE2
// ********************************************************************************
void ReplaceTokenByValue(String& s, struct EventStruct *event, bool sendBinary)
{
// example string:
// write?db=testdb&type=%1%%vname1%%/1%%2%;%vname2%%/2%%3%;%vname3%%/3%%4%;%vname4%%/4%&value=%1%%val1%%/1%%2%;%val2%%/2%%3%;%val3%%/3%%4%;%val4%%/4%
// %1%%vname1%,Standort=%tskname% Wert=%val1%%/1%%2%%LF%%vname2%,Standort=%tskname% Wert=%val2%%/2%%3%%LF%%vname3%,Standort=%tskname%
// Wert=%val3%%/3%%4%%LF%%vname4%,Standort=%tskname% Wert=%val4%%/4%
#ifndef BUILD_NO_DEBUG
if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP before parsing: "));
addLog(LOG_LEVEL_DEBUG_MORE, s);
}
#endif
const uint8_t valueCount = getValueCountForTask(event->TaskIndex);
DeleteNotNeededValues(s, valueCount);
#ifndef BUILD_NO_DEBUG
if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after parsing: "));
addLog(LOG_LEVEL_DEBUG_MORE, s);
}
#endif
parseControllerVariables(s, event, !sendBinary);
#ifndef BUILD_NO_DEBUG
if (loglevelActiveFor(LOG_LEVEL_DEBUG_MORE)) {
addLog(LOG_LEVEL_DEBUG_MORE, F("HTTP after replacements: "));
addLog(LOG_LEVEL_DEBUG_MORE, s);
}
#endif
}
#endif // ifdef USES_C011