vaguerent/plugins/commands/serve/util/exception_transformer.rb
Paul Hinze 1341bfe0af
Tweak Ruby->Go error handling so exit codes match
In legacy Vagrant, any exception raised that's a subclass of
Vagrant::Errors::VagrantError is considered user-facing and so causes
the error message to be printed to the console and the process to use
exit code 1. Anything outside of that causes the process to use exit
code 255. (See `bin/vagrant` for the code.)

Here we mirror that behavior by treating errors that have a
LocalizedMessage as user-facing and those without as unexpected. This
allows the basic virtualbox component to pass in vagrant-spec!
2022-04-25 12:26:49 -05:00

95 lines
4.0 KiB
Ruby

require 'google/protobuf/well_known_types'
require 'google/rpc/error_details_pb'
module VagrantPlugins
module CommandServe
module Util
# Adds exception logging to all public instance methods
module ExceptionTransformer
def self.included(klass)
# Get all the public instance methods. Need to search ancestors as well
# for modules like the Guest service which includes the CapabilityPlatform
# module
klass_public_instance_methods = klass.public_instance_methods
# Remove all generic instance methods from the list of ones to modify
logged_methods = klass_public_instance_methods - Object.public_instance_methods
logged_methods.each do |m_name|
klass.define_method(m_name) do |*args, **opts, &block|
begin
super(*args, **opts, &block)
rescue => err
# Since we are a generated wrapper method, it's common for this
# transformer to be hit multiple times in a given callstack.
# That means we need this check to avoid double-wrapping an
# error.
if err.is_a?(GRPC::BadStatus)
raise
end
# Here we build a gRPC-friendly version of the error so that it
# can be unpacked on the client side.
#
# This is using the error model introduced here:
# https://grpc.io/docs/guides/error/#richer-error-model
#
# And detailed here:
# https://cloud.google.com/apis/design/errors#error_model
#
# IMPORTANT: As mentioned in both those links, gRPC error
# details are returned in headers, and total headers are
# limited to 8KB. That means we need to be careful not to go
# over that limit in our messages here (which is easy to do
# when big backtraces are involved).
#
# If we go over that limit, we'll get opaque "RST_STREAM with
# error code 2" messages from clients, as discussed here:
# https://github.com/grpc/grpc-go/issues/4265
#
# Therefore, here we truncate both message and backtrace to
# 1024 characters. The message is used in three places and the
# backtrace in one, so this should hopfully keep the total
# headers below the limit in most cases.
message = ExceptionTransformer.truncate_to(err.message, 1024)
backtrace = ExceptionTransformer.truncate_to(err.backtrace.join("\n"), 1024)
metadata = {}
# VagrantErrors are user-facing and so get their message packed
# into the details.
if err.is_a? Vagrant::Errors::VagrantError
localized_msg_details_any = Google::Protobuf::Any.new
localized_msg_details_any.pack(
Google::Rpc::LocalizedMessage.new(locale: "en-US", message: message)
)
proto = Google::Rpc::Status.new(
code: GRPC::Core::StatusCodes::UNKNOWN,
details: [localized_msg_details_any]
)
metadata[GRPC_DETAILS_METADATA_KEY] = Google::Rpc::Status.encode(proto)
end
grpc_error = GRPC::BadStatus.new(
GRPC::Core::StatusCodes::UNKNOWN,
"#{message}\n#{backtrace}",
metadata,
)
raise grpc_error
end
end
end
end
# Truncates a string to a given length if necessary, appending an
# ellipsis if it does
def self.truncate_to(str, len)
if str.length <= len
str
else
str[0, len-3] + "..."
end
end
GRPC_DETAILS_METADATA_KEY = "grpc-status-details-bin".freeze
end
end
end
end