CORDA-2586 explorer exception handling (#4957)

* Initial version of new(old) dialog that won't print a stacktrace for rpc exceptions.

* Decoupled CordaVersionProvider. Moved common files to common-logging to lower dependencies on the node explorer.

* Removed unused import and duplicate documentation comment.

* Moved error code rewrite policy in the new common/logging module according to PR review.

* Removed extra line.

* Updated log4j configurations with new package name where logging policies will be contained.

* Included common-logging module with cliutils.
This commit is contained in:
Stefan Iliev 2019-04-09 20:14:37 +01:00 committed by Anthony Keenan
parent 746fcc32e5
commit e4615f7f47
20 changed files with 167 additions and 34 deletions

View File

@ -0,0 +1,26 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
dependencies {
compile group: "org.jetbrains.kotlin", name: "kotlin-stdlib-jdk8", version: kotlin_version
compile group: "org.jetbrains.kotlin", name: "kotlin-reflect", version: kotlin_version
compile group: "com.typesafe", name: "config", version: typesafe_config_version
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
testCompile project(":test-utils")
}
jar {
baseName 'corda-common-logging'
}
publish {
name jar.baseName
}

View File

@ -0,0 +1,28 @@
package net.corda.common.logging
import com.jcabi.manifests.Manifests
class CordaVersion {
companion object {
private const val UNKNOWN = "Unknown"
const val current_major_release = "4.0-SNAPSHOT"
const val platformEditionCode = "OS"
private fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: UNKNOWN }
val revision: String by lazy { manifestValue("Corda-Revision") ?: UNKNOWN }
val vendor: String by lazy { manifestValue("Corda-Vendor") ?: UNKNOWN }
val platformVersion: Int by lazy { manifestValue("Corda-Platform-Version")?.toInt() ?: 1 }
internal val semanticVersion: String by lazy { if(releaseVersion == UNKNOWN) current_major_release else releaseVersion }
}
fun getVersion(): Array<String> {
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
arrayOf("Version: $releaseVersion", "Revision: $revision", "Platform Version: $platformVersion", "Vendor: $vendor")
} else {
arrayOf("No version data is available in the MANIFEST file.")
}
}
}

View File

@ -1,4 +1,4 @@
package net.corda.cliutils
package net.corda.common.logging
import org.apache.logging.log4j.core.Core
import org.apache.logging.log4j.core.LogEvent

View File

