Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 8a280d4

Browse files
committed
Migrate launcher into separate chatoverflow-launcher repository
1 parent 381c67d commit 8a280d4

26 files changed

+1417
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
target/

bootstrap/build.sbt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name := "chatoverflow-launcher-bootstrap"
2+
// s"$version-$versionSuffix" has to represent the tag name of the current release inorder for the Updater to know the current version.
3+
version := "0.3"
4+
lazy val versionSuffix = "prealpha"
5+
assemblyJarName in assembly := "ChatOverflow-Launcher.jar"
6+
7+
// Coursier is used to download the deps of the framework
8+
// Excluding argonaut and it's dependencies because we don't use any json with Coursier and that way we are able
9+
// to reduce the assembly jar file size from about 17MB (way too big) to only 8,8 MB, which is acceptable.
10+
libraryDependencies += "io.get-coursier" %% "coursier" % "2.0.0-RC3-2" excludeAll(
11+
ExclusionRule(organization = "org.scala-lang", name = "scala-reflect"),
12+
ExclusionRule(organization = "io.argonaut", name = "argonaut_2.12"),
13+
ExclusionRule(organization = "com.chuusai", name = "shapeless_2.12"),
14+
ExclusionRule(organization = "com.github.alexarchambault", name = "argonaut-shapeless_6.2_2.12")
15+
)
16+
17+
// Command Line Parsing
18+
libraryDependencies += "com.github.scopt" %% "scopt" % "3.5.0"
19+
20+
fork := true
21+
22+
packageBin / includePom := false
23+
24+
Compile / compile := {
25+
IO.write((Compile / classDirectory).value / "version.txt", version.value + "-" + versionSuffix)
26+
(Compile / compile).value
27+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import java.io.File
2+
3+
import CLI._
4+
5+
/**
6+
* The bootstrap launcher downloads all required libraries and starts chat overflow with the correct parameters.
7+
*/
8+
object Bootstrap {
9+
10+
// Java home path (jre installation folder)
11+
private val javaHomePath: String = System.getProperty("java.home")
12+
13+
// Chat Overflow Launcher / Main class (should not change anymore)
14+
private val chatOverflowMainClass = "org.codeoverflow.chatoverflow.Launcher"
15+
16+
/**
17+
* Launcher entry point.
18+
* Validates installation, downloads dependencies and start ChatOverflow.
19+
*
20+
* @param args the arguments, which are passed to ChatOverflow
21+
*/
22+
def main(args: Array[String]): Unit = {
23+
val conf: Config = ArgsParser.parse(args, Config()) match {
24+
case Some(value) => value
25+
case None => System.exit(1); null
26+
}
27+
28+
if (testValidity(conf.directory)) {
29+
println("Valid ChatOverflow installation. Checking libraries...")
30+
31+
val deps = new DependencyDownloader(conf.directory).fetchDependencies().map(u => new File(u.getFile))
32+
if (deps.nonEmpty) {
33+
val javaPath = createJavaPath()
34+
if (javaPath.isDefined) {
35+
println("Found java installation. Starting ChatOverflow...")
36+
37+
// Start chat overflow!
38+
val command = List(javaPath.get, "-cp", s"bin/*${deps.mkString(File.pathSeparator, File.pathSeparator, "")}", chatOverflowMainClass) ++ args
39+
val processBuilder = new java.lang.ProcessBuilder(command: _*)
40+
.inheritIO().directory(new File(conf.directory))
41+
42+
processBuilder.environment().put("CHATOVERFLOW_BOOTSTRAP", "true")
43+
44+
val process = processBuilder.start()
45+
46+
val exitCode = process.waitFor()
47+
println(s"ChatOverflow stopped with exit code: $exitCode")
48+
} else {
49+
println("Unable to find java installation. Unable to start.")
50+
}
51+
} else {
52+
println("Error: Problem with libraries. Unable to start.")
53+
}
54+
} else {
55+
println("Error: Invalid ChatOverflow installation. Please extract all provided files properly. Unable to start.")
56+
}
57+
}
58+
59+
/**
60+
* Takes the java home path of the launcher and tries to find the java(.exe)
61+
*
62+
* @return the path to the java runtime or none, if the file was not found
63+
*/
64+
private def createJavaPath(): Option[String] = {
65+
66+
// Check validity of java.home path first
67+
if (!new File(javaHomePath).exists()) {
68+
None
69+
} else {
70+
71+
// Check for windows and unix java versions
72+
// This should work on current and older java JRE/JDK installations,
73+
// see: https://stackoverflow.com/questions/52584888/how-to-use-jdk-without-jre-in-java-11
74+
val javaExePath = s"$javaHomePath/bin/java.exe"
75+
val javaPath = s"$javaHomePath/bin/java"
76+
77+
if (new File(javaExePath).exists()) {
78+
Some(javaExePath)
79+
} else if (new File(javaPath).exists()) {
80+
Some(javaPath)
81+
} else {
82+
None
83+
}
84+
}
85+
86+
}
87+
88+
/**
89+
* Checks, if the installation is valid
90+
*/
91+
private def testValidity(currentFolderPath: String): Boolean = {
92+
// The first check is the existence of a bin folder
93+
val binDir = new File(s"$currentFolderPath/bin")
94+
check(binDir.exists() && binDir.isDirectory, "The bin directory doesn't exist") && {
95+
// Next are the existence of a framework, api and gui jar
96+
val jars = binDir.listFiles().filter(_.getName.endsWith(".jar"))
97+
98+
check(jars.exists(_.getName.toLowerCase.startsWith("chatoverflow_")), "There is no api jar in the bin directory.") &&
99+
check(jars.exists(_.getName.toLowerCase.startsWith("chatoverflow-api")), "There is no api jar in the bin directory.") &&
100+
check(jars.exists(_.getName.toLowerCase.startsWith("chatoverflow-gui")),
101+
"Note: No gui jar detected. The ChatOverflow gui won't be usable.", required = false)
102+
}
103+
}
104+
105+
/**
106+
* Helper method for [[Bootstrap.testValidity()]]. Checks condition, prints description if the condition is false and
107+
* returns false if the condition is false and the check is required.
108+
*/
109+
private def check(condition: Boolean, description: String, required: Boolean = true): Boolean = {
110+
if (condition) {
111+
true
112+
} else {
113+
println(description)
114+
!required
115+
}
116+
}
117+
}

bootstrap/src/main/scala/CLI.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import java.io.File
2+
import java.nio.file.Paths
3+
4+
5+
object CLI {
6+
7+
/**
8+
* Everything in here also has to be defined in the CLI class of the framework, because
9+
* all arguments including bootstrap specific ones are passed through to the framework.
10+
* Filtering these options out would be way too difficult, because you need to know if a option
11+
* is a simple flag or is followed by a value. Scopt doesn't expose anything to get this so we would
12+
* need to use reflect, which is very ugly.
13+
* This, while not that elegant as I would like it to be, is just simple and works.
14+
*/
15+
object ArgsParser extends scopt.OptionParser[Config]("ChatOverflow Launcher") {
16+
opt[File]("directory")
17+
.action((x, c) => c.copy(directory = x.getAbsolutePath))
18+
.text("The directory in which ChatOverflow will be executed")
19+
.validate(f =>
20+
if (!f.exists())
21+
Left("Directory doesn't exist")
22+
else if (!f.isDirectory)
23+
Left("Path isn't a directory")
24+
else
25+
Right()
26+
)
27+
28+
override def errorOnUnknownArgument: Boolean = false
29+
30+
override def reportWarning(msg: String): Unit = ()
31+
}
32+
33+
case class Config(directory: String = Paths.get("").toAbsolutePath.toString)
34+
35+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import java.io.{File, InputStream}
2+
import java.net.{URL, URLClassLoader}
3+
4+
import coursier.Fetch
5+
import coursier.cache.FileCache
6+
import coursier.cache.loggers.{FileTypeRefreshDisplay, RefreshLogger}
7+
import coursier.core.{Configuration, Dependency}
8+
import coursier.maven.{MavenRepository, PomParser}
9+
10+
import scala.io.Source
11+
12+
class DependencyDownloader(directory: String) {
13+
private val pomFile = "dependencies.pom"
14+
private val logger = RefreshLogger.create(System.out, FileTypeRefreshDisplay.create())
15+
private val cache = FileCache().noCredentials.withLogger(logger)
16+
17+
// Classloader containing all jars, used to get the dependencies from the framework jar
18+
private val jarFiles = {
19+
val jarsOpt = Option(new File(s"$directory/bin").listFiles())
20+
jarsOpt.getOrElse(Array()).filter(_.getName.endsWith(".jar")).map(_.toURI.toURL)
21+
}
22+
private val classloader = new URLClassLoader(jarFiles)
23+
24+
private def getPomIs: InputStream = classloader.getResourceAsStream(pomFile)
25+
26+
/**
27+
* Parses the pom file of the framework jar and returns a seq of all dependencies that are required to run it.
28+
*
29+
* @return the seq of dependencies, if it is empty an error has occurred and logged.
30+
*/
31+
private def parsePom(): Seq[Dependency] = {
32+
if (getPomIs == null) {
33+
println("Couldn't find the pom containing all required dependencies for the framework in the jar.")
34+
return Seq()
35+
}
36+
37+
val pomFile = Source.fromInputStream(getPomIs)
38+
val parser = coursier.core.compatibility.xmlParseSax(pomFile.mkString, new PomParser)
39+
parser.project match {
40+
case Right(deps) =>
41+
deps.dependencies
42+
.filterNot(_._1 == Configuration.provided) // Provided deps are... well provided and no download is required
43+
.map(_._2)
44+
.filter(_.module.name.value != "chatoverflow-api") // We already have the api locally inside the bin directory
45+
case Left(errorMsg) =>
46+
println(s"Pom containing all required dependencies for the framework couldn't be parsed: $errorMsg")
47+
Seq()
48+
}
49+
}
50+
51+
/**
52+
* Fetches all required dependencies for the framework using Coursier.
53+
*
54+
* @return a seq of urls of jar files that need to be included to the classpath. A empty seq signifies an error.
55+
*/
56+
def fetchDependencies(): Seq[URL] = {
57+
val deps = parsePom()
58+
if (deps.isEmpty)
59+
return Seq()
60+
61+
// IntelliJ may warn you that a implicit is missing. This is one of the many bugs in IntelliJ, the code compiles fine.
62+
val jars: Seq[File] = Fetch()
63+
.withCache(cache)
64+
.addDependencies(deps: _*)
65+
.addRepositories(MavenRepository("https://jcenter.bintray.com")) // JCenter is used for JDA (DiscordConnector)
66+
.run
67+
68+
jars.map(_.toURI.toURL)
69+
}
70+
}

build.sbt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name := "chatoverflow-launcher"
2+
3+
lazy val launcherBootstrapProject = project in file("bootstrap")
4+
lazy val launcherUpdaterProject = project in file("updater")
5+
6+
lazy val launcherProject = (project in file("."))
7+
.aggregate(launcherBootstrapProject, launcherUpdaterProject)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@ECHO OFF
2+
3+
java -jar ChatOverflow.jar %*
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
3+
called_path=${0%/*}
4+
cd $called_path
5+
java -jar ChatOverflow.jar "$@"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
3+
java -jar ChatOverflow.jar "$@"

0 commit comments

Comments
 (0)