mirror of
				https://github.com/packwiz/packwiz-installer.git
				synced 2025-11-04 12:34:31 +01:00 
			
		
		
		
	Start porting to Kotlin
This commit is contained in:
		
							
								
								
									
										123
									
								
								src/main/kotlin/link/infra/packwiz/installer/Main.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/main/kotlin/link/infra/packwiz/installer/Main.kt
									
									
									
									
									
										Normal 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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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?
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user