Package se.llbit.chunky.launcher

Source Code of se.llbit.chunky.launcher.ChunkyDeployer$ShutdownThread

/* Copyright (c) 2013-2014 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky.  If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.launcher;

import java.awt.Component;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.launcher.VersionInfo.Library;
import se.llbit.chunky.launcher.VersionInfo.LibraryStatus;
import se.llbit.json.JsonArray;
import se.llbit.json.JsonObject;
import se.llbit.json.JsonParser;
import se.llbit.json.JsonParser.SyntaxError;
import se.llbit.json.JsonValue;

/**
* Deploys the embedded Chunky version, or launches an existing local version.
* @author Jesper Öqvist <jesper@llbit.se>
*/
public class ChunkyDeployer {

  /**
   * Check the integrity of an installed version.
   * @param version
   * @return <code>true</code> if the version is installed locally
   */
  public static boolean checkVersionIntegrity(String version) {
    File chunkyDir = PersistentSettings.getSettingsDirectory();
    File versionsDir = new File(chunkyDir, "versions");
    File libDir = new File(chunkyDir, "lib");
    if (!versionsDir.isDirectory() || !libDir.isDirectory()) {
      return false;
    }

    File versionFile = new File(versionsDir, version + ".json");
    if (!versionFile.isFile()) {
      return false;
    }

    // check version
    try {
      FileInputStream in = new FileInputStream(versionFile);
      JsonParser parser = new JsonParser(in);
      JsonObject obj = parser.parse().object();
      in.close();
      String versionName = obj.get("name").stringValue("");
      if (!versionName.equals(version)) {
        System.err.println("Stored version name does not match file name");
        return false;
      }
      JsonArray array = obj.get("libraries").array();
      for (JsonValue value: array.getElementList()) {
        VersionInfo.Library lib = new VersionInfo.Library(value.object());
        switch (lib.testIntegrity(libDir)) {
        case INCOMPLETE_INFO:
          System.err.println("Missing library name or checksum");
          return false;
        case MD5_MISMATCH:
          System.err.println("Library MD5 checksum mismatch");
          return false;
        case MISSING:
          System.err.println("Missing library " + lib.name);
          return false;
        default:
          break;
        }
      }
      return true;
    } catch (IOException e) {
      System.err.println("Could not read version info file: " + e.getMessage());
    } catch (SyntaxError e) {
      System.err.println("Corrupted version info file: " + e.getMessage());
    }
    return false;
  }

  /**
   * Unpacks the embedded Chunky jar files.
   */
  public void deploy() {
    List<VersionInfo> versions = availableVersions();
    VersionInfo embedded = embeddedVersion();
    if (embedded != null && (!versions.contains(embedded) || !checkVersionIntegrity(embedded.name))) {
      if (System.getProperty("log4j.logLevel", "WARN").equals("INFO")) {
        System.out.println("Deploying embedded version: " + embedded.name);
      }
      deployEmbeddedVersion(embedded);
    }
  }

  public static List<VersionInfo> availableVersions() {
    File chunkyDir = PersistentSettings.getSettingsDirectory();
    File versionsDir = new File(chunkyDir, "versions");
    if (!versionsDir.isDirectory()) {
      return Collections.emptyList();
    }

    List<VersionInfo> versions = new ArrayList<VersionInfo>();

    for (File versionFile: versionsDir.listFiles()) {
      if (versionFile.getName().endsWith(".json")) {
        try {
          FileInputStream in = new FileInputStream(versionFile);
          JsonParser parser = new JsonParser(in);
          versions.add(new VersionInfo(parser.parse().object()));
          in.close();
        } catch (IOException e) {
          System.err.println("Could not read version info file: " + e.getMessage());
        } catch (SyntaxError e) {
          System.err.println("Corrupted version info file: " + e.getMessage());
        }
      }
    }

    Collections.sort(versions);
    return versions;
  }

