| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.jack.launcher; |
| |
| import com.google.common.collect.Iterators; |
| |
| import com.android.jack.server.api.v01.JackServer; |
| import com.android.jack.server.api.v01.LauncherHandle; |
| import com.android.jack.server.api.v01.NotInstalledException; |
| import com.android.jack.server.api.v01.ServerException; |
| import com.android.sched.util.FinalizerRunner; |
| import com.android.sched.util.UncomparableVersion; |
| import com.android.sched.util.Version; |
| import com.android.sched.util.log.LoggerFactory; |
| import com.android.sched.util.stream.ByteStreamSucker; |
| |
| import java.io.File; |
| import java.io.FileFilter; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.lang.Thread.UncaughtExceptionHandler; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.ServiceConfigurationError; |
| import java.util.ServiceLoader; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.CheckForNull; |
| import javax.annotation.Nonnegative; |
| import javax.annotation.Nonnull; |
| |
| /** |
| * A launcher for the server. |
| */ |
| public final class ServerLauncher { |
| |
| private static class ServerInfo { |
| private final int id; |
| |
| @Nonnull |
| private final Version version; |
| |
| public ServerInfo(int id, @Nonnull Version version) { |
| this.id = id; |
| this.version = version; |
| } |
| } |
| |
| private static class TaskRunner implements Runnable { |
| private static class Task { |
| @Nonnull |
| private final String name; |
| @Nonnull |
| private final Runnable runnable; |
| |
| public Task(@Nonnull String name, @Nonnull Runnable runnable) { |
| this.name = name; |
| this.runnable = runnable; |
| } |
| } |
| @Nonnull |
| private final BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(); |
| |
| @Override |
| public void run() { |
| try { |
| while (true) { |
| Task next = taskQueue.take(); |
| Thread thread = new Thread(next.runnable, next.name); |
| thread.setDaemon(false); |
| thread.start(); |
| } |
| } catch (InterruptedException e) { |
| logger.log(Level.FINE, "Thread " + Thread.currentThread().getName() + " interrupted"); |
| Thread.currentThread().interrupt(); |
| } |
| } |
| |
| void executeTask(@Nonnull String name, @Nonnull Runnable task) { |
| taskQueue.add(new Task(name, task)); |
| } |
| } |
| |
| private class RunServerTask implements Runnable { |
| @Nonnull |
| private final ServerInfo serverInfo; |
| |
| @Nonnull |
| private final Map<String, Object> parameters; |
| |
| public RunServerTask(@Nonnull ServerInfo serverInfo, |
| @Nonnull Map<String, Object> parameters) { |
| this.serverInfo = serverInfo; |
| this.parameters = parameters; |
| } |
| |
| @Override |
| public void run() { |
| File serverJar = getServerJar(serverDir, serverInfo.id); |
| try { |
| JackServer server = loadServer(serverJar); |
| synchronized (currentServerLock) { |
| assert currentServer == null; |
| currentServer = server; |
| } |
| logger.log(Level.FINE, "Starting server " + serverInfo.version.getVerboseVersion() + |
| " from " + serverJar.getName()); |
| server.setHandle(new ServerLauncherHandle()); |
| server.run(parameters); |
| logger.log(Level.FINE, "Server " + serverInfo.version.getVerboseVersion() + |
| " from " + serverJar.getName() + " ended"); |
| } catch (ServerException e) { |
| logger.log(Level.SEVERE, "Server " + serverInfo.version.getVerboseVersion() + |
| " from " + serverJar.getName() + " error: " + e.getMessage(), e); |
| } catch (InterruptedException e) { |
| logger.log(Level.FINE, "Server " + serverInfo.version.getVerboseVersion() + |
| " from " + serverJar.getName() + " ended on interruption"); |
| Thread.currentThread().interrupt(); |
| } finally { |
| decrementServerCount(); |
| } |
| } |
| } |
| |
| private static class Deleter implements Runnable { |
| @Nonnull |
| private final File[] toDelete; |
| |
| private Deleter(@Nonnull File[] toDelete) { |
| this.toDelete = toDelete; |
| } |
| |
| @Override |
| public void run() { |
| for (File file : toDelete) { |
| if (!file.delete()) { |
| logger.log(Level.WARNING, "Failed to delete file '" + file.getPath() + "'"); |
| } else { |
| logger.log(Level.FINE, "Deleted file '" + file.getPath() + "'"); |
| } |
| } |
| } |
| } |
| |
| private static class NotAServerJarFileName extends Exception { |
| private static final long serialVersionUID = 1L; |
| } |
| |
| private class ServerLauncherHandle implements LauncherHandle { |
| |
| @Override |
| @Nonnull |
| public File getServerDir() { |
| return ServerLauncher.this.getServerDir(); |
| } |
| |
| @Override |
| public void replaceServer(@Nonnull InputStream newServer, |
| @Nonnull Map<String, Object> parameters, boolean forced) |
| throws IOException, ServerException, NotInstalledException { |
| ServerLauncher.this.replaceServer(newServer, parameters, forced); |
| } |
| |
| @Override |
| @Nonnull |
| public ClassLoader getLauncherClassLoader() { |
| return ServerLauncher.class.getClassLoader(); |
| } |
| |
| @Override |
| @Nonnull |
| public void deleteFilesOnGarbage(@Nonnull File[] filesToDelete, @Nonnull Object watched) { |
| finalizer.registerFinalizer(new Deleter(filesToDelete), watched); |
| } |
| } |
| |
| @Nonnull |
| private static final Logger logger = LoggerFactory.getLogger(); |
| |
| @Nonnull |
| private static final String HELP = "--help"; |
| |
| @Nonnull |
| private static final String VERSION = "--version"; |
| |
| @Nonnull |
| private static final String TMP_SUFFIX = ".tmp"; |
| |
| private static final int ABORT_EXIT_CODE = 255; |
| /** |
| * Usage, syntax or configuration file error. |
| */ |
| public static final int FAILURE_USAGE = 2; |
| |
| @Nonnull |
| private static final String DEFAULT_JACK_DIR = "."; |
| |
| @Nonnull |
| private static final Pattern SERVER_JAR_PATTERN = Pattern.compile("server-(\\d+)\\.jar"); |
| |
| @Nonnull |
| private final FinalizerRunner finalizer = new FinalizerRunner("Launcher finalizer"); |
| |
| @Nonnull |
| private final File serverDir; |
| |
| /** |
| * Used to synchronize access to currentServerInfo and currentServer. |
| */ |
| @Nonnull |
| private final Object currentServerLock = new Object(); |
| @CheckForNull |
| private ServerInfo currentServerInfo; |
| @CheckForNull |
| private JackServer currentServer; |
| |
| @Nonnegative |
| private int serverCount; |
| |
| @Nonnull |
| private final Object serverCountLock = new Object(); |
| |
| @Nonnull |
| private final TaskRunner taskRunner = new TaskRunner(); |
| |
| public static void main(@Nonnull String[] args) { |
| if (args.length == 0) { |
| Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { |
| @Override |
| public void uncaughtException(Thread t, Throwable e) { |
| logger.log(Level.SEVERE, "Uncaught exception in thread '" + t.getName() + "'", e); |
| abort("Internal error"); |
| } |
| }); |
| String jackDir = System.getProperty("jack.home", DEFAULT_JACK_DIR); |
| try { |
| new ServerLauncher(new File(jackDir)).run(); |
| } catch (ServerException e) { |
| abort("Failed to start server: " + e.getMessage()); |
| } catch (InterruptedException e) { |
| logger.log(Level.FINE, "ServerLauncher was interrupted"); |
| } |
| } else if (args.length == 1) { |
| String command = args[0]; |
| switch (command) { |
| case HELP: |
| printHelp(System.out); |
| break; |
| case VERSION: |
| try { |
| printVersion(System.out); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| abort("Failed to read version"); |
| } |
| break; |
| default: |
| printHelp(System.err); |
| System.exit(FAILURE_USAGE); |
| break; |
| } |
| } else { |
| printHelp(System.err); |
| System.exit(FAILURE_USAGE); |
| } |
| } |
| |
| private static void printVersion(@Nonnull PrintStream printStream) throws IOException { |
| Version version = new Version("jack-launcher", ServerLauncher.class.getClassLoader()); |
| printStream.println("Jack server launcher."); |
| printStream.println("Version: " + version.getVerboseVersion() + '.'); |
| } |
| |
| private static void printHelp(@Nonnull PrintStream printStream) { |
| printStream.println("Usage: <options>"); |
| printStream.println("Start server."); |
| printStream.println(); |
| printStream.println("Options:"); |
| printStream.println(" --help : display help"); |
| printStream.println(" --version : display version"); |
| } |
| |
| public ServerLauncher(@Nonnull File serverDir) { |
| this.serverDir = serverDir; |
| } |
| |
| public void run() throws ServerException, InterruptedException { |
| Thread taskThread = new Thread(taskRunner, "Task runner"); |
| taskThread.setDaemon(true); |
| taskThread.start(); |
| |
| File[] serverDirFiles = serverDir.listFiles(new FileFilter() { |
| @Override |
| public boolean accept(File pathname) { |
| return pathname.isFile(); |
| } |
| }); |
| if (serverDirFiles == null) { |
| throw new ServerException("Failed to list server install directory '" + serverDir + "'"); |
| } |
| |
| List<Integer> serverIds = new LinkedList<Integer>(); |
| for (File candidate : serverDirFiles) { |
| try { |
| serverIds.add(Integer.valueOf(getServerId(candidate))); |
| } catch (NotAServerJarFileName e) { |
| // Not a server jar, is it a forgotten tmp? |
| if (candidate.getName().endsWith(TMP_SUFFIX)) { |
| deleteFile(candidate); |
| } |
| } |
| } |
| if (serverIds.size() == 0) { |
| throw new ServerException("No installed Jack server"); |
| } |
| Collections.sort(serverIds); |
| Iterator<Integer> iteratorAll = serverIds.iterator(); |
| Iterator<Integer> toDeleteIterator = Iterators.limit(iteratorAll, serverIds.size() - 1); |
| while (toDeleteIterator.hasNext()) { |
| deleteFile(getServerJar(serverDir, toDeleteIterator.next().intValue())); |
| } |
| |
| int serverId = iteratorAll.next().intValue(); |
| try { |
| startInitialServer(serverDir, serverId); |
| } catch (IOException e) { |
| throw new ServerException("Failed to start installed server", e); |
| } |
| |
| waitServers(); |
| taskThread.interrupt(); |
| finalizer.shutdown(); |
| } |
| |
| private void waitServers() throws InterruptedException { |
| synchronized (serverCountLock) { |
| while (serverCount > 0) { |
| serverCountLock.wait(); |
| } |
| } |
| logger.log(Level.FINE, "Last server exited"); |
| } |
| |
| private void startInitialServer(@Nonnull File serverDir, @Nonnegative final int serverId) |
| throws ServerException, IOException { |
| // Don't inline in long living method, it could prevent the server to be garbaged because of |
| // locals |
| File serverJar = getServerJar(serverDir, serverId); |
| final JackServer server = loadServer(serverJar); |
| synchronized (currentServerLock) { |
| currentServer = server; |
| server.setHandle(new ServerLauncherHandle()); |
| |
| currentServerInfo = new ServerInfo(serverId, |
| new Version("jack-server", server.getClass().getClassLoader())); |
| } |
| |
| incrementServerCount(); |
| taskRunner.executeTask("Server " + serverId, new Runnable() { |
| @Override |
| public void run() { |
| logger.log(Level.FINE, "Starting server " + serverId); |
| try { |
| server.onSystemStart(); |
| server.run(new HashMap<String, Object>()); |
| logger.log(Level.FINE, "Server " + serverId + " ended"); |
| } catch (ServerException e) { |
| logger.log(Level.SEVERE, "Server " + serverId + " Exception", e); |
| } catch (InterruptedException e) { |
| logger.log(Level.FINE, "Server " + serverId + " ended on interruption"); |
| Thread.currentThread().interrupt(); |
| } finally { |
| decrementServerCount(); |
| } |
| } |
| }); |
| } |
| |
| private static File getServerJar(@Nonnull File serverDir, @Nonnegative int serverId) { |
| return new File(serverDir, "server-" + serverId + ".jar"); |
| } |
| |
| @Nonnull |
| private static JackServer loadServer(@Nonnull File jar) throws ServerException { |
| ClassLoader classLoader; |
| try { |
| classLoader = new URLClassLoader(new URL[]{jar.toURI().toURL()}, |
| ServerLauncher.class.getClassLoader()); |
| } catch (IOException e) { |
| throw new ServerException("Failed to open jar '" + jar.getPath() + "'", e); |
| } |
| ServiceLoader<JackServer> serviceLoader = ServiceLoader.load(JackServer.class, classLoader); |
| try { |
| JackServer server = serviceLoader.iterator().next(); |
| assert server.getClass().getClassLoader() == classLoader; |
| return server; |
| } catch (NoSuchElementException e) { |
| throw new ServerException("Jar '" + jar.getPath() + "' does not define a server"); |
| } catch (ServiceConfigurationError e) { |
| throw new ServerException( |
| "Jar '" + jar.getPath() + "' does not define a valid server: " + e.getMessage(), e); |
| } |
| } |
| |
| private static void deleteFile(@Nonnull File file) { |
| if (!file.delete()) { |
| logger.log(Level.WARNING, "Failed to delete file '" + file.getPath() + "'"); |
| } |
| } |
| |
| private static void abort(@Nonnull String message) { |
| System.err.println(message); |
| System.exit(ABORT_EXIT_CODE); |
| } |
| |
| private static int getServerId(File serverJar) throws NotAServerJarFileName { |
| Matcher matcher = SERVER_JAR_PATTERN.matcher(serverJar.getName()); |
| if (!matcher.matches()) { |
| throw new NotAServerJarFileName(); |
| } |
| return Integer.parseInt(matcher.group(1)); |
| } |
| |
| @Nonnull |
| public File getServerDir() { |
| return serverDir; |
| } |
| |
| private void replaceServer(@Nonnull InputStream jarIn, @Nonnull Map<String, Object> parameters, |
| boolean forced) |
| throws IOException, ServerException, NotInstalledException { |
| FileOutputStream out = null; |
| File tmpInstall = null; |
| synchronized (currentServerLock) { |
| try { |
| tmpInstall = File.createTempFile("jacklauncher-", TMP_SUFFIX, serverDir); |
| out = new FileOutputStream(tmpInstall); |
| new ByteStreamSucker(jarIn, out).suck(); |
| out.close(); |
| out = null; |
| |
| ClassLoader tmpLoader; |
| try { |
| tmpLoader = new URLClassLoader(new URL[]{tmpInstall.toURI().toURL()}, |
| ServerLauncher.class.getClassLoader()); |
| } catch (IOException e) { |
| throw new ServerException("Failed to open jar '" + tmpInstall.getPath() + "'", e); |
| } |
| |
| Version candidateVersion = new Version("jack-server", tmpLoader); |
| assert currentServerInfo != null; |
| Version currentVerion = currentServerInfo.version; |
| |
| if (!forced) { |
| try { |
| if (!candidateVersion.isNewerThan(currentVerion)) { |
| if (candidateVersion.equals(currentVerion)) { |
| logger.log(Level.INFO, "Server version " |
| + currentVerion.getVerboseVersion() + " was already installed"); |
| return; |
| } else { |
| throw new NotInstalledException("Not installing server " |
| + candidateVersion.getVerboseVersion() |
| + " since it is not newer than current server " |
| + currentServerInfo.version.getVerboseVersion()); |
| } |
| } |
| } catch (UncomparableVersion e) { |
| if (!candidateVersion.isComparable()) { |
| throw new NotInstalledException("Not installing server '" |
| + candidateVersion.getVerboseVersion() + "' without force request"); |
| } |
| // else: current is experimental or eng, candidate is not, lets proceed |
| } |
| } |
| |
| assert currentServerInfo != null; |
| int newServerId = currentServerInfo.id + 1; |
| File newInstalledServer = getServerJar(serverDir, newServerId); |
| if (!tmpInstall.renameTo(newInstalledServer)) { |
| throw new IOException("Failed to rename '" + tmpInstall + "' to '" + newInstalledServer |
| + "'"); |
| } |
| tmpInstall = null; |
| |
| File replacedServerJar = getServerJar(serverDir, currentServerInfo.id); |
| assert currentServer != null; |
| finalizer.registerFinalizer(new Deleter(new File[]{replacedServerJar}), |
| currentServer.getClass().getClassLoader()); |
| currentServerInfo = new ServerInfo(newServerId, candidateVersion); |
| } finally { |
| if (out != null) { |
| try { |
| out.close(); |
| } catch (IOException e) { |
| logger.log(Level.WARNING, "Exception during close", e); |
| } |
| } |
| if (tmpInstall != null) { |
| if (!tmpInstall.delete()) { |
| logger.log(Level.WARNING, "Failed to delete temp file '" + tmpInstall + "'"); |
| } |
| } |
| |
| currentServer = null; |
| assert currentServerInfo != null; |
| logger.log(Level.FINE, "Starting server " + currentServerInfo.version.getVerboseVersion()); |
| incrementServerCount(); |
| assert currentServerInfo != null; |
| taskRunner.executeTask("Server " + currentServerInfo.id, |
| new RunServerTask(currentServerInfo, parameters)); |
| } |
| } |
| } |
| |
| private void incrementServerCount() { |
| synchronized (serverCountLock) { |
| serverCount++; |
| } |
| } |
| |
| private void decrementServerCount() { |
| synchronized (serverCountLock) { |
| serverCount--; |
| serverCountLock.notifyAll(); |
| } |
| } |
| |
| } |