[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:
Frédéric Metrich
2026-10-13 00:28:38 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 J. Nick Koston
parent 4cae63bbaa
commit dc9a7cffbd
18 changed files with 292 additions and 42 deletions
+58
View File
@@ -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)
+24
View File
@@ -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);
+5
View File
@@ -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",
}
+85
View File
@@ -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
+1
View File
@@ -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"),
],
)