  /**
   * Unpack embedded libraries and deploy the embedded Chunky version.
   * @param version
   */
  private static void deployEmbeddedVersion(VersionInfo version) {
    File chunkyDir = PersistentSettings.getSettingsDirectory();
    File versionsDir = new File(chunkyDir, "versions");
    if (!versionsDir.isDirectory()) {
      versionsDir.mkdirs();
    }
    File libDir = new File(chunkyDir, "lib");
    if (!libDir.isDirectory()) {
      libDir.mkdirs();
    }
    try {
      File versionJson = new File(versionsDir, version.name + ".json");
      version.writeTo(versionJson);

      ClassLoader parentCL = ChunkyLauncher.class.getClassLoader();

      // deploy libraries that were not already installed correctly
      for (Library lib: version.libraries) {
        if (lib.testIntegrity(libDir) != LibraryStatus.PASSED) {
          unpackLibrary(parentCL, "lib/" + lib.name,
              new File(libDir, lib.name));
        }
      }
    } catch (SecurityException e) {
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Unpack the jar file to the target directory.
   * @param parentCL
   * @param name
   * @param dest destination file
   * @return the unpacked Jar file
   * @throws IOException
   */
  private static void unpackLibrary(ClassLoader parentCL, String name, File dest)
      throws IOException {

    BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dest));
    InputStream in = parentCL.getResourceAsStream(name);
    byte[] buffer = new byte[4096];
    int len;
    while ((len = in.read(buffer)) != -1) {
      out.write(buffer, 0, len);
    }
    out.close();
  }

  private static VersionInfo embeddedVersion() {
    try {
      ClassLoader parentCL = ChunkyLauncher.class.getClassLoader();
      InputStream in = parentCL.getResourceAsStream("version.json");
      try {
        if (in != null) {
          JsonParser parser = new JsonParser(in);
          return new VersionInfo(parser.parse().object());
        }
      } catch (IOException e) {
      } catch (SyntaxError e) {
      } finally {
        if (in != null) {
          try {
            in.close();
          } catch (IOException e) {
          }
        }
      }
    } catch (SecurityException e) {
    }
    return null;
  }