@ -1,11 +1,11 @@
package net.corda.cliutils
package net.corda.common.logging
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.message.Message
import org.apache.logging.log4j.message.SimpleMessage
import java.util.*
internal fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message {
fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message {
return when {
error != null && level.isInRange(Level.FATAL, Level.WARN) -> CompositeMessage("$formattedMessage [errorCode=${error.errorCode()}, moreInformationAt=${error.errorCodeLocationUrl()}]", format, parameters, throwable)
@ -13,9 +13,9 @@ internal fun Message.withErrorCodeFor(error: Throwable?, level: Level): Message
}
}
private fun Throwable.errorCodeLocationUrl() = "https://errors.corda.net/${CordaVersionProvider.platformEditionCode}/${CordaVersionProvider.semanticVersion}/${errorCode()}"
fun Throwable.errorCodeLocationUrl() = "https://errors.corda.net/${CordaVersion.platformEditionCode}/${CordaVersion.semanticVersion}/${errorCode()}"
private fun Throwable.errorCode(hashedFields: (Throwable) -> Array<out Any?> = Throwable::defaultHashedFields): String {
fun Throwable.errorCode(hashedFields: (Throwable) -> Array<out Any?> = Throwable::defaultHashedFields): String {
val hash = staticLocationBasedHash(hashedFields)
return hash.toBase(36)

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" packages="net.corda.cliutils">
<Configuration status="info" packages="net.corda.common.logging">
<Properties>
<Property name="log-path">${sys:log-path:-logs}</Property>

View File

@ -73,6 +73,7 @@ dependencies {
compile project(':tools:cliutils')
compile project(':common-validation')
compile project(':common-configuration-parsing')
compile project(':common-logging')
// Backwards compatibility goo: Apps expect confidential-identities to be loaded by default.
// We could eventually gate this on a target-version check.

View File

@ -4,8 +4,8 @@ import io.netty.channel.unix.Errors
import net.corda.cliutils.printError
import net.corda.cliutils.CliWrapperBase
import net.corda.cliutils.CordaCliWrapper
import net.corda.cliutils.CordaVersionProvider
import net.corda.cliutils.ExitCodes
import net.corda.common.logging.CordaVersion
import net.corda.core.contracts.HashAttachmentConstraint
import net.corda.core.crypto.Crypto
import net.corda.core.internal.*
@ -269,9 +269,9 @@ open class NodeStartup : NodeStartupLogging {
open fun getVersionInfo(): VersionInfo {
return VersionInfo(
PLATFORM_VERSION,
CordaVersionProvider.releaseVersion,
CordaVersionProvider.revision,
CordaVersionProvider.vendor
CordaVersion.releaseVersion,
CordaVersion.revision,
CordaVersion.vendor
)
}

View File

@ -71,6 +71,10 @@ project(":common-validation").projectDir = new File("$settingsDir/common/validat
include 'common-configuration-parsing'
project(":common-configuration-parsing").projectDir = new File("$settingsDir/common/configuration-parsing")
include 'common-logging'
project(":common-logging").projectDir = new File("$settingsDir/common/logging")
// Common libraries - end
apply from: 'buildCacheSettings.gradle'

View File

@ -6,6 +6,7 @@ dependencies {
compile project(':core')
compile project(':node-api')
compile project(':tools:cliutils')
compile project(":common-logging")
// Unit testing helpers.
compile "junit:junit:$junit_version"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info" packages="net.corda.cliutils">
<Configuration status="info" packages="net.corda.common.logging">
<Properties>
<Property name="log-path">${sys:log-path:-logs}</Property>

View File

@ -6,6 +6,7 @@ apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':client:jackson')
compile project(':tools:cliutils')
compile project(":common-logging")
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"

View File

@ -7,6 +7,7 @@ description 'Network bootstrapper'
dependencies {
compile project(':node-api')
compile project(':tools:cliutils')
compile project(":common-logging")
compile project(':common-configuration-parsing')
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"

View File

@ -7,6 +7,7 @@ description 'CLI Utilities'
dependencies {
compile project(":core")
compile project(":common-logging")
compile "info.picocli:picocli:$picocli_version"
compile "commons-io:commons-io:$commons_io_version"

View File

@ -1,33 +1,16 @@
package net.corda.cliutils
import com.jcabi.manifests.Manifests
import picocli.CommandLine
import net.corda.common.logging.CordaVersion
/**
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
* in the manifest file.
*/
class CordaVersionProvider : CommandLine.IVersionProvider {
companion object {
private const val UNKNOWN = "Unknown"
const val current_major_release = "4.0-SNAPSHOT"
const val platformEditionCode = "OS"
private fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null
val releaseVersion: String by lazy { manifestValue("Corda-Release-Version") ?: UNKNOWN }
val revision: String by lazy { manifestValue("Corda-Revision") ?: UNKNOWN }
val vendor: String by lazy { manifestValue("Corda-Vendor") ?: UNKNOWN }
val platformVersion: Int by lazy { manifestValue("Corda-Platform-Version")?.toInt() ?: 1 }
internal val semanticVersion: String by lazy { if(releaseVersion == UNKNOWN) current_major_release else releaseVersion }
}
val version = CordaVersion()
override fun getVersion(): Array<String> {
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
arrayOf("Version: $releaseVersion", "Revision: $revision", "Platform Version: $platformVersion", "Vendor: $vendor")
} else {
arrayOf("No version data is available in the MANIFEST file.")
}
return version.getVersion()
}
}

View File

@ -10,6 +10,7 @@ import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.*
import net.corda.common.logging.CordaVersion
private class ShellExtensionsGenerator(val parent: CordaCliWrapper) {
private companion object {
@ -72,7 +73,7 @@ private class ShellExtensionsGenerator(val parent: CordaCliWrapper) {
// If on Windows, Path.toString() returns a path with \ instead of /, but for bash Windows users we want to convert those back to /'s
private fun Path.toStringWithDeWindowsfication(): String = this.toAbsolutePath().toString().replace("\\", "/")
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersion.releaseVersion}, Revision: ${CordaVersion.revision}"
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
private fun generateAutoCompleteFile(alias: String) {

View File

@ -19,6 +19,7 @@ dependencies {
compile project(':finance:contracts')
compile project(':finance:workflows')
compile project(':tools:worldmap')
compile project(':common-logging')
// Log4J: logging framework (with SLF4J bindings)
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"

View File

@ -0,0 +1,77 @@
package net.corda.explorer.ui
import impl.org.controlsfx.i18n.Localization.getString
import impl.org.controlsfx.i18n.Localization.localize
import javafx.event.ActionEvent
import javafx.event.EventHandler
import net.corda.common.logging.errorCodeLocationUrl
import org.controlsfx.dialog.ProgressDialog
import javafx.scene.control.*
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import javafx.scene.text.Text
import javafx.scene.text.TextFlow
import java.awt.Desktop
import java.io.PrintWriter
import java.io.StringWriter
import java.net.URI
/*
Will generate a window showing the exception message with a generated link and if requested a stacktrace.
The link opens the default browser towards the error.corda.com/ redirection pages.
*/
class AdvancedExceptionDialog(_exception: Throwable) : Dialog<ButtonType>() {
internal val exception = _exception
init {
val dialogPane = super.getDialogPane()
//Dialog title
super.setTitle(getString("exception.dlg.title"))
dialogPane.headerText = getString("exception.dlg.header")
dialogPane.styleClass.add("exception-dialog")
dialogPane.stylesheets.add(ProgressDialog::class.java.getResource("dialogs.css").toExternalForm())
dialogPane.buttonTypes.addAll(ButtonType.OK)
val hyperlink = Hyperlink(exception.errorCodeLocationUrl())
hyperlink.onAction = EventHandler<ActionEvent> {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse( URI(exception.errorCodeLocationUrl()))
} //This should be tested out on other platforms, works on my mac but the stackoverflow opinions are mixed.
}
val textFlow = TextFlow(Text("${exception.message}\n"), hyperlink)
dialogPane.content = textFlow
}
}
//Attach a stacktrace for the exception that was used in the initialization of the dialog.
fun AdvancedExceptionDialog.withStacktrace() : AdvancedExceptionDialog
{
val sw = StringWriter()
val pw = PrintWriter(sw)
exception.printStackTrace(pw)
val textArea = TextArea(sw.toString()).apply {
isEditable = false
isWrapText = false
maxWidth = Double.MAX_VALUE
maxHeight = Double.MAX_VALUE
}
GridPane.setVgrow(textArea, Priority.ALWAYS)
GridPane.setHgrow(textArea, Priority.ALWAYS)
val root = GridPane().apply {
maxWidth = Double.MAX_VALUE
add(Label(localize(getString("exception.dlg.label"))), 0, 0)
add(textArea,0 ,1)
}
dialogPane.expandableContent = root
return this
}

View File

@ -4,9 +4,11 @@ import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.*
import net.corda.client.jfx.model.NodeMonitorModel
import net.corda.client.jfx.model.objectProperty
import net.corda.client.rpc.RPCException
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.explorer.model.SettingsModel
import org.controlsfx.dialog.ExceptionDialog
import net.corda.explorer.ui.AdvancedExceptionDialog
import net.corda.explorer.ui.withStacktrace
import tornadofx.*
import kotlin.system.exitProcess
@ -50,10 +52,14 @@ class LoginView : View(WINDOW_TITLE) {
}
getModel<SettingsModel>().commit()
LoginStatus.loggedIn
} catch (e: RPCException) {
e.printStackTrace()
AdvancedExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
LoginStatus.exception
} catch (e: Exception) {
// TODO : Handle this in a more user friendly way.
e.printStackTrace()
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
AdvancedExceptionDialog(e).withStacktrace().apply { initOwner(root.scene.window) }.showAndWait()
LoginStatus.exception
} finally {
root.isDisable = false

View File

@ -11,6 +11,7 @@ apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':tools:shell')
compile project(':tools:cliutils')
compile project(":common-logging")
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
compile "org.slf4j:jul-to-slf4j:$slf4j_version"

View File

@ -29,6 +29,7 @@ dependencies {
compile project(':client:rpc')
compile project(':client:jackson')
compile project(':tools:cliutils')
compile project(":common-logging")
// Web stuff: for HTTP[S] servlets
compile "org.eclipse.jetty:jetty-servlet:$jetty_version"