From ffd23d095bd291e352a466a2f0b02e97f90d44fd Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Sat, 8 Apr 2023 22:04:36 +0300 Subject: [PATCH] web: print api in namespace also fixing leftover API dependency issues --- code/espurna/terminal.cpp | 24 +++++-- code/espurna/web.cpp | 98 +++++++++++++-------------- code/espurna/web.h | 60 +---------------- code/espurna/web_asyncwebprint.ipp | 70 ------------------- code/espurna/web_print.h | 104 +++++++++++++++++++++++++++++ code/espurna/web_print.ipp | 90 +++++++++++++++++++++++++ 6 files changed, 259 insertions(+), 187 deletions(-) delete mode 100644 code/espurna/web_asyncwebprint.ipp create mode 100644 code/espurna/web_print.h create mode 100644 code/espurna/web_print.ipp diff --git a/code/espurna/terminal.cpp b/code/espurna/terminal.cpp index 9335c65b..f0418b16 100644 --- a/code/espurna/terminal.cpp +++ b/code/espurna/terminal.cpp @@ -25,7 +25,7 @@ Copyright (C) 2020-2022 by Maxim Prokhorov #include @@ -694,12 +694,16 @@ void setup() { #if TERMINAL_WEB_API_SUPPORT namespace api { +STRING_VIEW_INLINE(Path, TERMINAL_WEB_API_PATH); +STRING_VIEW_INLINE(Key, "termWebApiPath"); + // XXX: new `apiRegister()` depends that `webServer()` is available, meaning we can't call this setup func // before the `webSetup()` is called. ATM, just make sure it is in order. void setup() { #if API_SUPPORT - apiRegister(getSetting("termWebApiPath", TERMINAL_WEB_API_PATH), + apiRegister( + getSetting(Key, Path), [](ApiRequest& api) { api.handle([](AsyncWebServerRequest* request) { auto* response = request->beginResponseStream(F("text/plain")); @@ -726,8 +730,9 @@ void setup() { (*cmd) += '\n'; } - api.handle([&](AsyncWebServerRequest* request) { - AsyncWebPrint::scheduleFromRequest(request, + api.handle([cmd](AsyncWebServerRequest* request) { + espurna::web::print::scheduleFromRequest( + request, [cmd](Print& out) { api_find_and_call(*cmd, out); }); @@ -738,8 +743,12 @@ void setup() { ); #else webRequestRegister([](AsyncWebServerRequest* request) { - String path(F(API_BASE_PATH)); - path += getSetting("termWebApiPath", TERMINAL_WEB_API_PATH); + STRING_VIEW_INLINE(BasePath, API_BASE_PATH); + + String path; + path += BasePath; + path += getSetting(Key, Path); + if (path != request->url()) { return false; } @@ -767,7 +776,8 @@ void setup() { auto cmd = std::make_shared(std::move(line)); - AsyncWebPrint::scheduleFromRequest(request, + espurna::web::print::scheduleFromRequest( + request, [cmd](Print& out) { api_find_and_call(*cmd, out); }); diff --git a/code/espurna/web.cpp b/code/espurna/web.cpp index d2208073..f5771142 100644 --- a/code/espurna/web.cpp +++ b/code/espurna/web.cpp @@ -64,13 +64,11 @@ namespace { #include "static/server.key.h" #endif // WEB_SSL_ENABLED -AsyncWebPrint::AsyncWebPrint(AsyncWebPrintConfig config, AsyncWebServerRequest* request) : - _config(config), - _request(request), - _state(State::None) -{} +namespace espurna { +namespace web { +namespace print { -bool AsyncWebPrint::_addBuffer() { +bool RequestPrint::_addBuffer() { if ((_buffers.size() + 1) > _config.backlog.count) { if (!_exhaustBuffers()) { _state = State::Error; @@ -93,61 +91,57 @@ bool AsyncWebPrint::_addBuffer() { // HTTP client (curl, python requests etc., as discovered in testing) will then drop the connection // - Returning 0 will immediatly close the connection from our side // - Calling _prepareRequest() **before** _buffers are filled will result in returning 0 -// - Calling yield() / delay() while request AsyncWebPrint is active **may** trigger this callback out of sequence +// - Calling yield() / delay() while request handler is active **may** trigger this callback out of sequence // (e.g. Stream.write(...), Stream.read(...), DEBUG_MSG(...), or any other API trying to switch contexts) // - Receiving data (tcp ack from the previous packet) **will** trigger the callback when switching contexts. -void AsyncWebPrint::_prepareRequest() { - _state = State::Sending; +size_t RequestPrint::_handleRequest(uint8_t* data, size_t maxLen) { + switch (_state) { + case State::None: + return RESPONSE_TRY_AGAIN; + case State::Error: + case State::Done: + return 0; + case State::Sending: + break; + } - auto *response = _request->beginChunkedResponse(_config.mimeType, [this](uint8_t *buffer, size_t maxLen, size_t) -> size_t { - switch (_state) { - case State::None: - return RESPONSE_TRY_AGAIN; - case State::Error: - case State::Done: - return 0; - case State::Sending: - break; + size_t written = 0; + while ((written < maxLen) && !_buffers.empty()) { + auto& chunk =_buffers.front(); + auto have = maxLen - written; + if (chunk.size() > have) { + std::copy(chunk.data(), chunk.data() + have, data + written); + chunk.erase(chunk.begin(), chunk.begin() + have); + written += have; + } else { + std::copy(chunk.data(), chunk.data() + chunk.size(), data + written); + _buffers.pop_front(); + written += chunk.size(); } + } - size_t written = 0; - while ((written < maxLen) && !_buffers.empty()) { - auto& chunk =_buffers.front(); - auto have = maxLen - written; - if (chunk.size() > have) { - std::copy(chunk.data(), chunk.data() + have, buffer + written); - chunk.erase(chunk.begin(), chunk.begin() + have); - written += have; - } else { - std::copy(chunk.data(), chunk.data() + chunk.size(), buffer + written); - _buffers.pop_front(); - written += chunk.size(); - } - } + return written; +} +void RequestPrint::_prepareRequest() { + _state = State::Sending; - return written; - }); + auto *response = _request->beginChunkedResponse( + _config.mimeType, + [this](uint8_t*data, size_t maxLen, size_t) -> size_t { + return this->_handleRequest(data, maxLen); + }); response->addHeader(F("Connection"), F("close")); _request->send(response); } -void AsyncWebPrint::setState(State state) { - _state = state; -} - -AsyncWebPrint::State AsyncWebPrint::getState() { - return _state; +size_t RequestPrint::write(uint8_t b) { + return write(&b, 1); } -size_t AsyncWebPrint::write(uint8_t b) { - const uint8_t tmp[1] {b}; - return write(tmp, 1); -} - -bool AsyncWebPrint::_exhaustBuffers() { +bool RequestPrint::_exhaustBuffers() { // XXX: espasyncwebserver will trigger write callback if we setup response too early // exploring code, callback handler responds to a special return value RESPONSE_TRY_AGAIN // but, it seemingly breaks chunked response logic @@ -156,13 +150,11 @@ bool AsyncWebPrint::_exhaustBuffers() { _prepareRequest(); } - constexpr espurna::duration::Seconds Timeout { 5 }; - using TimeSource = espurna::time::CoreClock; const auto start = TimeSource::now(); do { - if (TimeSource::now() - start > Timeout) { + if (TimeSource::now() - start > _config.backlog.timeout) { _buffers.clear(); break; } @@ -172,12 +164,12 @@ bool AsyncWebPrint::_exhaustBuffers() { return _buffers.empty(); } -void AsyncWebPrint::flush() { +void RequestPrint::flush() { _exhaustBuffers(); _state = State::Done; } -size_t AsyncWebPrint::write(const uint8_t* data, size_t size) { +size_t RequestPrint::write(const uint8_t* data, size_t size) { if (_state == State::Error) { return 0; } @@ -209,6 +201,10 @@ size_t AsyncWebPrint::write(const uint8_t* data, size_t size) { return full_size; } +} // namespace print +} // namespace web +} // namespace espurna + // ----------------------------------------------------------------------------- namespace { diff --git a/code/espurna/web.h b/code/espurna/web.h index 0469bfa5..16dee3d1 100644 --- a/code/espurna/web.h +++ b/code/espurna/web.h @@ -19,65 +19,7 @@ Copyright (C) 2016-2019 by Xose Pérez #include #include "web_utils.h" - -struct AsyncWebPrintConfig { - struct Backlog { - size_t count; - size_t size; - espurna::duration::Seconds timeout; - }; - - const char* const mimeType; - Backlog backlog; -}; - -class AsyncWebPrint : public Print { -public: - enum class State { - None, - Sending, - Done, - Error - }; - - using BufferType = std::vector; - using TimeSource = espurna::time::CoreClock; - - // To be able to safely output data right from the request callback, - // we schedule a 'printer' task that will print into the request response buffer via AsyncChunkedResponse - // Note: implementation must be included in the header - template - static void scheduleFromRequest(AsyncWebPrintConfig config, AsyncWebServerRequest*, CallbackType); - - template - static void scheduleFromRequest(AsyncWebServerRequest*, CallbackType); - - State getState(); - void setState(State); - - // note: existing implementation only expects this to be available via AsyncWebPrint -#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) - void flush(); -#else - void flush() final override; -#endif - - size_t write(uint8_t) final override; - size_t write(const uint8_t *buffer, size_t size) final override; - -protected: - AsyncWebPrintConfig _config; - - std::list _buffers; - AsyncWebServerRequest* const _request; - State _state; - - AsyncWebPrint(AsyncWebPrintConfig, AsyncWebServerRequest* req); - - bool _addBuffer(); - bool _exhaustBuffers(); - void _prepareRequest(); -}; +#include "web_print.h" using web_body_callback_f = std::function; using web_request_callback_f = std::function; diff --git a/code/espurna/web_asyncwebprint.ipp b/code/espurna/web_asyncwebprint.ipp deleted file mode 100644 index b7f1ef49..00000000 --- a/code/espurna/web_asyncwebprint.ipp +++ /dev/null @@ -1,70 +0,0 @@ -/* - -Part of the WEBSERVER MODULE - -Copyright (C) 2016-2019 by Xose Pérez - -*/ - -#pragma once - -#include "web.h" -#include "libs/TypeChecks.h" - -#if WEB_SUPPORT - -namespace asyncwebprint { -namespace traits { - -template -using print_callable_t = decltype(std::declval()(std::declval())); - -template -using is_print_callable = is_detected; - -} -} - -template -void AsyncWebPrint::scheduleFromRequest(AsyncWebPrintConfig config, AsyncWebServerRequest* request, CallbackType callback) { - static_assert(asyncwebprint::traits::is_print_callable::value, "CallbackType needs to be a callable with void(Print&)"); - - // because of async nature of the server, we need to make sure we outlive 'request' object - auto print = std::shared_ptr(new AsyncWebPrint(config, request)); - - // attach one ptr to onDisconnect capture, so we can detect disconnection before scheduled function runs - request->onDisconnect([print, request]() { -#if API_SUPPORT - // TODO: in case this comes from `apiRegister`'ed endpoint, there's still a lingering ApiRequestHelper that we must remove - if (request->_tempObject) { - auto* ptr = reinterpret_cast(request->_tempObject); - delete ptr; - request->_tempObject = nullptr; - } -#endif - print->setState(AsyncWebPrint::State::Done); - }); - - // attach another capture to the scheduled function, so we execute as soon as we exit next loop() - espurnaRegisterOnce([callback, print]() { - if (State::None != print->getState()) return; - callback(*print.get()); - print->flush(); - }); -} - -static constexpr AsyncWebPrintConfig AsyncWebPrintDefaults { - .mimeType = "text/plain", - .backlog = { - .count = 2, - .size = TCP_MSS, - .timeout = espurna::duration::Seconds(5) - } -}; - -template -void AsyncWebPrint::scheduleFromRequest(AsyncWebServerRequest* request, CallbackType callback) { - AsyncWebPrint::scheduleFromRequest(AsyncWebPrintDefaults, request, callback); -} - -#endif diff --git a/code/espurna/web_print.h b/code/espurna/web_print.h new file mode 100644 index 00000000..7bf5e0e7 --- /dev/null +++ b/code/espurna/web_print.h @@ -0,0 +1,104 @@ +/* + +Part of WEBSERVER MODULE + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#pragma once + +#include "espurna.h" + +#include + +#include +#include +#include + +namespace espurna { +namespace web { +namespace print { + +struct Config { + struct Backlog { + size_t count; + size_t size; + duration::Seconds timeout; + }; + + const char* const mimeType; + Backlog backlog; +}; + +class RequestPrint : public Print { +public: + enum class State { + None, + Sending, + Done, + Error + }; + + using BufferType = std::vector; + using TimeSource = espurna::time::CoreClock; + + // To be able to safely output data right from the request callback, + // we schedule a 'printer' task that will print into the request response buffer via AsyncChunkedResponse + // Note: implementation must be included in the header + template + static void scheduleFromRequest(Config config, AsyncWebServerRequest*, CallbackType); + + template + static void scheduleFromRequest(AsyncWebServerRequest*, CallbackType); + + void flush() final override; + + size_t write(uint8_t) final override; + size_t write(const uint8_t *buffer, size_t size) final override; + + State state() const { + return _state; + } + + void state(State state) { + _state = state; + } + + AsyncWebServerRequest* request() const { + return _request; + } + +private: + Config _config; + + std::list _buffers; + AsyncWebServerRequest* const _request; + State _state; + + RequestPrint(Config config, AsyncWebServerRequest* request) : + _config(config), + _request(request), + _state(State::None) + {} + + bool _addBuffer(); + bool _exhaustBuffers(); + + void _prepareRequest(); + size_t _handleRequest(uint8_t* data, size_t maxLen); + + void _onDisconnect(); + + template + void _callback(CallbackType&&); +}; + +template +void scheduleFromRequest(AsyncWebServerRequest* request, T&& callback) { + RequestPrint::scheduleFromRequest(request, std::forward(callback)); +} + +} // namespace print +} // namespace web +} // namespace espurna diff --git a/code/espurna/web_print.ipp b/code/espurna/web_print.ipp new file mode 100644 index 00000000..458bc25a --- /dev/null +++ b/code/espurna/web_print.ipp @@ -0,0 +1,90 @@ +/* + +Part of the WEBSERVER MODULE + +Copyright (C) 2016-2019 by Xose Pérez + +*/ + +#pragma once + +#include "web.h" +#include "api.h" +#include "libs/TypeChecks.h" + +namespace espurna { +namespace web { +namespace print { +namespace traits { + +template +using print_callable_t = decltype(std::declval()(std::declval())); + +template +using is_print_callable = is_detected; + +} // namespace traits + +void RequestPrint::_onDisconnect() { +#if API_SUPPORT + // TODO: in case this comes from `apiRegister`'ed endpoint, there's still a lingering ApiRequestHelper that we must remove + auto* req = request(); + if (req->_tempObject) { + auto* ptr = reinterpret_cast(req->_tempObject); + delete ptr; + req->_tempObject = nullptr; + } +#endif + state(State::Done); +} + +template +void RequestPrint::_callback(CallbackType&& callback) { + if (State::None != state()) { + return; + } + + callback(*this); + flush(); +} + +template +void RequestPrint::scheduleFromRequest(Config config, AsyncWebServerRequest* request, CallbackType callback) { + static_assert( + traits::is_print_callable::value, + "CallbackType needs to be a callable with void(Print&)"); + + // because of async nature of the server, we need to make sure we outlive 'request' object + auto print = std::shared_ptr( + new RequestPrint(config, request)); + + // attach one ptr to onDisconnect capture, so we can detect disconnection before scheduled function runs + request->onDisconnect( + [print]() { + print->_onDisconnect(); + }); + + // attach another capture to the scheduled function, so we execute as soon as we exit next loop() + espurnaRegisterOnce( + [callback, print]() { + print->_callback(callback); + }); +} + +static constexpr auto DefaultConfig = Config{ + .mimeType = "text/plain", + .backlog = { + .count = 2, + .size = TCP_MSS, + .timeout = duration::Seconds(5) + }, +}; + +template +void RequestPrint::scheduleFromRequest(AsyncWebServerRequest* request, CallbackType callback) { + RequestPrint::scheduleFromRequest(DefaultConfig, request, callback); +} + +} // namespace print +} // namespace web +} // namespace espurna