blob: 3534a4cdb3c40deac15b09ffff180207173c82e3 [file] [log] [blame]
/*
* 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.server;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
import com.google.common.cache.Weigher;
import com.android.jack.api.JackProvider;
import com.android.jack.api.ResourceController;
import com.android.jack.api.ResourceController.Category;
import com.android.jack.api.ResourceController.Impact;
import com.android.jack.server.ServerLogConfiguration.ServerLogConfigurationException;
import com.android.jack.server.api.v01.LauncherHandle;
import com.android.jack.server.api.v01.ServerException;
import com.android.jack.server.router.AcceptContentTypeParameterRouter;
import com.android.jack.server.router.AcceptContentTypeRouter;
import com.android.jack.server.router.BooleanCodec;
import com.android.jack.server.router.ContentTypeParameterRouter;
import com.android.jack.server.router.ContentTypeRouter;
import com.android.jack.server.router.ErrorContainer;
import com.android.jack.server.router.MethodRouter;
import com.android.jack.server.router.PartContentTypeParameterRouter;
import com.android.jack.server.router.PartContentTypeRouter;
import com.android.jack.server.router.PartParserRouter;
import com.android.jack.server.router.PathRouter;
import com.android.jack.server.router.RootContainer;
import com.android.jack.server.router.TextPlainPartParser;
import com.android.jack.server.tasks.GC;
import com.android.jack.server.tasks.GetJackVersions;
import com.android.jack.server.tasks.GetLauncherHome;
import com.android.jack.server.tasks.GetLauncherLog;
import com.android.jack.server.tasks.GetLauncherVersion;
import com.android.jack.server.tasks.GetServerVersion;
import com.android.jack.server.tasks.InstallJack;
import com.android.jack.server.tasks.InstallServer;
import com.android.jack.server.tasks.JackTaskBase64Out;
import com.android.jack.server.tasks.JackTaskRawOut;
import com.android.jack.server.tasks.JillTask;
import com.android.jack.server.tasks.QueryJackVersion;
import com.android.jack.server.tasks.QueryServerVersion;
import com.android.jack.server.tasks.ReloadConfig;
import com.android.jack.server.tasks.ResetStats;
import com.android.jack.server.tasks.SetLoggerParameters;
import com.android.jack.server.tasks.Stat;
import com.android.jack.server.tasks.Stop;
import com.android.jack.server.type.CommandOutBase64;
import com.android.jack.server.type.CommandOutRaw;
import com.android.jack.server.type.ExactCodeVersionFinder;
import com.android.jack.server.type.TextPlain;
import com.android.sched.util.FinalizerRunner;
import com.android.sched.util.Version;
import com.android.sched.util.codec.IntCodec;
import com.android.sched.util.codec.PairCodec.Pair;
import com.android.sched.util.codec.ParsingException;
import com.android.sched.util.file.CannotChangePermissionException;
import com.android.sched.util.file.CannotCreateFileException;
import com.android.sched.util.file.Directory;
import com.android.sched.util.file.FileAlreadyExistsException;
import com.android.sched.util.file.FileOrDirectory.ChangePermission;
import com.android.sched.util.file.FileOrDirectory.Existence;
import com.android.sched.util.file.FileOrDirectory.Permission;
import com.android.sched.util.file.NoSuchFileException;
import com.android.sched.util.file.NotDirectoryException;
import com.android.sched.util.file.NotFileException;
import com.android.sched.util.file.WrongPermissionException;
import com.android.sched.util.findbugs.SuppressFBWarnings;
import org.simpleframework.http.ContentType;
import org.simpleframework.http.Method;
import org.simpleframework.http.Status;
import org.simpleframework.http.core.Container;
import org.simpleframework.http.core.ContainerSocketProcessor;
import org.simpleframework.transport.Socket;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.ServerSocketChannel;
import java.nio.file.Files;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
/**
* Server controlling the number of Jack compilations that are executed simultaneously.
*/
public class JackHttpServer implements HasVersion {
@Nonnull
public static final String VERSION_CODE = "jack-server";
/**
* Define an assertion status.
*/
public static enum Assertion {
ENABLED {
@Override
public boolean isEnabled() {
return true;
}
},
DISABLED {
@Override
public boolean isEnabled() {
return false;
}
};
public abstract boolean isEnabled();
}
/**
* A program that can be run by this server as a service.
*/
public static class Program<T> implements HasVersion {
private abstract static class ProgramReference<U, T extends Reference<U>> {
@Nonnull
private T program = newReference(null);
@Nonnull
private T assertingProgram = newReference(null);
@Nonnull
protected abstract T newReference(@CheckForNull U program);
@CheckForNull
public U get(@Nonnull Assertion assertion) {
switch (assertion) {
case ENABLED:
return assertingProgram.get();
case DISABLED:
return program.get();
default:
throw new AssertionError();
}
}
public void set(@Nonnull Assertion status, @CheckForNull U program) {
switch (status) {
case ENABLED:
assertingProgram = newReference(program);
break;
case DISABLED:
this.program = newReference(program);
break;
default:
throw new AssertionError();
}
}
}
private static class ProgramSoftReference<U> extends ProgramReference<U, SoftReference<U>> {
@Override
@Nonnull
protected SoftReference<U> newReference(@CheckForNull U referent) {
return new SoftReference<U>(referent);
}
}
@Nonnull
private final Version version;
@Nonnull
private final File jar;
@Nonnull
private final ProgramSoftReference<T> loadedProgram;
/**
* This is used to track garbage collection of classloaders on this program. It shared between
* classloaders which are preventing its collection as long as the classloaders are not
* collected.
*/
@Nonnull
private WeakReference<URL[]> urlPath;
public Program(@Nonnull Version version, @Nonnull File jar, @CheckForNull URL[] path) {
this.version = version;
this.jar = jar;
loadedProgram = new ProgramSoftReference<T>();
urlPath = new WeakReference<URL[]>(path);
}
@Override
@Nonnull
public Version getVersion() {
return version;
}
@Nonnull
private File getJar() {
return jar;
}
@Nonnull
private URL[] getUrlPath() {
URL[] path = urlPath.get();
if (path == null) {
try {
path = new URL[] {jar.toURI().toURL()};
} catch (MalformedURLException e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw new AssertionError();
}
urlPath = new WeakReference<URL[]>(path);
}
return path;
}
@CheckForNull
private Object getGCProbe() {
return urlPath.get();
}
@CheckForNull
private T getLoadedProgram(@Nonnull Assertion status) {
return loadedProgram.get(status);
}
/**
* Should be called only by code synchronized on {@link #installedJack}, or at creation.
*/
private void setLoadedProgram(@Nonnull Assertion status, @CheckForNull T program) {
if (program == null) {
loadedProgram.set(status, null);
} else {
assert loadedProgram.get(status) == null;
this.loadedProgram.set(status, program);
}
}
}
/**
* Thrown when attempting to start new task while server is closed or shutdown is in progress.
*/
public static class ServerClosedException extends Exception {
private static final long serialVersionUID = 1L;
}
private static class URLClassLoaderWithProbe extends URLClassLoader {
// The purpose of this subclass of URLClassLoader is to ensure urls won't be garbaged
// collected before this classloader.
@SuppressWarnings("unused")
private final URL[] urls;
public URLClassLoaderWithProbe(@Nonnull URL[] urls, @CheckForNull ClassLoader parent) {
super(urls, parent);
this.urls = urls;
}
}
private static class VersionKey implements HasVersion {
@Nonnull
private final Version version;
public VersionKey(@Nonnull Version version) {
this.version = version;
}
@Override
public final boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof VersionKey) {
VersionKey other = (VersionKey) obj;
return version.getReleaseCode() == other.version.getReleaseCode()
&& version.getSubReleaseCode() == other.version.getSubReleaseCode();
}
return false;
}
@Override
public final int hashCode() {
return (version.getReleaseCode() * 7) ^ (version.getSubReleaseCode() * 17);
}
@Override
@Nonnull
public Version getVersion() {
return version;
}
@Nonnull
@Override
public String toString() {
return version.toString();
}
}
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 class TimedServerMode {
@Nonnegative
private final long delay;
private final ServerMode newMode;
public TimedServerMode(@Nonnegative long delay, @Nonnull ServerMode serverMode) {
this.delay = delay;
this.newMode = serverMode;
}
public void registerTo(@Nonnull Timer timer) {
timer.schedule(new TimerTask() {
@Override
public void run() {
setServerMode(newMode);
}
}, delay);
}
}
private static interface ServerModeWatcher {
void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode);
}
@Nonnull
private static final String JAR_SUFFIX = ".jar";
@Nonnull
private static final String DELETED_SUFFIX = ".deleted";
@Nonnull
private static final String DELETED_JAR_SUFFIX = JAR_SUFFIX + DELETED_SUFFIX;
@Nonnull
private static final String LOG_FILE_PATTERN = "logs/jack-server-%u-%g.log";
@Nonnull
private static final String KEYSTORE_SERVER = "server.jks";
@Nonnull
private static final String KEYSTORE_CLIENT = "client.jks";
@Nonnull
private static final String SERVER_KEY_ALIAS = "server";
@Nonnull
private static final String CLIENT_KEY_ALIAS = "client";
@Nonnull
private static final char[] KEYSTORE_PASSWORD = "Jack-Server".toCharArray();
@Nonnull
private static final String PEM_CLIENT = "client.pem";
@Nonnull
private static final String PEM_SERVER = "server.pem";
private static final FileFilter JAR_FILTER = new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.getName().endsWith(JAR_SUFFIX)
&& !new File(pathname.getPath() + DELETED_SUFFIX).exists();
}
};
@Nonnull
private static Logger logger = Logger.getLogger(JackHttpServer.class.getName());
private int portService;
private int portAdmin;
private long maxJarSize;
@Nonnull
private final LauncherHandle launcherHandle;
@Nonnull
private final File serverDir;
@CheckForNull
private Connection serviceConnection;
@CheckForNull
private Connection adminConnection;
@CheckForNull
private ServerParameters serverParameters;
@CheckForNull
private Timer timer;
@Nonnull
private final Object lock = new Object();
private int maxServices;
@Nonnull
private final ServerInfo serviceInfo = new ServerInfo();
@Nonnull
private final ServerInfo adminInfo = new ServerInfo();
private boolean shuttingDown;
private Cache<VersionKey, Program<JackProvider>> installedJack = null;
@CheckForNull
private ServerSocketChannel adminChannel;
@CheckForNull
private ServerSocketChannel serviceChannel;
@Nonnull
private ServerLogConfiguration logConfiguration;
@Nonnull
private final String currentUser;
@CheckForNull
private String[] filteredCiphersArray = null;
@Nonnull
private final FinalizerRunner finalizer = new FinalizerRunner("Server finalizer");
private ServerMode serverMode = ServerMode.WAIT;
@Nonnull
private final List<TimedServerMode> delayedModes =
new ArrayList<JackHttpServer.TimedServerMode>();
@Nonnull
private final Map<ServerMode, ServerModeWatcher> modeWatchers = new HashMap<>();
@Nonnull
public static Version getServerVersion() {
try {
return new Version(VERSION_CODE, JackHttpServer.class.getClassLoader());
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to read Jack-server version properties", e);
throw new AssertionError();
}
}
JackHttpServer(@Nonnull LauncherHandle launcherHandle)
throws IOException, ServerLogConfigurationException, NotFileException,
WrongPermissionException, CannotCreateFileException {
this.launcherHandle = launcherHandle;
serverDir = launcherHandle.getServerDir();
logConfiguration = ServerLogConfiguration.setupLog(
serverDir.getPath().replace(File.separatorChar, '/') + '/' + LOG_FILE_PATTERN);
currentUser = getCurrentUser(serverDir);
addServerModeWatcher(ServerMode.WORK, new ServerModeWatcher() {
@Override
public void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode) {
cancelTimer();
}
});
addServerModeWatcher(ServerMode.WAIT, new ServerModeWatcher() {
@Override
public void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode) {
startTimer();
cleanJacks(EnumSet.of(Category.CODE, Category.MEMORY), Collections.<Impact>emptySet());
}
});
addServerModeWatcher(ServerMode.IDLE, new ServerModeWatcher() {
@Override
public void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode) {
cleanJacks(EnumSet.of(Category.CODE, Category.MEMORY), EnumSet.of(Impact.LATENCY));
assert timer != null;
timer.schedule(new TimerTask() {
// Even if its just a hint, a gc would be nice.
@SuppressFBWarnings("DM_GC")
@Override
public void run() {
System.gc();
}
}, 0L, 60 * 60 * 1000);
}
});
addServerModeWatcher(ServerMode.DEEP_IDLE, new ServerModeWatcher() {
@Override
public void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode) {
cleanJacks(EnumSet.of(Category.CODE, Category.MEMORY),
EnumSet.of(Impact.LATENCY, Impact.PERFORMANCE));
}
});
addServerModeWatcher(ServerMode.SLEEP, new ServerModeWatcher() {
@Override
public void changedMode(@Nonnull ServerMode oldMode, @Nonnull ServerMode newMode) {
freeLoadedPrograms();
}
});
loadConfig();
}
@Nonnull
public ServerLogConfiguration getLogConfiguration() {
return logConfiguration.clone();
}
public void setLogConfiguration(ServerLogConfiguration logConfiguration)
throws IOException {
logConfiguration.apply();
this.logConfiguration = logConfiguration;
}
@Nonnull
public String getLogPattern() {
return ServerLogConfiguration.getLogFilePattern(
serverDir.getAbsolutePath().replace(File.separatorChar, '/') + '/' + LOG_FILE_PATTERN);
}
private void buildInstalledJackCache() throws IOException, NotDirectoryException,
WrongPermissionException, CannotChangePermissionException, NoSuchFileException,
FileAlreadyExistsException, CannotCreateFileException {
Cache<VersionKey, Program<JackProvider>> previousInstalledJack = installedJack;
installedJack = CacheBuilder.newBuilder()
.weigher(new Weigher<VersionKey, Program<JackProvider>>() {
@Override
public int weigh(VersionKey version, Program<JackProvider> program) {
long length = program.getJar().length();
return (int) Math.min(Integer.MAX_VALUE, length);
}
})
.maximumWeight(maxJarSize == -1 ? Long.MAX_VALUE : maxJarSize)
.removalListener(new RemovalListener<VersionKey, Program<JackProvider>>() {
@Override
public void onRemoval(
@Nonnull RemovalNotification<VersionKey, Program<JackProvider>> notification) {
Program<JackProvider> program = notification.getValue();
final File jar = program.getJar();
Object gcProbe = program.getGCProbe();
if (gcProbe != null) {
logger.info("Queuing " + jar.getPath() + " for deletion");
final File deleteMarker = new File(jar.getPath() + DELETED_SUFFIX);
try {
if (!deleteMarker.createNewFile()) {
throw new IOException("File already exists");
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to create delete file marker '" + deleteMarker
+ "' aborting deletion by finalizer", e);
return;
}
finalizer.registerFinalizer(new Deleter(new File[]{deleteMarker, jar}), gcProbe);
deleteMarker.deleteOnExit();
jar.deleteOnExit();
} else {
logger.info("Deleting " + jar.getPath() + " immediatly");
if (!jar.delete()) {
logger.log(Level.SEVERE, "Failed to delete file '" + jar + "'");
}
}
}
})
.concurrencyLevel(1)
.build();
if (previousInstalledJack != null) {
installedJack.putAll(previousInstalledJack.asMap());
} else {
loadInstalledJacks();
}
}
@Nonnull
public ServerParameters getServerParameters() {
assert serverParameters != null;
return serverParameters;
}
public void addInstalledJack(@Nonnull Program<JackProvider> jack) {
synchronized (installedJack) {
installedJack.put(new VersionKey(jack.getVersion()), jack);
}
logger.log(Level.INFO, "New installed Jack " + jack.getVersion().getVerboseVersion() + " in "
+ jack.getJar().getPath());
}
private void loadInstalledJacks() throws IOException, NotDirectoryException,
WrongPermissionException, CannotChangePermissionException, NoSuchFileException,
FileAlreadyExistsException, CannotCreateFileException {
File jackDir = new File(serverDir, "jack");
new Directory(jackDir.getPath(), null, Existence.MAY_EXIST,
Permission.READ | Permission.WRITE | Permission.EXECUTE, ChangePermission.NOCHANGE);
File[] jars = jackDir.listFiles(JAR_FILTER);
if (jars == null) {
throw new IOException("Failed to list Jack installation directory '"
+ jackDir + "'");
}
for (File jackJar : jars) {
try {
URL[] path = new URL[]{jackJar.toURI().toURL()};
JackProvider jackProvider = loadJack(path, Assertion.DISABLED);
Version version = new Version("jack", jackProvider.getClass().getClassLoader());
Program<JackProvider> jackProgram = new Program<JackProvider>(version, jackJar, path);
jackProgram.setLoadedProgram(Assertion.DISABLED, jackProvider);
installedJack.put(new VersionKey(version), jackProgram);
logger.log(Level.INFO, "Jack " + version.getVerboseVersion()
+ " available in " + jackJar.getPath());
} catch (UnsupportedProgramException | IOException e) {
logger.log(Level.SEVERE, "Invalid installed jack file '" + jackJar
+ "'. Deleting.");
if (!jackJar.delete()) {
logger.log(Level.WARNING,
"Failed to delete invalid installed jack file '" + jackJar + "'");
}
}
}
}
public void reloadConfig() throws IOException, WrongPermissionException, NotFileException,
ServerException, CannotCreateFileException {
shutdownConnections();
try {
checkAccess(serverDir, EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE));
loadConfig();
} catch (CannotCreateFileException | IOException | NotFileException
| WrongPermissionException e) {
shutdown();
throw e;
}
start(new HashMap<String, Object>());
}
private void loadConfig() throws IOException, WrongPermissionException, NotFileException,
CannotCreateFileException {
logger.log(Level.INFO, "Loading config of jack server version: "
+ getVersion().getVerboseVersion());
ConfigFile config = new ConfigFile(serverDir);
checkAccess(config.getStorageFile(), EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
portService = config.getServicePort();
portAdmin = config.getAdminPort();
maxJarSize = config.getMaxJarSize();
delayedModes.clear();
addServerMode(config.getIdleDelay(), ServerMode.IDLE);
addServerMode(config.getDeepIdleDelay(), ServerMode.DEEP_IDLE);
addServerMode(config.getTimeout(), ServerMode.SLEEP);
maxServices = config.getMaxServices();
List<Pair<Integer, Long>> maxServicesByMem = config.getMaxServiceByMem();
if (!maxServicesByMem.isEmpty()) {
long maxMemory = Runtime.getRuntime().maxMemory();
for (Pair<Integer, Long> pair : maxServicesByMem) {
if (maxMemory < pair.getSecond().longValue()) {
maxServices = Math.min(pair.getFirst().intValue(), maxServices);
}
}
}
if (config.isModified() && config.getConfigVersion() < ConfigFile.CURRENT_CONFIG_VERSION) {
config.store();
}
}
@Override
@Nonnull
public Version getVersion() {
return getServerVersion();
}
void start(@Nonnull Map<String, Object> parameters) throws ServerException {
try {
buildInstalledJackCache();
} catch (IOException | NotDirectoryException | WrongPermissionException
| CannotChangePermissionException | NoSuchFileException | FileAlreadyExistsException
| CannotCreateFileException e) {
throw new ServerException("Problem while loading installed Jack", e);
}
InetSocketAddress serviceAddress = new InetSocketAddress("127.0.0.1", portService);
InetSocketAddress adminAddress = new InetSocketAddress("127.0.0.1", portAdmin);
serverParameters = new ServerParameters(parameters);
ContainerSocketProcessor adminProcessor = null;
ContainerSocketProcessor serviceProcessor = null;
try {
synchronized (lock) {
shuttingDown = false;
}
logger.log(Level.INFO, "Starting service connection server on " + serviceAddress);
try {
assert serverParameters != null;
serviceChannel = serverParameters.getServiceSocket(serviceAddress);
} catch (IOException e) {
if (e.getCause() instanceof BindException) {
throw new ServerException("Problem while opening service port: "
+ e.getCause().getMessage());
} else {
throw new ServerException("Problem while opening service port", e);
}
}
logger.log(Level.INFO, "Starting admin connection on " + adminAddress);
try {
assert serverParameters != null;
adminChannel = serverParameters.getAdminSocket(adminAddress);
} catch (IOException e) {
if (e.getCause() instanceof BindException) {
throw new ServerException("Problem while opening admin port: "
+ e.getCause().getMessage());
} else {
throw new ServerException("Problem while opening admin port", e);
}
}
SSLContext sslContext = setupSsl();
try {
Container router = createServiceRouter();
serviceProcessor = new ContainerSocketProcessor(new RootContainer(router), maxServices) {
@Override
public void process(Socket socket) throws IOException {
configureSocket(socket);
super.process(socket);
}
};
SocketConnection connection = new SocketConnection(serviceProcessor);
serviceConnection = connection;
connection.connect(serviceChannel, sslContext);
} catch (IOException e) {
throw new ServerException("Problem during service connection ", e);
}
try {
Container router = createAdminRouter();
adminProcessor = new ContainerSocketProcessor(new RootContainer(router), 1) {
@Override
public void process(Socket socket) throws IOException {
configureSocket(socket);
super.process(socket);
}
};
SocketConnection connection = new SocketConnection(adminProcessor);
adminConnection = connection;
connection.connect(adminChannel, sslContext);
} catch (IOException e) {
throw new ServerException("Problem during admin connection ", e);
}
startTimer();
} catch (ServerException e) {
if (serviceProcessor != null) {
try {
serviceProcessor.stop();
} catch (IOException stopException) {
logger.log(Level.SEVERE, "Cannot close the service processor: ", stopException);
}
}
if (adminProcessor != null) {
try {
adminProcessor.stop();
} catch (IOException stopException) {
logger.log(Level.SEVERE, "Cannot close the admin processor: ", stopException);
}
}
shutdown();
throw e;
}
}
private void checkAccess(@Nonnull File file, @Nonnull Set<PosixFilePermission> check)
throws IOException {
FileOwnerAttributeView ownerAttribute =
Files.getFileAttributeView(file.toPath(), FileOwnerAttributeView.class);
if (!currentUser.equals(ownerAttribute.getOwner().getName())) {
throw new IOException("'" + file.getPath() + "' is not owned by '" + currentUser
+ "' but by '" + ownerAttribute.getOwner().getName() + "'");
}
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(file.toPath());
if (!check.equals(permissions)) {
throw new IOException("'" + file.getPath() + "' must have permission "
+ PosixFilePermissions.toString(check) + " but have "
+ PosixFilePermissions.toString(permissions));
}
}
private void refreshPEMFiles(@Nonnull KeyStore keystoreServer, @Nonnull KeyStore keystoreClient)
throws IOException, UnrecoverableKeyException, KeyStoreException,
NoSuchAlgorithmException {
{
File clientPEM = new File(getServerDir(), PEM_CLIENT);
PEMWriter pem = new PEMWriter(clientPEM);
try {
pem.writeKey(keystoreClient.getKey(CLIENT_KEY_ALIAS, KEYSTORE_PASSWORD));
pem.writeCertificate(keystoreClient.getCertificate(CLIENT_KEY_ALIAS));
} finally {
pem.close();
}
}
{
File serverPEM = new File(getServerDir(), PEM_SERVER);
PEMWriter pem = new PEMWriter(serverPEM);
try {
pem.writeCertificate(keystoreServer.getCertificate(SERVER_KEY_ALIAS));
} finally {
pem.close();
}
}
}
@Nonnull
private ServerSocketChannel openSocket(@Nonnull InetSocketAddress serviceAddress,
@CheckForNull Object existingChannel) throws IOException,
SocketException {
if (existingChannel instanceof ServerSocketChannel) {
return (ServerSocketChannel) existingChannel;
} else {
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
ServerSocket socket = channel.socket();
socket.setReuseAddress(true);
socket.bind(serviceAddress, 100);
return channel;
}
}
public void waitServerShutdown() throws InterruptedException {
synchronized (lock) {
while ((!shuttingDown) || serviceInfo.currentLocal > 0 || adminInfo.currentLocal > 0) {
lock.wait();
}
}
}
@Nonnull
public Program<JackProvider> selectJack(@Nonnull VersionFinder finder)
throws NoSuchVersionException {
synchronized (installedJack) {
VersionKey selected = finder.select(installedJack.asMap().keySet());
if (selected == null) {
throw new NoSuchVersionException();
}
Program<JackProvider> program = installedJack.getIfPresent(selected);
assert program != null;
return program;
}
}
@Nonnull
public Collection<Program<JackProvider>> getInstalledJacks() {
synchronized (installedJack) {
return new ArrayList<>(installedJack.asMap().values());
}
}
@Nonnull
public JackProvider getProvider(@Nonnull Program<JackProvider> program, @Nonnull Assertion status)
throws UnsupportedProgramException {
synchronized (program) {
JackProvider jackProvider = program.getLoadedProgram(status);
if (jackProvider == null) {
jackProvider = loadJack(program.getUrlPath(), status);
program.setLoadedProgram(status, jackProvider);
}
return jackProvider;
}
}
@Nonnull
public VersionFinder parseVersionFinder(@Nonnull ContentType versionType,
@Nonnull String versionString) throws TypeNotSupportedException, ParsingException {
if (versionType.getType().equals(ExactCodeVersionFinder.SELECT_EXACT_VERSION_CONTENT_TYPE)
&& "1".equals(versionType.getParameter("version"))) {
return ExactCodeVersionFinder.parse(versionString);
} else {
throw new TypeNotSupportedException(versionString);
}
}
// We have no privilege restriction for now and there is no call back here from a Jack thread so
// we should be fine keeping the code simple
@SuppressFBWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
public JackProvider loadJack(@Nonnull URL[] path, @Nonnull Assertion status)
throws UnsupportedProgramException {
URLClassLoader jackLoader;
jackLoader = new URLClassLoaderWithProbe(path, this.getClass().getClassLoader());
jackLoader.setDefaultAssertionStatus(status.isEnabled());
ServiceLoader<JackProvider> serviceLoader = ServiceLoader.load(JackProvider.class, jackLoader);
JackProvider provider;
try {
provider = serviceLoader.iterator().next();
} catch (NoSuchElementException | ServiceConfigurationError e) {
logger.log(Level.SEVERE, "Failed to load jack from " + Arrays.toString(path), e);
throw new UnsupportedProgramException("Jack");
}
return provider;
}
private void startTimer() {
synchronized (lock) {
if (timer != null) {
cancelTimer();
}
if (delayedModes.isEmpty()) {
return;
}
logger.log(Level.INFO, "Start timer");
timer = new Timer("jack-server-timeout");
assert timer != null;
for (TimedServerMode mode : delayedModes) {
mode.registerTo(timer);
}
}
}
public void shutdown() {
synchronized (lock) {
if (!shuttingDown) {
shutdownConnections();
shuttingDown = true;
lock.notifyAll();
}
}
}
public void shutdownServerOnly() {
synchronized (lock) {
if (!shuttingDown) {
shutdownSimpleServer();
shuttingDown = true;
lock.notifyAll();
}
}
}
private void cleanJacks(@Nonnull Set<Category> categories, @Nonnull Set<Impact> impacts) {
for (Program<JackProvider> program : getInstalledJacks()) {
JackProvider provider = program.getLoadedProgram(Assertion.DISABLED);
if (provider instanceof ResourceController) {
((ResourceController) provider).clean(categories, impacts);
}
provider = program.getLoadedProgram(Assertion.ENABLED);
if (provider instanceof ResourceController) {
((ResourceController) provider).clean(categories, impacts);
}
}
}
// Even if its just a hint, this is a nice time for a gc.
@SuppressFBWarnings("DM_GC")
private void freeLoadedPrograms() {
Collection<Program<JackProvider>> programs = getInstalledJacks();
for (Program<JackProvider> program : programs) {
synchronized (program) {
program.setLoadedProgram(Assertion.ENABLED, null);
}
}
System.gc();
}
private void shutdownConnections() {
shutdownSimpleServer();
try {
if (serviceChannel != null) {
logger.log(Level.FINE, "Closing service server socket");
serviceChannel.close();
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Cannot close the service server socket: ", e);
}
serviceChannel = null;
try {
if (adminChannel != null) {
logger.log(Level.FINE, "Closing admin server socket");
adminChannel.close();
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Cannot close the admin server socket: ", e);
}
adminChannel = null;
}
private void shutdownSimpleServer() {
synchronized (lock) {
delayedModes.clear();
cancelTimer();
}
Connection conn = serviceConnection;
if (conn != null) {
logger.log(Level.INFO, "Shutdowning service connection");
logger.log(Level.INFO, "# max of concurrent compilations: " + serviceInfo.maxLocal);
logger.log(Level.INFO, "# total of compilations: " + serviceInfo.totalLocal);
logger.log(Level.INFO, "# max of concurrent forward compilations: " + serviceInfo.maxForward);
logger.log(Level.INFO, "# total of forward compilations: " + serviceInfo.totalForward);
try {
conn.close();
logger.log(Level.INFO, "Done");
} catch (IOException e) {
logger.log(Level.SEVERE, "Cannot shutdown the service connection: ", e);
}
}
conn = adminConnection;
if (conn != null) {
logger.log(Level.INFO, "Shutdowning admin connection");
try {
conn.close();
logger.log(Level.INFO, "Done");
} catch (IOException e) {
logger.log(Level.SEVERE, "Cannot shutdown the admin connection: ", e);
}
}
}
private void cancelTimer() {
synchronized (lock) {
if (timer != null) {
logger.log(Level.INFO, "Cancel timer");
timer.cancel();
timer.purge();
timer = null;
}
}
}
public long startingServiceTask() {
return startingTask(serviceInfo);
}
public void endingServiceTask() {
endingTask(serviceInfo);
}
public long startingAdministrativeTask() throws ServerClosedException {
synchronized (lock) {
if (shuttingDown) {
throw new ServerClosedException();
}
return startingTask(adminInfo);
}
}
public void endingAdministrativeTask() {
endingTask(adminInfo);
}
private long startingTask(@Nonnull ServerInfo info) {
long id;
synchronized (lock) {
id = info.totalLocal;
info.totalLocal++;
setServerMode(ServerMode.WORK);
info.currentLocal++;
if (info.currentLocal > info.maxLocal) {
info.maxLocal = info.currentLocal;
}
}
return id;
}
private void endingTask(@Nonnull ServerInfo info) {
synchronized (lock) {
info.currentLocal--;
if (adminInfo.currentLocal == 0
&& serviceInfo.currentLocal == 0) {
setServerMode(ServerMode.WAIT);
}
lock.notifyAll();
}
}
@Nonnull
public File getServerDir() {
return serverDir;
}
@Nonnull
public LauncherHandle getLauncherHandle() {
return launcherHandle;
}
@Nonnull
public ServerInfo getServiceStat() {
synchronized (lock) {
return serviceInfo.clone();
}
}
public void resetMaxServiceStat() {
synchronized (lock) {
serviceInfo.maxForward = serviceInfo.currentForward;
serviceInfo.maxLocal = serviceInfo.currentLocal;
}
}
public void uninstallJack(@Nonnull Program<JackProvider> existingJack) {
installedJack.invalidate(existingJack.getVersion());
}
@Nonnull
private static String getCurrentUser(@Nonnull File serverDir) throws IOException {
Set<PosixFilePermission> check = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(serverDir.toPath());
if (!check.equals(permissions)) {
throw new IOException("'" + serverDir.getPath() + "' must have permission "
+ PosixFilePermissions.toString(check) + " but have "
+ PosixFilePermissions.toString(permissions));
}
File tmp = File.createTempFile("jackserver-", ".tmp", serverDir);
try {
String tmpUser = Files.getFileAttributeView(tmp.toPath(),
FileOwnerAttributeView.class).getOwner().getName();
FileOwnerAttributeView ownerAttribute =
Files.getFileAttributeView(serverDir.toPath(), FileOwnerAttributeView.class);
if (!tmpUser.equals(ownerAttribute.getOwner().getName())) {
throw new IOException("'" + serverDir.getPath() + "' is not owned by '" + tmpUser
+ "' but by '" + ownerAttribute.getOwner().getName() + "'");
}
return tmpUser;
} finally {
if (!tmp.delete()) {
logger.log(Level.WARNING, "Failed to delete temp file '" + tmp.getPath() + "'");
}
}
}
@Nonnull
private PathRouter createAdminRouter() {
return new PathRouter()
.add("/gc",
new MethodRouter()
.add(Method.POST, new GC(this)))
.add("/stat",
new MethodRouter()
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new Stat(this)))
.add(Method.DELETE, new ResetStats(this)))
.add("/server/stop",
new MethodRouter()
.add(Method.POST, new Stop(this)))
.add("/server/reload",
new MethodRouter()
.add(Method.POST, new ReloadConfig(this)))
.add("/jack",
new MethodRouter()
.add(Method.PUT,
new ContentTypeRouter()
.add("multipart/form-data",
new PartParserRouter<>("force", new TextPlainPartParser<>(new BooleanCodec()),
new PartContentTypeRouter("jar")
.add("application/octet-stream", new InstallJack(this)))))
.add(Method.HEAD,
new ContentTypeRouter()
.add(ExactCodeVersionFinder.SELECT_EXACT_VERSION_CONTENT_TYPE,
new ContentTypeParameterRouter("version")
.add("1", new QueryJackVersion(this))))
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new GetJackVersions(this))))
.add("/server",
new MethodRouter()
.add(Method.PUT,
new ContentTypeRouter()
.add("multipart/form-data",
new PartParserRouter<>("force", new TextPlainPartParser<>(new BooleanCodec()),
new PartContentTypeRouter("jar")
.add("application/octet-stream", new InstallServer(this)))))
.add(Method.HEAD,
new ContentTypeRouter()
.add(ExactCodeVersionFinder.SELECT_EXACT_VERSION_CONTENT_TYPE,
new ContentTypeParameterRouter("version")
.add("1", new QueryServerVersion(this))))
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new GetServerVersion(this))))
.add("/launcher",
new MethodRouter()
.add(Method.PUT,
new ErrorContainer(Status.BAD_REQUEST))
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new GetLauncherVersion(this))))
.add("/launcher/home",
new MethodRouter()
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new GetLauncherHome(this))))
.add("/launcher/log",
new MethodRouter()
.add(Method.GET,
new AcceptContentTypeRouter()
.add(TextPlain.CONTENT_TYPE_NAME, new GetLauncherLog(this))))
.add("/launcher/log/level",
new MethodRouter()
.add(Method.PUT,
new ContentTypeRouter()
.add("multipart/form-data",
new PartContentTypeRouter("level")
.add(TextPlain.CONTENT_TYPE_NAME,
new PartParserRouter<>("limit",
new TextPlainPartParser<>(new IntCodec(0, Integer.MAX_VALUE)),
new PartParserRouter<>("count",
new TextPlainPartParser<>(new IntCodec(1, Integer.MAX_VALUE)),
new SetLoggerParameters(this)))))));
}
@Nonnull
private Container createServiceRouter() {
return new MethodRouter()
.add(Method.POST,
new ContentTypeRouter()
.add("multipart/form-data",
new AcceptContentTypeParameterRouter("version")
.add("1",
new PartContentTypeRouter("cli")
.add(TextPlain.CONTENT_TYPE_NAME,
new PartContentTypeRouter("pwd")
.add(TextPlain.CONTENT_TYPE_NAME,
new PartContentTypeRouter("version")
.add(ExactCodeVersionFinder.SELECT_EXACT_VERSION_CONTENT_TYPE,
new PartContentTypeParameterRouter("version", "version")
.add("1",
new PathRouter()
.add("/jack",
new PartParserRouter<>("assert",
new TextPlainPartParser<>(new BooleanCodec(), Boolean.FALSE),
new AcceptContentTypeRouter()
.add(CommandOutRaw.JACK_COMMAND_OUT_CONTENT_TYPE,
new JackTaskRawOut(this))
.add(CommandOutBase64.JACK_COMMAND_OUT_CONTENT_TYPE,
new JackTaskBase64Out(this))
.add("/jill", new JillTask(this)))))))))));
}
@Nonnull
private SSLContext setupSsl() throws ServerException {
FileInputStream keystoreServerIn = null;
FileInputStream keystoreClientIn = null;
SSLContext sslContext = null;
try {
File keystoreServerFile = new File(serverDir, KEYSTORE_SERVER);
File keystoreClientFile = new File(serverDir, KEYSTORE_CLIENT);
checkAccess(keystoreServerFile, EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
checkAccess(keystoreClientFile, EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
keystoreServerIn = new FileInputStream(keystoreServerFile);
KeyStore keystoreServer = KeyStore.getInstance("jks");
keystoreServer.load(keystoreServerIn, KEYSTORE_PASSWORD);
keystoreClientIn = new FileInputStream(keystoreClientFile);
KeyStore keystoreClient = KeyStore.getInstance("jks");
keystoreClient.load(keystoreClientIn, KEYSTORE_PASSWORD);
refreshPEMFiles(keystoreServer, keystoreClient);
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keystoreServer, KEYSTORE_PASSWORD);
TrustManagerFactory tm =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tm.init(keystoreClient);
sslContext = SSLContext.getInstance("SSLv3");
sslContext.init(keyManagerFactory.getKeyManagers(), tm.getTrustManagers(), null);
} catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException
| UnrecoverableKeyException | KeyManagementException e) {
throw new ServerException("Failed to setup ssl context", e);
} finally {
if (keystoreClientIn != null) {
try {
keystoreClientIn.close();
} catch (IOException e) {
// ignore
}
}
if (keystoreServerIn != null) {
try {
keystoreServerIn.close();
} catch (IOException e) {
// ignore
}
}
}
return sslContext;
}
private void configureSocket(@Nonnull Socket socket) {
SSLEngine engine = socket.getEngine();
if (filteredCiphersArray == null) {
// Synchronization not necessary since there's no going back to null and duplicate
// computations would produce the same result.
String[] enabledCyphers = engine.getEnabledCipherSuites();
ArrayList<String> filteredCiphers = new ArrayList<>(enabledCyphers.length);
// Filter out TLS_DHE and TLS_EDH because they are weak when running on a jre 7
// and may cause connection issues depending on curl and libraries version.
Pattern excludePattern = Pattern.compile("TLS_(DHE)|(EDH).*");
for (String string : enabledCyphers) {
if (!excludePattern.matcher(string).matches()) {
filteredCiphers.add(string);
}
}
filteredCiphersArray = filteredCiphers.toArray(
new String[filteredCiphers.size()]);
}
engine.setEnabledCipherSuites(filteredCiphersArray);
engine.setNeedClientAuth(true);
}
private void addServerMode(@Nonnegative int delay, @Nonnull ServerMode newMode) {
delayedModes.add(new TimedServerMode(delay * 1000L, newMode));
}
private void setServerMode(@Nonnull ServerMode newMode) {
synchronized (lock) {
if (this.serverMode.equals(newMode)) {
return;
}
ServerMode oldMode = this.serverMode;
this.serverMode = newMode;
logger.log(Level.INFO, "Server mode changing from " + oldMode + " to " + newMode);
ServerModeWatcher watcher = modeWatchers.get(newMode);
if (watcher != null) {
watcher.changedMode(oldMode, newMode);
}
}
}
private void addServerModeWatcher(@Nonnull ServerMode newMode,
@Nonnull ServerModeWatcher watcher) {
assert modeWatchers.get(newMode) == null;
modeWatchers.put(newMode, watcher);
}
}