Java tutorial
/* * JBoss, Home of Professional Open Source * Copyright 2012, Red Hat Middleware LLC, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of 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.jboss.arquillian.daemon.server; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundByteHandlerAdapter; import io.netty.channel.ChannelInboundMessageHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioEventLoopGroup; 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 java.io.InputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.charset.Charset; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import org.jboss.arquillian.daemon.protocol.wire.WireProtocol; import org.jboss.shrinkwrap.api.GenericArchive; import org.jboss.shrinkwrap.api.importer.ZipImporter; /** * Netty-based implementation of a {@link 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 <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a> */ final class NettyServer extends ServerBase implements Server { private static final Logger log = Logger.getLogger(NettyServer.class.getName()); private static final EofDecoder EOF_DECODER; static { try { EOF_DECODER = new EofDecoder(); } catch (final UnsupportedEncodingException e) { throw new RuntimeException("Could not get encoding: " + WireProtocol.CHARSET, e); } } private static final String NAME_CHANNEL_HANDLER_EOF = "EOFHandler"; private static final String NAME_CHANNEL_HANDLER_ACTION_CONTROLLER = "ActionControllerHandler"; 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_DEPLOY_HANDLER = "DeployHandler"; private static final String NAME_CHANNEL_HANDLER_COMMAND = "CommandHandler"; private static final String[] NAME_CHANNEL_HANDLERS = { NAME_CHANNEL_HANDLER_EOF, NAME_CHANNEL_HANDLER_ACTION_CONTROLLER, NAME_CHANNEL_HANDLER_STRING_DECODER, NAME_CHANNEL_HANDLER_FRAME_DECODER, NAME_CHANNEL_HANDLER_DEPLOY_HANDLER, NAME_CHANNEL_HANDLER_COMMAND }; private ServerBootstrap bootstrap; NettyServer(final InetSocketAddress bindAddress) { super(bindAddress); } /** * {@inheritDoc} * * @see org.jboss.arquillian.daemon.server.ServerBase#startInternal() */ @Override protected void startInternal() throws ServerLifecycleException, IllegalStateException { // Set up Netty Boostrap final ServerBootstrap bootstrap = new ServerBootstrap() .group(new NioEventLoopGroup(), new NioEventLoopGroup()).channel(NioServerSocketChannel.class) .localAddress(this.getBindAddress()).childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(final SocketChannel channel) throws Exception { final ChannelPipeline pipeline = channel.pipeline(); NettyServer.this.resetPipeline(pipeline); } }).childOption(ChannelOption.TCP_NODELAY, true).childOption(ChannelOption.SO_KEEPALIVE, true); this.bootstrap = bootstrap; // 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()); this.setBoundAddress(boundAddress); } /** * {@inheritDoc} * * @see org.jboss.arquillian.daemon.server.ServerBase#stopInternal() */ @Override protected void stopInternal() throws ServerLifecycleException, IllegalStateException { // Shutdown bootstrap.shutdown(); } /** * Handler for all {@link String}-based commands to the server as specified in {@link WireProtocol} * * @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a> */ private class StringCommandHandler extends ChannelInboundMessageHandlerAdapter<String> { /** * {@inheritDoc} * * @see io.netty.channel.ChannelInboundMessageHandlerAdapter#messageReceived(io.netty.channel.ChannelHandlerContext, * java.lang.Object) */ @Override public void messageReceived(final ChannelHandlerContext ctx, final String message) throws Exception { if (log.isLoggable(Level.FINEST)) { log.finest("Got command: " + message); } // Get the buffer for the response final ByteBuf out = ctx.nextOutboundByteBuffer(); // We want to catch any and all errors to to write out a proper response to the client try { // Reset the pipeline for the next call final ChannelPipeline pipeline = ctx.pipeline(); NettyServer.this.resetPipeline(pipeline); // Stop if (WireProtocol.COMMAND_STOP.equals(message)) { // Set the response to tell the client OK NettyServer.sendResponse(ctx, out, WireProtocol.RESPONSE_OK_PREFIX + message); // Now stop in another thread (after we send the response, else we might prematurely close the // connection) NettyServer.this.stopAsync(); } // Undeployment else if (message.startsWith(WireProtocol.COMMAND_UNDEPLOY_PREFIX)) { // Get out the deployment final String deploymentName = message.substring(WireProtocol.COMMAND_UNDEPLOY_PREFIX.length()) .trim(); if (log.isLoggable(Level.FINEST)) { log.finest("Requesting undeployment of: " + deploymentName); } final GenericArchive removedArchive = NettyServer.this.getDeployedArchives() .remove(deploymentName); // Check that we resulted in undeployment if (removedArchive == null) { if (log.isLoggable(Level.FINEST)) { log.finest("Not current deployment: " + deploymentName); } final String response = WireProtocol.RESPONSE_ERROR_PREFIX + "Deployment " + deploymentName + " could not be found in current deployments."; NettyServer.sendResponse(ctx, out, response); return; } // Tell the client OK if (log.isLoggable(Level.FINEST)) { log.finest("Undeployed: " + deploymentName); } final String response = WireProtocol.RESPONSE_OK_PREFIX + deploymentName; NettyServer.sendResponse(ctx, out, response); } // Test else if (message.startsWith(WireProtocol.COMMAND_TEST_PREFIX)) { // Parse out the arguments final StringTokenizer tokenizer = new StringTokenizer(message); tokenizer.nextToken(); tokenizer.nextToken(); final String archiveId = tokenizer.nextToken(); final String testClassName = tokenizer.nextToken(); final String methodName = tokenizer.nextToken(); // Execute the test and get the result final Serializable testResult = NettyServer.this.executeTest(archiveId, testClassName, methodName); ObjectOutputStream objectOutstream = null; try { // Write the test result out.discardReadBytes(); objectOutstream = new ObjectOutputStream(new ByteBufOutputStream(out)); objectOutstream.writeObject(testResult); objectOutstream.flush(); ctx.flush(); return; } finally { if (objectOutstream != null) { objectOutstream.close(); } } } // Unsupported command else { 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(); NettyServer.sendResponse(ctx, out, WireProtocol.RESPONSE_ERROR_PREFIX + "Caught unexpected error servicing request: " + t.getMessage()); } } /** * Ignores all exceptions on messages received if the server is not running, else delegates to the super * implementation. * * @see io.netty.channel.ChannelStateHandlerAdapter#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 (!NettyServer.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); } } } /** * Handles deployment only * * @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a> */ private final class DeployHandlerAdapter extends ChannelInboundByteHandlerAdapter { @Override public void inboundBufferUpdated(final ChannelHandlerContext ctx, final ByteBuf in) throws Exception { if (log.isLoggable(Level.FINEST)) { log.finest("Using the " + this.getClass().getSimpleName()); } try { // Read in the archive using the isolated CL context of this domain final InputStream instream = new ByteBufInputStream(in); final GenericArchive archive = NettyServer.this.getShrinkwrapDomain().getArchiveFactory() .create(ZipImporter.class).importFrom(instream).as(GenericArchive.class); instream.close(); if (log.isLoggable(Level.FINEST)) { log.finest("Got archive: " + archive.toString(true)); } // Store the archive final String id = archive.getId(); NettyServer.this.getDeployedArchives().put(id, archive); // Tell the client OK, and let it know the ID of the archive (so it may be undeployed) final ByteBuf out = ctx.nextOutboundByteBuffer(); NettyServer.sendResponse(ctx, out, WireProtocol.RESPONSE_OK_PREFIX + WireProtocol.COMMAND_DEPLOY_PREFIX + id); } finally { NettyServer.this.resetPipeline(ctx.pipeline()); } } } /** * Determines the type of request and adjusts the pipeline to handle appropriately * * @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a> */ private final class ActionControllerHandler extends ChannelInboundByteHandlerAdapter { @Override public void inboundBufferUpdated(final ChannelHandlerContext ctx, final ByteBuf in) throws Exception { // We require at least three bytes to determine the action taken if (in.readableBytes() < 3) { return; } // Get the pipeline so we can dynamically adjust it and fire events final ChannelPipeline pipeline = ctx.pipeline(); // Pull out the magic header int readerIndex = in.readerIndex(); final int magic1 = in.getUnsignedByte(readerIndex); final int magic2 = in.getUnsignedByte(readerIndex + 1); final int magic3 = in.getUnsignedByte(readerIndex + 2); // String-based Command? if (this.isStringCommand(magic1, magic2, magic3)) { // Write a line break into the buffer so we mark the frame in.writeBytes(Delimiters.lineDelimiter()[0]); // Adjust the pipeline such that we use the command handler pipeline.addLast(NAME_CHANNEL_HANDLER_FRAME_DECODER, new DelimiterBasedFrameDecoder(2000, Delimiters.lineDelimiter())); pipeline.addLast(NAME_CHANNEL_HANDLER_STRING_DECODER, new StringDecoder(Charset.forName(WireProtocol.CHARSET))); pipeline.addLast(NAME_CHANNEL_HANDLER_COMMAND, new StringCommandHandler()); pipeline.remove(NAME_CHANNEL_HANDLER_ACTION_CONTROLLER); pipeline.remove(NAME_CHANNEL_HANDLER_EOF); } // Deploy command? else if (this.isDeployCommand(magic1, magic2, magic3)) { // Set the reader index so we strip out the command portion, leaving only the bytes containing the // archive (the frame decoder will strip off the EOF delimiter) in.readerIndex(in.readerIndex() + WireProtocol.COMMAND_DEPLOY_PREFIX.length()); // Adjust the pipeline such that we use the deploy handler only pipeline.addLast(NAME_CHANNEL_HANDLER_DEPLOY_HANDLER, new DeployHandlerAdapter()); pipeline.remove(NAME_CHANNEL_HANDLER_ACTION_CONTROLLER); pipeline.remove(NAME_CHANNEL_HANDLER_EOF); } else { // Unknown command/protocol NettyServer.sendResponse(ctx, ctx.nextOutboundByteBuffer(), WireProtocol.RESPONSE_ERROR_PREFIX + "Unsupported Command"); in.clear(); ctx.close(); return; } // Write the bytes to the next inbound buffer and re-fire so the updated handlers in the pipeline can have a // go at it final ByteBuf nextInboundByteBuffer = ctx.nextInboundByteBuffer(); nextInboundByteBuffer.writeBytes(in); pipeline.fireInboundBufferUpdated(); } /** * Returns to the client that some error was encountered * * @see io.netty.channel.ChannelStateHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, * java.lang.Throwable) */ @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { NettyServer.sendResponse(ctx, ctx.nextOutboundByteBuffer(), cause.getMessage()); } /** * Determines whether we have a {@link String}-based command * * @param magic1 * @param magic2 * @param magic3 * @return */ private boolean isStringCommand(final int magic1, final int magic2, final int magic3) { // First the bytes matches command prefix? return magic1 == WireProtocol.PREFIX_STRING_COMMAND.charAt(0) && magic2 == WireProtocol.PREFIX_STRING_COMMAND.charAt(1) && magic3 == WireProtocol.PREFIX_STRING_COMMAND.charAt(2); } /** * Determines whether we have a deployment command * * @param magic1 * @param magic2 * @param magic3 * @return */ private boolean isDeployCommand(final int magic1, final int magic2, final int magic3) { return magic1 == WireProtocol.COMMAND_DEPLOY_PREFIX.charAt(0) && magic2 == WireProtocol.COMMAND_DEPLOY_PREFIX.charAt(1) && magic3 == WireProtocol.COMMAND_DEPLOY_PREFIX.charAt(2); } } /** * {@link DelimiterBasedFrameDecoder} implementation to use the {@link WireProtocol#COMMAND_EOF_DELIMITER}, * stripping it from the buffer. Is {@link Sharable} to allow this to be added/removed more than once. * * @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a> */ @Sharable private static final class EofDecoder extends DelimiterBasedFrameDecoder { public EofDecoder() throws UnsupportedEncodingException { super(Integer.MAX_VALUE, true, Unpooled.wrappedBuffer(WireProtocol.COMMAND_EOF_DELIMITER.getBytes(WireProtocol.CHARSET))); } } private void resetPipeline(final ChannelPipeline pipeline) { // Remove all we've added for (final String handlerName : NAME_CHANNEL_HANDLERS) { try { pipeline.remove(handlerName); } catch (final NoSuchElementException ignore) { } } // Manually set up pipeline for action controller pipeline.addLast(NAME_CHANNEL_HANDLER_EOF, EOF_DECODER); pipeline.addLast(NAME_CHANNEL_HANDLER_ACTION_CONTROLLER, new ActionControllerHandler()); } private static void sendResponse(final ChannelHandlerContext ctx, final ByteBuf out, final String response) { out.discardReadBytes(); try { out.writeBytes(response.getBytes(WireProtocol.CHARSET)); out.writeBytes(Delimiters.lineDelimiter()[0]); } catch (final UnsupportedEncodingException uee) { throw new RuntimeException("Unsupported encoding", uee); } ctx.flush(); } }