fr.letroll.ttorrentandroid.tracker.Tracker.java Source code

Java tutorial

Introduction

Here is the source code for fr.letroll.ttorrentandroid.tracker.Tracker.java

Source

/**
 * Copyright (C) 2011-2012 Turn, Inc.
 *
 * 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 fr.letroll.ttorrentandroid.tracker;

import com.google.common.collect.Iterables;
import com.google.common.net.InetAddresses;
import fr.letroll.ttorrentandroid.common.Torrent;

import fr.letroll.ttorrentandroid.common.TorrentUtils;
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Gauge;
import com.yammer.metrics.core.MetricsRegistry;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.CheckForNull;
import javax.annotation.CheckForSigned;
import javax.annotation.Nonnull;
import org.simpleframework.http.core.Container;
import org.simpleframework.http.core.ContainerServer;
import org.simpleframework.transport.Server;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * BitTorrent tracker.
 *
 * <p>
 * The tracker usually listens on port 6969 (the standard BitTorrent tracker
 * port). Torrents must be registered directly to this tracker with the
 * {@link #announce(TrackedTorrent torrent)}</code> method.
 * </p>
 *
 * @author mpetazzoni
 */
public class Tracker {

    private static final Logger LOG = LoggerFactory.getLogger(Tracker.class);
    /** Request path handled by the tracker announce request handler. */
    public static final String ANNOUNCE_URL = "/announce";
    /** Default tracker listening port (BitTorrent's default is 6969). */
    public static final int DEFAULT_TRACKER_PORT = 6969;
    /** Default server name and version announced by the tracker. */
    public static final String DEFAULT_VERSION_STRING = "BitTorrent Tracker (ttorrent)";
    private static final int PEER_COLLECTION_FREQUENCY_SECONDS = 15;

    public static class Listener {

        private Connection connection;
        private SocketAddress connectionAddress;
    }

    private final String version;
    private MetricsRegistry metricsRegistry = Metrics.defaultRegistry();
    private final ConcurrentMap<InetSocketAddress, Listener> listeners = new ConcurrentHashMap<InetSocketAddress, Listener>();
    /** The in-memory repository of torrents tracked. */
    private final ConcurrentMap<String, TrackedTorrent> torrents = new ConcurrentHashMap<String, TrackedTorrent>();
    private TrackerMetrics metrics;
    private ScheduledExecutorService scheduler;
    private final Object lock = new Object();

    public Tracker() {
        this(DEFAULT_VERSION_STRING);
    }

    /**
     * Create a new BitTorrent tracker listening at the given address.
     *
     * You will want to call {@link #addAddress(java.net.InetSocketAddress)}
     * to add listen addresses.
     */
    public Tracker(String version) {
        this.version = version;
    }

    /**
     * Create a new BitTorrent tracker listening at the given address.
     *
     * @param address The address to bind to.
     * @throws java.io.IOException Throws an <em>IOException</em> if the tracker
     * cannot be initialized.
     */
    public Tracker(@Nonnull InetSocketAddress address) throws IOException {
        this();
        addListenAddress(address);
    }

    /**
     * Create a new BitTorrent tracker listening at the given address on the
     * default port.
     *
     * @param address The address to bind to.
     * @throws java.io.IOException Throws an <em>IOException</em> if the tracker
     * cannot be initialized.
     */
    public Tracker(@Nonnull InetAddress address) throws IOException {
        this(new InetSocketAddress(address, DEFAULT_TRACKER_PORT));
    }

    /** Call this BEFORE you start the tracker. */
    // TODO: As per Client, allow adding after tracker is started.
    public void addListenAddress(@Nonnull InetSocketAddress address) {
        listeners.put(address, new Listener());
    }

    public void addListenInterface(@Nonnull NetworkInterface iface, @CheckForSigned int port) {
        for (InetAddress ifaddr : Collections.list(iface.getInetAddresses())) {
            addListenAddress(new InetSocketAddress(ifaddr, port));
        }
    }

