diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..215c89a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Ignore Eclipse meta files +.metadata +.recommenders + +# Ignore Gradle\IDE project-specific directories +.gradle +.settings + +# Ignore build output directories +build +bin + +# Eclipse Core +.project + +# JDT-specific (Eclipse Java Development Tools) +.classpath diff --git a/MTEUpdater/build.gradle b/MTEUpdater/build.gradle new file mode 100644 index 0000000..6ab32d5 --- /dev/null +++ b/MTEUpdater/build.gradle @@ -0,0 +1,60 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java Library project to get you started. + * For more details take a look at the Java Libraries chapter in the Gradle + * user guide available at https://docs.gradle.org/5.0/userguide/java_library_plugin.html + */ + +plugins { + // Apply the java plugin to add support for Java + id 'java' + // Apply the application plugin to add support for building an application + id 'application' + // Gradle plugin for creating fat/uber JARs with support for package relocation + id 'com.github.johnrengelman.shadow' version '5.0.0' +} + +repositories { + // Use jcenter for resolving your dependencies. + // You can declare any Maven/Ivy/file repository here. + jcenter() + mavenCentral() +} + +dependencies { + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation 'com.google.guava:guava:26.0-jre' + + // https://mvnrepository.com/artifact/commons-io/commons-io + compile group: 'commons-io', name: 'commons-io', version: '2.6' + + // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.8.1' + + // https://mvnrepository.com/artifact/org.apache.commons/commons-compress + compile group: 'org.apache.commons', name: 'commons-compress', version: '1.18' + + // https://mvnrepository.com/artifact/org.tukaani/xz + compile group: 'org.tukaani', name: 'xz', version: '1.8' + + // Use JUnit test framework + testImplementation 'junit:junit:4.12' +} + +// Define the main class for the application +mainClassName = 'io.mte.updater.Main' + +// Change the jar build name +shadowJar { + baseName = 'MTE-Updater' + classifier = null + version = null +} + +// Define the main class for the jar manifest +jar { + manifest { + attributes 'Main-Class': 'updater/Main' + } +} diff --git a/MTEUpdater/build/libs/MTE-Updater.bat b/MTEUpdater/build/libs/MTE-Updater.bat new file mode 100644 index 0000000..eda8848 --- /dev/null +++ b/MTEUpdater/build/libs/MTE-Updater.bat @@ -0,0 +1,105 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem MTE-Updater startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME%.. +set JAVA_VERSION=18 + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto checkJavaVersion + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:checkJavaVersion +@REM Check if an appropriate version of Java is installed +if not defined JAVA_VERSION goto init + +echo Checking your Java version... +PATH %PATH%;%JAVA_HOME%\bin\ +for /f tokens^=2-5^ delims^=.-_^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j%%k" +if %jver% lss %JAVA_VERSION% ( + echo. + echo ERROR: Incorrect version of Java ^(%jver:~0,1%.%jver:~-1%^) + echo. + echo You do not have the correct version of Java installed + echo Please install Java SE Runtime Environment 8u202 ^(Java 8^) + echo which you can download from the official Java website + echo. + echo The URL has been copied to your clipboard + echo just paste it into your browser search bar and press enter + echo https://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html| clip + + goto fail +) +goto init + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Add default JVM options here. You can also use JAVA_OPTS and MTE_UPDATER_OPTS to pass JVM options to this script. +set APPPATH=MTE-Updater.jar +set "MTE_UPDATER_OPTS=-Dprogram.name=%APPPATH%" + +@rem Execute MTEUpdater +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %MTE_UPDATER_OPTS% -jar "%APPPATH%" --launcher %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable MTE_UPDATER_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%MTE_UPDATER_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff --git a/MTEUpdater/gradle.properties b/MTEUpdater/gradle.properties new file mode 100644 index 0000000..dd93083 --- /dev/null +++ b/MTEUpdater/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.home=C:/Program Files/Java/jdk1.8.0_202/ diff --git a/MTEUpdater/gradle/wrapper/gradle-wrapper.jar b/MTEUpdater/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..457aad0 Binary files /dev/null and b/MTEUpdater/gradle/wrapper/gradle-wrapper.jar differ diff --git a/MTEUpdater/gradle/wrapper/gradle-wrapper.properties b/MTEUpdater/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..75b8c7c --- /dev/null +++ b/MTEUpdater/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/MTEUpdater/gradlew b/MTEUpdater/gradlew new file mode 100644 index 0000000..af6708f --- /dev/null +++ b/MTEUpdater/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/MTEUpdater/gradlew.bat b/MTEUpdater/gradlew.bat new file mode 100644 index 0000000..6d57edc --- /dev/null +++ b/MTEUpdater/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/MTEUpdater/settings.gradle b/MTEUpdater/settings.gradle new file mode 100644 index 0000000..4bfc503 --- /dev/null +++ b/MTEUpdater/settings.gradle @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/5.0/userguide/multi_project_builds.html + */ + +rootProject.name = 'MTEUpdater' diff --git a/MTEUpdater/src/main/java/io/mte/updater/Execute.java b/MTEUpdater/src/main/java/io/mte/updater/Execute.java new file mode 100644 index 0000000..385a54a --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/Execute.java @@ -0,0 +1,189 @@ +package io.mte.updater; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; + +public class Execute { + + /** + * This method blocks until thread execution until input data is available
+ * User must press Enter to continue running the application + */ + public static void pause() { + UserInput.waitForEnter(); + } + /** + * Performs a {@code Thread.sleep} using the provided time unit + * @param unit seconds, minutes, days... + * @param time amount of time to wait + */ + public static void wait(TimeUnit unit, long time) { + try { + unit.sleep(time); + } catch(InterruptedException e) { + Logger.error("Thread was interrupted while sleeping"); + } + } + + /** + * Terminate the currently running Java Virtual Machine.
+ * Note that this will prompt the user to continue before closing the java console window + * @param code exit status (nonzero value indicates abnormal termination) + * @param clean clean all temporary files created while updating + */ + public static void exit(int code, boolean clean) { + exit (code, clean, true); + } + + /** + * Terminate the currently running Java Virtual Machine.
+ * @param code exit status (nonzero value indicates abnormal termination) + * @param clean clean all temporary files created while updating + * @param pause prompt the user to press enter to continue + */ + public static void exit(int code, boolean clean, boolean pause) { + + if (code == 0) Logger.verbose("Closing updater application..."); + else Logger.print("Terminating updater application..."); + + if (clean == true) + FileHandler.updaterCleanup(); + /* + * If we need to pause do it before we close the logfile stream + * otherwise we get errors because we are still trying to print logs + */ + if (pause == true && !Main.isLauncher()) + Execute.pause(); + + Logger.LogFile.close(); + UserInput.close(); + System.exit(code); + } + + /** + * Get process id of the currently running Java application + * @return numerical value corresponding to the process id + */ + public static short getProcessId() { + String processName = ManagementFactory.getRuntimeMXBean().getName(); + return Short.parseShort(processName.substring(0, processName.indexOf("@"))); + } + + private static boolean command(String cmd) { + + try { + Logger.print(Logger.Level.DEBUG, "Excecuting cmd command: %s in a new window", cmd); + Runtime.getRuntime().exec(cmd, null, null); + return true; + } + catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to execute Windows command: %s", cmd); + return false; + } + } + /** + * Use {@code Runtime.exec } to start a new program through Windows console.
+ * If the program is a batch script or java app, it will be opened in a new console window. + * + * @param path name or path to the program to start + * @return {@code true} if the command executed without errors + */ + public static boolean start(Path path) { + return command("cmd.exe /c start " + path.toString()); + } + + /** + * Use {@code ProcessBuilder} to start a new application or script process.
+ * Note that if you start a console application this way it will run hidden + * + * @param process path to the application or script we want to start + * @param wait pause current thread and wait for process to terminate + * @param log redirect process input stream to our logfile + * @return instance of the process started or {@code null} if an error occurred + */ + public static Process start(String process, boolean wait, boolean log) { + + Logger.print(Logger.Level.DEBUG, "Starting new process %s" + + ((wait) ? " and waiting for it to terminate" : ""), process); + try { + ProcessBuilder builder = new ProcessBuilder(process); + Process proc = builder.start(); + if (wait == true) proc.waitFor(); + + if (log == true) { + Charset charset = Charset.defaultCharset(); + Logger.LogFile.print(IOUtils.toString(proc.getInputStream(), charset)); + } + return proc; + } + catch (IOException | InterruptedException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to start new process %s", process); + return null; + } + } + /** + * Launch a new JVM process inside a console window from an executable JAR.
+ * Note that the jar file should be located inside this app's root directory. + * + * @param property system property name + * @param value system property value + * @param name java jar filename + * @param args jvm arguments + * @return {@code true} if the process launched successfully, {@code false} otherwise + */ + public static boolean launch(String property, String value, String name, String[] args) { + + String cmd = "cmd.exe /c start java " + "-D" + property + "=" + value + " -jar " + name; + for (int i = 0; i <= args.length - 1; i++) + cmd += " " + args[i]; + + return command(cmd); + } + + /** + * Find out if process with a given PID is running + * @param pid identifier of the process to find + * @return {@code true} if the process was found in the tasklist, {@code false} otherwise + */ + public static boolean isProcessRunning(String pid) + { + try { + ProcessBuilder processBuilder = new ProcessBuilder("tasklist.exe"); + Process process = processBuilder.start(); + + Scanner scanner = new Scanner(process.getInputStream(), "UTF-8"); + Scanner scannerRef = scanner.useDelimiter("\\A"); + String tasksList = scannerRef.hasNext() ? scannerRef.next() : ""; + scanner.close(); + + return tasksList.contains(pid); + } + catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to find if process %s is running", pid); + return false; + } + } + /** + * Try to kill the process with the given PID through cmd. Wait in intervals of
+ * 250ms until the process has terminated or the wait time has elapsed + * + * @param pid identifier of the process to terminate + * @param wait time in seconds to wait for execution to terminate + * @return {@code true} if the process has been killed within the wait time, {@code false} otherwise + */ + public static boolean kill(int pid, int wait) { + + String sPid = String.valueOf(pid); + Execute.command("TASKKILL /F /PID " + pid); + for (long w = TimeUnit.SECONDS.toMillis(wait); isProcessRunning(sPid) && w >= 0; w-=250) + wait(TimeUnit.MILLISECONDS, 250); + + return !isProcessRunning(sPid); + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/FileHandler.java b/MTEUpdater/src/main/java/io/mte/updater/FileHandler.java new file mode 100644 index 0000000..da89579 --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/FileHandler.java @@ -0,0 +1,441 @@ +package io.mte.updater; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.regex.Pattern; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +public class FileHandler { + + public static final UnzipUtility unzipUtility = new UnzipUtility(); + + private static List tempFiles; + protected final VersionFile local; + protected VersionFile remote; + + private static enum ReleaseFiles { + + APPLICATION("MTE-Updater.jar"), + GUIDE("Morrowind_2019.md"), + LAUNCHER("MTE-Updater.bat"); + + private File instance; + + ReleaseFiles(String name) { + instance = new File(Dir.localDirectory + File.separator + name); + } + + private static class Dir { + /** This is where the latest release will be unpacked */ + private static final String localDirectory = + FilenameUtils.removeExtension(RemoteHandler.RELEASE_FILENAME); + } + + /** + * Compare remote release files with their local counterparts + * @return a list of release files newer then local versions + */ + public static ArrayList compare() { + + ArrayList list = new ArrayList(); + for (ReleaseFiles file : ReleaseFiles.values()) { + + String name = file.instance.getName(); + File localFile = new File(name); + + if (!localFile.exists()) { + Logger.print(Logger.Level.VERBOSE, "Local file %s not found, going to update", name); + list.add(file.instance); + continue; + } + Logger.print(Logger.Level.DEBUG, "Comparing %s release to local version", name); + try { + if (!FileUtils.contentEquals(file.instance, localFile)) + list.add(file.instance); + } + catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to compare release file %s to local version", name); + continue; + } + } + return list; + } + } + + // Create file instances here at runtime + // if there is any problems we can terminate application + FileHandler() { + + // Store all our temporary file references here + tempFiles = new ArrayList(); + local = new VersionFile(RemoteHandler.VERSION_FILENAME); + } + + public class VersionFile extends File { + + private static final long serialVersionUID = 1L; + + public final String filename; + private final float releaseVer; + private final String commitSHA; + + public VersionFile(String pathname) { + + super(pathname); + filename = this.getName(); + /* + * Find out if the version file is local by checking its extension + * Remote files should have the "remote" extension + */ + boolean isLocal = !FilenameUtils.getExtension(filename).equals("remote"); + + if (!this.exists()) { + /* + * Remote files should always be present because we initialize + * them after we perform a download from repository + */ + if (isLocal != true) { + Exception e = new java.io.FileNotFoundException(); + Logger.print(Logger.Level.ERROR, e, "Unable to find %s version file!", filename); + Execute.exit(1, true); + } + //Logger.print(Logger.Level.VERBOSE, + // "Unable to find local version file %s, going to update", filename); + + releaseVer = 0; + commitSHA = ""; + return; + } + + String contents = readFile(filename); + CharSequence versionLine[] = contents.split(" "); + + // The version file should contain a single line with two numbers, + // first one being the release version and second the last release commit SHA + boolean validVersionFile = false; + String versionNumber = ""; + + if (versionLine.length == 2) + { + versionNumber = versionLine[0].toString(); + int vnLength = versionNumber.length(); + int vnDecimal = versionNumber.indexOf("."); + + // Version must be formatted properly + if (vnLength > 1 && vnDecimal > 0 && vnDecimal <= (vnLength -2)) + { + versionNumber = versionNumber.replace(".", ""); + + if (Pattern.matches("[a-z0-9]+", versionLine[1]) && StringUtils.isNumeric(versionNumber)) { + validVersionFile = true; + } + } + } + if (validVersionFile) { + releaseVer = Float.parseFloat(versionLine[0].toString()); + commitSHA = versionLine[1].toString(); + } + else { + // We still have to initialize these variables to avoid errors + releaseVer = 0; + commitSHA = null; + Logger.print(Logger.Level.ERROR, "Malformed version file %s !", filename); + Execute.exit(1, true); + } + } + + public String getReleaseVersion() { + return Float.toString(releaseVer); + } + public String getCommitSHA() { + return commitSHA; + } + } + + protected static void launchApplication() { + /* + * Create a copy of this application as a temporary file + */ + try { + Logger.debug("Creating a temporary copy of application"); + String newSelfPath = FilenameUtils.removeExtension(Main.appPath.toString()) + ".tmp"; + Path selfUpdater = Files.copy(Main.appPath, Paths.get(newSelfPath), StandardCopyOption.REPLACE_EXISTING); + + String name = selfUpdater.getFileName().toString(); + Execute.launch("program.name", name, name, new String[] + { "--update-self", String.valueOf(Main.processId), Logger.getLevel().getArguments()[0] }); + + // Exit gracefully so we don't have to be terminated + Execute.exit(0, false); + } + catch (IOException e) { + Logger.error("Unable to create a copy of this application", e); + Execute.exit(1, false); + } + } + + void doUpdate(String localSHA, String remoteSHA) { + + // Don't show changes if local version file is not present + if (localSHA != null && !localSHA.isEmpty()) { + /* + * Open the Github website with the compare arguments in URL + */ + URI compareURL = RemoteHandler.getGithubCompareLink(localSHA, remoteSHA, true, true); + if (compareURL == null || !RemoteHandler.browseWebpage(compareURL)) { + return; + } + } + // Download latest release files + Logger.print("\nDownloading release files..."); + if (!RemoteHandler.downloadLatestRelease(this)) + return; + + // Extract the release files to a new directory + Logger.print("Extracting release files..."); + if (!extractReleaseFiles()) + return; + + // Move files from the target directory + Logger.print("Updating local MTE files..."); + updateLocalFiles(); + + // Update the guide version file + Logger.print("Updating mte version file..."); + + try (PrintWriter writer = new PrintWriter(local)) { + writer.print(remote.getReleaseVersion() + " " + remote.getCommitSHA()); + Logger.print("\nYou're all set, good luck on your adventures!"); + writer.close(); + } catch (FileNotFoundException e) { + Logger.error("ERROR: Unable to find mte version file!", e); + Execute.exit(1, true); + } + } + + void updateLocalFiles() { + + Logger.verbose("Preparing to update release files..."); + ArrayList releaseFiles = ReleaseFiles.compare(); + + if (!releaseFiles.isEmpty()) { + for (Iterator iter = releaseFiles.iterator(); iter.hasNext(); ) { + + File updateFile = iter.next(); + + if (updateFile == null || !updateFile.exists()) { + FileNotFoundException e = new FileNotFoundException(); + Logger.print(Logger.Level.ERROR, e, "Unable to find release file %s!", updateFile.getName()); + continue; + } + else { + Path from = updateFile.toPath(); + Path to = Paths.get(updateFile.getName()); + + Logger.print(Logger.Level.DEBUG, "Updating local file %s", updateFile.getName()); + Logger.print(Logger.Level.DEBUG, "Destination path: %s", to.toString()); + + try { + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + + } catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to overwrite local release file %s !", to.getFileName().toString()); + continue; + } + } + } + } + } + + boolean extractReleaseFiles() { + + try { + unzipUtility.unzip(RemoteHandler.RELEASE_FILENAME, ReleaseFiles.Dir.localDirectory); + registerTempFile(new File(ReleaseFiles.Dir.localDirectory)); + return true; + } catch (IOException e) { + Logger.error("Unable to extract the GH repo file!", e); + return false; + } + } + + /** + * Create a version file instance and register as a temporary file + */ + void registerRemoteVersionFile() { + + remote = new VersionFile(RemoteHandler.VERSION_FILENAME + ".remote"); + registerTempFile(remote); + } + + /** + * Clean up all temporary files created in the update process + */ + static void updaterCleanup() { + + Logger.verbose("Recycling residual temporary files"); + String fileEntryName = "unknown"; + try { + ListIterator tempFileItr = tempFiles.listIterator(); + while (tempFileItr.hasNext()) { + + File fileEntry = tempFileItr.next(); + fileEntryName = fileEntry.getName(); + Logger.print(Logger.Level.DEBUG, "Recycling entry: %s", fileEntryName); + /* + * Make sure the file exists before we attempt to delete it + * If it's a directory use AC-IO to delete the directory recursively + */ + if (fileEntry.exists()) { + if (fileEntry.isDirectory()) + FileUtils.deleteDirectory(fileEntry); + else + // this might not work every time though + fileEntry.deleteOnExit(); + } + } + } catch (SecurityException | IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to delete temporary file %s !", fileEntryName); + } + /* + * Time to delete the temporary updater jar file + * Do this only if we are in the appropriate run mode + */ + if (Main.isSelfUpdating()) + updateSelf(); + } + + /** + * Run the final stage of the updater process. + * The application will create an un-installer batch script and then run it.
+ * The script will delete the jvm application file as well as itself. + * If we ran the {@link #updaterCleanup()} method before, this should leave + * our root folder completely clean of all temporary files. + */ + private static void updateSelf() { + + File uninstaller = new File("MTE-Updater-uninstall.bat"); + try { + uninstaller.createNewFile(); + } catch (IOException e) { + Logger.error("Unable to create uninstaller file", e); + Execute.exit(1, false); + } + String[] batchLines = + { + "@echo off", + "set \"process=" + System.getProperty("program.name") + "\"", + "set \"logfile=" + Logger.LogFile.NAME + "\"", + "echo Running uninstaller script >> %logfile%", + ":uninstall", + "timeout /t 1 /nobreak > nul", + "2>nul ren %process% %process% && goto next || goto uninstall", + ":next", + "if exist %process% (", + " echo Recycling temporary application file >> %logfile%", + " del %process%", + ") else ( echo [ERROR] Unable to delete application file %process% >> %logfile% )", + "if exist %~n0%~x0 (", + " echo Recycling uninstaller script >> %logfile%", + " del %~n0%~x0", + ") else ( echo [ERROR] Unable to delete uninstaller script %~n0%~x0 >> %logfile% )" + }; + + try (PrintWriter writer = new PrintWriter(uninstaller.getName())) { + + for (int i = 0; i <= batchLines.length - 1; i++) { + writer.println(batchLines[i]); + } + writer.close(); + } + catch (FileNotFoundException e) { + Logger.error("ERROR: Unable to locate uninstaller script!", e); + return; + } + + Logger.debug("Launching uninstaller from JVM"); + Execute.start(uninstaller.getName(), false, false); + } + + /** + * Any files added here will be deleted before terminating application + * @param tmpFile + */ + void registerTempFile(File tmpFile) { + + if (tmpFile.exists()) { + Logger.print(Logger.Level.DEBUG, "Registering temporary file %s", tmpFile.getName()); + tempFiles.add(tmpFile); + } + else + Logger.print(Logger.Level.WARNING, "Trying to register a non-existing " + + "temporary file %s", tmpFile.getName()); + } + + /** + * Here we are using URL openStream method to create the input stream. Then we + * are using a file output stream to read data from the input stream and write + * to the file. + * + * @param url + * @param file + * @throws IOException + */ + boolean downloadUsingStream(URL url, String file) throws IOException { + + Logger.print(Logger.Level.DEBUG, "Downloading file %s from %s", file, url.toString()); + BufferedInputStream bis = new BufferedInputStream(url.openStream()); + FileOutputStream fis = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + int count = 0; + while ((count = bis.read(buffer, 0, 1024)) != -1) { + fis.write(buffer, 0, count); + } + + fis.close(); + bis.close(); + + File dlFile = new File(file); + if (!dlFile.exists()) { + Logger.print(Logger.Level.ERROR, "Unable to find downloaded file %s", dlFile.getName()); + return false; + } + return true; + } + + /** + * Read from a text file and return the compiled string + * + * @param filename Name of the file to read from the root directory + * @return Content of the text file + */ + String readFile(String filename) { + + // Using Apache Commons IO here + try (FileInputStream inputStream = new FileInputStream(filename)) { + return IOUtils.toString(inputStream, "UTF-8"); + } catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to read file %s", filename); + return null; + } + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/Logger.java b/MTEUpdater/src/main/java/io/mte/updater/Logger.java new file mode 100644 index 0000000..4abf506 --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/Logger.java @@ -0,0 +1,291 @@ +package io.mte.updater; + +import java.io.File; +import java.io.PrintWriter; +import java.util.regex.Matcher; + +public class Logger { + /** + *

+ * The following logger levels regulate what kind of + * log output is permitted by the application.
+ * Define logger level by running the application with the appropriate JVM argument. + *

+ *

LOG

+ *
    + *
  • This is the default behavior, prints only normal logs.
  • + *
  • Run with JVM argument: none
  • + *
+ *

VERBOSE

+ *
    + *
  • When you want more information, prints extended logs and warnings.
  • + *
  • Run with JVM argument: -v, -verbose
  • + *
+ *

DEBUG

+ *
    + *
  • When you are debugging the application, print everything including debug logs.
  • + *
  • Run with JVM argument: -d, -debug
  • + *
+ */ + // TODO Add new level type that prints stackTrace directly in console + public enum Level { + + LOG(Short.parseShort("0"), "", "[LOG]", ""), + ERROR(Short.parseShort("0"), "ERROR", "[ERROR]", ""), + WARNING(Short.parseShort("1"), "Warning", "[WARNING]", ""), + VERBOSE(Short.parseShort("1"), "", "[LOG]", "-v", "-verbose"), + DEBUG(Short.parseShort("2"), "DEBUG", "[DEBUG]", "-d", "-debug"); + + private final short level; + private final String[] arguments; + private final String[] tags; + + Level(short lvl, String consoleTag, String logTag, String...args) { + + tags = new String[] { consoleTag, logTag }; + level = lvl; + arguments = args; + } + + public static Level getLoggerLevel(String[] args) { + /* + * Iterate through every enum entry + */ + for (Level entry : Level.values()) { + /* + * Iterate through the list of arguments supplied + */ + for (int i = args.length - 1; i >= 0; i--) { + /* + * Compare current argument supplied with each argument in the + * current level entry to find a match + */ + for (int i2 = entry.arguments.length - 1; i2 >= 0; i2--) { + if (args[i].equals(entry.arguments[i2])) + return entry; + } + } + }/* + * If no logging argument has been passed + * just set the regular logging level + */ + return LOG; + } + public String[] getArguments() { + return arguments; + } + private String getConsoleTag() { + return tags[0]; + } + private String getLogTag() { + return tags[1]; + } + } + + public static class LogFile { + + public static final String NAME = "MTE-Updater.log"; + + private static LogFile instance; + private static PrintWriter writer; + private static File file; + + LogFile() { + try { + file = new File(NAME); + file.createNewFile(); + writer = new PrintWriter(NAME); + } + catch (java.io.IOException e) { + Logger.print(Level.ERROR, e, "Unable to create log or access log file %s", NAME); + } + } + /** + * Purge the log file and create a new instance + */ + private static void init() { + if (instance == null) + instance = new LogFile(); + else + warning("Trying to initialize LogFile more then once"); + } + public static void close() { + writer.close(); + } + /*private static void clear() { + try { + writer = new PrintWriter(NAME); + writer.close(); + } + catch (java.io.FileNotFoundException e) { + error("Unable to close log file, missing in action", e); + } + }*/ + public static void print(String log, Level lvl) { + if (instance != null) { + writer.println(lvl.getLogTag() + " " + log.replace("\n", "")); + writer.flush(); + } + } + public static void print(String log) { + print(log, Logger.Level.LOG); + } + public static void print(Exception e) { + if (e != null && instance != null) + e.printStackTrace(writer); + } + } + + private static Level LOGGER_LEVEL; + private static Logger logger; + + private Logger(String[] args) { + LOGGER_LEVEL = Level.getLoggerLevel(args); + LogFile.init(); + } + /** + * Call only once from the main method to create a new logger instance + * @param args jvm arguments to search for a logger level argument + * @param test perform a series of logging tests + */ + public static void init(String[] args, boolean test) { + + if (logger != null) + warning("Trying to initialize logger more then once"); + + Logger.logger = new Logger(args); + verbose("Logger initialized with level: " + Logger.getLevel()); + if (test == true) test(); + } + + /* Public getter function to retrieve logger level */ + public static Level getLevel() { + return LOGGER_LEVEL; + } + + /** Did the application start in debug mode */ + public static boolean isDebug() { + return LOGGER_LEVEL == Level.DEBUG; + } + + /** + * See if we are allowed to print the log with argument level + * @param lvl Level of the log we want to print + * */ + private static boolean canPrintLog(Level lvl) { + + if (logger == null) { + print("[Warning] " + "Trying to print a log before the logger has initialized"); + return false; + } + else + return lvl.level <= Logger.getLevel().level; + } + + /** + * Employ {@code printf} method to output log when you have string items you want
+ * wrapped in single quotation marks. Also accepts a single item as an argument. + * + * @param lvl Logging level of this log + * @param format A format string to process and print + * @param items Array of string items to wrap + */ + public static void print(Level lvl, String format, String...items) { + + if (items == null || items.length == 1 && items[0].isEmpty()) { + warning("Attempting to print log with incorrect number of arguments (0)"); + } + else if (canPrintLog(lvl)) { + if (items.length > 1) { + /* + * Wrap each string item with single quotation marks + */ + items = String.join("' '", items).split("\\s+"); + items[0] = "'" + items[0]; + items[items.length - 1] += "'"; + /* + * Format the log string like printf method does + */ + for (int i = 0; i <= items.length - 1; i++) { + format = format.replaceFirst("%s", Matcher.quoteReplacement(items[i])); + } + print(format, lvl); + } + else { + String format2 = (String)("'" + Matcher.quoteReplacement(items[0]) + "'"); + print(format.replaceFirst("%s", format2), lvl); + } + } + } + /** + *

+ * Employ {@code printf} method to output log when you have string + * items you want wrapped in single
quotation marks + * Use this overload method when you want to print stack trace to logfile. + *

+ * See the {@link Logger#print(Level, String, String...) overloaded method} for additional information + *

+ */ + public static void print(Level lvl, Exception e, String format, String...items) { + print(lvl, format, items); + LogFile.print(e); + } + + public static void print(String log) { + System.out.println(log); + LogFile.print(log, Level.LOG); + } + private static boolean print(String log, Level lvl) { + if (canPrintLog(lvl)) { + String tag = lvl.getConsoleTag(); + System.out.println(tag + (tag.isEmpty() ? "" : ": ") + log); + LogFile.print(log, lvl); + return true; + } + else return false; + } + + public static void error(String log) { + /* + * Don't ask for permission here because there are situations where + * a method might request error logging before the logger has initialized + * + * if (canPrintLog(Level.ERROR)) + */ + print(log, Level.ERROR); + LogFile.print(new Exception()); + } + public static void error(String log, Exception e) { + //if (canPrintLog(Level.ERROR)) + print(log, Level.ERROR); + LogFile.print(e); + } + public static void verbose(String log) { + print(log, Level.VERBOSE); + } + public static boolean warning(String log) { + return print(log, Level.WARNING); + } + public static void warning(String msg, Exception e) { + if (warning(msg) == true) + LogFile.print(e); + } + public static void debug(String log) { + print(log, Level.DEBUG); + } + + public static void test() { + + print("Performing a series of logging tests:\n"); + print("This is a regular log"); + print(Logger.Level.LOG, "This %s a %s log", "is", "constructed"); + print(Logger.Level.LOG, "This is also a %s log", "constructed"); + verbose("This is a verbose log"); + warning("This is a warning log"); + warning("This is a warning log with a stack trace", new Exception()); + error("This is an error log"); + error("This is an error log with a stack trace", new Exception()); + debug("This is a debug log"); + print("Finished testing the logging system!"); + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/Main.java b/MTEUpdater/src/main/java/io/mte/updater/Main.java new file mode 100644 index 0000000..5a73867 --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/Main.java @@ -0,0 +1,162 @@ +package io.mte.updater; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +import io.mte.updater.UserInput.Key; + +public class Main { + + public static final Path root = Paths.get(System.getProperty("user.dir")); + public static final Path appPath = Paths.get(root + File.separator + System.getProperty("program.name")); + public static final short processId = Execute.getProcessId(); + + // TODO: Document this variable + public static String runMode; + + // Use this class instance to handle all file related stuff + public static final FileHandler fileHandler = new FileHandler(); + + public static void main(String[] args) + { + try { + // Initialize logger first so we can output logs + Logger.init(args, false); + + if (args != null && args.length > 0) + processJVMArguments(args); + + updateMWSE(); + runMTEUpdater(); + Execute.exit(0, true); + } + catch(Exception e) { + Logger.error("Unhandled exception occured in main method", e); + Execute.exit(1, false); + } + } + + private static void processJVMArguments(String[] args) { + + Logger.print(Logger.Level.DEBUG, "Started application with %s arguments", String.join(" ", args)); + //for (int i = args.length - 1; i >= 0; i--) {} + + // The first argument should always be a run mode definition + runMode = args[0]; + + if (runMode == null || runMode.isEmpty()) { + Exception e = new IllegalArgumentException(); + Logger.error("Application run mode has not been defined", e); + Execute.exit(1, false); + } + else if (isLauncher()) { + FileHandler.launchApplication(); + } + else if (isSelfUpdating()) { + /* + * We expect to find the process id of the main Java + * application in the following argument + */ + if (args.length > 1 && !args[1].isEmpty()) { + /* + * The launcher process should exit on its own + * but if it hang for some reason we terminate it here + */ + if (Execute.isProcessRunning(args[1])) { + + Logger.debug("Launcher application is still running, terminating now..."); + if (!Execute.kill(Integer.parseInt(args[1]), 5)) { + Logger.error("Unable to terminate java application"); + Execute.exit(1, false); + } + } + } + else { + Logger.error("Expected launcher PID passed as JVM argument..."); + Execute.exit(1, false); + } + } + else { + Exception e = new IllegalArgumentException(); + Logger.error("Unknown application run mode definition", e); + Execute.exit(1, false); + } + } + + private static void runMTEUpdater() { + + Logger.verbose("Start updating mte..."); + + Logger.print("\nDownloading mte version file..."); + if (!RemoteHandler.downloadRemoteVersionFile(fileHandler)) + return; + + fileHandler.registerRemoteVersionFile(); + + Logger.print("Comparing version numbers..."); + + String remoteSHA = fileHandler.remote.getCommitSHA(); + String localSHA = fileHandler.local.getCommitSHA(); + + // Compare version numbers to see if we need to update + if (!remoteSHA.equals(localSHA)) { + Logger.print("\nYour version of the guide is out of date"); + + if (localSHA.isEmpty()) { + Logger.verbose("Local version file not found, skip showing updates"); + fileHandler.doUpdate(localSHA, remoteSHA); + return; + } + // Continue asking for input until the user says yes or no + Logger.print("Would you like to see a list of recent updates?"); + Key input = UserInput.waitFor(Key.YES, Key.NO); + + if (input == Key.YES) { + fileHandler.doUpdate(localSHA, remoteSHA); + } + else if (input == Key.NO) { + + Logger.print("\nIt is strongly recommended that you update"); + Logger.print("You can always check the release section of our repository on Github:"); + Logger.print(RemoteHandler.Link.releasesPage.toString() + "\n"); + } + } else + Logger.print("\nYour version of the guide is up-to-date!"); + } + + /** + *

Run the MWSE auto-updater program

+ * This is intended to make the users life easier so they only
+ * have to run one updater that does it all for them + */ + private static void updateMWSE() { + /* + * Don't update mwse if we are running in debug mode + */ + if (!Logger.isDebug()) { + Logger.print("Attempting to update MWSE build..."); + File mwse = new File("MWSE-Update.exe"); + + if (mwse != null && mwse.exists()) { + Process proc = Execute.start(mwse.getName(), true, true); + if (proc == null || proc.exitValue() != 0) + Logger.warning("Unable to update, check logfile for more details"); + } + else + Logger.verbose("Unable to find mwse updater, skipping..."); + } + } + + // TODO: Move these values into an enum + + /** + * Did the JVM run as a launcher? + */ + public static boolean isLauncher() { + return runMode.equals("--launcher"); + } + public static boolean isSelfUpdating() { + return runMode.equals("--update-self"); + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java b/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java new file mode 100644 index 0000000..095b46c --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/RemoteHandler.java @@ -0,0 +1,127 @@ +package io.mte.updater; + +import java.net.URI; +import java.net.URL; + +public class RemoteHandler { + + public final static String RELEASE_FILENAME = "MTE-Release.7z"; + public final static String VERSION_FILENAME = "mte-version.txt"; + + public static class Link { + + /* These URL definitions should probably never change */ + private static final URL github = constructURL("https://github.com"); + private static final URL ghusercontent = constructURL("https://raw.githubusercontent.com"); + + /* These relative paths are used to construct URL's */ + private static final String repoPath = "Tyler799/Morrowind-2019"; + private static final String updaterBranch = "updater"; + private static final String masterBranch = "master"; + private static final String comparePath = "compare"; + private static final String releasePath = "releases/download"; + + /* Append this to compare links to activate rich diff display */ + private static final String richDiff = "?short_path=4a4f391#diff-4a4f391a7396ba51c9ba42372b55d34e"; + + public static final URL repository = constructURL(github, repoPath); + public static final URL commitCompare = constructURL(github, repoPath, comparePath); + public static final URL versionFile = constructURL(ghusercontent, repoPath, masterBranch, VERSION_FILENAME); + public static final URL releasesPage = constructURL(github, repoPath, "download"); + + private static URL constructURL(URL url, String...paths) { + return constructURL(url.toString() + "/" + String.join("/", paths)); + } + private static URL constructURL(URL url, String path) { + return constructURL(url.toString() + "/" + path); + } + + private static URL constructURL(String url) { + + try { + return new URL(url); + } catch (java.net.MalformedURLException e) { + Logger.print(Logger.Level.ERROR, e, "%s is not a valid URL format", url.toString()); + return null; + } + } + } + + /** + * Create a hyperlink to a direct comparison between two commits made in the + * guide repository. It's recommended to use richDiff and displayCommits. + * + * @param commit1 SHA of base commit to compare against + * @param commit2 SHA of comparing commit + * @param richDiff Make the comparison display more user friendly + * @param range View default comparison for the given commit range + * @return URI wrapped url or {@code null} if an exception was thrown + */ + static URI getGithubCompareLink(String commit1, String commit2, boolean richDiff, boolean range) { + + URL compareUrl = Link.constructURL(Link.commitCompare, commit1 + + (range ? "..." : "..") + commit2 + (richDiff ? Link.richDiff : "")); + + // Apply URI wrapper to string + try { + return new java.net.URI(compareUrl.toString()); + } catch (java.net.URISyntaxException e) { + Logger.error("URL string violates RFC 2396!", e); + e.printStackTrace(); + return null; + } + } + + /** + * Open a webpage with the provided hyperlink using the default user browser + * + * @param url web address we want to browse + * @return {@code true} if the operation was successful, {@code false} otherwise + */ + static boolean browseWebpage(URI url) { + + try { + java.awt.Desktop.getDesktop(); + if (java.awt.Desktop.isDesktopSupported()) { + java.awt.Desktop.getDesktop().browse(url); + return true; + } else { + Logger.error("Desktop class is not suppored on this platform"); + return false; + } + } catch (Exception e) { + if (e instanceof java.io.IOException) + Logger.error("Unable to open web browser, default browser is not found or it failed to launch", e); + else if (e instanceof SecurityException) + Logger.error("Unable to open web browser, security manager denied permission", e); + return false; + } + } + + static boolean downloadRemoteVersionFile(FileHandler handler) { + + try { + return handler.downloadUsingStream(Link.versionFile, VERSION_FILENAME + ".remote"); + } + catch (java.io.IOException e) { + Logger.error("Unable to download project version file!", e); + return false; + } + } + + static boolean downloadLatestRelease(FileHandler handler) { + + try { + URL releaseLink = Link.constructURL(Link.repository, Link.releasePath, "v" + handler.remote.getReleaseVersion(), RELEASE_FILENAME); + if (handler.downloadUsingStream(releaseLink, RELEASE_FILENAME)) { + handler.registerTempFile(new java.io.File(RELEASE_FILENAME)); + return true; + } + return false; + } + catch (java.io.IOException e) { + Logger.error("Unable to download repository files!", e); + return false; + } + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java b/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java new file mode 100644 index 0000000..b43dc5b --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/UnzipUtility.java @@ -0,0 +1,244 @@ +package io.mte.updater; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry; +import org.apache.commons.compress.archivers.sevenz.SevenZFile; +import org.apache.commons.io.FilenameUtils; + +public class UnzipUtility { + /** + * Size of the buffer to read/write data + */ + private static final int BUFFER_SIZE = 4096; + + /** + * Extracts a zip file specified by the zipFilePath to a directory specified by + * destDirectory (will be created if does not exists) + * + * @param zipFilePath + * @param destDirectory + */ + public boolean unzip(String zipFilePath, String destDirectory) throws IOException { + + // Make sure the zip file under the given path exists + File zipFile = new File(zipFilePath); + if (!zipFile.exists()) { + Logger.print(Logger.Level.ERROR, "Unable to find zip file with path %s", zipFilePath); + return false; + } + File destDir = new File(destDirectory); + if (!destDir.exists()) { + destDir.mkdir(); + } + + Logger.print(Logger.Level.DEBUG, "Unziping from %s to %s \n", zipFilePath, destDirectory); + /* + * Handle 7z files using Apache Commons Compress library + */ + if (FilenameUtils.getExtension(zipFilePath).equals("7z")) { + Logger.debug("Detected 7Zip archive, using Apache Commons Compress library"); + try (SevenZFile sevenZFile = new SevenZFile(new File(zipFilePath))) { + SevenZArchiveEntry entry = sevenZFile.getNextEntry(); + + // Abort if zip file is empty + if (entry == null) { + Logger.error("Zip contains no valid entries!"); + Logger.print("Aborting unzipping operation..."); + return false; + } + while (entry != null) { + + Logger.print(Logger.Level.DEBUG, "Iterating over zip entry %s", entry.getName()); + String filePath = destDirectory + File.separator + entry.getName(); + + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + File extrFile = extractFile(sevenZFile, entry, filePath); + if (extrFile == null || !extrFile.exists()) { + Logger.print(Logger.Level.ERROR, "Unable to find extracted file %s!", entry.getName()); + } + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + dir.mkdir(); + } + // iterate over next zip entry + entry = sevenZFile.getNextEntry(); + } + /* + * Don't forget to close the archive here, as no warning messages + * are displayed before compiling the application + */ + sevenZFile.close(); + } + catch (IOException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to read archive %s!", zipFile.getName()); + return false; + } + return true; + } + /* + * Use the standard method for regular zip files + */ + ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath)); + ZipEntry entry = null; + + try { + entry = zipIn.getNextEntry(); + // Abort if zip file is empty + if (entry == null) { + Logger.error("Zip contains no valid entries!"); + Logger.print("Aborting unzipping operation..."); + closeZipInputStream(zipIn); + return false; + } + } + catch (IOException e) { + Logger.error("ZIP file error has occurred, unable to get new zip entry!", e); + closeZipInputStream(zipIn); + return false; + } + // iterates over entries in the zip file + while (entry != null) { + + Logger.print(Logger.Level.DEBUG, "Iterating over zip entry %s", entry.getName()); + String filePath = destDirectory + File.separator + entry.getName(); + + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + File extrFile = extractFile(zipIn, entry.getName(), filePath); + if (extrFile == null || !extrFile.exists()) { + Logger.print(Logger.Level.ERROR, "Unable to find extracted file %s!", entry.getName()); + } + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + dir.mkdir(); + } + try { + zipIn.closeEntry(); + // iterate over next zip entry + entry = zipIn.getNextEntry(); + } + catch (IOException e) { + Logger.error("Unable to close or get next zip entry!", e); + Logger.print("Aborting unzipping operation..."); + return false; + } + } + if (!closeZipInputStream(zipIn)) + return false; + + return true; + } + + /** + * Extracts a regular zip file entry using standard methods + * + * @param zipIn Zip stream we are extracting from + * @param filename Used for debug purposes + * @param filePath Where we want to extract + * @return New instance of the extracted file or {@code null} if an error occurred + */ + @SuppressWarnings("resource") + private File extractFile(ZipInputStream zipIn, String filename, String filePath) { + + Logger.print(Logger.Level.DEBUG, "Extracting zip file %s to %s", filename, filePath); + BufferedOutputStream bos = null; + try { + bos = new BufferedOutputStream(new FileOutputStream(filePath)); + } + catch (java.io.FileNotFoundException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to create new output stream for path %s!", filePath); + return null; + } + byte[] bytesIn = new byte[BUFFER_SIZE]; + int read = 0; + try { + while ((read = zipIn.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + } + catch (IOException e) { + Logger.error("Unable to read or write from zip input stream!", e); + closeZipOutputStream(bos); + return null; + } + if (!closeZipOutputStream(bos)) + return null; + /* + * We are getting a warning here that the output stream might not be closed + * but that is not true, we are trying to close it in 'closeZipOutputStream()' + */ + return new File(filePath); + } + + /** + * Extract a 7z archive file using Apache Commons Compress library + * + * @param sevenZFile Zip file we are extracting from + * @param entry Zip entry that we are extracting + * @param filePath Extraction destination + * @return New instance of the extracted file or {@code null} if an error occurred + */ + @SuppressWarnings("resource") + private File extractFile(SevenZFile sevenZFile, SevenZArchiveEntry entry, String filePath) { + + Logger.print(Logger.Level.DEBUG, "Extracting zip file %s to %s", entry.getName(), filePath); + BufferedOutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(filePath)); + } + catch (java.io.FileNotFoundException e) { + Logger.print(Logger.Level.ERROR, e, "Unable to create new output stream for path %s!", filePath); + return null; + } + try { + byte[] content = new byte[(int) entry.getSize()]; + sevenZFile.read(content, 0, content.length); + out.write(content); + } + catch (IOException e) { + Logger.error("Unable to read or write from zip input stream!", e); + closeZipOutputStream(out); + return null; + } + if (!closeZipOutputStream(out)) + return null; + /* + * We are getting a warning here that the output stream might not be closed + * but that is not true, we are trying to close it in 'closeZipOutputStream()' + */ + return new File(filePath); + } + + private boolean closeZipOutputStream(BufferedOutputStream stream) + { + try { + stream.close(); + return true; + } + catch (IOException e) { + Logger.error("Unable to close zip output stream!", e); + return false; + } + } + private boolean closeZipInputStream(ZipInputStream stream) + { + try { + stream.close(); + return true; + } + catch (IOException e) { + Logger.error("Unable to close zip input stream!", e); + return false; + } + } +} diff --git a/MTEUpdater/src/main/java/io/mte/updater/UserInput.java b/MTEUpdater/src/main/java/io/mte/updater/UserInput.java new file mode 100644 index 0000000..c25ff62 --- /dev/null +++ b/MTEUpdater/src/main/java/io/mte/updater/UserInput.java @@ -0,0 +1,99 @@ +package io.mte.updater; + +import java.io.IOException; +import java.util.Scanner; + +/** + * Always use this instead of System InputStream to read user input.
+ */ +public class UserInput { + + private final Scanner reader = new Scanner(System.in); + private final java.io.InputStream is = System.in; + private static final UserInput ui = new UserInput(); + + public enum Key { + + YES("y", "yes"), + NO("n", "no"); + + private final String[] keys; + + private Key(String...s) { + keys = s; + } + public static boolean isValid(String input, Key key) { + + for (int i = key.keys.length - 1; i >= 0; i--) + { + if (input.equalsIgnoreCase(key.keys[i])) + return true; + } + return false; + } + } + /** + * Close the system input stream. Note that you will not be able to read user input
+ * after doing this so this should only be done just before exiting the application + */ + public static void close() { + ui.reader.close(); + } + /** + * Read last user keyboard input (safe to call each operation cycle). + * @return last user keyboard input + */ + public static String read() { + return ui.reader.next(); + } + /** + * Block thread execution until correct input data is available,
+ * the end of the stream is detected, or an exception is thrown + * @param keys list of user input keys to wait for + */ + public static Key waitFor(Key...keys) { + + if (keys == null || keys.length == 0) { + Logger.error("Invalid key argument passed"); + Execute.exit(1, false, false); + return null; + } + else { + if (Logger.isDebug()) { + String[] keyNames = java.util.Arrays.stream(keys).map(Enum::name).toArray(String[]::new); + Logger.print(Logger.Level.DEBUG, "Wating for user input: %s", String.join(", ", keyNames)); + } + while (ui.reader.hasNext()) + { + String input = ui.reader.next(); + for (int i = keys.length - 1; i >= 0; i--) + if (Key.isValid(input, keys[i])) + { + Logger.print(Logger.Level.DEBUG, "Found valid user key input: %s", keys[i].toString()); + return keys[i]; + } + } + Logger.error("No more tokes in user input stream"); + Execute.exit(1, false, false); + return null; + } + } + /** Block thread execution until the user presses {@code enter} key */ + public static void waitForEnter() { + /* + * Do not use scanner to scan for user input, I've been getting unknown exceptions being + * thrown with no message or stack trace. It just doesn't seem to work for some reason + * + * Using direct System InputStream seems like the best idea, and although it only works + * for ENTER at least it works and won't crash + */ + try { + Logger.print("Press Enter to continue..."); + ui.is.read(); + } + catch(IOException e) { + Logger.error("Something went wrong while reading user input", e); + Execute.exit(1, false, false); + } + } +} diff --git a/mte-version.txt b/mte-version.txt new file mode 100644 index 0000000..6f56aed --- /dev/null +++ b/mte-version.txt @@ -0,0 +1 @@ +1.4 4f5c3bd786cacb05f3f29b44b52d968ff672e3a1 \ No newline at end of file