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!
95 lines
4.0 KiB
Ruby
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
|