mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[ota] Refuse a second OTA while one is already running (#20588)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
Claude Opus 5.5
J. Nick Koston
parent
4cae63bbaa
commit
dc9a7cffbd
@@ -1,5 +1,9 @@
|
||||
#include "ota_backend.h"
|
||||
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
#include <atomic>
|
||||
#endif
|
||||
|
||||
namespace esphome::ota {
|
||||
|
||||
bool version_is_older(const char *candidate, const char *reference) {
|
||||
@@ -30,6 +34,60 @@ bool version_is_older(const char *candidate, const char *reference) {
|
||||
}
|
||||
}
|
||||
|
||||
// Non-blocking; may be released by another task than the one that took it.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// uint32_t: narrower atomics compile to a masked CAS on Xtensa.
|
||||
static std::atomic<uint32_t> update_locked{0}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static bool update_lock_test_and_set() { return update_locked.exchange(1) == 0; }
|
||||
static void update_lock_clear() { update_locked.store(0); }
|
||||
#else
|
||||
static bool update_locked = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
// BK72xx has no atomic exchange.
|
||||
static Mutex &update_lock_mutex() {
|
||||
static Mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool update_lock_test_and_set() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
LockGuard guard(update_lock_mutex());
|
||||
#else
|
||||
// lwIP callbacks may run from an interrupt (RP2040).
|
||||
InterruptLock guard;
|
||||
#endif
|
||||
if (update_locked)
|
||||
return false;
|
||||
update_locked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void update_lock_clear() {
|
||||
#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS
|
||||
LockGuard guard(update_lock_mutex());
|
||||
#endif
|
||||
update_locked = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool UpdateLock::try_acquire() {
|
||||
if (!this->held_)
|
||||
this->held_ = update_lock_test_and_set();
|
||||
return this->held_;
|
||||
}
|
||||
|
||||
void UpdateLock::release() {
|
||||
if (!this->held_)
|
||||
return;
|
||||
update_lock_clear();
|
||||
this->held_ = false;
|
||||
}
|
||||
|
||||
void UpdateLock::force_release() { update_lock_clear(); }
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ enum OTAResponseTypes {
|
||||
OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92,
|
||||
OTA_RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93,
|
||||
OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED = 0x94,
|
||||
OTA_RESPONSE_ERROR_UPDATE_BUSY = 0x95,
|
||||
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
|
||||
};
|
||||
|
||||
@@ -95,6 +96,29 @@ enum OTAType : uint8_t {
|
||||
OTA_TYPE_UPDATE_BOOTLOADER = 0x02,
|
||||
};
|
||||
|
||||
/// Serializes flash updates. Taken in begin(), released when the backend is destroyed, or kept until
|
||||
/// reboot after a successful end(). Also for components writing the update partition outside OTA.
|
||||
class UpdateLock {
|
||||
public:
|
||||
UpdateLock() = default;
|
||||
~UpdateLock() { this->release(); }
|
||||
UpdateLock(const UpdateLock &) = delete;
|
||||
UpdateLock &operator=(const UpdateLock &) = delete;
|
||||
|
||||
/// Returns false if held elsewhere.
|
||||
bool try_acquire();
|
||||
void release();
|
||||
bool is_held() const { return this->held_; }
|
||||
/// Keep the lock set until reboot; is_held() becomes false.
|
||||
void hold_until_reboot() { this->held_ = false; }
|
||||
|
||||
protected:
|
||||
/// Tests only.
|
||||
static void force_release();
|
||||
|
||||
bool held_{false};
|
||||
};
|
||||
|
||||
// The OTA backend method surface. Exactly one backend exists per build,
|
||||
// selected in ota_backend_factory.h where this concept is asserted on
|
||||
// make_ota_backend()'s return type. Semantics beyond the signatures:
|
||||
|
||||
@@ -14,6 +14,8 @@ ESPHOME_LOG_TAG(TAG, "ota");
|
||||
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size, OTAType ota_type) {
|
||||
if (!this->lock_.try_acquire())
|
||||
return OTA_RESPONSE_ERROR_UPDATE_BUSY;
|
||||
if (ota_type != OTA_TYPE_UPDATE_APP) {
|
||||
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
}
|
||||
@@ -57,6 +59,7 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
if (Update.end(!this->md5_set_)) {
|
||||
this->lock_.hold_until_reboot();
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
@@ -66,7 +69,11 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
|
||||
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||
}
|
||||
|
||||
void ArduinoLibreTinyOTABackend::abort() { Update.abort(); }
|
||||
void ArduinoLibreTinyOTABackend::abort() {
|
||||
if (!this->lock_.is_held()) // refused in begin()
|
||||
return;
|
||||
Update.abort();
|
||||
}
|
||||
|
||||
} // namespace esphome::ota
|
||||
#endif // USE_LIBRETINY
|
||||
|
||||
@@ -16,6 +16,7 @@ class ArduinoLibreTinyOTABackend final {
|
||||
static constexpr bool supports_compression() { return false; }
|
||||
|
||||
private:
|
||||
UpdateLock lock_;
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ ESPHOME_LOG_TAG(TAG, "ota");
|
||||
std::unique_ptr<ArduinoRP2OTABackend> make_ota_backend() { return make_unique<ArduinoRP2OTABackend>(); }
|
||||
|
||||
OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type) {
|
||||
if (!this->lock_.try_acquire())
|
||||
return OTA_RESPONSE_ERROR_UPDATE_BUSY;
|
||||
if (ota_type != OTA_TYPE_UPDATE_APP) {
|
||||
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
}
|
||||
@@ -63,6 +65,7 @@ OTAResponseTypes ArduinoRP2OTABackend::end() {
|
||||
// Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5
|
||||
// This matches the behavior of the old web_server OTA implementation
|
||||
if (Update.end(!this->md5_set_)) {
|
||||
this->lock_.hold_until_reboot();
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
@@ -73,6 +76,8 @@ OTAResponseTypes ArduinoRP2OTABackend::end() {
|
||||
}
|
||||
|
||||
void ArduinoRP2OTABackend::abort() {
|
||||
if (!this->lock_.is_held()) // refused in begin()
|
||||
return;
|
||||
Update.end();
|
||||
rp2::preferences_prevent_write(false);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class ArduinoRP2OTABackend final {
|
||||
static constexpr bool supports_compression() { return USE_ARDUINO_VERSION_CODE >= VERSION_CODE(4, 0, 3); }
|
||||
|
||||
private:
|
||||
UpdateLock lock_;
|
||||
bool md5_set_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ ESPHOME_LOG_TAG(TAG, "ota");
|
||||
std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); }
|
||||
|
||||
OTAResponseTypes ESP8266OTABackend::begin(size_t image_size, OTAType ota_type) {
|
||||
if (!this->lock_.try_acquire())
|
||||
return OTA_RESPONSE_ERROR_UPDATE_BUSY;
|
||||
if (ota_type != OTA_TYPE_UPDATE_APP) {
|
||||
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
}
|
||||
@@ -303,10 +305,13 @@ OTAResponseTypes ESP8266OTABackend::end() {
|
||||
this->buffer_.reset();
|
||||
esp8266::preferences_prevent_write(false);
|
||||
|
||||
this->lock_.hold_until_reboot();
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
void ESP8266OTABackend::abort() {
|
||||
if (!this->lock_.is_held()) // refused in begin()
|
||||
return;
|
||||
this->buffer_.reset();
|
||||
this->buffer_len_ = 0;
|
||||
this->image_size_ = 0;
|
||||
|
||||
@@ -23,6 +23,7 @@ class ESP8266OTABackend final {
|
||||
static constexpr bool supports_compression() { return true; }
|
||||
|
||||
protected:
|
||||
UpdateLock lock_;
|
||||
/// Erase flash sector if current address is at sector boundary
|
||||
bool erase_sector_if_needed_();
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ ESPHOME_LOG_TAG(TAG, "ota");
|
||||
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
|
||||
|
||||
OTAResponseTypes IDFOTABackend::begin(size_t image_size, ota::OTAType ota_type) {
|
||||
if (!this->lock_.try_acquire())
|
||||
return OTA_RESPONSE_ERROR_UPDATE_BUSY;
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
this->ota_type_ = ota_type;
|
||||
if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
|
||||
@@ -196,7 +198,7 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
// signature, and only re-points boot at an already-installed app -- so it is
|
||||
// intentionally not run through the signature verifier below.
|
||||
if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
|
||||
return this->update_partition_table();
|
||||
return this->hold_lock_if_ok_(this->update_partition_table());
|
||||
}
|
||||
if (!this->is_app_or_bootloader_update_()) {
|
||||
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
@@ -209,7 +211,7 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
}
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
if (this->ota_type_ == ota::OTA_TYPE_UPDATE_BOOTLOADER) {
|
||||
return this->finalize_bootloader_update_(err);
|
||||
return this->hold_lock_if_ok_(this->finalize_bootloader_update_(err));
|
||||
}
|
||||
#endif
|
||||
if (err == ESP_OK) {
|
||||
@@ -242,7 +244,7 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
#endif
|
||||
err = esp_ota_set_boot_partition(this->partition_);
|
||||
if (err == ESP_OK) {
|
||||
return OTA_RESPONSE_OK;
|
||||
return this->hold_lock_if_ok_(OTA_RESPONSE_OK);
|
||||
}
|
||||
}
|
||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||
@@ -260,6 +262,8 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
}
|
||||
|
||||
void IDFOTABackend::abort() {
|
||||
if (!this->lock_.is_held()) // refused in begin()
|
||||
return;
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
if (this->partition_table_part_ != nullptr) {
|
||||
esp_partition_deregister_external(this->partition_table_part_);
|
||||
|
||||
@@ -36,6 +36,12 @@ class IDFOTABackend final {
|
||||
static constexpr bool supports_compression() { return false; }
|
||||
|
||||
protected:
|
||||
UpdateLock lock_;
|
||||
OTAResponseTypes hold_lock_if_ok_(OTAResponseTypes result) {
|
||||
if (result == OTA_RESPONSE_OK)
|
||||
this->lock_.hold_until_reboot();
|
||||
return result;
|
||||
}
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
// copy_dest_part non-null means the running app must be copied INTO this slot of the current
|
||||
// table before the new partition table is committed. The destination is in the current table
|
||||
|
||||
@@ -200,6 +200,8 @@ bool validate_executable_(const char *staging_path, const std::string &exe_path)
|
||||
std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); }
|
||||
|
||||
OTAResponseTypes HostOTABackend::begin(size_t image_size, OTAType ota_type) {
|
||||
if (!this->lock_.try_acquire())
|
||||
return OTA_RESPONSE_ERROR_UPDATE_BUSY;
|
||||
if (ota_type != OTA_TYPE_UPDATE_APP)
|
||||
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
|
||||
// 0 = unknown size (web_server multipart); cap at MAX_OTA_SIZE.
|
||||
@@ -318,10 +320,13 @@ OTAResponseTypes HostOTABackend::end() {
|
||||
host::arm_reexec(this->final_path_);
|
||||
this->staging_path_.clear();
|
||||
ESP_LOGI(TAG, "OTA staged at %s; will re-exec on reboot", this->final_path_.c_str());
|
||||
this->lock_.hold_until_reboot();
|
||||
return OTA_RESPONSE_OK;
|
||||
}
|
||||
|
||||
void HostOTABackend::abort() {
|
||||
if (!this->lock_.is_held()) // refused in begin()
|
||||
return;
|
||||
if (this->fd_ >= 0) {
|
||||
::close(this->fd_);
|
||||
this->fd_ = -1;
|
||||
|
||||
@@ -22,6 +22,7 @@ class HostOTABackend final {
|
||||
static constexpr bool supports_compression() { return false; }
|
||||
|
||||
protected:
|
||||
UpdateLock lock_;
|
||||
md5::MD5Digest md5_{};
|
||||
std::string staging_path_;
|
||||
std::string final_path_;
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
#include "esphome/components/captive_portal/captive_portal.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_LIBRETINY)
|
||||
#include <Update.h>
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
#if USE_ESP32
|
||||
using PlatformString = std::string;
|
||||
#elif USE_ARDUINO
|
||||
@@ -64,6 +58,10 @@ class OTARequestHandler : public AsyncWebHandler {
|
||||
void report_ota_progress_(AsyncWebServerRequest *request);
|
||||
void schedule_ota_reboot_();
|
||||
void ota_init_(const char *filename);
|
||||
/// Abort and drop the backend, releasing the lock.
|
||||
void abort_session_();
|
||||
/// Reply "busy" without touching the running upload.
|
||||
void refuse_(AsyncWebServerRequest *request);
|
||||
|
||||
uint32_t last_ota_progress_{0};
|
||||
uint32_t ota_read_length_{0};
|
||||
@@ -72,6 +70,9 @@ class OTARequestHandler : public AsyncWebHandler {
|
||||
|
||||
private:
|
||||
ota::OTABackendPtr ota_backend_{nullptr};
|
||||
/// Request that owns ota_backend_.
|
||||
const AsyncWebServerRequest *ota_request_{nullptr};
|
||||
const AsyncWebServerRequest *refused_request_{nullptr};
|
||||
};
|
||||
|
||||
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||
@@ -118,38 +119,13 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
// fires a separate start-marker call with data==nullptr/len==0 before the
|
||||
// first real chunk; gate on len>0 so we only trigger once per upload.)
|
||||
if (index == 0 && len > 0) {
|
||||
// If a previous upload was interrupted (e.g. client closed the tab, TCP
|
||||
// reset) the backend from that session may still be open. Tear it down
|
||||
// so flash state doesn't get concatenated with the new image (which can
|
||||
// produce a technically-valid-sized but corrupted firmware that bricks
|
||||
// the device once it reboots).
|
||||
// A stalled upload times out and onDisconnect() frees it, so refusing can't block for long.
|
||||
if (this->ota_backend_) {
|
||||
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify listeners that the previous session was aborted before the new one starts.
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
|
||||
#endif
|
||||
this->ota_backend_.reset();
|
||||
ESP_LOGW(TAG, "OTA upload refused: another upload is in progress");
|
||||
this->refuse_(request);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify OTA started - use notify_state_deferred_ since we're in web server task
|
||||
this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
// Platform-specific pre-initialization
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_LIBRETINY)
|
||||
if (Update.isRunning()) {
|
||||
Update.abort();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
this->ota_backend_ = ota::make_ota_backend();
|
||||
if (!this->ota_backend_) {
|
||||
ESP_LOGE(TAG, "Failed to create OTA backend");
|
||||
@@ -164,6 +140,22 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
// is unknown (contentLength includes multipart overhead)
|
||||
// Pass 0 to indicate unknown size
|
||||
error_code = this->ota_backend_->begin(0);
|
||||
if (error_code == ota::OTA_RESPONSE_ERROR_UPDATE_BUSY) {
|
||||
// Nothing started: no state notifications.
|
||||
ESP_LOGW(TAG, "OTA upload refused: another update is in progress");
|
||||
this->ota_backend_.reset();
|
||||
this->refuse_(request);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify OTA started - use notify_state_deferred_ since we're in web server task
|
||||
this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
|
||||
#endif
|
||||
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
|
||||
this->ota_backend_.reset();
|
||||
@@ -172,9 +164,18 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// An interrupted upload gets no final chunk.
|
||||
this->ota_request_ = request;
|
||||
request->onDisconnect([this, request]() {
|
||||
if (this->ota_request_ == request && this->ota_backend_) {
|
||||
ESP_LOGW(TAG, "OTA upload interrupted; aborting");
|
||||
this->abort_session_();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this->ota_backend_) {
|
||||
if (!this->ota_backend_ || request != this->ota_request_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,6 +186,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
ESP_LOGE(TAG, "OTA write failed: %d", error_code);
|
||||
this->ota_backend_->abort();
|
||||
this->ota_backend_.reset();
|
||||
this->ota_request_ = nullptr;
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
@@ -212,28 +214,54 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
this->schedule_ota_reboot_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "OTA end failed: %d", error_code);
|
||||
// Reset the platform updater before the lock is released.
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
|
||||
#endif
|
||||
}
|
||||
this->ota_backend_.reset();
|
||||
this->ota_request_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void OTARequestHandler::abort_session_() {
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
|
||||
#endif
|
||||
this->ota_backend_.reset();
|
||||
this->ota_request_ = nullptr;
|
||||
}
|
||||
|
||||
void OTARequestHandler::refuse_(AsyncWebServerRequest *request) {
|
||||
this->refused_request_ = request;
|
||||
request->onDisconnect([this, request]() {
|
||||
if (this->refused_request_ == request)
|
||||
this->refused_request_ = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||
AsyncWebServerResponse *response;
|
||||
// Use the ota_success_ flag to determine the actual result
|
||||
const bool refused = request == this->refused_request_;
|
||||
if (refused)
|
||||
this->refused_request_ = nullptr;
|
||||
#ifdef USE_ESP8266
|
||||
static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!";
|
||||
static const char UPDATE_FAILED[] PROGMEM = "Update Failed!";
|
||||
static const char UPDATE_BUSY[] PROGMEM = "Update Failed! Another update is in progress, retry later.";
|
||||
static const char TEXT_PLAIN[] PROGMEM = "text/plain";
|
||||
static const char CONNECTION_STR[] PROGMEM = "Connection";
|
||||
static const char CLOSE_STR[] PROGMEM = "close";
|
||||
const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED;
|
||||
const char *msg = refused ? UPDATE_BUSY : (this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED);
|
||||
response = request->beginResponse_P(200, TEXT_PLAIN, msg);
|
||||
response->addHeader(CONNECTION_STR, CLOSE_STR);
|
||||
#else
|
||||
const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
|
||||
const char *msg = refused ? "Update Failed! Another update is in progress, retry later."
|
||||
: this->ota_success_ ? "Update Successful!"
|
||||
: "Update Failed!";
|
||||
response = request->beginResponse(200, "text/plain", msg);
|
||||
response->addHeader("Connection", "close");
|
||||
#endif
|
||||
|
||||
@@ -311,6 +311,10 @@ esp_err_t AsyncWebServer::handle_raw_body_(httpd_req_t *r, const char *content_t
|
||||
}
|
||||
|
||||
AsyncWebServerRequest::~AsyncWebServerRequest() {
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
if (this->on_disconnect_)
|
||||
this->on_disconnect_();
|
||||
#endif
|
||||
delete this->rsp_;
|
||||
for (auto *param : this->params_) {
|
||||
delete param; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
@@ -183,6 +183,12 @@ class AsyncWebServerRequest {
|
||||
std::string arg(const char *name);
|
||||
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
|
||||
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
/// Called when the request ends, including early returns.
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
void onDisconnect(std::function<void()> fn) { this->on_disconnect_ = std::move(fn); }
|
||||
#endif
|
||||
|
||||
operator httpd_req_t *() const { return this->req_; }
|
||||
optional<std::string> get_header(const char *name) const;
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
@@ -198,6 +204,9 @@ class AsyncWebServerRequest {
|
||||
optional<std::string> find_query_value_(const char *name) const;
|
||||
std::vector<AsyncWebParameter *> params_;
|
||||
std::string post_query_;
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
std::function<void()> on_disconnect_;
|
||||
#endif
|
||||
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
|
||||
AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {}
|
||||
void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type);
|
||||
|
||||
@@ -54,6 +54,7 @@ RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91
|
||||
RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92
|
||||
RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93
|
||||
RESPONSE_ERROR_ENCRYPTION_REQUIRED = 0x94
|
||||
RESPONSE_ERROR_UPDATE_BUSY = 0x95
|
||||
RESPONSE_ERROR_UNKNOWN = 0xFF
|
||||
|
||||
OTA_VERSION_1_0 = 1
|
||||
@@ -203,6 +204,10 @@ _ERROR_MESSAGES: dict[int, str] = {
|
||||
"of the YAML this upload uses, or update your esphome installation if it "
|
||||
"predates OTA encryption."
|
||||
),
|
||||
RESPONSE_ERROR_UPDATE_BUSY: (
|
||||
"The device is busy with another update that writes to its flash. "
|
||||
"Wait for it to finish, then retry."
|
||||
),
|
||||
RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/ota/ota_backend_factory.h"
|
||||
|
||||
namespace esphome::ota::testing {
|
||||
|
||||
TEST(UpdateLock, SecondHolderIsRefused) {
|
||||
UpdateLock first;
|
||||
UpdateLock second;
|
||||
EXPECT_TRUE(first.try_acquire());
|
||||
EXPECT_FALSE(second.try_acquire());
|
||||
first.release();
|
||||
EXPECT_TRUE(second.try_acquire());
|
||||
}
|
||||
|
||||
TEST(UpdateLock, AcquireIsIdempotentForTheHolder) {
|
||||
UpdateLock lock;
|
||||
EXPECT_TRUE(lock.try_acquire());
|
||||
EXPECT_TRUE(lock.try_acquire());
|
||||
lock.release();
|
||||
UpdateLock other;
|
||||
EXPECT_TRUE(other.try_acquire());
|
||||
}
|
||||
|
||||
TEST(UpdateLock, ReleaseByNonHolderKeepsLock) {
|
||||
UpdateLock holder;
|
||||
UpdateLock other;
|
||||
EXPECT_TRUE(holder.try_acquire());
|
||||
other.release();
|
||||
EXPECT_FALSE(other.try_acquire());
|
||||
}
|
||||
|
||||
TEST(UpdateLock, DestructorReleases) {
|
||||
{
|
||||
UpdateLock scoped;
|
||||
EXPECT_TRUE(scoped.try_acquire());
|
||||
}
|
||||
UpdateLock next;
|
||||
EXPECT_TRUE(next.try_acquire());
|
||||
}
|
||||
|
||||
TEST(UpdateLock, IsHeldTracksOwnership) {
|
||||
UpdateLock holder;
|
||||
UpdateLock other;
|
||||
EXPECT_FALSE(holder.is_held());
|
||||
EXPECT_TRUE(holder.try_acquire());
|
||||
EXPECT_TRUE(holder.is_held());
|
||||
EXPECT_FALSE(other.try_acquire());
|
||||
EXPECT_FALSE(other.is_held());
|
||||
}
|
||||
|
||||
class TestUpdateLock : public UpdateLock {
|
||||
public:
|
||||
using UpdateLock::force_release;
|
||||
};
|
||||
|
||||
TEST(UpdateLock, HoldUntilRebootOutlivesHolder) {
|
||||
{
|
||||
UpdateLock committed;
|
||||
EXPECT_TRUE(committed.try_acquire());
|
||||
committed.hold_until_reboot();
|
||||
EXPECT_FALSE(committed.is_held());
|
||||
}
|
||||
UpdateLock next;
|
||||
EXPECT_FALSE(next.try_acquire());
|
||||
TestUpdateLock::force_release();
|
||||
EXPECT_TRUE(next.try_acquire());
|
||||
}
|
||||
|
||||
TEST(UpdateLock, SecondBackendBeginIsBusy) {
|
||||
auto first = make_ota_backend();
|
||||
auto second = make_ota_backend();
|
||||
ASSERT_EQ(first->begin(1024), OTA_RESPONSE_OK);
|
||||
EXPECT_EQ(second->begin(1024), OTA_RESPONSE_ERROR_UPDATE_BUSY);
|
||||
// Native OTA calls abort() after a refused begin().
|
||||
second->abort();
|
||||
uint8_t data[16] = {};
|
||||
EXPECT_EQ(first->write(data, sizeof(data)), OTA_RESPONSE_OK);
|
||||
first->abort();
|
||||
first.reset();
|
||||
EXPECT_EQ(second->begin(1024), OTA_RESPONSE_OK);
|
||||
second->abort();
|
||||
}
|
||||
|
||||
} // namespace esphome::ota::testing
|
||||
@@ -263,6 +263,7 @@ def test_receive_exactly_closed_connection_is_network_error(mock_socket: Mock) -
|
||||
espota2.RESPONSE_ERROR_BOOTLOADER_UPDATE,
|
||||
"An error occurred while updating the bootloader",
|
||||
),
|
||||
(espota2.RESPONSE_ERROR_UPDATE_BUSY, "The device is busy with another update"),
|
||||
(espota2.RESPONSE_ERROR_UNKNOWN, "Unknown error from ESP"),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user