blob: 1f8bcdef44c38ba3e6816da66bb021b5263458ab [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.
#include "pw_software_update/config.h"
#define PW_LOG_LEVEL PW_SOFTWARE_UPDATE_CONFIG_LOG_LEVEL
#include <mutex>
#include <string_view>
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_software_update/bundled_update_service.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/util.h"
#include "pw_sync/mutex.h"
#include "pw_tokenizer/tokenize.h"
// TODO(keir): Convert all the CHECKs in the RPC service to gracefully report
// errors.
//
// TODO: It may be worth figuring out how to make this a function to prevent
// code bloat. It's hard due to the tokenized message handling.
#define SET_ERROR(res, message, ...) \
do { \
PW_LOG_ERROR(message, __VA_ARGS__); \
if (status_.state != \
pw_software_update_BundledUpdateState_Enum_FINISHED) { \
PW_CHECK_OK(backend_.BeforeUpdateAbort()); \
if (status_.has_transfer_id) { \
backend_.DisableBundleTransferHandler(); \
} \
status_.has_transfer_id = false; \
if (bundle_open_) { \
/* TODO: Revisit this check; may be able to recover */ \
PW_CHECK_OK(bundle_.Close()); \
bundle_open_ = false; \
} \
status_.state = pw_software_update_BundledUpdateState_Enum_FINISHED; \
status_.result = res; \
status_.has_result = true; \
size_t note_size = sizeof(status_.note.bytes); \
PW_TOKENIZE_TO_BUFFER( \
status_.note.bytes, &note_size, message, __VA_ARGS__); \
status_.note.size = note_size; \
status_.has_note = true; \
} \
} while (false)
namespace pw::software_update {
namespace {
constexpr std::string_view kTopLevelTargetsName = "targets";
constexpr std::string_view kUserManifestTargetFileName = "user_manifest";
} // namespace
Status BundledUpdateService::GetStatus(
ServerContext&,
const pw_protobuf_Empty&,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
response = status_;
return OkStatus();
}
Status BundledUpdateService::Start(
ServerContext&,
const pw_software_update_StartRequest& request,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
// Check preconditions.
if (status_.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>(status_.state));
response = status_;
return Status::FailedPrecondition();
}
PW_DCHECK(!status_.has_transfer_id);
PW_DCHECK(!status_.has_result);
PW_DCHECK(status_.current_state_progress_hundreth_percent == 0);
PW_DCHECK(status_.bundle_filename[0] == '\0');
PW_DCHECK(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_;
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_;
return possible_transfer_id.status();
}
// Update state.
status_.transfer_id = possible_transfer_id.value();
status_.has_transfer_id = true;
if (request.has_bundle_filename) {
const StatusWithSize sws = string::Copy(request.bundle_filename,
status_.bundle_filename,
sizeof(status_.bundle_filename));
PW_DCHECK_OK(sws.status(),
"bundle_filename options max_sizes do not match");
status_.has_bundle_filename = true;
}
status_.state = pw_software_update_BundledUpdateState_Enum_TRANSFERRING;
response = status_;
return OkStatus();
}
// TODO: Check for "ABORTING" state and bail if it's set.
void BundledUpdateService::DoVerify() {
{
std::lock_guard guard(mutex_);
if (status_.state == pw_software_update_BundledUpdateState_Enum_VERIFIED) {
return; // Already done!
}
// Ensure we're in the right state.
if (status_.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>(status_.state));
return;
}
status_.state = pw_software_update_BundledUpdateState_Enum_VERIFYING;
}
// Notify backend about pending verify.
if (const Status status = backend_.BeforeBundleVerify(); !status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::BeforeBundleVerify() failed");
return;
}
// Do the actual verify.
ManifestAccessor manifest; // TODO(pwbug/456): Place-holder for now.
Status status = bundle_.OpenAndVerify(manifest);
{
std::lock_guard lock(mutex_);
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.
stream::IntervalReader user_manifest =
bundle_.GetTargetPayload(kUserManifestTargetFileName);
if (user_manifest.ok()) {
const size_t bundle_offset = user_manifest.start();
if (!backend_.VerifyUserManifest(user_manifest, bundle_offset).ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::VerifyUserManifest() failed");
return;
}
}
// Notify backend we're done verifying.
status = backend_.AfterBundleVerified();
{
std::lock_guard lock(mutex_);
if (!status.ok()) {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_VERIFY_FAILED,
"Backend::AfterBundleVerified() failed");
return;
}
status_.state = pw_software_update_BundledUpdateState_Enum_VERIFIED;
}
}
Status BundledUpdateService::Verify(
ServerContext&,
const pw_protobuf_Empty&,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
// Already done? Bail.
if (status_.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 ((status_.state !=
pw_software_update_BundledUpdateState_Enum_TRANSFERRING) &&
(status_.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>(status_.state));
response = status_;
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 FinalizeApply 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_;
return status;
}
work_enqueued_ = true;
response = status_;
return OkStatus();
}
Status BundledUpdateService::Apply(
ServerContext&,
const pw_protobuf_Empty&,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
// 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 (status_.state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
}
if ((status_.state !=
pw_software_update_BundledUpdateState_Enum_TRANSFERRED) &&
(status_.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>(status_.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 FinalizeApply 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_;
return status;
}
work_enqueued_ = true;
return OkStatus();
}
void BundledUpdateService::DoApply() {
{
std::lock_guard guard(mutex_);
PW_LOG_DEBUG("Attempting to apply the update");
if (status_.state != pw_software_update_BundledUpdateState_Enum_VERIFIED) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Apply() must be called from VERIFIED state. State: %d",
static_cast<int>(status_.state));
return;
}
status_.state = pw_software_update_BundledUpdateState_Enum_APPLYING;
}
protobuf::StringToMessageMap signed_targets_metadata_map =
bundle_.GetDecoder().AsStringToMessageMap(static_cast<uint32_t>(
pw::software_update::UpdateBundle::Fields::TARGETS_METADATA));
if (const Status status = signed_targets_metadata_map.status();
!status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Update bundle does not contain the targets_metadata map: %d",
static_cast<int>(status.code()));
return;
}
// There should only be one element in the map, which is the top-level
// targets metadata.
protobuf::Message signed_targets_metadata =
signed_targets_metadata_map[kTopLevelTargetsName];
if (const Status status = signed_targets_metadata.status(); !status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"The targets_metadata map does not contain the targets entry: %d",
static_cast<int>(status.code()));
return;
}
protobuf::Message targets_metadata = signed_targets_metadata.AsMessage(
static_cast<uint32_t>(pw::software_update::SignedTargetsMetadata::Fields::
SERIALIZED_TARGETS_METADATA));
if (const Status status = targets_metadata.status(); !status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"The targets targets_metadata entry does not contain the "
"serialized_target_metadata: %d",
static_cast<int>(status.code()));
return;
}
protobuf::RepeatedMessages target_files =
targets_metadata.AsRepeatedMessages(static_cast<uint32_t>(
pw::software_update::TargetsMetadata::Fields::TARGET_FILES));
if (const Status status = target_files.status(); !status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"The serialized_target_metadata does not contain target_files: %d",
static_cast<int>(status.code()));
return;
}
// In order to report apply progress, quickly scan to see how many bytes will
// be applied.
size_t target_file_bytes_to_apply = 0;
protobuf::StringToBytesMap target_payloads =
bundle_.GetDecoder().AsStringToBytesMap(static_cast<uint32_t>(
pw::software_update::UpdateBundle::Fields::TARGET_PAYLOADS));
if (!target_payloads.status().ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to iterate the UpdateBundle target_payloads map entries: %d",
static_cast<int>(target_payloads.status().code()));
return;
}
for (pw::protobuf::StringToBytesMapEntry target_payload : target_payloads) {
protobuf::Bytes target_payload_bytes = target_payload.Value();
if (!target_payload_bytes.status().ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to read a UpdateBundle target_payloads map entry: %d",
static_cast<int>(target_payload_bytes.status().code()));
return;
}
target_file_bytes_to_apply +=
target_payload_bytes.GetBytesReader().ConservativeReadLimit();
}
size_t target_file_bytes_applied = 0;
for (pw::protobuf::Message file_name : target_files) {
// TODO: Use a config.h parameter for this.
constexpr size_t kFileNameMaxSize = 32;
std::array<std::byte, kFileNameMaxSize> buf = {};
protobuf::String name = file_name.AsString(static_cast<uint32_t>(
pw::software_update::TargetFile::Fields::FILE_NAME));
if (!name.status().ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"The serialized_target_metadata failed to iterate target files: %d",
static_cast<int>(name.status().code()));
return;
}
const Result<ByteSpan> read_result = name.GetBytesReader().Read(buf);
if (!read_result.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(
pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"The serialized_target_metadata failed to read target filename: %d",
static_cast<int>(read_result.status().code()));
return;
}
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.
}
stream::IntervalReader file_reader =
bundle_.GetTargetPayload(file_name_view);
const size_t bundle_offset = file_reader.start();
if (const Status status = backend_.ApplyTargetFile(
file_name_view, file_reader, bundle_offset);
!status.ok()) {
std::lock_guard lock(mutex_);
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: %d/%d Bytes (%ld%%)",
target_file_bytes_applied,
target_file_bytes_to_apply,
progress_hundreth_percent / 100);
{
std::lock_guard lock(mutex_);
status_.current_state_progress_hundreth_percent =
progress_hundreth_percent;
status_.has_current_state_progress_hundreth_percent = true;
}
}
// Finalize the apply.
//
// TODO(davidrogers): Ensure the backend documentation and API contract is
// clear in regards to the flushing expectations for RPCs and logs surrounding
// the reboot inside of this call.
if (const Status status = backend_.FinalizeApply(); !status.ok()) {
std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Failed to apply target file: %d",
static_cast<int>(status.code()));
return;
}
{
std::lock_guard lock(mutex_);
status_.current_state_progress_hundreth_percent = 0;
status_.has_current_state_progress_hundreth_percent = false;
status_.state = pw_software_update_BundledUpdateState_Enum_FINISHED;
status_.result = pw_software_update_BundledUpdateResult_Enum_SUCCESS;
}
}
Status BundledUpdateService::Abort(
ServerContext&,
const pw_protobuf_Empty&,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
if (status_.state == pw_software_update_BundledUpdateState_Enum_APPLYING) {
return Status::FailedPrecondition();
}
if (status_.state == pw_software_update_BundledUpdateState_Enum_INACTIVE ||
status_.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_.state = pw_software_update_BundledUpdateState_Enum_ABORTING;
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_ABORTED,
"Update abort requested");
response = status_;
return OkStatus();
}
Status BundledUpdateService::Reset(
ServerContext&,
const pw_protobuf_Empty&,
pw_software_update_BundledUpdateStatus& response) {
std::lock_guard lock(mutex_);
if (status_.state == pw_software_update_BundledUpdateState_Enum_INACTIVE) {
return OkStatus(); // Already done.
}
if (status_.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>(status_.state));
response = status_;
return Status::FailedPrecondition();
}
status_ = {};
status_.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_;
return OkStatus();
}
void BundledUpdateService::NotifyTransferSucceeded() {
std::lock_guard lock(mutex_);
if (status_.state !=
pw_software_update_BundledUpdateState_Enum_TRANSFERRING) {
// This can happen if the update gets Abort()'d during the transfer and
// the transfer completes successfuly.
PW_LOG_WARN(
"Got transfer succeeded notification when not in TRANSFERRING state. "
"State: %d",
static_cast<int>(status_.state));
return;
}
PW_DCHECK(status_.has_transfer_id);
backend_.DisableBundleTransferHandler();
status_.has_transfer_id = false;
status_.state = pw_software_update_BundledUpdateState_Enum_TRANSFERRED;
}
} // namespace pw::software_update