Rework error handling to be more robust

This commit is contained in:
comp500 2020-12-15 17:28:23 +00:00
parent 1d4c94f5b6
commit 0858c90079
10 changed files with 170 additions and 115 deletions

View File

@ -14,8 +14,6 @@ java {
dependencies {
implementation("commons-cli:commons-cli:1.4")
implementation("com.moandjiezana.toml:toml4j:0.7.2")
// TODO: Implement tests
//testImplementation "junit:junit:4.12"
implementation("com.google.code.gson:gson:2.8.1")
implementation("com.squareup.okio:okio:2.2.2")
implementation(kotlin("stdlib-jdk8"))

View File

@ -8,6 +8,7 @@ import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import link.infra.packwiz.installer.ui.data.IOptionDetails
import link.infra.packwiz.installer.util.Log
import okio.Buffer
import okio.HashingSink
import okio.buffer
@ -125,6 +126,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
fun download(packFolder: String, indexUri: SpaceSafeURI) {
if (err != null) return
// TODO: is this necessary if we overwrite?
// Ensure it is removed
cachedFile?.let {
if (!it.optionValue || !correctSide()) {
@ -133,8 +135,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
try {
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
} catch (e: IOException) {
// TODO: how much of a problem is this? use log4j/other log library to show warning?
e.printStackTrace()
Log.warn("Failed to delete file before downloading", e)
}
it.cachedLocation = null
}
@ -190,7 +191,7 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
data.clear()
} else {
// TODO: no more PRINTLN!!!!!!!!!
// TODO: move println to something visible in the error window
println("Invalid hash for " + metadata.destURI.toString())
println("Calculated: " + fileSource.hash)
println("Expected: $hash")

View File

@ -5,6 +5,7 @@ package link.infra.packwiz.installer
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.ui.cli.CLIHandler
import link.infra.packwiz.installer.ui.gui.GUIHandler
import link.infra.packwiz.installer.util.Log
import org.apache.commons.cli.DefaultParser
import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException
@ -29,7 +30,7 @@ class Main(args: Array<String>) {
val cmd = try {
parser.parse(options, args)
} catch (e: ParseException) {
e.printStackTrace()
Log.fatal("Failed to parse command line arguments", e)
if (guiEnabled) {
EventQueue.invokeAndWait {
try {
@ -37,7 +38,8 @@ class Main(args: Array<String>) {
} catch (ignored: Exception) {
// Ignore the exceptions, just continue using the ugly L&F
}
JOptionPane.showMessageDialog(null, e.message, "packwiz-installer", JOptionPane.ERROR_MESSAGE)
JOptionPane.showMessageDialog(null, "Failed to parse command line arguments: $e",
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
}
}
exitProcess(1)
@ -51,33 +53,34 @@ class Main(args: Array<String>) {
val unparsedArgs = cmd.args
if (unparsedArgs.size > 1) {
ui.handleExceptionAndExit(RuntimeException("Too many arguments specified!"))
ui.showErrorAndExit("Too many arguments specified!")
} else if (unparsedArgs.isEmpty()) {
ui.handleExceptionAndExit(RuntimeException("URI to install from must be specified!"))
ui.showErrorAndExit("pack.toml URI to install from must be specified!")
}
cmd.getOptionValue("title")?.also(ui::setTitle)
val title = cmd.getOptionValue("title")
if (title != null) {
ui.setTitle(title)
}
ui.show()
val uOptions = UpdateManager.Options().apply {
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from) ?: side
packFolder = cmd.getOptionValue("pack-folder") ?: packFolder
manifestFile = cmd.getOptionValue("meta-file") ?: manifestFile
}
try {
uOptions.downloadURI = SpaceSafeURI(unparsedArgs[0])
val uOptions = try {
UpdateManager.Options.construct(
downloadURI = SpaceSafeURI(unparsedArgs[0]),
side = cmd.getOptionValue("side")?.let((UpdateManager.Options.Side)::from),
packFolder = cmd.getOptionValue("pack-folder"),
manifestFile = cmd.getOptionValue("meta-file")
)
} catch (e: URISyntaxException) {
// TODO: better error message?
ui.handleExceptionAndExit(e)
ui.showErrorAndExit("Failed to read pack.toml URI", e)
}
// Start update process!
try {
UpdateManager(uOptions, ui)
} catch (e: Exception) { // TODO: better error message?
ui.handleExceptionAndExit(e)
} catch (e: Exception) {
ui.showErrorAndExit("Update process failed", e)
}
println("Finished successfully!")
ui.dispose()
@ -111,17 +114,17 @@ class Main(args: Array<String>) {
try {
startup(args)
} catch (e: Exception) {
e.printStackTrace()
Log.fatal("Error from main", e)
if (guiEnabled) {
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
"A fatal error occurred: \n$e",
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
// In case the EventQueue is broken, exit after 1 minute
Thread.sleep(60 * 1000.toLong())
}
// In case the EventQueue is broken, exit after 1 minute
Thread.sleep(60 * 1000.toLong())
exitProcess(1)
}
}

View File

@ -19,6 +19,9 @@ import link.infra.packwiz.installer.ui.IUserInterface
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import link.infra.packwiz.installer.ui.data.InstallProgress
import link.infra.packwiz.installer.util.Log
import link.infra.packwiz.installer.util.ifletOrErr
import link.infra.packwiz.installer.util.ifletOrWarn
import okio.buffer
import java.io.FileNotFoundException
import java.io.FileReader
@ -43,11 +46,17 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
data class Options(
var downloadURI: SpaceSafeURI? = null,
var manifestFile: String = "packwiz.json", // TODO: make configurable
var packFolder: String = ".",
var side: Side = Side.CLIENT
val downloadURI: SpaceSafeURI,
val manifestFile: String,
val packFolder: String,
val side: Side
) {
// Horrible workaround for default params not working cleanly with nullable values
companion object {
fun construct(downloadURI: SpaceSafeURI, manifestFile: String?, packFolder: String?, side: Side?) =
Options(downloadURI, manifestFile ?: "packwiz.json", packFolder ?: ".", side ?: Side.CLIENT)
}
enum class Side {
@SerializedName("client")
CLIENT("client"),
@ -111,11 +120,9 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
} catch (e: FileNotFoundException) {
ManifestFile()
} catch (e: JsonSyntaxException) {
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Invalid local manifest file, try deleting ${opts.manifestFile}", e)
} catch (e: JsonIOException) {
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Failed to read local manifest file, try deleting ${opts.manifestFile}", e)
}
if (ui.cancelButtonPressed) {
@ -125,19 +132,16 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
ui.submitProgress(InstallProgress("Loading pack file..."))
val packFileSource = try {
val src = getFileSource(opts.downloadURI!!)
val src = getFileSource(opts.downloadURI)
getHasher("sha256").getHashingSource(src)
} catch (e: Exception) {
// TODO: run cancellation window?
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Failed to download pack.toml", e)
}
val pf = packFileSource.buffer().use {
try {
Toml().read(it.inputStream()).to(PackFile::class.java)
} catch (e: IllegalStateException) {
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Failed to parse pack.toml", e)
}
}
@ -169,40 +173,41 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
}
if (invalid) {
println("File $fileUri invalidated, marked for redownloading")
Log.info("File $fileUri invalidated, marked for redownloading")
invalidatedUris.add(fileUri)
}
}
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
println("Modpack is already up to date!")
Log.info("Modpack is already up to date!")
// todo: --force?
if (!ui.optionsButtonPressed) {
return
}
}
println("Modpack name: " + pf.name)
Log.info("Modpack name: ${pf.name}")
if (ui.cancelButtonPressed) {
showCancellationDialog()
handleCancellation()
}
try {
val index = pf.index!!
getNewLoc(opts.downloadURI, index.file)?.let { newLoc ->
index.hashFormat?.let { hashFormat ->
processIndex(
newLoc,
getHash(index.hashFormat!!, index.hash!!),
hashFormat,
manifest,
invalidatedUris
)
ifletOrWarn(pf.index, "No index file found") { index ->
ui.ifletOrErr(index.hashFormat, index.hash, "Pack has no hash or hashFormat for index") { hashFormat, hash ->
ui.ifletOrErr(getNewLoc(opts.downloadURI, index.file), "Pack has invalid index file: " + index.file) { newLoc ->
processIndex(
newLoc,
getHash(hashFormat, hash),
hashFormat,
manifest,
invalidatedUris
)
}
}
}
} catch (e1: Exception) {
ui.handleExceptionAndExit(e1)
ui.showErrorAndExit("Failed to process index file", e1)
}
handleCancellation()
@ -221,8 +226,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
try {
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
} catch (e: IOException) {
// TODO: add message?
ui.handleException(e)
ui.showErrorAndExit("Failed to save local manifest file", e)
}
}
@ -232,7 +236,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
println("Modpack files are already up to date!")
Log.info("Modpack files are already up to date!")
if (!ui.optionsButtonPressed) {
return
}
@ -243,20 +247,18 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
val src = getFileSource(indexUri)
getHasher(hashFormat).getHashingSource(src)
} catch (e: Exception) {
// TODO: run cancellation window?
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Failed to download index file", e)
}
val indexFile = try {
Toml().read(indexFileSource.buffer().inputStream()).to(IndexFile::class.java)
} catch (e: IllegalStateException) {
ui.handleExceptionAndExit(e)
return
ui.showErrorAndExit("Failed to parse index file", e)
}
if (!indexFileSource.hashIsEqual(indexHash)) {
ui.handleExceptionAndExit(RuntimeException("Your index hash is invalid! Please run packwiz refresh on the pack again"))
return
ui.showErrorAndExit("Your index file hash is invalid! The pack developer should packwiz refresh on the pack again")
}
if (ui.cancelButtonPressed) {
showCancellationDialog()
return
@ -274,8 +276,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
try {
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
} catch (e: IOException) {
// TODO: should this be shown to the user in some way?
e.printStackTrace()
Log.warn("Failed to delete optional disabled file", e)
}
// Set to null, as it doesn't exist anymore
file.cachedLocation = null
@ -285,8 +286,8 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
if (!alreadyDeleted) {
try {
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
} catch (e: IOException) { // TODO: should this be shown to the user in some way?
e.printStackTrace()
} catch (e: IOException) {
Log.warn("Failed to delete file removed from index", e)
}
}
it.remove()
@ -302,14 +303,14 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
// TODO: progress bar?
if (indexFile.files.isEmpty()) {
println("Warning: Index is empty!")
Log.warn("Index is empty!")
}
val tasks = createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side)
// If the side changes, invalidate EVERYTHING just in case
// Might not be needed, but done just to be safe
val invalidateAll = opts.side != manifest.cachedSide
if (invalidateAll) {
println("Side changed, invalidating all mods")
Log.info("Side changed, invalidating all mods")
}
tasks.forEach{ f ->
// TODO: should linkedfile be checked as well? should this be done in the download section?
@ -377,18 +378,15 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
}
for (i in tasks.indices) {
var task: DownloadTask?
task = try {
val task: DownloadTask = try {
completionService.take().get()
} catch (e: InterruptedException) {
ui.handleException(e)
null
ui.showErrorAndExit("Interrupted when consuming download tasks", e)
} catch (e: ExecutionException) {
ui.handleException(e)
null
ui.showErrorAndExit("Failed to execute download task", e)
}
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
task?.cachedFile?.let { file ->
task.cachedFile?.let { file ->
if (task.failed()) {
val oldFile = file.revert
if (oldFile != null) {
@ -399,17 +397,11 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
}
var progress: String
if (task != null) {
val exDetails = task.exceptionDetails
if (exDetails != null) {
progress = "Failed to download ${exDetails.name}: ${exDetails.exception.message}"
exDetails.exception.printStackTrace()
} else {
progress = "Downloaded ${task.name}"
}
val exDetails = task.exceptionDetails
val progress = if (exDetails != null) {
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
} else {
progress = "Failed to download, unknown reason"
"Downloaded ${task.name}"
}
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))

View File

@ -14,6 +14,7 @@ object HandlerManager {
RequestHandlerFile()
)
// TODO: get rid of nullable stuff here
@JvmStatic
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
if (loc == null) {
@ -32,6 +33,8 @@ object HandlerManager {
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
// Caching system? Copy from already downloaded files?
// TODO: change to use something more idiomatic than exceptions?
@JvmStatic
@Throws(Exception::class)
fun getFileSource(loc: SpaceSafeURI): Source {

View File

@ -3,16 +3,15 @@ package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import link.infra.packwiz.installer.ui.data.IOptionDetails
import link.infra.packwiz.installer.ui.data.InstallProgress
import kotlin.system.exitProcess
interface IUserInterface {
fun show()
fun dispose()
fun handleException(e: Exception)
fun handleExceptionAndExit(e: Exception) {
handleException(e)
exitProcess(1)
fun showErrorAndExit(message: String): Nothing {
showErrorAndExit(message, null)
}
fun showErrorAndExit(message: String, e: Exception?): Nothing
fun setTitle(title: String) {}
fun submitProgress(progress: InstallProgress)

View File

@ -5,6 +5,7 @@ import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import link.infra.packwiz.installer.ui.data.IOptionDetails
import link.infra.packwiz.installer.ui.data.InstallProgress
import link.infra.packwiz.installer.util.Log
import kotlin.system.exitProcess
class CLIHandler : IUserInterface {
@ -13,8 +14,13 @@ class CLIHandler : IUserInterface {
@Volatile
override var cancelButtonPressed = false
override fun handleException(e: Exception) {
e.printStackTrace()
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
if (e != null) {
Log.fatal(message, e)
} else {
Log.fatal(message)
}
exitProcess(1)
}
override fun show() {}
@ -36,7 +42,7 @@ class CLIHandler : IUserInterface {
for (opt in options) {
opt.optionValue = true
// TODO: implement option choice in the CLI?
println("Warning: accepting option " + opt.name + " as option choosing is not implemented in the CLI")
Log.warn("Accepting option ${opt.name} as option choosing is not implemented in the CLI")
}
return false // Can't be cancelled!
}
@ -44,9 +50,9 @@ class CLIHandler : IUserInterface {
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
println("Failed to download modpack, the following errors were encountered:")
for (ex in exceptions) {
println(ex.name + ": ")
print(ex.name + ": ")
ex.exception.printStackTrace()
}
exitProcess(1)
return ExceptionListResult.CANCEL
}
}

View File

@ -5,6 +5,7 @@ import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import link.infra.packwiz.installer.ui.data.IOptionDetails
import link.infra.packwiz.installer.ui.data.InstallProgress
import link.infra.packwiz.installer.util.Log
import java.awt.EventQueue
import java.util.concurrent.CompletableFuture
import javax.swing.JDialog
@ -27,8 +28,7 @@ class GUIHandler : IUserInterface {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} catch (e: Exception) {
println("Failed to set look and feel:")
e.printStackTrace()
Log.warn("Failed to set look and feel", e)
}
frmPackwizlauncher = InstallWindow(this).apply {
title = this@GUIHandler.title
@ -44,23 +44,23 @@ class GUIHandler : IUserInterface {
frmPackwizlauncher.dispose()
}
override fun handleException(e: Exception) {
e.printStackTrace()
EventQueue.invokeAndWait {
JOptionPane.showMessageDialog(null,
"An error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
if (e != null) {
Log.fatal(message, e)
EventQueue.invokeAndWait {
JOptionPane.showMessageDialog(null,
"$message: $e",
title, JOptionPane.ERROR_MESSAGE)
}
}
override fun handleExceptionAndExit(e: Exception) {
e.printStackTrace()
EventQueue.invokeAndWait {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
}
} else {
Log.fatal(message)
EventQueue.invokeAndWait {
JOptionPane.showMessageDialog(null,
message,
title, JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
}
exitProcess(1)
}
override fun setTitle(title: String) {
@ -78,8 +78,7 @@ class GUIHandler : IUserInterface {
sb.append(") ")
}
sb.append(progress.message)
// TODO: better logging library?
println(sb.toString())
Log.info(sb.toString())
EventQueue.invokeLater {
frmPackwizlauncher.displayProgress(progress)
}

View File

@ -0,0 +1,38 @@
package link.infra.packwiz.installer.util
import link.infra.packwiz.installer.ui.IUserInterface
inline fun <T> iflet(value: T?, whenNotNull: (T) -> Unit) {
if (value != null) {
whenNotNull(value)
}
}
inline fun <T, U> IUserInterface.ifletOrErr(value: T?, message: String, whenNotNull: (T) -> U): U =
if (value != null) {
whenNotNull(value)
} else {
this.showErrorAndExit(message)
}
inline fun <T, U, V> IUserInterface.ifletOrErr(value: T?, value2: U?, message: String, whenNotNull: (T, U) -> V): V =
if (value != null && value2 != null) {
whenNotNull(value, value2)
} else {
this.showErrorAndExit(message)
}
inline fun <T> ifletOrWarn(value: T?, message: String, whenNotNull: (T) -> Unit) {
if (value != null) {
whenNotNull(value)
} else {
Log.warn(message)
}
}
inline fun <T, U> iflet(value: T?, whenNotNull: (T) -> U, whenNull: () -> U): U =
if (value != null) {
whenNotNull(value)
} else {
whenNull()
}

View File

@ -0,0 +1,16 @@
package link.infra.packwiz.installer.util
object Log {
fun info(message: String) = println(message)
fun warn(message: String) = println("[Warning] $message")
fun warn(message: String, exception: Exception) = println("[Warning] $message: $exception")
fun fatal(message: String) {
println("[FATAL] $message")
}
fun fatal(message: String, exception: Exception) {
println("[FATAL] $message: ")
exception.printStackTrace()
}
}