Java tutorial
/* * Copyright (c) 2014 Vincent W. Chen. * * This file is part of XJavaB. * * XJavaB is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * XJavaB 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with XJavaB. If not, see <http://www.gnu.org/licenses/>. */ package com.noodlewiz.xjavab.core; import java.io.IOException; import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.concurrent.TimeUnit; import com.google.common.base.Function; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Iterables; import com.google.common.collect.Range; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.noodlewiz.xjavab.core.bigreq.BigRequests; import com.noodlewiz.xjavab.core.bigreq.EnableReply; import com.noodlewiz.xjavab.core.ge.GenericEvent; import com.noodlewiz.xjavab.core.ge.QueryVersionReply; import com.noodlewiz.xjavab.core.internal.ChannelReader; import com.noodlewiz.xjavab.core.internal.ChannelWriter; import com.noodlewiz.xjavab.core.internal.ExtensionCache; import com.noodlewiz.xjavab.core.internal.ResponseManager; import com.noodlewiz.xjavab.core.internal.Unpacker; import com.noodlewiz.xjavab.core.internal.Util; import com.noodlewiz.xjavab.core.internal.XAuth; import com.noodlewiz.xjavab.core.internal.XRequest; import com.noodlewiz.xjavab.core.xcmisc.GetXIDRangeReply; import com.noodlewiz.xjavab.core.xcmisc.XCMisc; import com.noodlewiz.xjavab.core.xproto.Setup; import com.noodlewiz.xjavab.core.xproto.SetupAuthenticate; import com.noodlewiz.xjavab.core.xproto.SetupFailed; import com.noodlewiz.xjavab.core.xproto.SetupRequest; import com.noodlewiz.xjavab.core.xproto.internal.Packer; /** * A connection to an X server. */ public class Connection { /** X protocol major version. */ public static final int PROTOCOL_MAJOR_VERSION = 11; /** X protocol minor version. */ public static final int PROTOCOL_MINOR_VERSION = 0; private static final Logger mLogger = LoggerFactory.getLogger(Connection.class); private final Setup mSetup; private Iterator<Long> mXids; private final Object mXidsLock = new Object(); private ResponseManager mResponseManager; private SocketChannel mSocketChannel; private Thread mChannelReader; private ChannelWriter mChannelWriter; private Thread mChannelWriterThread; private long mMaxRequestLength; /** * Connects to the X server at localhost and port 6000. * * @throws IOException If some I/O error occurs */ public Connection() throws IOException { this(new InetSocketAddress("localhost", 6000)); } /** * Connects to the X server at given remote address. * * @param remote The socket address to connect to * @throws IOException If some I/O error occurs */ public Connection(final SocketAddress remote) throws IOException { mResponseManager = new ResponseManager(); try { mSocketChannel = SocketChannel.open(remote); mSocketChannel.configureBlocking(true); // Disable Nagle's algorithm. mSocketChannel.socket().setTcpNoDelay(true); mSocketChannel.finishConnect(); mLogger.info("Connected to socket."); } catch (final IOException e) { mLogger.error("Error while opening socket."); throw e; } mSetup = setup(); mLogger.info("Connection setup complete."); mMaxRequestLength = mSetup.maximumRequestLength; initXids(); final ExtensionCache extCache = new ExtensionCache(this); mChannelReader = new Thread(new ChannelReader(mSocketChannel, extCache, mResponseManager)); mChannelReader.start(); mChannelWriter = new ChannelWriter(mSocketChannel, extCache, mResponseManager); mChannelWriterThread = new Thread(mChannelWriter); mChannelWriterThread.start(); mLogger.info("Connection complete."); initExtensions(); mLogger.info("Extension initialization complete."); } private Setup setup() throws IOException { try { mSocketChannel.write(createSetupRequest()); return handleSetupReply(); } catch (final IOException e) { mLogger.error("Cannot establish connection to X server."); throw e; } } private static ByteBuffer createSetupRequest() { final XAuth auth = XAuth.getAuth(); final int nameLen = auth.name.length(); final int namePad = Util.padding(nameLen); final int dataLen = auth.data.length; final int dataPad = Util.padding(dataLen); final ByteBuffer buf = ByteBuffer.allocate(48); final SetupRequest request = new SetupRequest((byte) 0x42, PROTOCOL_MAJOR_VERSION, PROTOCOL_MINOR_VERSION, nameLen, dataLen, auth.name + Util.repeat("\0", namePad), new String(auth.data, StandardCharsets.ISO_8859_1) + Util.repeat("\0", dataPad)); Packer.pack(buf, request); buf.rewind(); return buf; } private Setup handleSetupReply() throws IOException { final ByteBuffer header = ByteBuffer.allocate(8); mSocketChannel.read(header); header.rewind(); final short status = Unpacker.unpackUByte(header); header.position(6); final int length = Unpacker.unpackUShort(header); final ByteBuffer reply = ByteBuffer.allocate(8 + length * 4); header.rewind(); reply.put(header); mSocketChannel.read(reply); reply.rewind(); switch (status) { case 0: return handleSetupFailed(reply.asReadOnlyBuffer()); case 1: return handleSetupSuccess(reply.asReadOnlyBuffer()); case 2: return handleSetupAuthenticate(reply.asReadOnlyBuffer()); default: throw new AssertionError("Invalid setup status: " + status); } } private static Setup handleSetupFailed(final ByteBuffer buf) { final SetupFailed info = com.noodlewiz.xjavab.core.xproto.internal.Unpacker.unpackSetupFailed(buf); mLogger.error("Connection setup failed. Reason: {}\nProtocol version: {} / {}", info.reason, info.protocolMajorVersion, info.protocolMinorVersion); throw new UnsupportedOperationException(); } private static Setup handleSetupSuccess(final ByteBuffer buf) { return com.noodlewiz.xjavab.core.xproto.internal.Unpacker.unpackSetup(buf); } private static Setup handleSetupAuthenticate(final ByteBuffer buf) { final SetupAuthenticate info = com.noodlewiz.xjavab.core.xproto.internal.Unpacker .unpackSetupAuthenticate(buf); mLogger.error("Connection setup requires additional authentication: {}", info.reason); throw new UnsupportedOperationException(); } private void initXids() { final long base = mSetup.resourceIdBase; final long mask = mSetup.resourceIdMask; final long shifts = BigInteger.valueOf(mask).getLowestSetBit(); final Iterable<Long> r = ContiguousSet.create(Range.closed(0L, mask >> shifts), DiscreteDomain.longs()); final Iterable<Long> ids = Iterables.transform(r, new Function<Long, Long>() { @Override public Long apply(final Long input) { return (input << shifts) | base; } }); mXids = ids.iterator(); } private void initExtensions() { // Big Requests extension. try { final EnableReply reply = BigRequests.enable(this).getReply(); mChannelWriter.enableBigRequests(mSetup.maximumRequestLength); mMaxRequestLength = reply.maximumRequestLength; } catch (InterruptedException e) { mLogger.error("Interrupted while querying Big Requests version."); // Do nothing. } // Generic Event extension. try { final int majorVersion = (int) GenericEvent.MAJOR_VERSION; final int minorVersion = (int) GenericEvent.MINOR_VERSION; final QueryVersionReply reply = GenericEvent.queryVersion(this, majorVersion, minorVersion).getReply(); if (reply.majorVersion != majorVersion || reply.minorVersion != minorVersion) { mLogger.warn("Generic Event extension version not supported."); } } catch (InterruptedException e) { mLogger.error("Interrupted while querying Generic Event version."); // Do nothing. } } // Sending of requests should be done through provided static functions, // therefore this method has to be kept private. However, the actual sending // of requests is done in this method, which will be called by all static // request sending functions. @SuppressWarnings("unused") private <T extends XReply> Receipt<T> sendInternal(final XRequest request, final Class<T> clazz) { return mChannelWriter.send(request, clazz); } /** * Returns the remote address to which this socket is connected. * * @return The remote address. * @throws IOException If an I/O error occurs */ public SocketAddress getRemoteAddress() throws IOException { return mSocketChannel.getRemoteAddress(); } /** * Access the data returned by the server during connection setup. * <p> * See the X protocol specification for more details. * * @return The data returned during connection setup */ public Setup getSetup() { return mSetup; } /** * Returns the maximum request length that the server accepts. * <p> * If the server does not support the Big Requests extension, then the * maximum request length field from {@link #getSetup()} will be returned. * Otherwise the reply from calling {@link BigRequests#enable(Connection)} * will be returned. * <p> * The returned length is measured in four-byte units. * * @return The maximum request length */ public long getMaximumRequestLength() { return mMaxRequestLength; } /** * Wait for the next event. Blocks until an event is available. * * @return The next event * @throws InterruptedException If another thread interrupts the current * thread while waiting for next event */ public XEvent waitEvent() throws InterruptedException { return mResponseManager.waitEvent(); } /** * Poll for the next event, if any. Returns immediately if no event is * available. * * @return The next event, or <code>null</code> if none is available */ public XEvent pollEvent() { return mResponseManager.pollEvent(); } /** * Poll for the next event, if any. Blocks until an event becomes available * or timeout expires. * * @param timeout How long to wait before giving up, in units of * <code>unit</code> * @param unit A {@link TimeUnit} determining how to interpret the * <code>timeout</code> parameter * @return The next event, or <code>null</code> if none is available after * timeout expires * @throws InterruptedException If another thread interrupts the current * thread while waiting for next event */ public XEvent pollEvent(final long timeout, final TimeUnit unit) throws InterruptedException { return mResponseManager.pollEvent(timeout, unit); } /** * Allocates an XID (resource identifier) for a new object. * * @return An XID */ public long genXid() { synchronized (mXidsLock) { if (!mXids.hasNext()) { // Ask for more XIDs from XC_MISC. try { final GetXIDRangeReply reply = XCMisc.getXIDRange(this).getReply(); final long start = reply.startId; final long end = start + reply.count; final Iterable<Long> ids = ContiguousSet.create(Range.closedOpen(start, end), DiscreteDomain.longs()); mXids = ids.iterator(); } catch (final InterruptedException e) { mLogger.error("Interrupted while retrieving XIDs.", e); System.exit(1); } catch (final RuntimeException e) { mLogger.error("Unable to allocate more XID."); throw e; } } return mXids.next(); } } /** * Disconnects from the X server. Connection will no longer be valid after * this method returns. * * @throws IOException If some I/O error occurs * @throws InterruptedException If another thread interrupts the current * thread while disconnecting */ public void disconnect() throws IOException, InterruptedException { try { // ChannelReader/Writer has to be interrupted first, so that it // would stop using shared resources (e.g. ResponseManager). mChannelReader.interrupt(); mChannelWriterThread.interrupt(); mChannelReader.join(); mChannelWriterThread.join(); mSocketChannel.close(); } catch (final InterruptedException e) { mLogger.error("Interrupted while waiting for reader/writer thread to die."); throw e; } catch (final IOException e) { mLogger.error("Error while closing socket channel."); throw e; } finally { mChannelReader = null; mChannelWriter = null; mChannelWriterThread = null; mSocketChannel = null; // Remove shared resources last. mXids = null; mResponseManager = null; } mLogger.info("Disconnected."); } }