Compare commits

..

30 Commits

Author SHA1 Message Date
comp500
46771ce870 Clarify error message for missing index file 2021-07-16 04:07:45 +01:00
comp500
b143f67acd Fix symlink check by catching the correct exception 2021-06-22 13:54:06 +01:00
comp500
03b0f1b09b Fix disable options button always changing to No optional mods... 2021-02-16 16:26:23 +00:00
comp500
6c6a0100fd Bundle licenses, disable relocation for now 2021-02-16 16:19:16 +00:00
comp500
6d47c0d61f Relocate shadow deps 2021-02-07 14:02:10 +00:00
comp500
226e754547 Add SHA1 support (see https://github.com/comp500/packwiz/pull/11) 2021-01-29 06:50:50 +00:00
comp500
2c02703101 Ask the user if they want to continue launching, if update/install fails 2020-12-15 20:24:49 +00:00
comp500
81a60cc759 Remove kotlinx.serialization gradle plugin as I didn't end up using it 2020-12-15 19:43:30 +00:00
comp500
92afa93fd7 Update deps, shrink fatjar (no obf) with proguard 2020-12-15 19:40:18 +00:00
comp500
0858c90079 Rework error handling to be more robust 2020-12-15 17:28:23 +00:00
comp500
1d4c94f5b6 Make the main window unresizable, to work better with tiling window managers 2020-12-13 17:00:35 +00:00
comp500
74ddca5d54 Remove unnecessary Futures from IUserInterface API 2020-12-13 16:57:12 +00:00
comp500
0df48d19a9 Separate IUserInterface logic out into GUIHandler 2020-12-13 16:34:00 +00:00
comp500
f5b22f37a4 Refactor GUI code, remove bad SwingWorker junk 2020-12-13 16:12:44 +00:00
comp500
f52cd19ad4 Show download exceptions properly in CLI 2020-12-11 18:18:10 +00:00
comp500
60887a4312 Whoops 2020-12-07 17:42:52 +00:00
comp500
a368268038 Fix support for symlinked directories 2020-12-07 17:38:21 +00:00
comp500
8beded7b41 Improve UX when there are no optional mods 2020-12-06 19:05:56 +00:00
comp500
91060dcd54 Put an error message there. Later is now! 2020-11-30 00:24:47 +00:00
comp500
e06ee21f3b Add User-Agent to download requests 2020-10-22 20:53:36 +01:00
comp500
b3370739a5 Fix Swing multithreading issue, clean up slightly 2020-09-29 02:14:56 +01:00
comp500
ecc6f0440a Remove IntelliJ metadata from repo 2020-09-29 02:14:05 +01:00
comp500
92b44352b3 Fix RequestHandlerGithub heuristics, so that Github Releases files work properly 2020-06-20 03:15:18 +01:00
comp500
1d5a787b02 Add JvmStatic to fix --help command (bootstrapper calls these) 2020-06-16 04:05:02 +01:00
comp500
b5983800e8 Update README.md 2020-05-11 17:48:41 +01:00
comp500
4b3c279e71 Add support for loading from file:// URIs 2020-05-08 22:57:03 +01:00
comp500
b413371306 Fix --help command 2020-05-08 18:08:53 +01:00
comp500
1d2ec61232 Fix disgusting getNewLoc call (!! already checks null!!) 2020-02-07 03:12:52 +00:00
comp500
a0da889a02 Optimise memory usage while computing Murmur2 2019-12-23 16:31:36 +00:00
comp500
432bb4e25f Fix Murmur2 hash implementation 2019-12-23 16:20:38 +00:00
39 changed files with 930 additions and 629 deletions

30
.gitignore vendored
View File

@@ -1,9 +1,9 @@
# Created by https://www.gitignore.io/api/java,gradle,intellij
# Edit at https://www.gitignore.io/?templates=java,gradle,intellij
# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij+all
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
@@ -33,6 +33,9 @@
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
@@ -72,13 +75,18 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
@@ -127,4 +135,4 @@ gradle-app.setting
### Gradle Patch ###
**/build/
# End of https://www.gitignore.io/api/java,gradle,intellij
# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -1,36 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="wbp.parser.entryPoint" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
</component>
</project>

10
.idea/misc.xml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="com.google.gson.annotations.SerializedName" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="11" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019
Copyright (c) 2021 comp500
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,2 +1,2 @@
# packwiz-installer
An installer for launching packwiz modpacks with MultiMC.
An installer for launching packwiz modpacks with MultiMC. You'll need [the bootstrapper](https://github.com/comp500/packwiz-installer-bootstrap/releases) to actually use this.

View File

@@ -1,23 +1,36 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath("com.guardsquare:proguard-gradle:7.0.0") {
exclude("com.android.tools.build")
}
}
}
plugins {
java
application
id("com.github.johnrengelman.shadow") version "5.0.0"
id("com.palantir.git-version") version "0.11.0"
id("com.github.breadmoirai.github-release") version "2.2.9"
kotlin("jvm") version "1.3.61"
id("com.github.johnrengelman.shadow") version "6.1.0"
id("com.palantir.git-version") version "0.12.3"
id("com.github.breadmoirai.github-release") version "2.2.12"
kotlin("jvm") version "1.4.21"
id("com.github.jk1.dependency-license-report") version "1.16"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
}
val shrinkClasspath: Configuration by configurations.creating
dependencies {
implementation("commons-cli:commons-cli:1.4")
shrinkClasspath("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("com.squareup.okio:okio:2.9.0")
implementation(kotlin("stdlib-jdk8"))
}
@@ -39,17 +52,53 @@ tasks.jar {
}
}
licenseReport {
renderers = arrayOf<com.github.jk1.license.render.ReportRenderer>(
com.github.jk1.license.render.InventoryMarkdownReportRenderer("licenses.md", "packwiz-installer")
)
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
}
// TODO: build relocated jar for minecraft launcher lib, non-relocated jar for packwiz-installer
//tasks.register<com.github.jengelman.gradle.plugins.shadow.tasks.ConfigureShadowRelocation>("relocateShadowJar") {
// target = tasks.shadowJar.get()
// prefix = "link.infra.packwiz.deps"
//}
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
tasks.shadowJar {
dependencies {
exclude(dependency("commons-cli:commons-cli:1.4"))
exclude(dependency("com.eclipsesource.minimal-json:minimal-json:0.9.5"))
// TODO: exclude meta inf files
}
exclude("**/*.kotlin_metadata")
exclude("**/*.kotlin_builtins")
exclude("META-INF/maven/**/*")
exclude("META-INF/proguard/**/*")
//dependsOn(tasks.named("relocateShadowJar"))
}
tasks.register<proguard.gradle.ProGuardTask>("shrinkJar") {
injars(tasks.shadowJar)
libraryjars(files(shrinkClasspath.files))
outjars("build/libs/" + tasks.shadowJar.get().outputs.files.first().name.removeSuffix(".jar") + "-shrink.jar")
if (System.getProperty("java.version").startsWith("1.")) {
libraryjars("${System.getProperty("java.home")}/lib/rt.jar")
libraryjars("${System.getProperty("java.home")}/lib/jce.jar")
} else {
throw RuntimeException("Compiling with Java 9+ not supported!")
}
keep("class link.infra.packwiz.installer.** { *; }")
dontoptimize()
dontobfuscate()
dontwarn("org.codehaus.mojo.animal_sniffer.*")
}
// Used for vscode launch.json
tasks.register<Copy>("copyJar") {
from(tasks.shadowJar)
from(tasks.named("shrinkJar"))
rename("packwiz-installer-(.*)\\.jar", "packwiz-installer.jar")
into("build/libs/")
}

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -74,13 +74,13 @@ public class Murmur2Lib {
int left = length - len_m;
if (left != 0) {
if (left >= 3) {
h ^= (int) data[length - 3] << 16;
h ^= (int) data[length - (left - 2)] << 16;
}
if (left >= 2) {
h ^= (int) data[length - 2] << 8;
h ^= (int) data[length - (left - 1)] << 8;
}
if (left >= 1) {
h ^= (int) data[length - 1];
h ^= data[length - left];
}
h *= M_32;
@@ -152,7 +152,7 @@ public class Murmur2Lib {
case 2:
h ^= (long) (data[tailStart + 1] & 0xff) << 8;
case 1:
h ^= (long) (data[tailStart] & 0xff);
h ^= data[tailStart] & 0xff;
h *= M_64;
}

View File

@@ -6,8 +6,9 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.metadata.hash.Hash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
import link.infra.packwiz.installer.ui.ExceptionDetails
import link.infra.packwiz.installer.ui.IOptionDetails
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
}
@@ -152,6 +153,9 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
}
}
// TODO: if already exists and has correct hash, ignore?
// TODO: add .disabled support?
try {
val hash: Hash
val fileHashFormat: String
@@ -176,11 +180,18 @@ internal class DownloadTask private constructor(val metadata: IndexFile.File, de
}
if (fileSource.hashIsEqual(hash)) {
Files.createDirectories(destPath.parent)
// isDirectory follows symlinks, but createDirectories doesn't
try {
Files.createDirectories(destPath.parent)
} catch (e: java.nio.file.FileAlreadyExistsException) {
if (!Files.isDirectory(destPath.parent)) {
throw e
}
}
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

@@ -1,9 +1,11 @@
@file:JvmName("Main")
package link.infra.packwiz.installer
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.ui.CLIHandler
import link.infra.packwiz.installer.ui.InputStateHandler
import link.infra.packwiz.installer.ui.InstallWindow
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
@@ -16,6 +18,9 @@ import kotlin.system.exitProcess
@Suppress("unused")
class Main(args: Array<String>) {
// Don't attempt to start a GUI if we are headless
private var guiEnabled = !GraphicsEnvironment.isHeadless()
private fun startup(args: Array<String>) {
val options = Options()
addNonBootstrapOptions(options)
@@ -25,65 +30,65 @@ class Main(args: Array<String>) {
val cmd = try {
parser.parse(options, args)
} catch (e: ParseException) {
e.printStackTrace()
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} catch (e1: Exception) {
// Ignore the exceptions, just continue using the ugly L&F
Log.fatal("Failed to parse command line arguments", e)
if (guiEnabled) {
EventQueue.invokeAndWait {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} catch (ignored: Exception) {
// Ignore the exceptions, just continue using the ugly L&F
}
JOptionPane.showMessageDialog(null, "Failed to parse command line arguments: $e",
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
}
}
JOptionPane.showMessageDialog(null, e.message, "packwiz-installer", JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
// if "headless", GUI creation will fail anyway!
val ui = if (cmd.hasOption("no-gui") || GraphicsEnvironment.isHeadless()) {
CLIHandler()
} else InstallWindow()
if (guiEnabled && cmd.hasOption("no-gui")) {
guiEnabled = false
}
val ui = if (guiEnabled) GUIHandler() else CLIHandler()
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(it)
val title = cmd.getOptionValue("title")
if (title != null) {
ui.title = title
}
val inputStateHandler = InputStateHandler()
ui.show(inputStateHandler)
ui.show()
val uOptions = UpdateManager.Options().apply {
side = cmd.getOptionValue("side")?.let { UpdateManager.Options.Side.from(it) } ?: 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!
// TODO: start in SwingWorker?
try {
ui.executeManager {
try {
UpdateManager(uOptions, ui, inputStateHandler)
} catch (e: Exception) { // TODO: better error message?
ui.handleExceptionAndExit(e)
}
}
} catch (e: Exception) { // TODO: better error message?
ui.handleExceptionAndExit(e)
UpdateManager(uOptions, ui)
} catch (e: Exception) {
ui.showErrorAndExit("Update process failed", e)
}
println("Finished successfully!")
ui.dispose()
}
companion object {
// Called by packwiz-installer-bootstrap to set up the help command
@JvmStatic
fun addNonBootstrapOptions(options: Options) {
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)")
options.addOption(null, "title", true, "Title of the installer window")
@@ -92,6 +97,7 @@ class Main(args: Array<String>) {
}
// TODO: link these somehow so they're only defined once?
@JvmStatic
private fun addBootstrapOptions(options: Options) {
options.addOption(null, "bootstrap-update-url", true, "Github API URL for checking for updates")
options.addOption(null, "bootstrap-update-token", true, "Github API Access Token, for private repositories")
@@ -108,15 +114,17 @@ class Main(args: Array<String>) {
try {
startup(args)
} catch (e: Exception) {
e.printStackTrace()
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
Log.fatal("Error from main", e)
if (guiEnabled) {
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n$e",
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
exitProcess(1)
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

@@ -18,8 +18,9 @@ import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
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.InputStateHandler
import link.infra.packwiz.installer.ui.InstallProgress
import link.infra.packwiz.installer.ui.data.InstallProgress
import link.infra.packwiz.installer.util.Log
import link.infra.packwiz.installer.util.ifletOrErr
import okio.buffer
import java.io.FileNotFoundException
import java.io.FileReader
@@ -27,14 +28,13 @@ import java.io.FileWriter
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.CompletionService
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorCompletionService
import java.util.concurrent.Executors
import kotlin.system.exitProcess
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface, private val stateHandler: InputStateHandler) {
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface) {
private var cancelled = false
private var cancelledStartGame = false
private var errorsOccurred = false
@@ -44,11 +44,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"),
@@ -110,40 +116,35 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
gson.fromJson(FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
ManifestFile::class.java)
} catch (e: FileNotFoundException) {
ui.firstInstall = true
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 (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
handleCancellation()
}
ui.submitProgress(InstallProgress("Loading pack file..."))
val packFileSource = try {
Objects.requireNonNull(opts.downloadURI)
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)
}
}
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
handleCancellation()
}
@@ -171,36 +172,42 @@ 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 (!stateHandler.optionsButton) {
if (!ui.optionsButtonPressed) {
return
}
}
println("Modpack name: " + pf.name)
Log.info("Modpack name: ${pf.name}")
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
handleCancellation()
}
try {
// This is badly written, I'll probably heavily refactor it at some point
// The port to Kotlin made this REALLY messy!!!!
getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.index)!!.file)?.let {
pf.index!!.hashFormat?.let { it1 ->
processIndex(it,
getHash(Objects.requireNonNull(pf.index!!.hashFormat)!!, Objects.requireNonNull(pf.index!!.hash)!!), it1, manifest, invalidatedUris)
// TODO: switch to OkHttp for better redirect handling
ui.ifletOrErr(pf.index, "No index file found, or the pack file is empty; note that Java doesn't automatically follow redirects from HTTP to HTTPS (and may cause this error)") { 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()
@@ -219,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)
}
}
@@ -230,8 +236,8 @@ 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!")
if (!stateHandler.optionsButton) {
Log.info("Modpack files are already up to date!")
if (!ui.optionsButtonPressed) {
return
}
}
@@ -241,22 +247,19 @@ 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)) {
// TODO: throw exception
println("I was meant to put an error message here but I'll do that later")
return
ui.showErrorAndExit("Your index file hash is invalid! The pack developer should packwiz refresh on the pack again")
}
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
return
}
@@ -273,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
@@ -284,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()
@@ -293,7 +295,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
}
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
return
}
@@ -301,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?
@@ -324,7 +326,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
f.updateFromCache(file)
}
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
return
}
@@ -335,17 +337,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
if (failedTaskDetails.isNotEmpty()) {
errorsOccurred = true
val exceptionListResult: ExceptionListResult
exceptionListResult = try {
ui.showExceptions(failedTaskDetails, tasks.size, true).get()
} catch (e: InterruptedException) { // Interrupted means cancelled???
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
when (exceptionListResult) {
when (ui.showExceptions(failedTaskDetails, tasks.size, true)) {
ExceptionListResult.CONTINUE -> {}
ExceptionListResult.CANCEL -> {
cancelled = true
@@ -358,7 +350,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
}
if (stateHandler.cancelButton) {
if (ui.cancelButtonPressed) {
showCancellationDialog()
return
}
@@ -367,23 +359,15 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
// If options changed, present all options again
if (stateHandler.optionsButton || optionTasks.any(DownloadTask::isNewOptional)) {
if (ui.optionsButtonPressed || optionTasks.any(DownloadTask::isNewOptional)) {
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
val cancelledResult = ui.showOptions(ArrayList(optionTasks))
try {
if (cancelledResult.get()) {
cancelled = true
// TODO: Should the UI be closed somehow??
return
}
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e)
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
if (ui.showOptions(ArrayList(optionTasks))) {
cancelled = true
handleCancellation()
}
}
ui.disableOptionsButton()
// TODO: keep this enabled? then apply changes after download process?
ui.disableOptionsButton(optionTasks.isNotEmpty())
// TODO: different thread pool type?
val threadPool = Executors.newFixedThreadPool(10)
@@ -395,18 +379,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) {
@@ -417,21 +398,15 @@ 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))
if (stateHandler.cancelButton) { // Stop all tasks, don't launch the game (it's in an invalid state!)
if (ui.cancelButtonPressed) { // Stop all tasks, don't launch the game (it's in an invalid state!)
threadPool.shutdown()
cancelled = true
return
@@ -444,18 +419,7 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
errorsOccurred = true
val exceptionListResult: ExceptionListResult
exceptionListResult = try {
ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false).get()
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
when (exceptionListResult) {
when (ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false)) {
ExceptionListResult.CONTINUE -> {}
ExceptionListResult.CANCEL -> cancelled = true
ExceptionListResult.IGNORE -> cancelledStartGame = true
@@ -464,23 +428,13 @@ class UpdateManager internal constructor(private val opts: Options, val ui: IUse
}
private fun showCancellationDialog() {
val cancellationResult: CancellationResult
cancellationResult = try {
ui.showCancellationDialog().get()
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
when (cancellationResult) {
when (ui.showCancellationDialog()) {
CancellationResult.QUIT -> cancelled = true
CancellationResult.CONTINUE -> cancelledStartGame = true
}
}
// TODO: move to UI?
private fun handleCancellation() {
if (cancelled) {
println("Update cancelled by user!")

View File

@@ -4,7 +4,8 @@ object HashUtils {
private val hashTypeConversion: Map<String, IHasher> = mapOf(
"sha256" to HashingSourceHasher("sha256"),
"sha512" to HashingSourceHasher("sha512"),
"murmur2" to Murmur2Hasher()
"murmur2" to Murmur2Hasher(),
"sha1" to HashingSourceHasher("sha1")
)
@JvmStatic

View File

@@ -5,7 +5,7 @@ import okio.Source
class HashingSourceHasher internal constructor(private val type: String) : IHasher {
// i love naming things
private inner class HashingSourceGeneralHashingSource internal constructor(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
private inner class HashingSourceGeneralHashingSource(val delegateHashing: HashingSource) : GeneralHashingSource(delegateHashing) {
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
HashingSourceHash(delegateHashing.hash.hex())
}
@@ -35,6 +35,7 @@ class HashingSourceHasher internal constructor(private val type: String) : IHash
"md5" -> return HashingSourceGeneralHashingSource(HashingSource.md5(delegate))
"sha256" -> return HashingSourceGeneralHashingSource(HashingSource.sha256(delegate))
"sha512" -> return HashingSourceGeneralHashingSource(HashingSource.sha512(delegate))
"sha1" -> return HashingSourceGeneralHashingSource(HashingSource.sha1(delegate))
}
throw RuntimeException("Invalid hash type provided")
}

View File

@@ -10,7 +10,7 @@ class Murmur2Hasher : IHasher {
val tempBuffer = Buffer()
override val hash: Hash by lazy(LazyThreadSafetyMode.NONE) {
val data = computeNormalizedArray(internalBuffer.readByteArray())
val data = internalBuffer.readByteArray()
Murmur2Hash(Murmur2Lib.hash32(data, data.size, 1))
}
@@ -19,27 +19,42 @@ class Murmur2Hasher : IHasher {
val out = delegate.read(tempBuffer, byteCount)
if (out > -1) {
sink.write(tempBuffer.clone(), out)
internalBuffer.write(tempBuffer, out)
computeNormalizedBufferFaster(tempBuffer, internalBuffer)
}
return out
}
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
private fun computeNormalizedArray(input: ByteArray): ByteArray {
val output = ByteArray(input.size)
// private fun computeNormalizedArray(input: ByteArray): ByteArray {
// val output = ByteArray(input.size)
// var index = 0
// for (b in input) {
// when (b) {
// 9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
// else -> {
// output[index] = b
// index++
// }
// }
// }
// val outputTrimmed = ByteArray(index)
// System.arraycopy(output, 0, outputTrimmed, 0, index)
// return outputTrimmed
// }
private fun computeNormalizedBufferFaster(input: Buffer, output: Buffer) {
var index = 0
for (b in input) {
when (b.toInt()) {
9, 10, 13, 32 -> {}
val arr = input.readByteArray()
for (b in arr) {
when (b) {
9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
else -> {
output[index] = b
arr[index] = b
index++
}
}
}
val outputTrimmed = ByteArray(index)
System.arraycopy(output, 0, outputTrimmed, 0, index)
return outputTrimmed
output.write(arr, 0, index)
}
}

View File

@@ -1,6 +1,7 @@
package link.infra.packwiz.installer.request
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.request.handlers.RequestHandlerFile
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP
import okio.Source
@@ -9,9 +10,11 @@ object HandlerManager {
private val handlers: List<IRequestHandler> = listOf(
RequestHandlerGithub(),
RequestHandlerHTTP()
RequestHandlerHTTP(),
RequestHandlerFile()
)
// TODO: get rid of nullable stuff here
@JvmStatic
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
if (loc == null) {
@@ -30,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

@@ -0,0 +1,18 @@
package link.infra.packwiz.installer.request.handlers
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.request.IRequestHandler
import okio.Source
import okio.source
import java.nio.file.Paths
open class RequestHandlerFile : IRequestHandler {
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
return "file" == loc.scheme
}
override fun getFileSource(loc: SpaceSafeURI): Source? {
val path = Paths.get(loc.toURL().toURI())
return path.source()
}
}

View File

@@ -65,7 +65,7 @@ class RequestHandlerGithub : RequestHandlerZip(true) {
if (!("http" == scheme || "https" == scheme)) {
return false
}
return "github.com" == loc.host
// TODO: sanity checks, support for more github urls
// TODO: more match testing?
return "github.com" == loc.host && branchMatcherPattern.matcher(loc.path).matches()
}
}

View File

@@ -4,6 +4,7 @@ import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.request.IRequestHandler
import okio.Source
import okio.source
import java.net.HttpURLConnection
open class RequestHandlerHTTP : IRequestHandler {
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
@@ -12,14 +13,17 @@ open class RequestHandlerHTTP : IRequestHandler {
}
override fun getFileSource(loc: SpaceSafeURI): Source? {
val conn = loc.toURL().openConnection()
val conn = loc.toURL().openConnection() as HttpURLConnection
// TODO: when do we send specific headers??? should there be a way to signal this?
// github *sometimes* requires it, sometimes not!
//conn.addRequestProperty("Accept", "application/octet-stream");
conn.addRequestProperty("Accept", "application/octet-stream")
// TODO: include version?
conn.addRequestProperty("User-Agent", "packwiz-installer")
conn.apply {
// 30 second read timeout
readTimeout = 30 * 1000
requestMethod = "GET"
}
return conn.getInputStream().source()
return conn.inputStream.source()
}
}

View File

@@ -25,7 +25,7 @@ abstract class RequestHandlerZip(private val modeHasFolder: Boolean) : RequestHa
}
}
private inner class ZipReader internal constructor(zip: Source) {
private inner class ZipReader(zip: Source) {
private val zis = ZipInputStream(zip.buffer().inputStream())
private val readFiles: MutableMap<SpaceSafeURI, Buffer> = HashMap()
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!

View File

@@ -1,47 +0,0 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
class CLIHandler : IUserInterface {
override fun handleException(e: Exception) {
e.printStackTrace()
}
override fun show(handler: InputStateHandler) {}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
println(sb.toString())
}
override fun executeManager(task: () -> Unit) {
task()
println("Finished successfully!")
}
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
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")
}
return CompletableFuture<Boolean>().apply {
complete(false) // Can't be cancelled!
}
}
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
future.complete(ExceptionListResult.CANCEL)
return future
}
}

View File

@@ -1,35 +1,27 @@
package link.infra.packwiz.installer.ui
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import kotlin.system.exitProcess
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import link.infra.packwiz.installer.ui.data.IOptionDetails
import link.infra.packwiz.installer.ui.data.InstallProgress
interface IUserInterface {
fun show(handler: InputStateHandler)
fun handleException(e: Exception)
@JvmDefault
fun handleExceptionAndExit(e: Exception) {
handleException(e)
exitProcess(1)
}
fun show()
fun dispose()
@JvmDefault
fun setTitle(title: String) {}
fun showErrorAndExit(message: String): Nothing {
showErrorAndExit(message, null)
}
fun showErrorAndExit(message: String, e: Exception?): Nothing
var title: String
fun submitProgress(progress: InstallProgress)
fun executeManager(task: () -> Unit)
// Return true if the installation was cancelled!
fun showOptions(options: List<IOptionDetails>): Future<Boolean>
fun showOptions(options: List<IOptionDetails>): Boolean
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult>
@JvmDefault
fun disableOptionsButton() {}
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult
fun disableOptionsButton(hasOptions: Boolean) {}
@JvmDefault
fun showCancellationDialog(): Future<CancellationResult> {
return CompletableFuture<CancellationResult>().apply {
complete(CancellationResult.QUIT)
}
}
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
enum class ExceptionListResult {
CONTINUE, CANCEL, IGNORE
@@ -38,4 +30,9 @@ interface IUserInterface {
enum class CancellationResult {
QUIT, CONTINUE
}
var optionsButtonPressed: Boolean
var cancelButtonPressed: Boolean
var firstInstall: Boolean
}

View File

@@ -1,21 +0,0 @@
package link.infra.packwiz.installer.ui
class InputStateHandler {
// TODO: convert to coroutines/locks?
@get:Synchronized
var optionsButton = false
private set
@get:Synchronized
var cancelButton = false
private set
@Synchronized
fun pressCancelButton() {
cancelButton = true
}
@Synchronized
fun pressOptionsButton() {
optionsButton = true
}
}

View File

@@ -1,219 +0,0 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import java.awt.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.*
import javax.swing.border.EmptyBorder
import kotlin.system.exitProcess
class InstallWindow : IUserInterface {
private val frmPackwizlauncher: JFrame
private val lblProgresslabel: JLabel
private val progressBar: JProgressBar
private val btnOptions: JButton
private var inputStateHandler: InputStateHandler? = null
private var title = "Updating modpack..."
private var worker: SwingWorkerButWithPublicPublish<Unit, InstallProgress>? = null
private val aboutToCrash = AtomicBoolean()
// TODO: separate JFrame junk from IUserInterface junk?
init {
frmPackwizlauncher = JFrame().apply {
title = this@InstallWindow.title
setBounds(100, 100, 493, 95)
defaultCloseOperation = JFrame.EXIT_ON_CLOSE
setLocationRelativeTo(null)
// Progress bar and loading text
add(JPanel().apply {
border = EmptyBorder(10, 10, 10, 10)
layout = BorderLayout(0, 0)
progressBar = JProgressBar().apply {
isIndeterminate = true
}
add(progressBar, BorderLayout.CENTER)
lblProgresslabel = JLabel("Loading...")
add(lblProgresslabel, BorderLayout.SOUTH)
}, BorderLayout.CENTER)
// Buttons
add(JPanel().apply {
border = EmptyBorder(0, 5, 0, 5)
layout = GridBagLayout()
btnOptions = JButton("Optional mods...").apply {
alignmentX = Component.CENTER_ALIGNMENT
addActionListener {
text = "Loading..."
isEnabled = false
inputStateHandler?.pressOptionsButton()
}
}
add(btnOptions, GridBagConstraints().apply {
gridx = 0
gridy = 0
})
add(JButton("Cancel").apply {
addActionListener {
isEnabled = false
inputStateHandler?.pressCancelButton()
}
}, GridBagConstraints().apply {
gridx = 0
gridy = 1
})
}, BorderLayout.EAST)
}
}
override fun show(handler: InputStateHandler) {
inputStateHandler = handler
EventQueue.invokeLater {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
frmPackwizlauncher.isVisible = true
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun handleException(e: Exception) {
e.printStackTrace()
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"An error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
title, JOptionPane.ERROR_MESSAGE)
}
}
override fun handleExceptionAndExit(e: Exception) {
e.printStackTrace()
// TODO: Fix this mess
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
aboutToCrash.set(true)
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
title, JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
// Pause forever, so it blocks while we wait for System.exit to take effect
try {
Thread.currentThread().join()
} catch (ex: InterruptedException) { // no u
}
}
override fun setTitle(title: String) {
this.title = title
frmPackwizlauncher.let { frame ->
EventQueue.invokeLater { frame.title = title }
}
}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
// TODO: better logging library?
println(sb.toString())
worker?.publishPublic(progress)
}
override fun executeManager(task: Function0<Unit>) {
EventQueue.invokeLater {
// TODO: rewrite this stupidity to use channels??!!!
worker = object : SwingWorkerButWithPublicPublish<Unit, InstallProgress>() {
override fun doInBackground() {
task.invoke()
}
override fun process(chunks: List<InstallProgress>) {
// Only process last chunk
if (chunks.isNotEmpty()) {
val (message, hasProgress, progress, progressTotal) = chunks[chunks.size - 1]
if (hasProgress) {
progressBar.isIndeterminate = false
progressBar.value = progress
progressBar.maximum = progressTotal
} else {
progressBar.isIndeterminate = true
progressBar.value = 0
}
lblProgresslabel.text = message
}
}
override fun done() {
if (aboutToCrash.get()) {
return
}
// TODO: a better way to do this?
frmPackwizlauncher.dispose()
println("Finished successfully!")
exitProcess(0)
}
}.also {
it.execute()
}
}
}
override fun showOptions(options: List<IOptionDetails>): Future<Boolean> {
val future = CompletableFuture<Boolean>()
EventQueue.invokeLater {
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
return future
}
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
EventQueue.invokeLater {
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
return future
}
override fun disableOptionsButton() {
btnOptions.apply {
text = "Optional mods..."
isEnabled = false
}
}
override fun showCancellationDialog(): Future<IUserInterface.CancellationResult> {
val future = CompletableFuture<IUserInterface.CancellationResult>()
EventQueue.invokeLater {
val buttons = arrayOf("Quit", "Ignore")
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
future.complete(if (result == 0) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
}
return future
}
}

View File

@@ -1,13 +0,0 @@
package link.infra.packwiz.installer.ui
import javax.swing.SwingWorker
// Q: AAA WHAT HAVE YOU DONE THIS IS DISGUSTING
// A: it just makes things easier, so i can easily have one interface for CLI/GUI
// if someone has a better way to do this please PR it
abstract class SwingWorkerButWithPublicPublish<T, V> : SwingWorker<T, V>() {
@SafeVarargs
fun publishPublic(vararg chunks: V) {
publish(*chunks)
}
}

View File

@@ -0,0 +1,62 @@
package link.infra.packwiz.installer.ui.cli
import link.infra.packwiz.installer.ui.IUserInterface
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 {
@Volatile
override var optionsButtonPressed = false
@Volatile
override var cancelButtonPressed = false
@Volatile
override var firstInstall = false
override var title: String = ""
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
if (e != null) {
Log.fatal(message, e)
} else {
Log.fatal(message)
}
exitProcess(1)
}
override fun show() {}
override fun dispose() {}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
println(sb.toString())
}
override fun showOptions(options: List<IOptionDetails>): Boolean {
for (opt in options) {
opt.optionValue = true
// TODO: implement option choice in the CLI?
Log.warn("Accepting option ${opt.name} as option choosing is not implemented in the CLI")
}
return false // Can't be cancelled!
}
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) {
print(ex.name + ": ")
ex.exception.printStackTrace()
}
return ExceptionListResult.CANCEL
}
}

View File

@@ -1,4 +1,4 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.data
data class ExceptionDetails(
val name: String,

View File

@@ -1,4 +1,4 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.data
interface IOptionDetails {
val name: String

View File

@@ -1,4 +1,4 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.data
data class InstallProgress(
val message: String,

View File

@@ -1,5 +1,7 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.gui
import link.infra.packwiz.installer.ui.IUserInterface
import link.infra.packwiz.installer.ui.data.ExceptionDetails
import java.awt.BorderLayout
import java.awt.Desktop
import java.awt.event.WindowAdapter
@@ -16,7 +18,7 @@ import javax.swing.border.EmptyBorder
class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFuture<IUserInterface.ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
private val lblExceptionStacktrace: JTextArea
private class ExceptionListModel internal constructor(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
private class ExceptionListModel(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
override fun getSize() = details.size
override fun getElementAt(index: Int) = details[index].name
fun getExceptionAt(index: Int) = details[index].exception

View File

@@ -0,0 +1,150 @@
package link.infra.packwiz.installer.ui.gui
import link.infra.packwiz.installer.ui.IUserInterface
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
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.system.exitProcess
class GUIHandler : IUserInterface {
private lateinit var frmPackwizlauncher: InstallWindow
@Volatile
override var optionsButtonPressed = false
@Volatile
override var cancelButtonPressed = false
@Volatile
override var firstInstall = false
override var title = "packwiz-installer"
set(value) {
field = value
EventQueue.invokeLater { frmPackwizlauncher.title = value }
}
init {
EventQueue.invokeAndWait {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} catch (e: Exception) {
Log.warn("Failed to set look and feel", e)
}
frmPackwizlauncher = InstallWindow(this).apply {
title = this@GUIHandler.title
}
}
}
override fun show() = EventQueue.invokeLater {
frmPackwizlauncher.isVisible = true
}
override fun dispose() = EventQueue.invokeAndWait {
frmPackwizlauncher.dispose()
}
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
val buttons = arrayOf("Quit", if (firstInstall) "Continue without installing" else "Continue without updating")
if (e != null) {
Log.fatal(message, e)
EventQueue.invokeAndWait {
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"$message: $e",
title,
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
if (result == 1) {
Log.info("User selected to continue without installing/updating, exiting with code 0...")
exitProcess(0)
} else {
Log.info("User selected to quit, exiting with code 1...")
exitProcess(1)
}
}
} else {
Log.fatal(message)
EventQueue.invokeAndWait {
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
message,
title,
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
if (result == 1) {
Log.info("User selected to continue without installing/updating, exiting with code 0...")
exitProcess(0)
} else {
Log.info("User selected to quit, exiting with code 1...")
exitProcess(1)
}
}
}
exitProcess(1)
}
override fun submitProgress(progress: InstallProgress) {
val sb = StringBuilder()
if (progress.hasProgress) {
sb.append('(')
sb.append(progress.progress)
sb.append('/')
sb.append(progress.progressTotal)
sb.append(") ")
}
sb.append(progress.message)
Log.info(sb.toString())
EventQueue.invokeLater {
frmPackwizlauncher.displayProgress(progress)
}
}
override fun showOptions(options: List<IOptionDetails>): Boolean {
val future = CompletableFuture<Boolean>()
EventQueue.invokeAndWait {
if (options.isEmpty()) {
JOptionPane.showMessageDialog(null,
"This modpack has no optional mods!",
"Optional mods", JOptionPane.INFORMATION_MESSAGE)
future.complete(false)
} else {
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
}
return future.get()
}
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
val future = CompletableFuture<ExceptionListResult>()
EventQueue.invokeLater {
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
isVisible = true
}
}
return future.get()
}
override fun disableOptionsButton(hasOptions: Boolean) = EventQueue.invokeLater {
frmPackwizlauncher.disableOptionsButton(hasOptions)
}
override fun showCancellationDialog(): IUserInterface.CancellationResult {
val future = CompletableFuture<IUserInterface.CancellationResult>()
EventQueue.invokeLater {
val buttons = arrayOf("Quit", "Ignore")
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
future.complete(if (result == 0) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
}
return future.get()
}
}

View File

@@ -0,0 +1,86 @@
package link.infra.packwiz.installer.ui.gui
import link.infra.packwiz.installer.ui.data.InstallProgress
import java.awt.BorderLayout
import java.awt.Component
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import javax.swing.*
import javax.swing.border.EmptyBorder
class InstallWindow(private val handler: GUIHandler) : JFrame() {
private var lblProgresslabel: JLabel
private var progressBar: JProgressBar
private var btnOptions: JButton
init {
setBounds(100, 100, 493, 95)
// Works better with tiling window managers - there isn't any reason to change window size currently anyway
isResizable = false
defaultCloseOperation = EXIT_ON_CLOSE
setLocationRelativeTo(null)
// Progress bar and loading text
add(JPanel().apply {
border = EmptyBorder(10, 10, 10, 10)
layout = BorderLayout(0, 0)
progressBar = JProgressBar().apply {
isIndeterminate = true
}
add(progressBar, BorderLayout.CENTER)
lblProgresslabel = JLabel("Loading...")
add(lblProgresslabel, BorderLayout.SOUTH)
}, BorderLayout.CENTER)
// Buttons
add(JPanel().apply {
border = EmptyBorder(0, 5, 0, 5)
layout = GridBagLayout()
btnOptions = JButton("Optional mods...").apply {
alignmentX = Component.CENTER_ALIGNMENT
addActionListener {
text = "Loading..."
isEnabled = false
handler.optionsButtonPressed = true
}
}
add(btnOptions, GridBagConstraints().apply {
gridx = 0
gridy = 0
})
add(JButton("Cancel").apply {
addActionListener {
isEnabled = false
handler.cancelButtonPressed = true
}
}, GridBagConstraints().apply {
gridx = 0
gridy = 1
})
}, BorderLayout.EAST)
}
fun displayProgress(progress: InstallProgress) {
if (progress.hasProgress) {
progressBar.isIndeterminate = false
progressBar.value = progress.progress
progressBar.maximum = progress.progressTotal
} else {
progressBar.isIndeterminate = true
progressBar.value = 0
}
lblProgresslabel.text = progress.message
}
fun disableOptionsButton(hasOptions: Boolean) {
btnOptions.apply {
text = if (hasOptions) { "Optional mods..." } else { "No optional mods" }
isEnabled = false
}
}
}

View File

@@ -1,4 +1,6 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.gui
import link.infra.packwiz.installer.ui.data.IOptionDetails
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {

View File

@@ -1,5 +1,6 @@
package link.infra.packwiz.installer.ui
package link.infra.packwiz.installer.ui.gui
import link.infra.packwiz.installer.ui.data.IOptionDetails
import java.awt.BorderLayout
import java.awt.FlowLayout
import java.awt.event.ActionEvent
@@ -18,7 +19,7 @@ class OptionsSelectWindow internal constructor(optList: List<IOptionDetails>, fu
private val tableModel: OptionTableModel
private val future: CompletableFuture<Boolean>
private class OptionTableModel internal constructor(givenOpts: List<IOptionDetails>) : TableModel {
private class OptionTableModel(givenOpts: List<IOptionDetails>) : TableModel {
private val opts: List<OptionTempHandler>
init {

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()
}
}

View File

@@ -0,0 +1,250 @@
# Licenses
packwiz-installer itself is under the MIT license, except for Murmur2Lib and bundled dependencies as follows:
- Murmur2Lib: Apache 2.0 ([Source](https://github.com/prasanthj/hasher/blob/master/src/main/java/hasher/Murmur2.java))
- Google Gson 2.8.1: Apache 2.0 ([Source](https://github.com/google/gson))
- Okio 2.9.0: Apache 2.0 ([Source](https://github.com/square/okio/))
- Commons CLI 1.4: Apache 2.0 ([Source](http://commons.apache.org/proper/commons-cli/))
- Jetbrains Annotations 13.0: Apache 2.0 ([Source](https://github.com/JetBrains/java-annotations))
- Kotlin Standard Library 1.4.21: Apache 2.0 ([Source](https://github.com/JetBrains/kotlin))
- toml4j 0.7.2: MIT ([Source](https://github.com/mwanji/toml4j))
## Associated notices
### Commons CLI
Apache Commons CLI
Copyright 2001-2017 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
## Full license texts
### MIT
MIT License
Copyright (c)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### Apache 2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.