Start porting to Kotlin

This commit is contained in:
comp500
2019-12-19 12:25:20 +00:00
parent c0c318772b
commit b45a2983e7
13 changed files with 464 additions and 521 deletions

View File

@@ -0,0 +1,123 @@
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 org.apache.commons.cli.DefaultParser
import org.apache.commons.cli.Options
import org.apache.commons.cli.ParseException
import java.awt.EventQueue
import java.awt.GraphicsEnvironment
import java.net.URISyntaxException
import javax.swing.JOptionPane
import javax.swing.UIManager
import kotlin.system.exitProcess
@Suppress("unused")
class Main(args: Array<String>) {
private fun startup(args: Array<String>) {
val options = Options()
addNonBootstrapOptions(options)
addBootstrapOptions(options)
val parser = DefaultParser()
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
}
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()
val unparsedArgs = cmd.args
if (unparsedArgs.size > 1) {
ui.handleExceptionAndExit(RuntimeException("Too many arguments specified!"))
} else if (unparsedArgs.isEmpty()) {
ui.handleExceptionAndExit(RuntimeException("URI to install from must be specified!"))
}
cmd.getOptionValue("title")?.also {
ui.setTitle(it)
}
val inputStateHandler = InputStateHandler()
ui.show(inputStateHandler)
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])
} catch (e: URISyntaxException) {
// TODO: better error message?
ui.handleExceptionAndExit(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)
}
}
companion object {
// Called by packwiz-installer-bootstrap to set up the help command
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")
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)")
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)")
}
// TODO: link these somehow so they're only defined once?
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")
options.addOption(null, "bootstrap-no-update", false, "Don't update packwiz-installer")
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file")
options.addOption("g", "no-gui", false, "Don't display a GUI to show update progress")
options.addOption("h", "help", false, "Display this message") // Implemented in packwiz-installer-bootstrap!
}
}
// Actual main() is in RequiresBootstrap!
init {
// Big overarching try/catch just in case everything breaks
try {
startup(args)
} catch (e: Exception) {
e.printStackTrace()
EventQueue.invokeLater {
JOptionPane.showMessageDialog(null,
"A fatal error occurred: \n" + e.javaClass.canonicalName + ": " + e.message,
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
exitProcess(1)
}
// In case the EventQueue is broken, exit after 1 minute
Thread.sleep(60 * 1000.toLong())
exitProcess(1)
}
}
}

View File

@@ -0,0 +1,24 @@
package link.infra.packwiz.installer.request
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import okio.Source
/**
* IRequestHandler handles requests for locations specified in modpack metadata.
*/
interface IRequestHandler {
fun matchesHandler(loc: SpaceSafeURI): Boolean
fun getNewLoc(loc: SpaceSafeURI): SpaceSafeURI {
return loc
}
/**
* Gets the Source for a location. Must be threadsafe.
* It is assumed that each location is read only once for the duration of an IRequestHandler.
* @param loc The location to be read
* @return The Source containing the data of the file
* @throws Exception Exception if it failed to download a file!!!
*/
fun getFileSource(loc: SpaceSafeURI): Source?
}

View File

@@ -0,0 +1,71 @@
package link.infra.packwiz.installer.request.handlers
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.regex.Pattern
import kotlin.concurrent.read
import kotlin.concurrent.write
class RequestHandlerGithub : RequestHandlerZip(true) {
override fun getNewLoc(loc: SpaceSafeURI): SpaceSafeURI {
return loc
}
companion object {
private val repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*")
private val branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*")
}
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
private val zipUriMap: MutableMap<String, SpaceSafeURI> = HashMap()
private val zipUriLock = ReentrantReadWriteLock()
private fun getRepoName(loc: SpaceSafeURI): String? {
val matcher = repoMatcherPattern.matcher(loc.path)
return if (matcher.matches()) {
matcher.group(1)
} else {
null
}
}
override fun getZipUri(loc: SpaceSafeURI): SpaceSafeURI {
val repoName = getRepoName(loc)
val branchName = getBranch(loc)
zipUriLock.read {
zipUriMap["$repoName/$branchName"]
}?.let { return it }
var zipUri = SpaceSafeURI("https://api.github.com/repos/$repoName/zipball/$branchName")
zipUriLock.write {
// If another thread sets the value concurrently, use the existing value from the
// thread that first acquired the lock.
zipUri = zipUriMap.putIfAbsent("$repoName/$branchName", zipUri) ?: zipUri
}
return zipUri
}
private fun getBranch(loc: SpaceSafeURI): String? {
val matcher = branchMatcherPattern.matcher(loc.path)
return if (matcher.matches()) {
matcher.group(1)
} else {
null
}
}
override fun getLocationInZip(loc: SpaceSafeURI): SpaceSafeURI {
val path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc)
return SpaceSafeURI(loc.scheme, loc.authority, path, null, null).relativize(loc)
}
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
val scheme = loc.scheme
if (!("http" == scheme || "https" == scheme)) {
return false
}
return "github.com" == loc.host
// TODO: sanity checks, support for more github urls
}
}

