package org.limewire.libtorrent; import java.io.File; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.limewire.bittorrent.ProxySetting; import org.limewire.bittorrent.Torrent; import org.limewire.bittorrent.TorrentEvent; import org.limewire.bittorrent.TorrentEventType; import org.limewire.bittorrent.TorrentException; import org.limewire.bittorrent.TorrentIpFilter; import org.limewire.bittorrent.TorrentIpPort; import org.limewire.bittorrent.TorrentManager; import org.limewire.bittorrent.TorrentManagerSettings; import org.limewire.bittorrent.TorrentParams; import org.limewire.bittorrent.TorrentSettingsAnnotation; import org.limewire.bittorrent.TorrentTrackerScraper.ScrapeCallback; import org.limewire.inject.LazySingleton; import org.limewire.libtorrent.callback.AlertCallback; import org.limewire.listener.EventListener; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import com.google.inject.Inject; import com.google.inject.name.Named; @LazySingleton public class LibTorrentSession implements TorrentManager { private static final Log LOG = LogFactory.getLog(LibTorrentSession.class); private final ScheduledExecutorService fastExecutor; private final LibTorrentWrapper libTorrent; private final Map<String, Torrent> torrents; private final BasicAlertCallback alertCallback = new BasicAlertCallback(); // We maintain a member variable in order to prevent the JVM from // garbage collecting something the C++ libtorrent code relies on. @SuppressWarnings("unused") private IpFilterCallback ipFilterCallback; private final AtomicReference<TorrentManagerSettings> torrentSettings = new AtomicReference<TorrentManagerSettings>( null); private final AtomicBoolean initialized = new AtomicBoolean(false); private final AtomicBoolean started = new AtomicBoolean(false); private final AtomicBoolean dhtStarted = new AtomicBoolean(false); private final AtomicBoolean upnpStarted = new AtomicBoolean(false); private final EventListener<TorrentEvent> torrentListener = new EventListener<TorrentEvent>() { @Override public void handleEvent(TorrentEvent event) { handleTorrentEvent(event); } }; /** * 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 Lock lock = new ReentrantLock(); private final List<ScheduledFuture<?>> torrentManagerTasks; @Inject public LibTorrentSession(LibTorrentWrapper torrentWrapper, @Named("fastExecutor") ScheduledExecutorService fastExecutor, @TorrentSettingsAnnotation TorrentManagerSettings torrentSettings) { this.fastExecutor = fastExecutor; this.libTorrent = torrentWrapper; this.torrents = new HashMap<String, Torrent>(); this.torrentSettings.set(torrentSettings); this.torrentManagerTasks = new ArrayList<ScheduledFuture<?>>(); } private void validateLibrary() { if (!initialized.get()) { throw new TorrentException("The Torrent Manager must be initialized first.", TorrentException.INITIALIZATION_EXCEPTION); } 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 Torrent addTorrent(TorrentParams params) throws IOException { assert started.get(); lock.lock(); try { validateLibrary(); File torrentFile = params.getTorrentFile(); File fastResumefile = params.getFastResumeFile(); List<URI> trackerList = params.getTrackers(); String firstTrackerURI = null; if (trackerList != null && trackerList.size()>0) { firstTrackerURI = trackerList.get(0).toASCIIString(); } String fastResumePath = fastResumefile != null ? fastResumefile.getAbsolutePath() : null; String torrentPath = torrentFile != null ? torrentFile.getAbsolutePath() : null; String saveDirectory = params.getDownloadFolder().getAbsolutePath(); String sha1 = params.getSha1(); Torrent torrent = new TorrentImpl(params, libTorrent, fastExecutor); libTorrent.add_torrent(sha1, firstTrackerURI, torrentPath, saveDirectory, fastResumePath); // Flush and add the SECONDARY i={1,..,n} trackers again so if there were user changes, ie. // when loading from a memento the new user tracker list will be used. if (trackerList != null) { LOG.debug("flushing trackers"); LibTorrentAnnounceEntry[] trackers = libTorrent.get_trackers(sha1); // Remove the trackers that were automatically added by loading the torrent file for ( int i=1 ; i<trackers.length ; i++ ) { LibTorrentAnnounceEntry tracker = trackers[i]; libTorrent.remove_tracker(sha1, tracker.uri, tracker.tier); } // Add back for changes for ( int i=1 ; i<trackerList.size() ; i++ ) { URI trackerURI = trackerList.get(i); if (trackerURI != null) { libTorrent.add_tracker(sha1, trackerURI.toASCIIString(), i); } } } updateStatus(torrent); torrents.put(sha1, torrent); torrent.addListener(torrentListener); return torrent; } finally { lock.unlock(); } } @Override public boolean isValid() { return libTorrent.isLoaded(); } @Override public Lock getLock() { return lock; } @Override public void removeTorrent(Torrent torrent) { validateLibrary(); lock.lock(); try { torrent.removeListener(torrentListener); torrents.remove(torrent.getSha1()); libTorrent.remove_torrent(torrent.getSha1()); } finally { lock.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.lock(); try { LibTorrentStatus torrentStatus = getStatus(torrent); torrent.updateStatus(torrentStatus); } finally { lock.unlock(); } } private void handleTorrentEvent(TorrentEvent event) { if (event.getType() == TorrentEventType.STOPPED) { removeTorrent(event.getTorrent()); } } @Override public void initialize() { if (!initialized.getAndSet(true)) { if (torrentSettings.get().isTorrentsEnabled()) { lock.lock(); try { libTorrent.initialize(torrentSettings.get()); if (libTorrent.isLoaded()) { setTorrentManagerSettings(torrentSettings.get()); } } finally { lock.unlock(); } } } } @Override public void setIpFilter(TorrentIpFilter ipFilter) { lock.lock(); try { IpFilterCallback ipFilterCallback = new IpFilterCallback(ipFilter); libTorrent.set_ip_filter(ipFilterCallback); this.ipFilterCallback = ipFilterCallback; } finally { lock.unlock(); } } public void scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { lock.lock(); try { ScheduledFuture<?> scheduledFuture = fastExecutor.scheduleWithFixedDelay(command, initialDelay, delay, unit); torrentManagerTasks.add(scheduledFuture); } finally { lock.unlock(); } } @Override public void start() { assert !started.get(); started.set(true); if (isValid()) { lock.lock(); try { scheduleWithFixedDelay(new AlertPoller(), 1000, 500, TimeUnit.MILLISECONDS); } finally { lock.unlock(); } } } @Override public void stop() { lock.lock(); try { for (ScheduledFuture<?> scheduledFuture : torrentManagerTasks) { scheduledFuture.cancel(true); } if (isValid()) { libTorrent.freeze_and_save_all_fast_resume_data(alertCallback); if (isDHTStarted()) { libTorrent.save_dht_state(torrentSettings.get().getDHTStateFile()); } libTorrent.abort_torrents(); } torrents.clear(); } finally { lock.unlock(); } } @Override public Torrent getTorrent(File torrentFile) { if (torrentFile != null) { lock.lock(); try { for (Torrent torrent : torrents.values()) { if (torrentFile.equals(torrent.getTorrentFile())) { return torrent; } } } finally { lock.unlock(); } } return null; } @Override public Torrent getTorrent(String sha1) { lock.lock(); try { return torrents.get(sha1); } finally { lock.unlock(); } } /** * Basic implementation of the AlertCallback interface used to delegate * alerts back to the appropriate torrent. */ private class BasicAlertCallback implements AlertCallback { private final Set<String> updatedTorrents = new HashSet<String>(); @Override public void callback(LibTorrentAlert alert) { if (LOG.isDebugEnabled()) { LOG.debug(alert.toString()); } String sha1 = alert.getSha1(); if (sha1 != null) { updatedTorrents.add(sha1); if (alert.getCategory() == LibTorrentAlert.storage_notification) { Torrent torrent = getTorrent(sha1); if (torrent != null) { libTorrent.save_fast_resume_data(alert, torrent.getFastResumeFile() .getAbsolutePath()); torrent.handleFastResumeAlert(alert); } } else if (alert.getCategory() == LibTorrentAlert.status_notification && alert.getMessage().indexOf("metadata successfully received") > -1) { Torrent torrent = getTorrent(sha1); if (torrent != null) { // Force update of torrent info torrent.getTorrentInfo(); } } } } /** * Updates the status of all torrents that have recently recieved an * event */ public void updateAlertedTorrents() { for (String sha1 : updatedTorrents) { Torrent torrent = getTorrent(sha1); if (torrent != null) { updateStatus(torrent); } } updatedTorrents.clear(); } } /** * 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() { // Handle any alerts for fastresume/progress/status changes libTorrent.get_alerts(alertCallback); // Update status of alerted torrents alertCallback.updateAlertedTorrents(); } } @Override public boolean isDownloadingTorrent(File torrentFile) { if (torrentFile != null) { lock.lock(); try { for (Torrent torrent : torrents.values()) { if (torrentFile.equals(torrent.getTorrentFile()) && !torrent.isFinished()) { return true; } } } finally { lock.unlock(); } } return false; } @Override public void setTorrentManagerSettings(TorrentManagerSettings settings) { validateLibrary(); lock.lock(); try { torrentSettings.set(settings); libTorrent.update_settings(settings); } finally { lock.unlock(); } } @Override public TorrentManagerSettings getTorrentManagerSettings() { return torrentSettings.get(); } @Override public boolean isInitialized() { return isValid(); } @Override public List<Torrent> getTorrents() { lock.lock(); try { return new ArrayList<Torrent>(this.torrents.values()); } finally { lock.unlock(); } } @Override public boolean isDHTStarted() { return dhtStarted.get(); } @Override public void startDHT(File dhtStateFile) { validateLibrary(); lock.lock(); try { libTorrent.start_dht(dhtStateFile); addDHTRouters(); dhtStarted.set(true); } finally { lock.unlock(); } } private void addDHTRouters() { for (TorrentIpPort ipPort : getTorrentManagerSettings().getBootStrapDHTRouters()) { libTorrent.add_dht_router(ipPort.getAddress(), ipPort.getPort()); } } @Override public void stopDHT() { validateLibrary(); lock.lock(); try { libTorrent.stop_dht(); dhtStarted.set(false); } finally { lock.unlock(); } } @Override public void saveDHTState(File dhtStateFile) { if (dhtStarted.get()) { validateLibrary(); lock.lock(); try { libTorrent.save_dht_state(dhtStateFile); } finally { lock.unlock(); } } } @Override public boolean isUPnPStarted() { return upnpStarted.get(); } @Override public void startUPnP() { validateLibrary(); lock.lock(); try { libTorrent.start_upnp(); libTorrent.start_natpmp(); upnpStarted.set(true); } finally { lock.unlock(); } } @Override public void stopUPnP() { validateLibrary(); lock.lock(); try { libTorrent.stop_upnp(); libTorrent.stop_natpmp(); upnpStarted.set(false); } finally { lock.unlock(); } } @Override public void setPeerProxy(ProxySetting proxy) { validateLibrary(); LibTorrentProxySetting proxySetting = null; if (proxy != null) { proxySetting = new LibTorrentProxySetting(proxy); } else { proxySetting = LibTorrentProxySetting.nullProxy(); } lock.lock(); try { libTorrent.set_peer_proxy(proxySetting); } finally { lock.unlock(); } } @Override public void setDHTProxy(ProxySetting proxy) { validateLibrary(); LibTorrentProxySetting proxySetting = null; if (proxy != null) { proxySetting = new LibTorrentProxySetting(proxy); } else { proxySetting = LibTorrentProxySetting.nullProxy(); } lock.lock(); try { libTorrent.set_dht_proxy(proxySetting); } finally { lock.unlock(); } } @Override public void setTrackerProxy(ProxySetting proxy) { validateLibrary(); LibTorrentProxySetting proxySetting = null; if (proxy != null) { proxySetting = new LibTorrentProxySetting(proxy); } else { proxySetting = LibTorrentProxySetting.nullProxy(); } lock.lock(); try { libTorrent.set_tracker_proxy(proxySetting); } finally { lock.unlock(); } } @Override public void setWebSeedProxy(ProxySetting proxy) { validateLibrary(); LibTorrentProxySetting proxySetting = null; if (proxy != null) { proxySetting = new LibTorrentProxySetting(proxy); } else { proxySetting = LibTorrentProxySetting.nullProxy(); } lock.lock(); try { libTorrent.set_web_seed_proxy(proxySetting); } finally { lock.unlock(); } } @Override public void queueTrackerScrapeRequest(String hexSha1Urn, URI trackerUri, ScrapeCallback callback) { validateLibrary(); lock.lock(); try { TrackerScrapeRequestCallback trc = new TrackerScrapeRequestCallback(callback); libTorrent.queue_tracker_scrape_request(hexSha1Urn, trackerUri.toASCIIString(), trc); } finally { lock.unlock(); } } }