blob: b424fab9b3434e001a9c9ca77b061e6a44715a41 [file] [log] [blame]
// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#define PW_LOG_MODULE_NAME "PWSU"
#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
#include "pw_software_update/bundled_update_service.h"
#include <mutex>
#include <string_view>
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_software_update/config.h"
#include "pw_software_update/manifest_accessor.h"
#include "pw_software_update/update_bundle.pwpb.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_status/try.h"
#include "pw_string/string_builder.h"
#include "pw_string/util.h"
#include "pw_sync/borrow.h"
#include "pw_sync/mutex.h"
#include "pw_tokenizer/tokenize.h"
namespace pw::software_update {
namespace {
using BorrowedStatus =
sync::BorrowedPointer<pw_software_update_BundledUpdateStatus, sync::Mutex>;
using BundledUpdateState = pw_software_update_BundledUpdateState_Enum;
using BundledUpdateStatus = pw_software_update_BundledUpdateStatus;
// TODO(keir): Convert all the CHECKs in the RPC service to gracefully report
// errors.
#define SET_ERROR(res, message, ...) \
do { \
PW_LOG_ERROR(message, __VA_ARGS__); \
if (!IsFinished()) { \
Finish(res); \
{ \
BorrowedStatus borrowed_status = status_.acquire(); \
size_t note_size = sizeof(borrowed_status->note.bytes); \
PW_TOKENIZE_TO_BUFFER( \
borrowed_status->note.bytes, &(note_size), message, __VA_ARGS__); \
borrowed_status->note.size = note_size; \
borrowed_status->has_note = true; \
} \
} \
} while (false)
} // namespace
Status BundledUpdateService::GetStatus(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Start(
const pw_software_update_StartRequest& request,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
// Check preconditions.
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_INACTIVE) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Start() can only be called from INACTIVE state. "
"Current state: %d. Abort() then Reset() must be called first",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
{
BorrowedStatus borrowed_status = status_.acquire();
PW_DCHECK(!borrowed_status->has_transfer_id);
PW_DCHECK(!borrowed_status->has_result);
PW_DCHECK(borrowed_status->current_state_progress_hundreth_percent == 0);
PW_DCHECK(borrowed_status->bundle_filename[0] == '\0');
PW_DCHECK(borrowed_status->note.size == 0);
}
// Notify the backend of pending transfer.
if (const Status status = backend_.BeforeUpdateStart(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Backend error on BeforeUpdateStart()");
response = *status_.acquire();
return status;
}
// Enable bundle transfer.
Result<uint32_t> possible_transfer_id =
backend_.EnableBundleTransferHandler(string::ClampedCString(
request.bundle_filename, sizeof(request.bundle_filename)));
if (!possible_transfer_id.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_TRANSFER_FAILED,
"Couldn't enable bundle transfer");
response = *status_.acquire();
return possible_transfer_id.status();
}
// Update state.
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->transfer_id = possible_transfer_id.value();
borrowed_status->has_transfer_id = true;
if (request.has_bundle_filename) {
const StatusWithSize sws =
string::Copy(request.bundle_filename,
borrowed_status->bundle_filename,
sizeof(borrowed_status->bundle_filename));
PW_DCHECK_OK(sws.status(),
"bundle_filename options max_sizes do not match");
borrowed_status->has_bundle_filename = true;
}
borrowed_status->state =
pw_software_update_BundledUpdateState_Enum_TRANSFERRING;
response = *borrowed_status;
}
return OkStatus();
}
Status BundledUpdateService::SetTransferred(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING &&
state != pw_software_update_BundledUpdateState_Enum_INACTIVE) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"SetTransferred() can only be called from TRANSFERRING or "
"INACTIVE state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return OkStatus();
}
NotifyTransferSucceeded();
response = *status_.acquire();
return OkStatus();
}
// TODO: Check for "ABORTING" state and bail if it's set.
void BundledUpdateService::DoVerify() {
std::lock_guard guard(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_VERIFIED) {
return; // Already done!
}
// Ensure we're in the right state.
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"DoVerify() must be called from TRANSFERRED state. State: %d",
static_cast<int>(state));
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_VERIFYING;
// Notify backend about pending verify.
if (const Status status = backend_.BeforeBundleVerify(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::BeforeBundleVerify() failed");
return;
}
// Do the actual verify.
Status status = bundle_.OpenAndVerify();
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Bundle::OpenAndVerify() failed");
return;
}
bundle_open_ = true;
// Have the backend verify the user_manifest if present.
if (!backend_.VerifyManifest(bundle_.GetManifest()).ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::VerifyUserManifest() failed");
return;
}
// Notify backend we're done verifying.
status = backend_.AfterBundleVerified();
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::AfterBundleVerified() failed");
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_VERIFIED;
}
Status BundledUpdateService::Verify(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
// Already done? Bail.
if (state == pw_software_update_BundledUpdateState_Enum_VERIFIED) {
PW_LOG_DEBUG("Skipping verify since already verified");
return OkStatus();
}
// TODO: Remove the transferring permitted state here ASAP.
// Ensure we're in the right state.
if ((state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING) &&
(state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED)) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Verify() must be called from TRANSFERRED state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
// TODO: We should probably make this mode idempotent.
// Already doing what was asked? Bail.
if (work_enqueued_) {
PW_LOG_DEBUG("Verification is already active");
return OkStatus();
}
// The backend's ApplyReboot as part of DoApply() shall be configured
// such that this RPC can send out the reply before the device reboots.
const Status status = work_queue_.PushWork([this] {
{
std::lock_guard y_lock(this->mutex_);
PW_DCHECK(this->work_enqueued_);
}
this->DoVerify();
{
std::lock_guard y_lock(this->mutex_);
this->work_enqueued_ = false;
}
});
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Unable to equeue apply to work queue");
response = *status_.acquire();
return status;
}
work_enqueued_ = true;
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Apply(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
// We do not wait to go into a finished error state if we're already
// applying, instead just let them know that yes we are working on it --
// hold on.
if (state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
}
if ((state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED) &&
(state != pw_software_update_BundledUpdateState_Enum_VERIFIED)) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Apply() must be called from TRANSFERRED or VERIFIED state. "
"State: %d",
static_cast<int>(state));
return Status::FailedPrecondition();
}
// TODO: We should probably make these all idempotent properly.
if (work_enqueued_) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
}
// The backend's ApplyReboot as part of DoApply() shall be configured
// such that this RPC can send out the reply before the device reboots.
const Status status = work_queue_.PushWork([this] {
{
std::lock_guard y_lock(this->mutex_);
PW_DCHECK(this->work_enqueued_);
}
// Error reporting is handled in DoVerify and DoApply.
this->DoVerify();
this->DoApply();
{
std::lock_guard y_lock(this->mutex_);
this->work_enqueued_ = false;
}
});
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Unable to equeue apply to work queue");
response = *status_.acquire();
return status;
}
work_enqueued_ = true;
return OkStatus();
}
void BundledUpdateService::DoApply() {
std::lock_guard guard(mutex_);
const BundledUpdateState state = status_.acquire()->state;
PW_LOG_DEBUG("Attempting to apply the update");
if (state != pw_software_update_BundledUpdateState_Enum_VERIFIED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Apply() must be called from VERIFIED state. State: %d",
static_cast<int>(state));
return;
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_APPLYING;
if (const Status status = backend_.BeforeApply(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"BeforeApply() returned unsuccessful result: %d",
static_cast<int>(status.code()));
return;
}
// In order to report apply progress, quickly scan to see how many bytes
// will be applied.
Result<uint64_t> total_payload_bytes = bundle_.GetTotalPayloadSize();
PW_CHECK_OK(total_payload_bytes.status());
size_t target_file_bytes_to_apply =
static_cast<size_t>(total_payload_bytes.value());
protobuf::RepeatedMessages target_files =
bundle_.GetManifest().GetTargetFiles();
PW_CHECK_OK(target_files.status());
size_t target_file_bytes_applied = 0;
for (pw::protobuf::Message file_name : target_files) {
std::array<std::byte, MAX_TARGET_NAME_LENGTH> buf = {};
protobuf::String name = file_name.AsString(static_cast<uint32_t>(
pw::software_update::TargetFile::Fields::FILE_NAME));
PW_CHECK_OK(name.status());
const Result<ByteSpan> read_result = name.GetBytesReader().Read(buf);
PW_CHECK_OK(read_result.status());
const ConstByteSpan file_name_span = read_result.value();
const std::string_view file_name_view(
reinterpret_cast<const char*>(file_name_span.data()),
file_name_span.size_bytes());
if (file_name_view.compare(kUserManifestTargetFileName) == 0) {
continue; // user_manifest is not applied by the backend.
}
// Try to get an IntervalReader for the current file.
stream::IntervalReader file_reader =
bundle_.GetTargetPayload(file_name_view);
if (file_reader.status().IsNotFound()) {
PW_LOG_INFO(
"Contents of file %s missing from bundle; ignoring",
pw::MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
continue;
}
if (!file_reader.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Could not open contents of file %s from bundle; "
"aborting update apply phase",
MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
return;
}
const size_t bundle_offset = file_reader.start();
if (const Status status = backend_.ApplyTargetFile(
file_name_view, file_reader, bundle_offset);
!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to apply target file: %d",
static_cast<int>(status.code()));
return;
}
target_file_bytes_applied += file_reader.interval_size();
const uint32_t progress_hundreth_percent =
(static_cast<uint64_t>(target_file_bytes_applied) * 100 * 100) /
target_file_bytes_to_apply;
PW_LOG_DEBUG("Apply progress: %zu/%zu Bytes (%ld%%)",
target_file_bytes_applied,
target_file_bytes_to_apply,
static_cast<unsigned long>(progress_hundreth_percent / 100));
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->current_state_progress_hundreth_percent =
progress_hundreth_percent;
borrowed_status->has_current_state_progress_hundreth_percent = true;
}
}
// TODO(davidrogers): Add new APPLY_REBOOTING to distinguish between pre and
// post reboot.
// Finalize the apply.
if (const Status status = backend_.ApplyReboot(); !status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to do the apply reboot: %d",
static_cast<int>(status.code()));
return;
}
// TODO(davidrogers): Move this to MaybeFinishApply() once available.
Finish(pw_software_update_BundledUpdateResult_Enum_SUCCESS);
}
Status BundledUpdateService::Abort(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
return Status::FailedPrecondition();
}
if (state == pw_software_update_BundledUpdateState_Enum_INACTIVE ||
state == pw_software_update_BundledUpdateState_Enum_FINISHED) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Tried to abort when already INACTIVE or FINISHED");
return Status::FailedPrecondition();
}
// TODO: Switch abort to async; this state change isn't externally visible.
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_ABORTING;
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_ABORTED,
"Update abort requested");
response = *status_.acquire();
return OkStatus();
}
Status BundledUpdateService::Reset(const pw_protobuf_Empty&,
BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state == pw_software_update_BundledUpdateState_Enum_INACTIVE) {
return OkStatus(); // Already done.
}
if (state != pw_software_update_BundledUpdateState_Enum_FINISHED) {
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"Reset() must be called from FINISHED or INACTIVE state. State: %d",
static_cast<int>(state));
response = *status_.acquire();
return Status::FailedPrecondition();
}
{
*status_.acquire() = {
.state = pw_software_update_BundledUpdateState_Enum_INACTIVE};
}
// Reset the bundle.
if (bundle_open_) {
// TODO: Revisit whether this is recoverable; maybe eliminate CHECK.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
response = *status_.acquire();
return OkStatus();
}
void BundledUpdateService::NotifyTransferSucceeded() {
std::lock_guard lock(mutex_);
const BundledUpdateState state = status_.acquire()->state;
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING) {
// This can happen if the update gets Abort()'d during the transfer and
// the transfer completes successfully.
PW_LOG_WARN(
"Got transfer succeeded notification when not in TRANSFERRING state. "
"State: %d",
static_cast<int>(state));
}
const bool transfer_ongoing = status_.acquire()->has_transfer_id;
if (transfer_ongoing) {
backend_.DisableBundleTransferHandler();
status_.acquire()->has_transfer_id = false;
} else {
PW_LOG_WARN("No ongoing transfer found, forcefully set TRANSFERRED.");
}
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_TRANSFERRED;
}
void BundledUpdateService::Finish(
pw_software_update_BundledUpdateResult_Enum result) {
if (result == pw_software_update_BundledUpdateResult_Enum_SUCCESS) {
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->current_state_progress_hundreth_percent = 0;
borrowed_status->has_current_state_progress_hundreth_percent = false;
} else {
// In the case of error, notify backend that we're about to abort the
// software update.
PW_CHECK_OK(backend_.BeforeUpdateAbort());
}
// Turn down the transfer if one is in progress.
const bool transfer_ongoing = status_.acquire()->has_transfer_id;
if (transfer_ongoing) {
backend_.DisableBundleTransferHandler();
}
status_.acquire()->has_transfer_id = false;
// Close out any open bundles.
if (bundle_open_) {
// TODO: Revisit this check; may be able to recover.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
{
BorrowedStatus borrowed_status = status_.acquire();
borrowed_status->state =
pw_software_update_BundledUpdateState_Enum_FINISHED;
borrowed_status->result = result;
borrowed_status->has_result = true;
}
}
} // namespace pw::software_update