// 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_pwpb.h" #include #include #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; // 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 = borrowed_status->note.max_size(); \ borrowed_status->note.resize(note_size); \ PW_TOKENIZE_TO_BUFFER( \ &borrowed_status->note, &(note_size), message, __VA_ARGS__); \ borrowed_status->note.resize(note_size); \ } \ } \ } while (false) } // namespace Status BundledUpdateService::GetStatus(const pw::protobuf::Empty::Message&, BundledUpdateStatus::Message& response) { response = *status_.acquire(); return OkStatus(); } Status BundledUpdateService::Start(const StartRequest::Message& request, BundledUpdateStatus::Message& response) { std::lock_guard lock(mutex_); // Check preconditions. const BundledUpdateState::Enum state = status_.acquire()->state; if (state != BundledUpdateState::Enum::kInactive) { SET_ERROR(BundledUpdateResult::Enum::kUnknownError, "Start() can only be called from INACTIVE state. " "Current state: %d. Abort() then Reset() must be called first", static_cast(state)); response = *status_.acquire(); return Status::FailedPrecondition(); } { BorrowedStatus borrowed_status = status_.acquire(); PW_DCHECK(!borrowed_status->transfer_id.has_value()); PW_DCHECK(!borrowed_status->result.has_value()); PW_DCHECK( !borrowed_status->current_state_progress_hundreth_percent.has_value()); PW_DCHECK(borrowed_status->bundle_filename.empty()); PW_DCHECK(borrowed_status->note.empty()); } // Notify the backend of pending transfer. if (const Status status = backend_.BeforeUpdateStart(); !status.ok()) { SET_ERROR(BundledUpdateResult::Enum::kUnknownError, "Backend error on BeforeUpdateStart()"); response = *status_.acquire(); return status; } // Enable bundle transfer. Result possible_transfer_id = backend_.EnableBundleTransferHandler(request.bundle_filename); if (!possible_transfer_id.ok()) { SET_ERROR(BundledUpdateResult::Enum::kTransferFailed, "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(); if (!request.bundle_filename.empty()) { borrowed_status->bundle_filename = request.bundle_filename; } borrowed_status->state = BundledUpdateState::Enum::kTransferring; response = *borrowed_status; } return OkStatus(); } Status BundledUpdateService::SetTransferred( const pw::protobuf::Empty::Message&, BundledUpdateStatus::Message& response) { const BundledUpdateState::Enum state = status_.acquire()->state; if (state != BundledUpdateState::Enum::kTransferring && state != BundledUpdateState::Enum::kInactive) { std::lock_guard lock(mutex_); SET_ERROR(BundledUpdateResult::Enum::kUnknownError, "SetTransferred() can only be called from TRANSFERRING or " "INACTIVE state. State: %d", static_cast(state)); response = *status_.acquire(); return OkStatus(); } NotifyTransferSucceeded(); response = *status_.acquire(); return OkStatus(); } // TODO(elipsitz): Check for "ABORTING" state and bail if it's set. void BundledUpdateService::DoVerify() { std::lock_guard guard(mutex_); const BundledUpdateState::Enum state = status_.acquire()->state; if (state == BundledUpdateState::Enum::kVerified) { return; // Already done! } // Ensure we're in the right state. if (state != BundledUpdateState::Enum::kTransferred) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "DoVerify() must be called from TRANSFERRED state. State: %d", static_cast(state)); return; } status_.acquire()->state = BundledUpdateState::Enum::kVerifying; // Notify backend about pending verify. if (const Status status = backend_.BeforeBundleVerify(); !status.ok()) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "Backend::BeforeBundleVerify() failed"); return; } // Do the actual verify. Status status = bundle_.OpenAndVerify(); if (!status.ok()) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "Bundle::OpenAndVerify() failed"); return; } bundle_open_ = true; // Have the backend verify the user_manifest if present. if (!backend_.VerifyManifest(bundle_.GetManifest()).ok()) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "Backend::VerifyUserManifest() failed"); return; } // Notify backend we're done verifying. status = backend_.AfterBundleVerified(); if (!status.ok()) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "Backend::AfterBundleVerified() failed"); return; } status_.acquire()->state = BundledUpdateState::Enum::kVerified; } Status BundledUpdateService::Verify(const pw::protobuf::Empty::Message&, BundledUpdateStatus::Message& response) { std::lock_guard lock(mutex_); const BundledUpdateState::Enum state = status_.acquire()->state; // Already done? Bail. if (state == BundledUpdateState::Enum::kVerified) { PW_LOG_DEBUG("Skipping verify since already verified"); return OkStatus(); } // TODO(elipsitz): Remove the transferring permitted state here ASAP. // Ensure we're in the right state. if ((state != BundledUpdateState::Enum::kTransferring) && (state != BundledUpdateState::Enum::kTransferred)) { SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed, "Verify() must be called from TRANSFERRED state. State: %d", static_cast(state)); response = *status_.acquire(); return Status::FailedPrecondition(); } // TODO(elipsitz): 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(BundledUpdateResult::Enum::kVerifyFailed, "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::Message&, BundledUpdateStatus::Message& response) { std::lock_guard lock(mutex_); const BundledUpdateState::Enum 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 == BundledUpdateState::Enum::kApplying) { PW_LOG_DEBUG("Apply is already active"); return OkStatus(); } if ((state != BundledUpdateState::Enum::kTransferred) && (state != BundledUpdateState::Enum::kVerified)) { SET_ERROR(BundledUpdateResult::Enum::kApplyFailed, "Apply() must be called from TRANSFERRED or VERIFIED state. " "State: %d", static_cast(state)); return Status::FailedPrecondition(); } // TODO(elipsitz): 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(BundledUpdateResult::Enum::kApplyFailed, "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::Enum state = status_.acquire()->state; PW_LOG_DEBUG("Attempting to apply the update"); if (state != BundledUpdateState::Enum::kVerified) { SET_ERROR(BundledUpdateResult::Enum::kApplyFailed, "Apply() must be called from VERIFIED state. State: %d", static_cast(state)); return; } status_.acquire()->state = BundledUpdateState::Enum::kApplying; if (const Status status = backend_.BeforeApply(); !status.ok()) { SET_ERROR(BundledUpdateResult::Enum::kApplyFailed, "BeforeApply() returned unsuccessful result: %d", static_cast(status.code())); return; } // In order to report apply progress, quickly scan to see how many bytes // will be applied. Result total_payload_bytes = bundle_.GetTotalPayloadSize(); PW_CHECK_OK(total_payload_bytes.status()); size_t target_file_bytes_to_apply = static_cast(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 buf = {}; protobuf::String name = file_name.AsString(static_cast( pw::software_update::TargetFile::Fields::kFileName)); PW_CHECK_OK(name.status()); const Result 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(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(file_name_view).c_str()); continue; } if (!file_reader.ok()) { SET_ERROR(BundledUpdateResult::Enum::kApplyFailed, "Could not open contents of file %s from bundle; " "aborting update apply phase", MakeString(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(BundledUpdateResult::Enum::kApplyFailed, "Failed to apply target file: %d", static_cast(status.code())); return; } target_file_bytes_applied += file_reader.interval_size(); const uint32_t progress_hundreth_percent = (static_cast(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(progress_hundreth_percent / 100)); { BorrowedStatus borrowed_status = status_.acquire(); borrowed_status->current_state_progress_hundreth_percent = progress_hundreth_percent; } } // 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(BundledUpdateResult::Enum::kApplyFailed, "Failed to do the apply reboot: %d", static_cast(status.code())); return; } // TODO(davidrogers): Move this to MaybeFinishApply() once available. Finish(BundledUpdateResult::Enum::kSuccess); } Status BundledUpdateService::Abort(const pw::protobuf::Empty::Message&, BundledUpdateStatus::Message& response) { std::lock_guard lock(mutex_); const BundledUpdateState::Enum state = status_.acquire()->state; if (state == BundledUpdateState::Enum::kApplying) { return Status::FailedPrecondition(); } if (state == BundledUpdateState::Enum::kInactive || state == BundledUpdateState::Enum::kFinished) { SET_ERROR(BundledUpdateResult::Enum::kUnknownError, "Tried to abort when already INACTIVE or FINISHED"); return Status::FailedPrecondition(); } // TODO(elipsitz): Switch abort to async; this state change isn't externally // visible. status_.acquire()->state = BundledUpdateState::Enum::kAborting; SET_ERROR(BundledUpdateResult::Enum::kAborted, "Update abort requested"); response = *status_.acquire(); return OkStatus(); } Status BundledUpdateService::Reset(const pw::protobuf::Empty::Message&, BundledUpdateStatus::Message& response) { std::lock_guard lock(mutex_); const BundledUpdateState::Enum state = status_.acquire()->state; if (state == BundledUpdateState::Enum::kInactive) { return OkStatus(); // Already done. } if (state != BundledUpdateState::Enum::kFinished) { SET_ERROR( BundledUpdateResult::Enum::kUnknownError, "Reset() must be called from FINISHED or INACTIVE state. State: %d", static_cast(state)); response = *status_.acquire(); return Status::FailedPrecondition(); } { BorrowedStatus status = status_.acquire(); *status = {}; // Force-init all fields to zero. status->state = BundledUpdateState::Enum::kInactive; } // Reset the bundle. if (bundle_open_) { // TODO(elipsitz): 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::Enum state = status_.acquire()->state; if (state != BundledUpdateState::Enum::kTransferring) { // 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(state)); } const bool transfer_ongoing = status_.acquire()->transfer_id.has_value(); if (transfer_ongoing) { backend_.DisableBundleTransferHandler(); status_.acquire()->transfer_id.reset(); } else { PW_LOG_WARN("No ongoing transfer found, forcefully set TRANSFERRED."); } status_.acquire()->state = BundledUpdateState::Enum::kTransferred; } void BundledUpdateService::Finish(BundledUpdateResult::Enum result) { if (result == BundledUpdateResult::Enum::kSuccess) { BorrowedStatus borrowed_status = status_.acquire(); borrowed_status->current_state_progress_hundreth_percent.reset(); } 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()->transfer_id.has_value(); if (transfer_ongoing) { backend_.DisableBundleTransferHandler(); } status_.acquire()->transfer_id.reset(); // Close out any open bundles. if (bundle_open_) { // TODO(elipsitz): Revisit this check; may be able to recover. PW_CHECK_OK(bundle_.Close()); bundle_open_ = false; } { BorrowedStatus borrowed_status = status_.acquire(); borrowed_status->state = BundledUpdateState::Enum::kFinished; borrowed_status->result = result; } } } // namespace pw::software_update