package org.limewire.libtorrent;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentException;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.bittorrent.TorrentInfo;
import org.limewire.bittorrent.TorrentManager;
import org.limewire.bittorrent.TorrentManagerSettings;
import org.limewire.bittorrent.TorrentPeer;
import org.limewire.bittorrent.TorrentSettingsAnnotation;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.inject.LazySingleton;
import org.limewire.inspection.DataCategory;
import org.limewire.inspection.Inspectable;
import org.limewire.inspection.InspectableContainer;
import org.limewire.inspection.InspectionPoint;
import org.limewire.libtorrent.callback.AlertCallback;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import com.google.inject.Inject;
import com.google.inject.name.Named;
@LazySingleton
public class TorrentManagerImpl implements TorrentManager {
private static final boolean PERIODICALLY_SAVE_FAST_RESUME_DATA = true;
private static final Log LOG = LogFactory.getLog(TorrentManagerImpl.class);
private final ScheduledExecutorService fastExecutor;
private final LibTorrentWrapper libTorrent;
private final Map<String, Torrent> torrents;
private final AtomicReference<TorrentManagerSettings> torrentSettings = new AtomicReference<TorrentManagerSettings>(
null);
/**
* Used to protect from calling libtorrent code with invalid torrent data.
* Locks access around libtorrent and removing/updating the torrents map to
* make sure the torrents torrent manger knows about are the same as what
* libtorrent knows about.
*/
private final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* Future for the job updating the torrent status.
*/
private ScheduledFuture<?> torrentFuture;
/**
* Future for the job listening to torrent alerts.
*/
private ScheduledFuture<?> alertFuture;
/**
* Future for the job creating resume files. The alert job must be running
* for the resume files to be created properly.
*/
private ScheduledFuture<?> resumeFileFuture;
@SuppressWarnings("unused")
@InspectableContainer
private class LazyInspectableContainer {
@InspectionPoint(value = "torrent manager", category = DataCategory.USAGE)
private final Inspectable inspectable = new Inspectable() {
@Override
public Object inspect() {
Map<String, Object> data = new HashMap<String, Object>();
int active = 0;
int seeding = 0;
int starting = 0;
lock.readLock().lock();
try {
for (Torrent torrent : torrents.values()) {
if (!torrent.isStarted()) {
starting++;
} else if (torrent.isFinished()) {
seeding++;
} else {
active++;
}
}
} finally {
lock.readLock().unlock();
}
data.put("active", active);
data.put("seeding", seeding);
data.put("starting", starting);
return data;
}
};
}
@Inject
public TorrentManagerImpl(LibTorrentWrapper torrentWrapper,
@Named("fastExecutor") ScheduledExecutorService fastExecutor,
@TorrentSettingsAnnotation TorrentManagerSettings torrentSettings) {
this.fastExecutor = fastExecutor;
this.libTorrent = torrentWrapper;
this.torrents = new ConcurrentHashMap<String, Torrent>();
this.torrentSettings.set(torrentSettings);
}
private void validateLibrary() {
if (!torrentSettings.get().isTorrentsEnabled()) {
throw new TorrentException("LibTorrent is disabled (through settings)",
TorrentException.DISABLED_EXCEPTION);
}
if (!isValid()) {
throw new TorrentException("There was a problem loading LibTorrent",
TorrentException.LOAD_EXCEPTION);
}
}
@Override
public void registerTorrent(Torrent torrent) {
validateLibrary();
addTorrent(torrent);
}
@Override
public boolean isValid() {
return libTorrent.isLoaded();
}
private void addTorrent(Torrent torrent) {
File torrentFile = torrent.getTorrentFile();
File fastResumefile = torrent.getFastResumeFile();
String trackerURI = torrent.getTrackerURL();
String fastResumePath = fastResumefile != null ? fastResumefile.getAbsolutePath() : null;
String torrentPath = torrentFile != null ? torrentFile.getAbsolutePath() : null;
String saveDirectory = torrent.getTorrentDataFile().getParentFile().getAbsolutePath();
lock.writeLock().lock();
try {
libTorrent.add_torrent(torrent.getSha1(), trackerURI, torrentPath, saveDirectory,
fastResumePath);
updateStatus(torrent);
torrents.put(torrent.getSha1(), torrent);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void removeTorrent(Torrent torrent) {
validateLibrary();
lock.writeLock().lock();
try {
torrents.remove(torrent.getSha1());
libTorrent.remove_torrent(torrent.getSha1());
} finally {
lock.writeLock().unlock();
}
}
@Override
public void pauseTorrent(Torrent torrent) {
validateLibrary();
lock.readLock().lock();
try {
String sha1 = torrent.getSha1();
libTorrent.pause_torrent(sha1);
updateStatus(torrent);
} finally {
lock.readLock().unlock();
}
}
@Override
public void resumeTorrent(Torrent torrent) {
validateLibrary();
lock.readLock().lock();
try {
String sha1 = torrent.getSha1();
libTorrent.resume_torrent(sha1);
updateStatus(torrent);
} finally {
lock.readLock().unlock();
}
}
@Override
public void recoverTorrent(Torrent torrent) {
validateLibrary();
lock.readLock().lock();
try {
String sha1 = torrent.getSha1();
libTorrent.clear_error_and_retry(sha1);
updateStatus(torrent);
} finally {
lock.readLock().unlock();
}
}
private LibTorrentStatus getStatus(Torrent torrent) {
validateLibrary();
LibTorrentStatus status = new LibTorrentStatus();
String sha1 = torrent.getSha1();
libTorrent.get_torrent_status(sha1, status);
libTorrent.free_torrent_status(status);
return status;
}
private void updateStatus(Torrent torrent) {
lock.readLock().lock();
try {
LibTorrentStatus torrentStatus = getStatus(torrent);
torrent.updateStatus(torrentStatus);
addMetaData(torrent);
} finally {
lock.readLock().unlock();
}
}
private void addMetaData(Torrent torrent) {
if (!torrent.hasMetaData()) {
// TODO add more data to the torrentInfo object
// use torrent_handle hasMetadata to get a real idea when the
// metadata is available.
List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries();
if (fileEntries.size() > 0) {
TorrentInfo torrentInfo = new TorrentInfo(fileEntries);
torrent.setTorrentInfo(torrentInfo);
}
}
}
@Override
public void moveTorrent(Torrent torrent, File directory) {
validateLibrary();
lock.writeLock().lock();
try {
String sha1 = torrent.getSha1();
libTorrent.move_torrent(sha1, directory.getAbsolutePath());
updateStatus(torrent);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void initialize() {
if (torrentSettings.get().isTorrentsEnabled()) {
lock.writeLock().lock();
try {
libTorrent.initialize(torrentSettings.get());
if (libTorrent.isLoaded()) {
setTorrentManagerSettings(torrentSettings.get());
libTorrent.start_upnp();
libTorrent.start_natpmp();
}
} finally {
lock.writeLock().unlock();
}
}
}
@Override
public void start() {
if (isValid()) {
lock.writeLock().lock();
try {
torrentFuture = fastExecutor.scheduleWithFixedDelay(new EventPoller(), 1000, 500,
TimeUnit.MILLISECONDS);
if (PERIODICALLY_SAVE_FAST_RESUME_DATA) {
alertFuture = fastExecutor.scheduleWithFixedDelay(new AlertPoller(), 1000, 500,
TimeUnit.MILLISECONDS);
resumeFileFuture = fastExecutor.scheduleWithFixedDelay(
new ResumeDataScheduler(), 10000, 10000, TimeUnit.MILLISECONDS);
}
} finally {
lock.writeLock().unlock();
}
}
}
@Override
public void stop() {
lock.writeLock().lock();
try {
if (resumeFileFuture != null) {
resumeFileFuture.cancel(true);
}
if (torrentFuture != null) {
torrentFuture.cancel(true);
}
if (alertFuture != null) {
alertFuture.cancel(true);
}
if (isValid()) {
libTorrent.freeze_and_save_all_fast_resume_data(new BasicAlertCallback());
libTorrent.abort_torrents();
}
torrents.clear();
} finally {
lock.writeLock().unlock();
}
}
@Override
public boolean isManagedTorrent(File torrentFile) {
if (torrentFile != null) {
synchronized (torrents) {
for (Torrent torrent : torrents.values()) {
if (torrentFile.equals(torrent.getTorrentFile())) {
return true;
}
}
}
}
return false;
}
@Override
public boolean isManagedTorrent(String sha1) {
return torrents.containsKey(sha1);
}
/**
* Basic implemenation of the AlertCallback interface used to delegate
* alerts back to the appropriate torrent.
*/
private class BasicAlertCallback implements AlertCallback {
@Override
public void callback(LibTorrentAlert alert) {
if (LOG.isDebugEnabled()) {
LOG.debug(alert.toString());
}
String sha1 = alert.getSha1();
if (sha1 != null) {
Torrent torrent = torrents.get(sha1);
if (torrent != null) {
torrent.alert(alert);
}
}
}
}
/**
* Used to clear the alert queue in the native code passing event back to
* the java code through the alertCallback interface.
*/
private class AlertPoller implements Runnable {
@Override
public void run() {
libTorrent.get_alerts(new BasicAlertCallback());
}
}
/**
* Iterates through the torrents updating the status of each one to the
* correct current state.
*/
private class EventPoller implements Runnable {
@Override
public void run() {
pumpStatus();
}
private void pumpStatus() {
for (Torrent torrent : torrents.values()) {
updateStatus(torrent);
}
}
}
/**
* Iterates through the torrents periodically saving a fastresume file for
* each file.
*/
private class ResumeDataScheduler implements Runnable {
private Iterator<String> torrentIterator = torrents.keySet().iterator();
@Override
public void run() {
lock.readLock().lock();
try {
if (!torrentIterator.hasNext()) {
torrentIterator = torrents.keySet().iterator();
if (!torrentIterator.hasNext()) {
return;
}
}
String sha1 = torrentIterator.next();
Torrent torrent = torrents.get(sha1);
if (torrent != null && torrent.hasMetaData()) {
libTorrent.signal_fast_resume_data_request(sha1);
}
} finally {
lock.readLock().unlock();
}
}
}
@Override
public boolean isDownloadingTorrent(File torrentFile) {
if (torrentFile != null) {
synchronized (torrents) {
for (Torrent torrent : torrents.values()) {
if (torrentFile.equals(torrent.getTorrentFile()) && !torrent.isFinished()) {
return true;
}
}
}
}
return false;
}
@Override
public void setTorrentManagerSettings(TorrentManagerSettings settings) {
validateLibrary();
torrentSettings.set(settings);
libTorrent.update_settings(settings);
}
@Override
public TorrentManagerSettings getTorrentManagerSettings() {
return torrentSettings.get();
}
@Override
public float getTotalDownloadRate() {
float rate = 0;
for (Torrent torrent : torrents.values()) {
TorrentStatus torrentStatus = torrent.getStatus();
if (torrentStatus != null) {
rate += torrentStatus.getDownloadRate();
}
}
return rate;
}
@Override
public float getTotalUploadRate() {
float rate = 0;
for (Torrent torrent : torrents.values()) {
TorrentStatus torrentStatus = torrent.getStatus();
if (torrentStatus != null) {
rate += torrentStatus.getUploadRate();
}
}
return rate;
}
@Override
public List<TorrentFileEntry> getTorrentFileEntries(Torrent torrent) {
validateLibrary();
TorrentFileEntry[] files = libTorrent.get_files(torrent.getSha1());
return Arrays.asList(files);
}
@Override
public List<TorrentPeer> getTorrentPeers(Torrent torrent) {
validateLibrary();
TorrentPeer[] peers = libTorrent.get_peers(torrent.getSha1());
return Arrays.asList(peers);
}
@Override
public void setAutoManaged(Torrent torrent, boolean autoManaged) {
libTorrent.set_auto_managed_torrent(torrent.getSha1(), autoManaged);
}
@Override
public void setTorrenFileEntryPriority(Torrent torrent, TorrentFileEntry torrentFileEntry,
int priority) {
libTorrent.set_file_priority(torrent.getSha1(), torrentFileEntry.getIndex(), priority);
}
@Override
public boolean isInitialized() {
return isValid();
}
}