org.wildfly.swarm.arquillian.daemon.server.Server Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of arquillian-daemon Show documentation
Show all versions of arquillian-daemon Show documentation
WildFly Swarm: Arquillian Daemon API
/**
* Copyright 2015-2016 Red Hat, Inc, and individual contributors.
*
* 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 org.wildfly.swarm.arquillian.daemon.server;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.shrinkwrap.api.ConfigurationBuilder;
import org.jboss.shrinkwrap.api.Domain;
import org.jboss.shrinkwrap.api.GenericArchive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.wildfly.swarm.arquillian.daemon.TestRunner;
import org.wildfly.swarm.arquillian.daemon.protocol.WireProtocol;
/**
* Netty-based implementation of a server; not thread-safe via the Java API (though invoking wire protocol
* operations through its communication channels is). Responsible for handling I/O aspects of the server daemon.
*
* @author Andrew Lee Rubinger
* @author Toby Crawley
*/
public class Server {
public static final int MAX_PORT = 65535;
private DeploymentUnit deploymentUnit;
Server(final InetSocketAddress bindAddress) {
// Precondition checks
assert bindAddress != null : "Bind address must be specified";
// Determine the ClassLoader to use in creating the SW Domain
final ClassLoader thisCl = Server.class.getClassLoader();
final Set classloaders = new HashSet<>(1);
classloaders.add(thisCl);
if (Server.log.isLoggable(Level.FINEST)) {
Server.log.finest("Using ClassLoader for ShrinkWrap Domain: " + thisCl);
}
this.shrinkwrapDomain = ShrinkWrap.createDomain(new ConfigurationBuilder().classLoaders(classloaders));
// Set
this.bindAddress = bindAddress;
}
public static Server create(final String bindAddress, final int bindPort) throws IllegalArgumentException {
// Precondition checks
if (bindPort < 0 || bindPort > MAX_PORT) {
throw new IllegalArgumentException("Bind port must be between 0 and " + MAX_PORT);
}
// Create the inetaddress and ensure it's resolved
final InetSocketAddress resolvedInetAddress = bindAddress == null ? new InetSocketAddress(bindPort)
: new InetSocketAddress(bindAddress, bindPort);
if (resolvedInetAddress.isUnresolved()) {
throw new IllegalArgumentException("Address \"" + bindAddress + "\" could not be resolved");
}
// Create and return a new server instance
return new Server(resolvedInetAddress);
}
public final void start() throws ServerLifecycleException, IllegalStateException {
// Precondition checks
if (this.isRunning()) {
throw new IllegalStateException("Already running");
}
// Set up Netty Boostrap
final EventLoopGroup parentGroup = new NioEventLoopGroup();
final EventLoopGroup childGroup = new NioEventLoopGroup();
this.eventLoopGroups.add(parentGroup);
this.eventLoopGroups.add(childGroup);
final ServerBootstrap bootstrap = new ServerBootstrap()
.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.localAddress(this.getBindAddress())
.childHandler(new ChannelInitializer() {
@Override
public void initChannel(final SocketChannel channel) throws Exception {
final ChannelPipeline pipeline = channel.pipeline();
setupPipeline(pipeline);
}
})
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Start 'er up
final ChannelFuture openChannel;
try {
openChannel = bootstrap.bind().sync();
} catch (final InterruptedException ie) {
Thread.interrupted();
throw new ServerLifecycleException("Interrupted while awaiting server start", ie);
} catch (final RuntimeException re) {
// Exception xlate
throw new ServerLifecycleException("Encountered error in binding; could not start server.", re);
}
// Set bound address
final InetSocketAddress boundAddress = ((InetSocketAddress) openChannel.channel().localAddress());
// Running
running = true;
// Create the shutdown service
this.shutdownService = Executors.newSingleThreadExecutor();
if (log.isLoggable(Level.INFO)) {
log.info("Arquillian Daemon server started on " + boundAddress.getHostName() + ":" +
boundAddress.getPort());
}
}
public final synchronized void stop() throws ServerLifecycleException, IllegalStateException {
// Use an anonymous logger because the JUL LogManager will not log after process shutdown has been received
final Logger log = Logger.getAnonymousLogger();
log.addHandler(new Handler() {
@Override
public void publish(final LogRecord record) {
System.out.println(PREFIX + record.getMessage());
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
private final String PREFIX = "[" + Server.class.getSimpleName() + "] ";
});
if (!this.isRunning()) {
throw new IllegalStateException("Server is not running");
}
if (log.isLoggable(Level.INFO)) {
log.info("Requesting shutdown...");
}
this.eventLoopGroups.forEach(EventLoopGroup::shutdownGracefully);
this.eventLoopGroups.clear();
// Kill the shutdown service
shutdownService.shutdownNow();
shutdownService = null;
// Not running
running = false;
if (log.isLoggable(Level.INFO)) {
log.info("Server shutdown.");
}
}
public final boolean isRunning() {
return running;
}
private static ChannelFuture sendResponse(final ChannelHandlerContext ctx, final String response) {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes(response.getBytes(WireProtocol.CHARSET));
ctx.write(buf);
return ctx.writeAndFlush(Delimiters.lineDelimiter()[0]);
}
/**
* The address configured to which we should bind
*
* @return
*/
protected final InetSocketAddress getBindAddress() {
return this.bindAddress;
}
/**
* @return the deployedArchives
*/
protected final ConcurrentMap getDeployedArchives() {
return deployedArchives;
}
/**
* @return the shrinkwrapDomain
*/
protected final Domain getShrinkwrapDomain() {
return shrinkwrapDomain;
}
public void setDeploymentUnit(DeploymentUnit deploymentUnit) {
this.deploymentUnit = deploymentUnit;
}
protected final Serializable executeTest(final String testClassName, final String methodName) {
return new TestRunner(deploymentUnit).executeTest(testClassName, methodName);
}
/**
* Asynchronously calls upon {@link Server#stop()}
*/
protected final void stopAsync() {
shutdownService.submit(() -> {
Server.this.stop();
return null;
});
}
private void setupPipeline(final ChannelPipeline pipeline) {
pipeline.addLast(NAME_CHANNEL_HANDLER_FRAME_DECODER,
new DelimiterBasedFrameDecoder(2000, Delimiters.lineDelimiter()));
pipeline.addLast(NAME_CHANNEL_HANDLER_STRING_DECODER,
new StringDecoder(WireProtocol.CHARSET));
pipeline.addLast(NAME_CHANNEL_HANDLER_COMMAND, new StringCommandHandler());
}
private static final Logger log = Logger.getLogger(Server.class.getName());
private static final String NAME_CHANNEL_HANDLER_STRING_DECODER = "StringDecoder";
private static final String NAME_CHANNEL_HANDLER_FRAME_DECODER = "FrameDecoder";
private static final String NAME_CHANNEL_HANDLER_COMMAND = "CommandHandler";
private final List eventLoopGroups = new ArrayList<>();
private final ConcurrentMap deployedArchives = new ConcurrentHashMap<>();
private final Domain shrinkwrapDomain;
private final InetSocketAddress bindAddress;
private ExecutorService shutdownService;
private boolean running;
/**
* Handler for all {@link String}-based commands to the server as specified in {@link WireProtocol}
*
* @author Andrew Lee Rubinger
*/
private class StringCommandHandler extends SimpleChannelInboundHandler {
/**
* Ignores all exceptions on messages received if the server is not running, else delegates to the super
* implementation.
*
* @see io.netty.channel.SimpleChannelInboundHandler#exceptionCaught(io.netty.channel.ChannelHandlerContext,
* java.lang.Throwable)
*/
@Override
public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception {
// If the server isn't running, ignore everything
if (!Server.this.isRunning()) {
// Ignore, but log if we've got a fine-grained enough level set
if (log.isLoggable(Level.FINEST)) {
log.finest("Got exception while server is not running: " + cause.getMessage());
}
ctx.close();
} else {
super.exceptionCaught(ctx, cause);
}
}
/**
* {@inheritDoc}
*
* @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(ChannelHandlerContext, Object)
*/
@Override
protected void channelRead0(final ChannelHandlerContext ctx, final String message) throws Exception {
// We want to catch any and all errors to to write out a proper response to the client
try {
// Stop
if (WireProtocol.COMMAND_STOP.equals(message)) {
// Set the response to tell the client OK
Server.sendResponse(ctx, WireProtocol.RESPONSE_OK_PREFIX + message)
.addListener(future -> Server.this.stopAsync());
} else if (message.startsWith(WireProtocol.COMMAND_TEST_PREFIX)) {
// Test
// Parse out the arguments
final StringTokenizer tokenizer = new StringTokenizer(message);
tokenizer.nextToken();
tokenizer.nextToken();
final String testClassName = tokenizer.nextToken();
final String methodName = tokenizer.nextToken();
// Execute the test and get the result
final Serializable testResult = Server.this.executeTest(testClassName, methodName);
ObjectOutputStream objectOutstream = null;
try {
// Write the test result
ByteBuf out = ctx.alloc().buffer();
objectOutstream = new ObjectOutputStream(new ByteBufOutputStream(out));
objectOutstream.writeObject(testResult);
objectOutstream.flush();
ctx.writeAndFlush(out);
} finally {
if (objectOutstream != null) {
objectOutstream.close();
}
}
} else {
// Unsupported command
throw new UnsupportedOperationException("This server does not support command: " + message);
}
} catch (final Throwable t) {
// Will be captured by any remote process which launched us and is piping in our output
t.printStackTrace();
Server.sendResponse(ctx, WireProtocol.RESPONSE_ERROR_PREFIX
+ "Caught unexpected error servicing request: " + t.getMessage());
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy