com.noodlewiz.xjavab.core.Connection.java Source code

Java tutorial

Introduction

Here is the source code for com.noodlewiz.xjavab.core.Connection.java

Source

/*
 * 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.");
    }

}