    private void add(@Nonnull Set<? super InetSocketAddress> out, @Nonnull InetSocketAddress in)
            throws SocketException {
        // LOG.info("Looking for addresses from " + in);
        InetAddress inaddr = in.getAddress();
        for (InetAddress address : TorrentUtils.getSpecificAddresses(inaddr))
            out.add(new InetSocketAddress(address, in.getPort()));
    }

    @Nonnull
    public Iterable<? extends InetSocketAddress> getListenAddresses() throws SocketException {
        Set<InetSocketAddress> out = new HashSet<InetSocketAddress>();
        for (Map.Entry<InetSocketAddress, Listener> e : listeners.entrySet()) {
            SocketAddress a = e.getValue().connectionAddress; // In case we bound ephemerally.
            if (a instanceof InetSocketAddress) // Also ensures != null.
                add(out, (InetSocketAddress) a);
            else
                add(out, e.getKey());
        }
        return out;
    }

    /**
     * Returns the full announce URL served by this tracker.
     *
     * <p>
     * This has the form http://ip:port/announce.
     * </p>
     */
    @Nonnull
    public List<URL> getAnnounceUrls() throws SocketException {
        List<URL> out = new ArrayList<URL>();
        for (InetSocketAddress address : getListenAddresses()) {
            try {
                out.add(new URL("http", InetAddresses.toUriString(address.getAddress()), address.getPort(),
                        Tracker.ANNOUNCE_URL));
            } catch (MalformedURLException e) {
                LOG.error("Could not build tracker URL from " + address, e);
            }
        }
        return out;
    }

    @Nonnull
    public List<URI> getAnnounceUris() throws SocketException {
        List<URI> out = new ArrayList<URI>();
        for (URL url : getAnnounceUrls()) {
            try {
                out.add(url.toURI());
            } catch (URISyntaxException e) {
                LOG.error("Could not build tracker URI from " + url, e);
            }
        }
        return out;
    }

    @Nonnull
    public MetricsRegistry getMetricsRegistry() {
        return metricsRegistry;
    }

    public void setMetricsRegistry(@Nonnull MetricsRegistry metricsRegistry) {
        this.metricsRegistry = metricsRegistry;
    }

    /**
     * Start the tracker thread.
     */
    public void start() throws IOException {
        LOG.info("Starting BitTorrent tracker on {}...", getAnnounceUrls());
        synchronized (lock) {

            if (this.metrics == null) {
                Object trackerId = Iterables.getFirst(getListenAddresses(), System.identityHashCode(this));
                this.metrics = new TrackerMetrics(getMetricsRegistry(), trackerId);

                metrics.addGauge("torrentCount", new Gauge<Integer>() {
                    @Override
                    public Integer value() {
                        return torrents.size();
                    }
                });
            }

            for (Map.Entry<InetSocketAddress, Listener> e : listeners.entrySet()) {
                Listener listener = e.getValue();
                if (listener.connection == null) {
                    // Creates a thread via: SocketConnection
                    // -> ListenerManager -> Listener
                    // -> DirectReactor -> ActionDistributor -> Daemon
                    Container container = new TrackerService(version, torrents, metrics);
                    Server server = new ContainerServer(container);
                    listener.connection = new SocketConnection(server);
                    listener.connectionAddress = listener.connection.connect(e.getKey());
                }
            }

            if (this.scheduler == null || this.scheduler.isShutdown()) {
                // TODO: Set a thread timeout, nothing is time critical.
                this.scheduler = new ScheduledThreadPoolExecutor(1);
                this.scheduler.scheduleWithFixedDelay(new PeerCollector(), PEER_COLLECTION_FREQUENCY_SECONDS,
                        PEER_COLLECTION_FREQUENCY_SECONDS, TimeUnit.SECONDS);
            }

        }
        LOG.info("Started BitTorrent tracker on {}...", getAnnounceUrls());
    }

