def HandleError(exc, command_path, known_error_handler=None): """Handles an error that occurs during command execution. It calls ConvertKnownError to convert exceptions to known types before processing. If it is a known type, it is printed nicely as as error. If not, it is raised as a crash. Args: exc: Exception, The original exception that occurred. command_path: str, The name of the command that failed (for error reporting). known_error_handler: f(exc): A function to process known errors. """ known_exc, print_error = ConvertKnownError(exc) if known_exc: msg = u'({0}) {1}'.format( console_attr.EncodeForConsole(command_path), console_attr.EncodeForConsole(known_exc)) log.debug(msg, exc_info=sys.exc_info()) if print_error: log.error(msg) # Uncaught errors will be handled in gcloud_main. if known_error_handler: known_error_handler(exc) if properties.VALUES.core.print_handled_tracebacks.GetBool(): raise _Exit(known_exc) else: # Make sure any uncaught exceptions still make it into the log file. log.debug(console_attr.EncodeForConsole(exc), exc_info=sys.exc_info()) raise
def Parse(value): if fn(value): return value encoded_value = console_attr.EncodeForConsole(value) formatted_err = u'Bad value [{0}]: {1}'.format(encoded_value, description) raise ArgumentTypeError(formatted_err)
def _ReportCrash(err): """Report the anonymous crash information to the Error Reporting service. Args: err: Exception, the error that caused the crash. """ stacktrace = traceback.format_exc(err) stacktrace = error_reporting_util.RemovePrivateInformationFromTraceback( stacktrace) command = properties.VALUES.metrics.command_name.Get() cid = metrics.GetCIDIfMetricsEnabled() client = _GetReportingClient() reporter = util.ErrorReporting(client) try: reporter.ReportEvent(error_message=stacktrace, service=CRASH_SERVICE, version=config.CLOUD_SDK_VERSION, project=CRASH_PROJECT, request_url=command, user=cid) except apitools_exceptions.HttpError as http_err: log.file_only_logger.error( 'Unable to report crash stacktrace:\n{0}'.format( console_attr.EncodeForConsole(http_err)))
def error(self, message): """Overrides argparse.ArgumentParser's .error(message) method. Specifically, it avoids reprinting the program name and the string "error:". Args: message: str, The error message to print. """ _, error, _ = sys.exc_info() parser = self if isinstance(error, parser_errors.ArgumentError): # This associates the error with the correct parser. parser = error.parser or self parser.ReportErrorMetrics(error, message) # No need to output help/usage text if we are in completion mode. However, # we do need to populate group/command level choices. These choices are not # loaded when there is a parser error since we do lazy loading. if '_ARGCOMPLETE' in os.environ: # pylint:disable=protected-access if self._calliope_command._sub_parser: self._calliope_command.LoadAllSubElements() else: message = console_attr.EncodeForConsole(message) log.error(u'({prog}) {message}'.format(prog=self.prog, message=message)) # multi-line message means hints already added, no need for usage. # pylint:disable=protected-access if '\n' not in message: argparse._sys.stderr.write(self._calliope_command.GetUsage()) self.exit(2)
def _HandleAllErrors(self, exc, command_path_string, specified_arg_names): """Handle all errors. Args: exc: Exception, The exception that was raised. command_path_string: str, The '.' separated command path. specified_arg_names: [str], The specified arg named scrubbed for metrics. Raises: exc or a core.exceptions variant that does not produce a stack trace. """ if session_capturer.SessionCapturer.capturer: session_capturer.SessionCapturer.capturer.CaptureException(exc) known_exc, print_error = exceptions.ConvertKnownError(exc) error_extra_info = {'error_code': getattr(exc, 'exit_code', 1)} if isinstance(exc, exceptions.HttpException): error_extra_info['http_status_code'] = exc.payload.status_code metrics.Commands(command_path_string, config.CLOUD_SDK_VERSION, specified_arg_names, error=exc.__class__, error_extra_info=error_extra_info) metrics.Error(command_path_string, exc.__class__, specified_arg_names, error_extra_info=error_extra_info) if known_exc: msg = u'({0}) {1}'.format( console_attr.EncodeForConsole(command_path_string), console_attr.EncodeForConsole(known_exc)) log.debug(msg, exc_info=sys.exc_info()) if print_error: log.error(msg) # Uncaught errors will be handled in gcloud_main. if self.__known_error_handler: self.__known_error_handler(exc) if properties.VALUES.core.print_handled_tracebacks.GetBool(): raise self._Exit(known_exc) else: # Make sure any uncaught exceptions still make it into the log file. log.debug(console_attr.EncodeForConsole(exc), exc_info=sys.exc_info()) raise
def error(self, message=None, context=None): """Overrides argparse.ArgumentParser's .error(message) method. Specifically, it avoids reprinting the program name and the string "error:". Args: message: str, The error message to print. context: _ErrorContext, A previous intercepted error context to reproduce. """ if context: # Reproduce a previous call to this method from the info in context. message = context.message parser = context.parser error = context.error if error: # argparse calls this method as the result of an exception that can be # checked in sys.exc_info()[1]. A side effect of this try-except is to # set sys.exc_info()[1] to context.error from the original call that was # saved below in self._error_context. This value might be checked later # in the execution (the test harness in particular checks it). try: raise error # pylint: disable=raising-bad-type except type(error): pass else: error = sys.exc_info()[1] else: error = sys.exc_info()[1] parser = self if '_ARGCOMPLETE' not in os.environ and ( self._probe_error or 'too few arguments' in message or 'Invalid choice' in message): # Save this context for later. We may be able to decuce a better error # message. For instance, argparse might complain about an invalid # command choice 'flag-value' for '--unknown-flag flag-value', but # with a little finagling in parse_known_args() we can verify that # '--unknown-flag' is in fact an unknwon flag and error out on that. self._error_context = _ErrorContext(message, parser, error) return parser.ReportErrorMetrics(error, message) # No need to output help/usage text if we are in completion mode. However, # we do need to populate group/command level choices. These choices are not # loaded when there is a parser error since we do lazy loading. if '_ARGCOMPLETE' in os.environ: # pylint:disable=protected-access if self._calliope_command._sub_parser: self._calliope_command.LoadAllSubElements() else: message = console_attr.EncodeForConsole(message) log.error(u'({prog}) {message}'.format(prog=self.prog, message=message)) # multi-line message means hints already added, no need for usage. # pylint:disable=protected-access if '\n' not in message: argparse._sys.stderr.write(self._calliope_command.GetUsage()) self.exit(2)
def __init__(self, args, invalid_arg): self.invalid_arg = invalid_arg cmd = os.path.basename(args[0]) if cmd.endswith('.py'): cmd = cmd[:-3] args = [cmd] + args[1:] super(InvalidCharacterInArgException, self).__init__( u'Failed to read command line argument [{0}] because it does ' u'not appear to be valid 7-bit ASCII.\n\n' u'{1}'.format(console_attr.EncodeForConsole(self.invalid_arg), _FormatNonAsciiMarkerString(args)))
def Parse(value): """Validates and returns a custom object from an argument string value.""" try: parsed_value = parser(value) if parser else value except ArgumentTypeError: pass else: if fn(parsed_value): return parsed_value encoded_value = console_attr.EncodeForConsole(value) formatted_err = u'Bad value [{0}]: {1}'.format(encoded_value, description) raise ArgumentTypeError(formatted_err)
def Print(self, *msg): """Writes the given message to the output stream, and adds a newline. This method has the same output behavior as the builtin print method but respects the configured verbosity. Args: *msg: str, The messages to print. """ from googlecloudsdk.core.console import console_attr # pylint: disable=g-import-not-at-top, avoid import loop msg = (console_attr.EncodeForConsole(x, escape=False) for x in msg) message = u' '.join(msg) self.write(message + u'\n')
def HandleGcloudCrash(err): """Checks if installation error occurred, then proceeds with Error Reporting. Args: err: Exception err. """ err_string = console_attr.EncodeForConsole(err) log.file_only_logger.exception('BEGIN CRASH STACKTRACE') if _IsInstallationCorruption(err): _PrintInstallationAction(err, err_string) else: log.error(u'gcloud crashed ({0}): {1}'.format( getattr(err, 'error_name', type(err).__name__), err_string)) ReportError(err, is_crash=True) log.err.Print( '\nIf you would like to report this issue, please run the ' 'following command:') log.err.Print(' gcloud feedback') log.err.Print('\nTo check gcloud for common problems, please run the ' 'following command:') log.err.Print(' gcloud info --run-diagnostics')
def ReportMetrics(self, wait_for_report=False): """Reports the collected metrics using a separate async process.""" if not self._metrics: return temp_metrics_file = tempfile.NamedTemporaryFile(delete=False) with temp_metrics_file: pickle.dump(self._metrics, temp_metrics_file) self._metrics = [] this_file = encoding.Decode(__file__) reporting_script_path = os.path.realpath( os.path.join(os.path.dirname(this_file), 'metrics_reporter.py')) execution_args = execution_utils.ArgsForPythonTool( reporting_script_path, temp_metrics_file.name) # popen is silly when it comes to non-ascii args. The executable has to be # _unencoded_, while the rest of the args have to be _encoded_. execution_args = execution_args[0:1] + [ console_attr.EncodeForConsole(a) for a in execution_args[1:] ] exec_env = os.environ.copy() exec_env['PYTHONPATH'] = os.pathsep.join(sys.path) try: p = subprocess.Popen(execution_args, env=exec_env, **self._async_popen_args) log.debug('Metrics reporting process started...') except OSError: # This can happen specifically if the Python executable moves between the # start of this process and now. log.debug('Metrics reporting process failed to start.') if wait_for_report: # NOTE: p.wait() can cause a deadlock. p.communicate() is recommended. # See python docs for more information. p.communicate() log.debug('Metrics reporting process finished.')
def ReportError(err, is_crash): """Report the anonymous crash information to the Error Reporting service. Args: err: Exception, the error that caused the crash. is_crash: bool, True if this is a crash, False if it is a user error. """ if properties.VALUES.core.disable_usage_reporting.GetBool(): return stacktrace = traceback.format_exc(err) stacktrace = error_reporting_util.RemovePrivateInformationFromTraceback( stacktrace) command = properties.VALUES.metrics.command_name.Get() cid = metrics.GetCIDIfMetricsEnabled() client = _GetReportingClient() reporter = util.ErrorReporting(client) try: method_config = client.projects_events.GetMethodConfig('Report') request = reporter.GenerateReportRequest( error_message=stacktrace, service=CRASH_SERVICE if is_crash else ERROR_SERVICE, version=config.CLOUD_SDK_VERSION, project=CRASH_PROJECT, request_url=command, user=cid) http_request = client.projects_events.PrepareHttpRequest( method_config, request) metrics.CustomBeacon(http_request.url, http_request.http_method, http_request.body, http_request.headers) except apitools_exceptions.Error as e: log.file_only_logger.error( 'Unable to report crash stacktrace:\n{0}'.format( console_attr.EncodeForConsole(e)))
def __init__(self, attr, string): self._string = console_attr.EncodeForConsole( string, encoding=attr.GetEncoding(), escape=False) self._adjust = attr.DisplayWidth(self._string) - len(self._string)
def _FormatNonAsciiMarkerString(args): r"""Format a string that will mark the first non-ASCII character it contains. Example: >>> args = ['command.py', '--foo=\xce\x94'] >>> _FormatNonAsciiMarkerString(args) == ( ... 'command.py --foo=\u0394\n' ... ' ^ invalid character' ... ) True Args: args: The arg list for the command executed Returns: unicode, a properly formatted string with two lines, the second of which indicates the non-ASCII character in the first. Raises: ValueError: if the given string is all ASCII characters """ # nonascii will be True if at least one arg contained a non-ASCII character nonascii = False # pos is the position of the first non-ASCII character in ' '.join(args) pos = 0 for arg in args: try: # idx is the index of the first non-ASCII character in arg for idx, char in enumerate(arg): char.decode('ascii') except UnicodeError: # idx will remain set, indicating the first non-ASCII character pos += idx nonascii = True break # this arg was all ASCII; add 1 for the ' ' between args pos += len(arg) + 1 if not nonascii: raise ValueError('The command line is composed entirely of ASCII ' 'characters.') # Make a string that, when printed in parallel, will point to the non-ASCII # character marker_string = ' ' * pos + _MARKER # Make sure that this will still print out nicely on an odd-sized screen align = len(marker_string) args_string = u' '.join( [console_attr.EncodeForConsole(arg) for arg in args]) width, _ = console_attr_os.GetTermSize() fill = '...' if width < len(_MARKER) + len(fill): # It's hopeless to try to wrap this and make it look nice. Preserve it in # full for logs and so on. return '\n'.join((args_string, marker_string)) # If len(args_string) < width < len(marker_string) (ex:) # # args_string = 'command BAD' # marker_string = ' ^ invalid character' # width = len('----------------') # # then the truncation can give a result like the following: # # args_string = 'command BAD' # marker_string = ' ^ invalid character' # # (This occurs when args_string is short enough to not be truncated, but # marker_string is long enough to be truncated.) # # ljust args_string to make it as long as marker_string before passing to # _TruncateToLineWidth, which will yield compatible truncations. rstrip at the # end to get rid of the new trailing spaces. formatted_args_string = _TruncateToLineWidth(args_string.ljust(align), align, width, fill=fill).rstrip() formatted_marker_string = _TruncateToLineWidth(marker_string, align, width) return u'\n'.join((formatted_args_string, formatted_marker_string))
def Exec(args, env=None, no_exit=False, out_func=None, err_func=None, in_str=None, **extra_popen_kwargs): """Emulates the os.exec* set of commands, but uses subprocess. This executes the given command, waits for it to finish, and then exits this process with the exit code of the child process. Args: args: [str], The arguments to execute. The first argument is the command. env: {str: str}, An optional environment for the child process. no_exit: bool, True to just return the exit code of the child instead of exiting. out_func: str->None, a function to call with the stdout of the executed process. This can be e.g. log.file_only_logger.debug or log.out.write. err_func: str->None, a function to call with the stderr of the executed process. This can be e.g. log.file_only_logger.debug or log.err.write. in_str: str, input to send to the subprocess' stdin. **extra_popen_kwargs: Any additional kwargs will be passed through directly to subprocess.Popen Returns: int, The exit code of the child if no_exit is True, else this method does not return. Raises: PermissionError: if user does not have execute permission for cloud sdk bin files. InvalidCommandError: if the command entered cannot be found. """ log.debug('Executing command: %s', args) # We use subprocess instead of execv because windows does not support process # replacement. The result of execv on windows is that a new processes is # started and the original is killed. When running in a shell, the prompt # returns as soon as the parent is killed even though the child is still # running. subprocess waits for the new process to finish before returning. env = _GetToolEnv(env=env) process_holder = _ProcessHolder() with _ReplaceSignal(signal.SIGTERM, process_holder.Handler): with _ReplaceSignal(signal.SIGINT, process_holder.Handler): if out_func: extra_popen_kwargs['stdout'] = subprocess.PIPE if err_func: extra_popen_kwargs['stderr'] = subprocess.PIPE if in_str: extra_popen_kwargs['stdin'] = subprocess.PIPE try: # popen is silly when it comes to non-ascii args. The executable has to # be _unencoded_, while the rest of the args have to be _encoded_. if args and isinstance(args, list): args = args[0:1] + [ console_attr.EncodeForConsole(a) for a in args[1:]] p = subprocess.Popen(args, env=env, **extra_popen_kwargs) except OSError as err: if err.errno == errno.EACCES: raise PermissionError(err.strerror) elif err.errno == errno.ENOENT: raise InvalidCommandError(args[0]) raise process_holder.process = p stdout, stderr = p.communicate(input=in_str) if out_func: out_func(stdout) if err_func: err_func(stderr) ret_val = p.returncode if no_exit: return ret_val sys.exit(ret_val)