  /**
   * Launch a specific Chunky version
   * @param parentComponent
   * @param settings
   * @return zero on success, non-zero if there is any problem
   * launching Chunky (waits 200ms to see if everything launched)
   */
  public int launchChunky(Component parentComponent, LauncherSettings settings, VersionInfo version,
      ChunkyMode mode) {
    List<String> command = buildCommandLine(version, settings);
    if (settings.verboseLauncher ||
        System.getProperty("log4j.logLevel", "WARN").equals("INFO")) {
      System.out.println(commandString(command));
    }
    ProcessBuilder procBuilder = new ProcessBuilder(command);
    final Logger logger;
    if (!settings.headless && settings.debugConsole) {
      DebugConsole console = new DebugConsole(null, settings.closeConsoleOnExit);
      console.setVisible(true);
      logger = console;
    } else {
      logger = new ConsoleLogger();
    }
    try {
      final Process proc = procBuilder.start();
      Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
          // kill the subprocess
          proc.destroy();
        }
      });
      final Thread outputScanner = new Thread("Output Logger") {
        @Override
        public void run() {
          InputStream is = proc.getInputStream();
          try {
            byte[] buffer = new byte[4096];
            while (true) {
              int size = is.read(buffer, 0, buffer.length);
              if (size == -1) {
                break;
              }
              logger.appendStdout(buffer, size);
            }
          } catch (IOException e) {
            try {
              is.close();
            } catch (IOException e1) {
            }
          }
        }
      };
      outputScanner.start();
      final Thread errorScanner = new Thread("Error Logger") {
        @Override
        public void run() {
          InputStream is = proc.getErrorStream();
          try {
            byte[] buffer = new byte[4096];
            while (true) {
              int size = is.read(buffer, 0, buffer.length);
              if (size == -1) {
                break;
              }
              logger.appendStderr(buffer, size);
            }
          } catch (IOException e) {
            try {
              is.close();
            } catch (IOException e1) {
            }
          }
        }
      };
      errorScanner.start();
      ShutdownThread shutdownThread = new ShutdownThread(proc, logger, outputScanner, errorScanner);
      shutdownThread.start();
      try {
        if (mode == ChunkyMode.GUI) {
          // just wait a little while to check for startup errors
          Thread.sleep(3000);
          return shutdownThread.exitValue;
        } else {
          // wait until completion so we can return correct exit code
          return shutdownThread.exitValue();
        }
      } catch (InterruptedException e) {
      }
      return 0;
    } catch (IOException e) {
      logger.appendErrorLine(e.getMessage());
      // 3 indicates launcher error
      return 3;
    }
  }

  /**
   * @param command
   * @return command in string form
   */
  public static String commandString(List<String> command) {
    StringBuilder sb = new StringBuilder();
    for (String part: command) {
      if (sb.length() > 0) {
        sb.append(" ");
      }
      sb.append(part);
    }
    return sb.toString();
  }

  public static List<String> buildCommandLine(VersionInfo version, LauncherSettings settings) {
    List<String> cmd = new LinkedList<String>();

    cmd.add(JreUtil.javaCommand(settings.javaDir));
    cmd.add("-Xmx" + settings.memoryLimit + "m");

    String[] parts = settings.javaOptions.split(" ");
    for (String part: parts) {
      if (!part.isEmpty()) {
        cmd.add(part);
      }
    }

    cmd.add("-classpath");
    cmd.add(classpath(version,settings));

    if (settings.verboseLogging) {
      cmd.add("-Dlog4j.logLevel=INFO");
    }

    cmd.add("se.llbit.chunky.main.Chunky");

    parts = settings.chunkyOptions.split(" ");
    for (String part: parts) {
      if (!part.isEmpty()) {
        cmd.add(part);
      }
    }

    return cmd;
  }

  private static String classpath(VersionInfo version, LauncherSettings settings) {
    File chunkyDir = PersistentSettings.getSettingsDirectory();
    File libDir = new File(chunkyDir, "lib");
    List<File> jars = new ArrayList<File>();
    for (VersionInfo.Library library: version.libraries) {
      jars.add(library.getFile(libDir));
    }
    String classpath = "";
    for (File file : jars) {
      if (!classpath.isEmpty()) {
        classpath += File.pathSeparator;
      }
      classpath += file.getAbsolutePath();
    }
    return classpath;
  }

  private static class ShutdownThread extends Thread {
    public volatile int exitValue = 0;
    private final Thread outputScanner;
    private final Thread errorScanner;
    private final Process proc;
    private final Logger logger;
    private boolean finished = false;

    public ShutdownThread(Process proc, Logger logger, Thread output, Thread error) {
      this.proc = proc;
      this.logger = logger;
      this.outputScanner = output;
      this.errorScanner = error;
    }

    public synchronized int exitValue() throws InterruptedException {
      while (!finished) {
        wait();
      }
      return exitValue;
    }

    @Override
    public void run() {
      try {
        outputScanner.join();
      } catch (InterruptedException e) {
      }
      try {
        errorScanner.join();
      } catch (InterruptedException e) {
      }
      try {
        proc.waitFor();
        exitValue = proc.exitValue();
        logger.processExited(exitValue);
      } catch (InterruptedException e) {
      }
      synchronized (this) {
        finished = true;
        notifyAll();
      }
    }
  }

  public static VersionInfo resolveVersion(String name) {
    List<VersionInfo> versions = availableVersions();
    VersionInfo version = VersionInfo.LATEST;
    for (VersionInfo info: versions) {
      if (info.name.equals(name)) {
        version = info;
        break;
      }
    }
    if (version == VersionInfo.LATEST) {
      if (versions.size() > 0) {
        return versions.get(0);
      } else {
        return VersionInfo.NONE;
      }
    } else {
      return version;
    }
  }

  public static boolean canLaunch(VersionInfo version, ChunkyLauncher launcher, boolean reportErrors) {
    if (version == VersionInfo.NONE) {
      // version not available!
      System.err.println("No version installed");
      if (reportErrors) {
        Dialogs.error(launcher,
            "Failed to launch Chunky - there is no local version installed. Try updating.",
            "Failed to Launch");
      }
      return false;
    }
    if (!ChunkyDeployer.checkVersionIntegrity(version.name)) {
      // TODO add some way to fix this??
      System.err.println("Version integrity check failed for version " + version.name);
      if (reportErrors) {
        Dialogs.error(launcher,
            "Version integrity check failed for version " + version.name + ". Try selecting another version.",
            "Failed to Launch");
      }
      return false;
    }
    return true;
  }
}
TOP

Related Classes of se.llbit.chunky.launcher.ChunkyDeployer$ShutdownThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.