// Copyright 2012 Google Inc. All Rights Reserved. // // 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 // // http://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. package com.google.collide.client; import com.google.collide.client.communication.FrontendApi; import com.google.collide.client.communication.FrontendApi.ApiCallback; import com.google.collide.client.communication.MessageFilter; import com.google.collide.client.communication.MessageFilter.MessageRecipient; import com.google.collide.client.history.HistoryUtils; import com.google.collide.client.history.HistoryUtils.SetHistoryListener; import com.google.collide.client.history.HistoryUtils.ValueChangeListener; import com.google.collide.client.status.StatusManager; import com.google.collide.client.status.StatusMessage; import com.google.collide.client.status.StatusMessage.MessageType; import com.google.collide.client.util.ExceptionUtils; import com.google.collide.client.util.logging.Log; import com.google.collide.dto.LogFatalRecordResponse; import com.google.collide.dto.RoutingTypes; import com.google.collide.dto.ServerError; import com.google.collide.dto.ServerError.FailureReason; import com.google.collide.dto.StackTraceElementDto; import com.google.collide.dto.ThrowableDto; import com.google.collide.dto.client.DtoClientImpls.LogFatalRecordImpl; import com.google.collide.dto.client.DtoClientImpls.StackTraceElementDtoImpl; import com.google.collide.dto.client.DtoClientImpls.ThrowableDtoImpl; import com.google.collide.json.client.JsoArray; import com.google.collide.shared.util.StringUtils; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT.UncaughtExceptionHandler; import elemental.client.Browser; /** * The global {@link UncaughtExceptionHandler} for Collide. In addition to * catching uncaught client exceptions, this handler is also responsible for * notifying the user of server to client errors. */ public class ExceptionHandler implements UncaughtExceptionHandler { private static final String FATAL_MESSAGE = "Hoist with our own petard. Something broke! We promise that if you reload Collide" + " all will be set right :)."; /** * Record recent history change events (up to some finite maximum) to assist * failure forensics. * */ private static class HistoryListener implements SetHistoryListener, ValueChangeListener { private static final int MAX_HISTORY_ENTRIES = 10; private final JsoArray<String> historyBuffer = JsoArray.create(); private void addHistoryString(String historyString) { historyBuffer.add(historyString); if (historyBuffer.size() > MAX_HISTORY_ENTRIES) { historyBuffer.remove(0); } } @Override public void onHistorySet(String historyString) { addHistoryString(historyString); } @Override public void onValueChanged(String historyString) { addHistoryString(historyString); } /** * Retrieve any stored history entries in descending chronological order * (most recent first). */ public JsoArray<String> getRecentHistory() { JsoArray<String> ret = historyBuffer.copy(); ret.reverse(); return ret; } } private final HistoryListener historyListener; private final MessageRecipient<ServerError> serverErrorReceiver = new MessageRecipient<ServerError>() { private StatusMessage serverError = null; @Override public void onMessageReceived(ServerError message) { Log.error(getClass(), "Server Error #" + message.getFailureReason() + ": " + message.getDetails()); if (serverError != null) { serverError.cancel(); } // Authorization errors are handled within the app. if (message.getFailureReason() != FailureReason.UNAUTHORIZED) { serverError = new StatusMessage(statusManager, MessageType.ERROR, "The server encountered an error (#" + message.getFailureReason() + ")"); serverError.setDismissable(true); serverError.addAction(StatusMessage.RELOAD_ACTION); serverError.fire(); } } }; private final FrontendApi frontendApi; private final StatusManager statusManager; public ExceptionHandler( MessageFilter messageFilter, FrontendApi frontendApi, StatusManager statusManager) { this.frontendApi = frontendApi; this.statusManager = statusManager; messageFilter.registerMessageRecipient(RoutingTypes.SERVERERROR, serverErrorReceiver); this.historyListener = new HistoryListener(); HistoryUtils.addSetHistoryListener(historyListener); HistoryUtils.addValueChangeListener(historyListener); } @Override public void onUncaughtException(Throwable e) { Log.error(getClass(), e.toString(), e); final Throwable exception = e; final JsoArray<String> recentHistory = historyListener.getRecentHistory(); final String currentWindowLocation = Browser.getWindow().getLocation().getHref(); ThrowableDtoImpl throwableDto = getThrowableAsDto(e); LogFatalRecordImpl logRecord = LogFatalRecordImpl .make() .setMessage("Client exception at: " + Browser.getWindow().getLocation().getHref()) .setThrowable(throwableDto) .setRecentHistory(recentHistory) .setPermutationStrongName(GWT.getPermutationStrongName()); frontendApi.LOG_REMOTE.send(logRecord, new ApiCallback<LogFatalRecordResponse>() { @Override public void onMessageReceived(final LogFatalRecordResponse message) { StatusMessage msg = new StatusMessage(statusManager, MessageType.FATAL, FATAL_MESSAGE); msg.addAction(StatusMessage.FEEDBACK_ACTION); msg.addAction(StatusMessage.RELOAD_ACTION); String stackTrace; if (!StringUtils.isNullOrEmpty(message.getStackTrace())) { stackTrace = message.getStackTrace(); } else { stackTrace = ExceptionUtils.getStackTraceAsString(exception); } msg.setLongText(calculateLongText(stackTrace)); msg.fire(); } private String calculateLongText(String stackTrace) { return "Client exception at " + currentWindowLocation + "\n\nRecent history:\n\t" + recentHistory.join("\n\t") + "\n\n" + stackTrace; } @Override public void onFail(FailureReason reason) { StatusMessage msg = new StatusMessage(statusManager, MessageType.FATAL, FATAL_MESSAGE); msg.addAction(StatusMessage.FEEDBACK_ACTION); msg.addAction(StatusMessage.RELOAD_ACTION); msg.setLongText(calculateLongText(ExceptionUtils.getStackTraceAsString(exception))); msg.fire(); } }); } /** * Serialize a {@link Throwable} as a {@link ThrowableDto}. */ private static ThrowableDtoImpl getThrowableAsDto(Throwable e) { ThrowableDtoImpl ret = ThrowableDtoImpl.make(); ThrowableDtoImpl currentDto = ret; Throwable currentCause = e; for (int causeCounter = 0; causeCounter < ExceptionUtils.MAX_CAUSE && currentCause != null; causeCounter++) { currentDto.setClassName(currentCause.getClass().getName()); currentDto.setMessage(currentCause.getMessage()); JsoArray<StackTraceElementDto> currentStackTrace = JsoArray.create(); StackTraceElement[] stackElems = currentCause.getStackTrace(); if (stackElems != null) { for (int i = 0; i < stackElems.length; ++i) { StackTraceElement stackElem = stackElems[i]; currentStackTrace.add(StackTraceElementDtoImpl .make() .setClassName(stackElem.getClassName()) .setFileName(stackElem.getFileName()) .setMethodName(stackElem.getMethodName()) .setLineNumber(stackElem.getLineNumber())); } currentDto.setStackTrace(currentStackTrace); } currentCause = currentCause.getCause(); if (currentCause != null) { ThrowableDtoImpl nextDto = ThrowableDtoImpl.make(); currentDto.setCause(nextDto); currentDto = nextDto; } } return ret; } }