View File

@@ -0,0 +1,25 @@
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
open class RequestHandlerHTTP : IRequestHandler {
override fun matchesHandler(loc: SpaceSafeURI): Boolean {
val scheme = loc.scheme
return "http" == scheme || "https" == scheme
}
override fun getFileSource(loc: SpaceSafeURI): Source? {
val conn = loc.toURL().openConnection()
// 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.apply {
// 30 second read timeout
readTimeout = 30 * 1000
}
return conn.getInputStream().source()
}
}

View File

@@ -0,0 +1,123 @@
package link.infra.packwiz.installer.request.handlers
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import okio.Buffer
import okio.Source
import okio.buffer
import okio.source
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.function.Predicate
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.concurrent.read
import kotlin.concurrent.withLock
import kotlin.concurrent.write
abstract class RequestHandlerZip(private val modeHasFolder: Boolean) : RequestHandlerHTTP() {
private fun removeFolder(name: String): String {
return if (modeHasFolder) {
// TODO: replace with proper path checks once switched to Path??
name.substring(name.indexOf("/") + 1)
} else {
name
}
}
private inner class ZipReader internal constructor(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!
val filesLock = ReentrantLock()
private var entry: ZipEntry? = null
private val zipSource = zis.source().buffer()
// File lock must be obtained before calling this function
private fun readCurrFile(): Buffer {
val fileBuffer = Buffer()
zipSource.readFully(fileBuffer, entry!!.size)
return fileBuffer
}
// File lock must be obtained before calling this function
private fun findFile(loc: SpaceSafeURI): Buffer? {
while (true) {
entry = zis.nextEntry
entry?.also {
val data = readCurrFile()
val fileLoc = SpaceSafeURI(removeFolder(it.name))
if (loc == fileLoc) {
return data
} else {
readFiles[fileLoc] = data
}
} ?: return null
}
}
fun getFileSource(loc: SpaceSafeURI): Source? {
filesLock.withLock {
// Assume files are only read once, allow GC by removing
readFiles.remove(loc)?.also { return it }
return findFile(loc)
}
}
fun findInZip(matches: Predicate<SpaceSafeURI>): SpaceSafeURI? {
filesLock.withLock {
readFiles.keys.find { matches.test(it) }?.let { return it }
do {
val entry = zis.nextEntry?.also {
val data = readCurrFile()
val fileLoc = SpaceSafeURI(removeFolder(it.name))
readFiles[fileLoc] = data
if (matches.test(fileLoc)) {
return fileLoc
}
}
} while (entry != null)
return null
}
}
}
private val cache: MutableMap<SpaceSafeURI, ZipReader> = HashMap()
private val cacheLock = ReentrantReadWriteLock()
protected abstract fun getZipUri(loc: SpaceSafeURI): SpaceSafeURI
protected abstract fun getLocationInZip(loc: SpaceSafeURI): SpaceSafeURI
abstract override fun matchesHandler(loc: SpaceSafeURI): Boolean
override fun getFileSource(loc: SpaceSafeURI): Source? {
val zipUri = getZipUri(loc)
var zr = cacheLock.read { cache[zipUri] }
if (zr == null) {
cacheLock.write {
// Recheck, because unlocking read lock allows another thread to modify it
zr = cache[zipUri]
if (zr == null) {
val src = super.getFileSource(zipUri) ?: return null
zr = ZipReader(src).also { cache[zipUri] = it }
}
}
}
return zr?.getFileSource(getLocationInZip(loc))
}
protected fun findInZip(loc: SpaceSafeURI, matches: Predicate<SpaceSafeURI>): SpaceSafeURI? {
val zipUri = getZipUri(loc)
return (cacheLock.read { cache[zipUri] } ?: cacheLock.write {
// Recheck, because unlocking read lock allows another thread to modify it
cache[zipUri] ?: run {
// Create the ZipReader if it doesn't exist, return null if getFileSource returns null
super.getFileSource(zipUri)?.let { ZipReader(it) }
?.also { cache[zipUri] = it }
}
})?.findInZip(matches)
}
}