| // Copyright 2023 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "mod.h" | |
| #include <fidl/fuchsia.fdomain/cpp/fidl.h> | |
| #include <fidl/fuchsia.fdomain/cpp/natural_ostream.h> | |
| #include <lib/stdcompat/span.h> | |
| #include <sstream> | |
| #include <fuchsia_controller_abi/utils.h> | |
| #include "error.h" | |
| // Defined in fuchsia_controller_py.cc | |
| extern struct PyModuleDef fuchsia_controller_internal; | |
| namespace fc = fuchsia_controller; | |
| namespace { | |
| // The purpose of this wrapper type is to allow for more descriptive error messages for zx_status_t | |
| // types. Currently the formatter cannot differentiate between this type and a regular integer, so | |
| // things like ZX_ERR_PEER_CLOSED would print as "-24" without this wrapper. So far the surface area | |
| // that needs to be handled is relatively small. Only the `TargetError` variant of | |
| // `fuchsia.fdomain.Error` needs to be handled, but in order to ensure all formatting works | |
| // correctly, we also need to wrap other types in this formatter, deconstruct them, and wrap their | |
| // inner errors when encountered. | |
| // | |
| // It does have a decent amount of boilerplate, but that's the price we're currently paying to have | |
| // more clarity around error messages. | |
| template <typename T> | |
| struct DescriptiveFormatter { | |
| const T &error; | |
| explicit DescriptiveFormatter(const T &err) : error(err) {} | |
| }; | |
| } // namespace | |
| namespace fidl::ostream { | |
| template <> | |
| struct Formatter<DescriptiveFormatter<fuchsia_fdomain::Error>> { | |
| static std::ostream &Format(std::ostream &os, | |
| const DescriptiveFormatter<fuchsia_fdomain::Error> &f) { | |
| auto err = f.error; | |
| switch (err.Which()) { | |
| case fuchsia_fdomain::Error::Tag::kTargetError: | |
| os << "fuchsia_fdomain::Error::target_error(" | |
| << error::zx_status_get_string(err.target_error().value()) << ")"; | |
| break; | |
| case fuchsia_fdomain::Error::Tag::kBadHandleId: | |
| case fuchsia_fdomain::Error::Tag::kNewHandleIdOutOfRange: | |
| case fuchsia_fdomain::Error::Tag::kNewHandleIdReused: | |
| case fuchsia_fdomain::Error::Tag::kWrongHandleType: | |
| case fuchsia_fdomain::Error::Tag::kStreamingReadInProgress: | |
| case fuchsia_fdomain::Error::Tag::kNoReadInProgress: | |
| case fuchsia_fdomain::Error::Tag::kWroteToSelf: | |
| case fuchsia_fdomain::Error::Tag::kClosedDuringRead: | |
| case fuchsia_fdomain::Error::Tag::kSignalsUnknown: | |
| case fuchsia_fdomain::Error::Tag::kRightsUnknown: | |
| case fuchsia_fdomain::Error::Tag::kSocketDispositionUnknown: | |
| case fuchsia_fdomain::Error::Tag::kSocketTypeUnknown: | |
| os << fidl::ostream::Formatted(err); | |
| break; | |
| default: | |
| os << "unhandled variant. This is a bug: " << fidl::ostream::Formatted(err); | |
| break; | |
| } | |
| return os; | |
| } | |
| }; | |
| template <> | |
| struct Formatter<DescriptiveFormatter<fuchsia_fdomain::WriteChannelError>> { | |
| static std::ostream &Format(std::ostream &os, | |
| const DescriptiveFormatter<fuchsia_fdomain::WriteChannelError> &f) { | |
| auto err = f.error; | |
| os << "fuchsia_fdomain::WriteChannelError::"; | |
| switch (err.Which()) { | |
| case fuchsia_fdomain::WriteChannelError::Tag::kError: | |
| os << "error(" << fidl::ostream::Formatted(DescriptiveFormatter(err.error().value())) | |
| << ")"; | |
| break; | |
| case fuchsia_fdomain::WriteChannelError::Tag::kOpErrors: | |
| os << "op_errors(["; | |
| if (err.op_errors().has_value()) { | |
| const auto &errors = err.op_errors().value(); | |
| for (auto iter = errors.cbegin(); iter != errors.cend(); iter++) { | |
| if (iter->has_value()) { | |
| os << fidl::ostream::Formatted(DescriptiveFormatter(iter->value())); | |
| } else { | |
| os << "null"; | |
| } | |
| if (iter != errors.cend()) { | |
| os << ", "; | |
| } | |
| } | |
| } | |
| os << "])"; | |
| break; | |
| } | |
| return os; | |
| } | |
| }; | |
| template <> | |
| struct Formatter<DescriptiveFormatter<fuchsia_fdomain::WriteSocketError>> { | |
| static std::ostream &Format(std::ostream &os, | |
| const DescriptiveFormatter<fuchsia_fdomain::WriteSocketError> &f) { | |
| auto err = f.error; | |
| os << "fuchsia_fdomain::WriteSocketError { error: " | |
| << fidl::ostream::Formatted(DescriptiveFormatter(err.error())) | |
| << ", wrote: " << fidl::ostream::Formatted(err.wrote()) << " }"; | |
| return os; | |
| } | |
| }; | |
| } // namespace fidl::ostream | |
| namespace { | |
| // Takes a PyTuple and sets the decode error (if we encountered one). | |
| template <typename T, typename V> | |
| PyObject *get_decode_error(const fit::result<T, V> &decode_res) { | |
| std::ostringstream ss; | |
| ss << "Unable to decode underlying FIDL error from buffer (this is a bug). Code: " | |
| << decode_res.error_value(); | |
| auto str = ss.str(); | |
| return PyUnicode_FromStringAndSize(str.data(), static_cast<Py_ssize_t>(str.size())); | |
| } | |
| // Decodes a FIDL error from the scratch memory and turns it into a Python exception. | |
| template <typename T> | |
| PyObject *decode_wire_error_type(mod::FuchsiaControllerState *state, char *err_buf, | |
| uint64_t err_len) { | |
| fit::result decode_res = | |
| fidl::Unpersist<T>(cpp20::span(reinterpret_cast<uint8_t *>(err_buf), err_len)); | |
| if (decode_res.is_error()) { | |
| return get_decode_error(decode_res); | |
| } | |
| // For the time being there's not any existing code that handles the various | |
| // kinds of errors that this could turn into. We're going to turn it into a | |
| // somewhat readable string, and depending on use-cases we can add | |
| // easier-to-debug information later. Ideally in the future we can leverage | |
| // Python bindings and simply use some kind of `unpersist` function for that | |
| // instead. | |
| DescriptiveFormatter descriptive_formatter(decode_res.value()); | |
| auto fostream = fidl::ostream::Formatted(descriptive_formatter); | |
| std::ostringstream ss; | |
| ss << fostream; | |
| auto output = ss.str(); | |
| return PyUnicode_FromStringAndSize(output.data(), static_cast<Py_ssize_t>(output.size())); | |
| } | |
| void set_fdomain_exception(mod::FuchsiaControllerState *state, fc_status_t err, char *err_buf, | |
| uint64_t err_len) { | |
| auto tuple = fc::abi::utils::Object(PyTuple_New(2)); | |
| if (tuple == nullptr) { | |
| std::ostringstream ss; | |
| ss << "Failed to allocate Tuple in %s" << __func__; | |
| auto out = ss.str(); | |
| PyErr_SetString(PyExc_RuntimeError, out.c_str()); | |
| return; | |
| } | |
| PyTuple_SetItem(tuple.get(), 0, PyLong_FromLong(err)); | |
| PyObject *err_message = nullptr; | |
| switch (err) { | |
| case FC_ERR_SOCKET_WRITE: | |
| err_message = | |
| decode_wire_error_type<::fuchsia_fdomain::WriteSocketError>(state, err_buf, err_len); | |
| break; | |
| case FC_ERR_CHANNEL_WRITE: { | |
| err_message = | |
| decode_wire_error_type<::fuchsia_fdomain::WriteChannelError>(state, err_buf, err_len); | |
| break; | |
| } | |
| case FC_ERR_FDOMAIN: { | |
| err_message = decode_wire_error_type<::fuchsia_fdomain::Error>(state, err_buf, err_len); | |
| break; | |
| } | |
| default: | |
| std::ostringstream ss; | |
| // It's a little awkward, but in the event that the caller just sent something wrong we | |
| // should let the user know they've run into a bug. This would only happen if | |
| // "set_python_exception" was written incorrectly. | |
| ss << "Received unrecognized fc_status_t error (" << err << ") in " << __func__ | |
| << ". This is a bug"; | |
| auto out = ss.str(); | |
| PyErr_SetString(PyExc_RuntimeError, out.c_str()); | |
| break; | |
| } | |
| if (err_message != nullptr) { | |
| PyTuple_SetItem(tuple.get(), 1, err_message); | |
| PyErr_SetObject(reinterpret_cast<PyObject *>(error::FcTransportStatusType), tuple.get()); | |
| } | |
| } | |
| } // namespace | |
| namespace mod { | |
| FuchsiaControllerState *get_module_state() { | |
| auto mod = PyState_FindModule(&fuchsia_controller_internal); | |
| if (mod == nullptr) { | |
| return nullptr; | |
| } | |
| return reinterpret_cast<FuchsiaControllerState *>(PyModule_GetState(mod)); | |
| } | |
| // Returns new reference. | |
| namespace { | |
| // Convenience RAII error state handler. | |
| class LastError { | |
| public: | |
| explicit LastError(mod::FuchsiaControllerState *state) : state_(state) { | |
| ffx_get_last_error(state_->ctx, &err_buf_, &err_len_, &err_type_); | |
| } | |
| ~LastError() { | |
| if (err_buf_ != nullptr) { | |
| ffx_free_error_buffer(err_buf_, err_len_); | |
| } | |
| } | |
| char *err_buf() { return err_buf_; } | |
| uint64_t err_len() const { return err_len_; } | |
| fc_err_type_t err_type() const { return err_type_; } | |
| bool is_fidl() { return err_buf_ != nullptr && err_type_ == FC_ERR_TYPE_FIDL; } | |
| bool is_string() { return err_buf_ != nullptr && err_type_ == FC_ERR_TYPE_STRING; } | |
| fc::abi::utils::Object to_string() { | |
| if (!is_string()) { | |
| PyErr_SetString(PyExc_RuntimeError, "Internal error: expected string error from buffer"); | |
| return fc::abi::utils::Object::null(); | |
| } | |
| auto str_obj = PyUnicode_FromStringAndSize(err_buf_, static_cast<Py_ssize_t>(err_len_)); | |
| // If this is null this will return an internal error about allocating the string. | |
| if (str_obj == nullptr) { | |
| PyErr_Clear(); | |
| PyErr_SetString(PyExc_RuntimeError, | |
| "Internal error: unable to allocate error string from buffer. This is a bug"); | |
| return fc::abi::utils::Object::null(); | |
| } | |
| return fc::abi::utils::Object(str_obj); | |
| } | |
| void set_fdomain_exception(fc_status_t err) { | |
| if (!is_fidl()) { | |
| PyErr_SetString(PyExc_RuntimeError, "Internal error: expected FIDL error from buffer"); | |
| return; | |
| } | |
| ::set_fdomain_exception(state_, err, err_buf_, err_len_); | |
| } | |
| private: | |
| mod::FuchsiaControllerState *state_; | |
| char *err_buf_ = nullptr; | |
| uint64_t err_len_ = 0; | |
| fc_err_type_t err_type_ = FC_ERR_TYPE_NONE; | |
| }; | |
| } // namespace | |
| void set_python_exception(fc_status_t err) { | |
| LastError last_err(get_module_state()); | |
| switch (err) { | |
| // If for some reason this is passed, some bug has been introduced, as there | |
| // is no reason to return `FC_OK`. | |
| case FC_OK: | |
| PyErr_SetString(PyExc_RuntimeError, | |
| "FC_OK was passed to raise an exception. This is likely a bug"); | |
| break; | |
| // None of these cases contain any internal structs, so setting the error | |
| // code is sufficient. | |
| case FC_ERR_INVALID_ARGS: | |
| case FC_ERR_NOT_SUPPORTED: | |
| case FC_ERR_NOT_FOUND: | |
| case FC_ERR_BUFFER_TOO_SMALL: | |
| case FC_ERR_SHOULD_WAIT: | |
| case FC_ERR_PROTOCOL_OBJECT_TYPE_INCOMPATIBLE: | |
| case FC_ERR_PROTOCOL_RIGHTS_INCOMPATIBLE: | |
| case FC_ERR_PROTOCOL_STREAM_EVENT_INCOMPATIBLE: | |
| case FC_ERR_CONNECTION_MISMATCH: | |
| case FC_ERR_STREAMING_ABORTED: { | |
| auto exception_err = fc::abi::utils::Object(PyLong_FromLong(err)); | |
| PyErr_SetObject(reinterpret_cast<PyObject *>(error::FcTransportStatusType), | |
| exception_err.get()); | |
| break; | |
| } | |
| // These errors are only surfaced from internal failures, which means the | |
| // Rust error that created this exception is surfaced. | |
| case FC_ERR_PROTOCOL: | |
| case FC_ERR_TRANSPORT: | |
| case FC_ERR_INTERNAL: { | |
| auto str_obj = last_err.to_string(); | |
| if (str_obj == nullptr) { | |
| return; | |
| } | |
| switch (err) { | |
| case FC_ERR_TRANSPORT: | |
| case FC_ERR_PROTOCOL: { | |
| auto tuple = fc::abi::utils::Object(PyTuple_New(2)); | |
| if (tuple == nullptr) { | |
| std::ostringstream ss; | |
| ss << "Failed to allocate Tuple in %s" << __func__; | |
| auto out = ss.str(); | |
| PyErr_SetString(PyExc_RuntimeError, out.c_str()); | |
| break; | |
| } | |
| PyTuple_SetItem(tuple.get(), 0, PyLong_FromLong(err)); | |
| // Important: use `take` here because this steals the reference. | |
| // PyErr_SetObject increases the refcount so only requires `get`. | |
| PyTuple_SetItem(tuple.get(), 1, str_obj.take()); | |
| PyErr_SetObject(reinterpret_cast<PyObject *>(error::FcTransportStatusType), tuple.get()); | |
| break; | |
| } | |
| default: | |
| PyErr_SetObject(PyExc_RuntimeError, str_obj.get()); | |
| break; | |
| } | |
| break; | |
| } | |
| case FC_ERR_SOCKET_WRITE: | |
| case FC_ERR_CHANNEL_WRITE: | |
| case FC_ERR_FDOMAIN: { | |
| last_err.set_fdomain_exception(err); | |
| break; | |
| } | |
| case FC_ERR_INTERRUPTED: { | |
| PyErr_SetNone(PyExc_KeyboardInterrupt); | |
| break; | |
| } | |
| default: { | |
| std::ostringstream ss; | |
| ss << "Received unrecognized fc_status_t (" << err << ")"; | |
| auto out = ss.str(); | |
| PyErr_SetString(PyExc_RuntimeError, out.c_str()); | |
| break; | |
| } | |
| } | |
| } | |
| } // namespace mod |