    /**
     * Stop the tracker.
     *
     * <p>
     * This effectively closes the listening HTTP connection to terminate
     * the service, and interrupts the peer collector thread as well.
     * </p>
     */
    public void stop() throws IOException {
        LOG.info("Stopping BitTorrent tracker on {}...", getAnnounceUrls());
        synchronized (lock) {

            if (this.metrics != null) {
                this.metrics.shutdown();
                this.metrics = null;
            }

            for (Map.Entry<InetSocketAddress, Listener> e : listeners.entrySet()) {
                Listener listener = e.getValue();
                if (listener.connection != null) {
                    listener.connection.close();
                    listener.connection = null;
                }
                listener.connectionAddress = null;
            }

            if (this.scheduler != null) {
                this.scheduler.shutdownNow();
                this.scheduler = null;
            }

        }
        LOG.info("Stopped BitTorrent tracker on {}...", getAnnounceUrls());
    }

    /**
     * Returns the list of tracker's torrents
     */
    public Collection<? extends TrackedTorrent> getTrackedTorrents() {
        return torrents.values();
    }

    /**
     * Announce a new torrent on this tracker.
     *
     * <p>
     * The fact that torrents must be announced here first makes this tracker a
     * closed BitTorrent tracker: it will only accept clients for torrents it
     * knows about, and this list of torrents is managed by the program
     * instrumenting this Tracker class.
     * </p>
     *
     * @param torrent The Torrent object to start tracking.
     * @return The torrent object for this torrent on this tracker. This may be
     * different from the supplied Torrent object if the tracker already
     * contained a torrent with the same hash.
     */
    @Nonnull
    public synchronized TrackedTorrent announce(@Nonnull TrackedTorrent torrent) {
        TrackedTorrent existing = this.torrents.get(torrent.getHexInfoHash());

        if (existing != null) {
            LOG.warn("Tracker already announced torrent for '{}' " + "with hash {}.", existing.getName(),
                    existing.getHexInfoHash());
            return existing;
        }

        this.torrents.put(torrent.getHexInfoHash(), torrent);
        LOG.info("Registered new torrent for '{}' with hash {}.", torrent.getName(), torrent.getHexInfoHash());
        return torrent;
    }

    @Nonnull
    public TrackedTorrent announce(@Nonnull Torrent torrent) {
        return announce(new TrackedTorrent(torrent));
    }

    /**
     * Stop announcing the given torrent.
     *
     * @param torrent The Torrent object to stop tracking.
     */
    public void remove(@CheckForNull Torrent torrent) {
        if (torrent == null)
            return;

        this.torrents.remove(torrent.getHexInfoHash());
    }

    /**
     * Stop announcing the given torrent after a delay.
     *
     * @param torrent The Torrent object to stop tracking.
     * @param delay The delay, in milliseconds, before removing the torrent.
     */
    public void remove(Torrent torrent, long delay) {
        if (torrent == null)
            return;

        scheduler.schedule(new TorrentRemover(torrent), delay, TimeUnit.SECONDS);
    }

    /**
     * Runnable for removing a torrent from a tracker.
     *
     * <p>
     * This task can be used to stop announcing a torrent after a certain delay.
     * </p>
     */
    private class TorrentRemover implements Runnable {

        private final Torrent torrent;

        TorrentRemover(@Nonnull Torrent torrent) {
            this.torrent = torrent;
        }

        @Override
        public void run() {
            remove(torrent);
        }
    }

    /**
     * The unfresh peer collector.
     *
     * <p>
     * Every PEER_COLLECTION_FREQUENCY_SECONDS, this runnable will collect
     * unfresh peers from all announced torrents.
     * </p>
     */
    private class PeerCollector implements Runnable {

        @Override
        public void run() {
            int count = 0;
            for (TrackedTorrent torrent : torrents.values()) {
                count += torrent.collectUnfreshPeers();
            }
            if (count > 0)
                LOG.debug("Collected {} stale peers.", count);
        }
    }
}