diff options
Diffstat (limited to 'libktorrent/torrent/torrentcontrol.cpp')
-rw-r--r-- | libktorrent/torrent/torrentcontrol.cpp | 1770 |
1 files changed, 1770 insertions, 0 deletions
diff --git a/libktorrent/torrent/torrentcontrol.cpp b/libktorrent/torrent/torrentcontrol.cpp new file mode 100644 index 0000000..71b4e64 --- /dev/null +++ b/libktorrent/torrent/torrentcontrol.cpp @@ -0,0 +1,1770 @@ +/*************************************************************************** + * Copyright (C) 2005 by * + * Joris Guisson <joris.guisson@gmail.com> * + * Ivan Vasic <ivasic@gmail.com> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program 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 General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#include <qdir.h> +#include <qfile.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kfiledialog.h> +#include <qtextstream.h> +#include <util/log.h> +#include <util/error.h> +#include <util/bitset.h> +#include <util/functions.h> +#include <util/fileops.h> +#include <util/waitjob.h> +#include <interfaces/functions.h> +#include <interfaces/trackerslist.h> +#include <datachecker/singledatachecker.h> +#include <datachecker/multidatachecker.h> +#include <datachecker/datacheckerlistener.h> +#include <datachecker/datacheckerthread.h> +#include <migrate/ccmigrate.h> +#include <migrate/cachemigrate.h> +#include <kademlia/dhtbase.h> + +#include "downloader.h" +#include "uploader.h" +#include "peersourcemanager.h" +#include "chunkmanager.h" +#include "torrent.h" +#include "peermanager.h" + +#include "torrentfile.h" +#include "torrentcontrol.h" + +#include "peer.h" +#include "choker.h" + +#include "globals.h" +#include "server.h" +#include "packetwriter.h" +#include "httptracker.h" +#include "udptracker.h" +#include "downloadcap.h" +#include "uploadcap.h" +#include "queuemanager.h" +#include "statsfile.h" +#include "announcelist.h" +#include "preallocationthread.h" +#include "timeestimator.h" +#include "settings.h" + +#include <net/socketmonitor.h> + + +using namespace kt; + +namespace bt +{ + + + + TorrentControl::TorrentControl() + : tor(0),psman(0),cman(0),pman(0),down(0),up(0),choke(0),tmon(0),prealloc(false) + { + istats.last_announce = 0; + stats.imported_bytes = 0; + stats.trk_bytes_downloaded = 0; + stats.trk_bytes_uploaded = 0; + stats.running = false; + stats.started = false; + stats.stopped_by_error = false; + stats.session_bytes_downloaded = 0; + stats.session_bytes_uploaded = 0; + istats.session_bytes_uploaded = 0; + old_datadir = QString::null; + stats.status = NOT_STARTED; + stats.autostart = true; + stats.user_controlled = false; + stats.priv_torrent = false; + stats.seeders_connected_to = stats.seeders_total = 0; + stats.leechers_connected_to = stats.leechers_total = 0; + istats.running_time_dl = istats.running_time_ul = 0; + istats.prev_bytes_dl = 0; + istats.prev_bytes_ul = 0; + istats.trk_prev_bytes_dl = istats.trk_prev_bytes_ul = 0; + istats.io_error = false; + istats.priority = 0; + stats.max_share_ratio = 0.00f; + istats.custom_output_name = false; + istats.diskspace_warning_emitted = false; + stats.max_seed_time = 0; + updateStats(); + prealoc_thread = 0; + dcheck_thread = 0; + istats.dht_on = false; + stats.num_corrupted_chunks = 0; + + m_eta = new TimeEstimator(this); + // by default no torrent limits + upload_gid = download_gid = 0; + upload_limit = download_limit = 0; + moving_files = false; + } + + + + + TorrentControl::~TorrentControl() + { + if (stats.running) + stop(false); + + if (tmon) + tmon->destroyed(); + delete choke; + delete down; + delete up; + delete cman; + delete pman; + delete psman; + delete tor; + delete m_eta; + } + + void TorrentControl::update() + { + UpdateCurrentTime(); + if (stats.status == kt::CHECKING_DATA || moving_files) + return; + + if (istats.io_error) + { + stop(false); + emit stoppedByError(this, error_msg); + return; + } + + if (prealoc_thread) + { + if (prealoc_thread->isDone()) + { + // thread done + if (prealoc_thread->errorHappened()) + { + // upon error just call onIOError and return + onIOError(prealoc_thread->errorMessage()); + delete prealoc_thread; + prealoc_thread = 0; + prealloc = true; // still need to do preallocation + return; + } + else + { + // continue the startup of the torrent + delete prealoc_thread; + prealoc_thread = 0; + prealloc = false; + stats.status = kt::NOT_STARTED; + saveStats(); + continueStart(); + } + } + else + return; // preallocation still going on, so just return + } + + + try + { + // first update peermanager + pman->update(); + bool comp = stats.completed; + + //helper var, check if needed to move completed files somewhere + bool moveCompleted = false; + + // then the downloader and uploader + up->update(choke->getOptimisticlyUnchokedPeerID()); + down->update(); + + stats.completed = cman->completed(); + if (stats.completed && !comp) + { + pman->killSeeders(); + QDateTime now = QDateTime::currentDateTime(); + istats.running_time_dl += istats.time_started_dl.secsTo(now); + updateStatusMsg(); + updateStats(); + + // download has just been completed + // only sent completed to tracker when we have all chunks (so no excluded chunks) + if (cman->haveAllChunks()) + psman->completed(); + + finished(this); + + //Move completed download to specified directory if needed + if(Settings::useCompletedDir()) + { + moveCompleted = true; + } + } + else if (!stats.completed && comp) + { + // restart download if necesarry + // when user selects that files which were previously excluded, + // should now be downloaded + if (!psman->isStarted()) + psman->start(); + else + psman->manualUpdate(); + istats.last_announce = bt::GetCurrentTime(); + istats.time_started_dl = QDateTime::currentDateTime(); + } + updateStatusMsg(); + + // get rid of dead Peers + Uint32 num_cleared = pman->clearDeadPeers(); + + // we may need to update the choker + if (choker_update_timer.getElapsedSinceUpdate() >= 10000 || num_cleared > 0) + { + // also get rid of seeders & uninterested when download is finished + // no need to keep them around, but also no need to do this + // every update, so once every 10 seconds is fine + if (stats.completed) + { + pman->killSeeders(); + } + + doChoking(); + choker_update_timer.update(); + // a good opportunity to make sure we are not keeping to much in memory + cman->checkMemoryUsage(); + } + + // to satisfy people obsessed with their share ratio + if (stats_save_timer.getElapsedSinceUpdate() >= 5*60*1000) + { + saveStats(); + stats_save_timer.update(); + } + + // Update DownloadCap + updateStats(); + + if (stats.download_rate > 0) + stalled_timer.update(); + + // do a manual update if we are stalled for more then 2 minutes + // we do not do this for private torrents + if (stalled_timer.getElapsedSinceUpdate() > 120000 && !stats.completed && + !stats.priv_torrent) + { + Out(SYS_TRK|LOG_NOTICE) << "Stalled for too long, time to get some fresh blood" << endl; + psman->manualUpdate(); + stalled_timer.update(); + } + + if(overMaxRatio() || overMaxSeedTime()) + { + if(istats.priority!=0) //if it's queued make sure to dequeue it + { + setPriority(0); + stats.user_controlled = true; + } + + stop(true); + emit seedingAutoStopped(this, overMaxRatio() ? kt::MAX_RATIO_REACHED : kt::MAX_SEED_TIME_REACHED); + } + + //Update diskspace if needed (every 1 min) + if(!stats.completed && stats.running && bt::GetCurrentTime() - last_diskspace_check >= 60 * 1000) + { + checkDiskSpace(true); + } + + //Move completed files if needed: + if (moveCompleted) + { + QString outdir = Settings::completedDir(); + if(!outdir.endsWith(bt::DirSeparator())) + outdir += bt::DirSeparator(); + + changeOutputDir(outdir); + } + } + catch (Error & e) + { + onIOError(e.toString()); + } + } + + void TorrentControl::onIOError(const QString & msg) + { + Out(SYS_DIO|LOG_IMPORTANT) << "Error : " << msg << endl; + stats.stopped_by_error = true; + stats.status = ERROR; + error_msg = msg; + istats.io_error = true; + } + + void TorrentControl::start() + { + // do not start running torrents + if (stats.running || stats.status == kt::ALLOCATING_DISKSPACE || moving_files) + return; + + stats.stopped_by_error = false; + istats.diskspace_warning_emitted = false; + istats.io_error = false; + try + { + bool ret = true; + aboutToBeStarted(this,ret); + if (!ret) + return; + } + catch (Error & err) + { + // something went wrong when files were recreated, set error and rethrow + onIOError(err.toString()); + return; + } + + try + { + cman->start(); + } + catch (Error & e) + { + onIOError(e.toString()); + throw; + } + + istats.time_started_ul = istats.time_started_dl = QDateTime::currentDateTime(); + resetTrackerStats(); + + if (prealloc) + { + // only start preallocation if we are allowed by the settings + if (Settings::diskPrealloc() && !cman->haveAllChunks()) + { + Out(SYS_GEN|LOG_NOTICE) << "Pre-allocating diskspace" << endl; + prealoc_thread = new PreallocationThread(cman); + stats.running = true; + stats.status = kt::ALLOCATING_DISKSPACE; + prealoc_thread->start(); + return; + } + else + { + prealloc = false; + } + } + + continueStart(); + } + + void TorrentControl::continueStart() + { + // continues start after the prealoc_thread has finished preallocation + pman->start(); + pman->loadPeerList(datadir + "peer_list"); + try + { + down->loadDownloads(datadir + "current_chunks"); + } + catch (Error & e) + { + // print out warning in case of failure + // we can still continue the download + Out(SYS_GEN|LOG_NOTICE) << "Warning : " << e.toString() << endl; + } + + loadStats(); + stats.running = true; + stats.started = true; + stats.autostart = true; + choker_update_timer.update(); + stats_save_timer.update(); + + + stalled_timer.update(); + psman->start(); + istats.last_announce = bt::GetCurrentTime(); + stalled_timer.update(); + } + + + void TorrentControl::stop(bool user,WaitJob* wjob) + { + QDateTime now = QDateTime::currentDateTime(); + if(!stats.completed) + istats.running_time_dl += istats.time_started_dl.secsTo(now); + istats.running_time_ul += istats.time_started_ul.secsTo(now); + istats.time_started_ul = istats.time_started_dl = now; + + // stop preallocation thread if necesarry + if (prealoc_thread) + { + prealoc_thread->stop(); + prealoc_thread->wait(); + + if (prealoc_thread->errorHappened() || prealoc_thread->isNotFinished()) + { + delete prealoc_thread; + prealoc_thread = 0; + prealloc = true; + saveStats(); // save stats, so that we will start preallocating the next time + } + else + { + delete prealoc_thread; + prealoc_thread = 0; + prealloc = false; + } + } + + if (stats.running) + { + psman->stop(wjob); + + if (tmon) + tmon->stopped(); + + try + { + down->saveDownloads(datadir + "current_chunks"); + } + catch (Error & e) + { + // print out warning in case of failure + // it doesn't corrupt the data, so just a couple of lost chunks + Out(SYS_GEN|LOG_NOTICE) << "Warning : " << e.toString() << endl; + } + + down->clearDownloads(); + if (user) + { + //make this torrent user controlled + setPriority(0); + stats.autostart = false; + } + } + pman->savePeerList(datadir + "peer_list"); + pman->stop(); + pman->closeAllConnections(); + pman->clearDeadPeers(); + cman->stop(); + + stats.running = false; + saveStats(); + updateStatusMsg(); + updateStats(); + stats.trk_bytes_downloaded = 0; + stats.trk_bytes_uploaded = 0; + + emit torrentStopped(this); + } + + void TorrentControl::setMonitor(kt::MonitorInterface* tmo) + { + tmon = tmo; + down->setMonitor(tmon); + if (tmon) + { + for (Uint32 i = 0;i < pman->getNumConnectedPeers();i++) + tmon->peerAdded(pman->getPeer(i)); + } + } + + + void TorrentControl::init(QueueManager* qman, + const QString & torrent, + const QString & tmpdir, + const QString & ddir, + const QString & default_save_dir) + { + // first load the torrent file + tor = new Torrent(); + try + { + tor->load(torrent,false); + } + catch (...) + { + delete tor; + tor = 0; + throw Error(i18n("An error occurred while loading the torrent." + " The torrent is probably corrupt or is not a torrent file.\n%1").arg(torrent)); + } + + initInternal(qman,tmpdir,ddir,default_save_dir,torrent.startsWith(tmpdir)); + + // copy torrent in tor dir + QString tor_copy = datadir + "torrent"; + if (tor_copy != torrent) + { + bt::CopyFile(torrent,tor_copy); + } + + } + + + void TorrentControl::init(QueueManager* qman, const QByteArray & data,const QString & tmpdir, + const QString & ddir,const QString & default_save_dir) + { + // first load the torrent file + tor = new Torrent(); + try + { + tor->load(data,false); + } + catch (...) + { + delete tor; + tor = 0; + throw Error(i18n("An error occurred while loading the torrent." + " The torrent is probably corrupt or is not a torrent file.")); + } + + initInternal(qman,tmpdir,ddir,default_save_dir,true); + // copy data into torrent file + QString tor_copy = datadir + "torrent"; + QFile fptr(tor_copy); + if (!fptr.open(IO_WriteOnly)) + throw Error(i18n("Unable to create %1 : %2") + .arg(tor_copy).arg(fptr.errorString())); + + fptr.writeBlock(data.data(),data.size()); + } + + void TorrentControl::checkExisting(QueueManager* qman) + { + // check if we haven't already loaded the torrent + // only do this when qman isn't 0 + if (qman && qman->allreadyLoaded(tor->getInfoHash())) + { + if (!stats.priv_torrent) + { + qman->mergeAnnounceList(tor->getInfoHash(),tor->getTrackerList()); + + throw Error(i18n("You are already downloading this torrent %1, the list of trackers of both torrents has been merged.").arg(tor->getNameSuggestion())); + } + else + { + throw Error(i18n("You are already downloading the torrent %1") + .arg(tor->getNameSuggestion())); + } + } + } + + void TorrentControl::setupDirs(const QString & tmpdir,const QString & ddir) + { + datadir = tmpdir; + + if (!datadir.endsWith(DirSeparator())) + datadir += DirSeparator(); + + outputdir = ddir.stripWhiteSpace(); + if (outputdir.length() > 0 && !outputdir.endsWith(DirSeparator())) + outputdir += DirSeparator(); + + if (!bt::Exists(datadir)) + { + bt::MakeDir(datadir); + } + } + + void TorrentControl::setupStats() + { + stats.completed = false; + stats.running = false; + stats.torrent_name = tor->getNameSuggestion(); + stats.multi_file_torrent = tor->isMultiFile(); + stats.total_bytes = tor->getFileLength(); + stats.priv_torrent = tor->isPrivate(); + + // check the stats file for the custom_output_name variable + StatsFile st(datadir + "stats"); + if (st.hasKey("CUSTOM_OUTPUT_NAME") && st.readULong("CUSTOM_OUTPUT_NAME") == 1) + { + istats.custom_output_name = true; + } + + // load outputdir if outputdir is null + if (outputdir.isNull() || outputdir.length() == 0) + loadOutputDir(); + } + + void TorrentControl::setupData(const QString & ddir) + { + // create PeerManager and Tracker + pman = new PeerManager(*tor); + //Out() << "Tracker url " << url << " " << url.protocol() << " " << url.prettyURL() << endl; + psman = new PeerSourceManager(this,pman); + connect(psman,SIGNAL(statusChanged( const QString& )), + this,SLOT(trackerStatusChanged( const QString& ))); + + + // Create chunkmanager, load the index file if it exists + // else create all the necesarry files + cman = new ChunkManager(*tor,datadir,outputdir,istats.custom_output_name); + // outputdir is null, see if the cache has figured out what it is + if (outputdir.length() == 0) + outputdir = cman->getDataDir(); + + // store the outputdir into the output_path variable, so others can access it + + connect(cman,SIGNAL(updateStats()),this,SLOT(updateStats())); + if (bt::Exists(datadir + "index")) + cman->loadIndexFile(); + + stats.completed = cman->completed(); + + // create downloader,uploader and choker + down = new Downloader(*tor,*pman,*cman); + connect(down,SIGNAL(ioError(const QString& )), + this,SLOT(onIOError(const QString& ))); + up = new Uploader(*cman,*pman); + choke = new Choker(*pman,*cman); + + + connect(pman,SIGNAL(newPeer(Peer* )),this,SLOT(onNewPeer(Peer* ))); + connect(pman,SIGNAL(peerKilled(Peer* )),this,SLOT(onPeerRemoved(Peer* ))); + connect(cman,SIGNAL(excluded(Uint32, Uint32 )),down,SLOT(onExcluded(Uint32, Uint32 ))); + connect(cman,SIGNAL(included( Uint32, Uint32 )),down,SLOT(onIncluded( Uint32, Uint32 ))); + connect(cman,SIGNAL(corrupted( Uint32 )),this,SLOT(corrupted( Uint32 ))); + } + + void TorrentControl::initInternal(QueueManager* qman, + const QString & tmpdir, + const QString & ddir, + const QString & default_save_dir, + bool first_time) + { + checkExisting(qman); + setupDirs(tmpdir,ddir); + setupStats(); + + if (!first_time) + { + // if we do not need to copy the torrent, it is an existing download and we need to see + // if it is not an old download + try + { + migrateTorrent(default_save_dir); + } + catch (Error & err) + { + + throw Error( + i18n("Cannot migrate %1 : %2") + .arg(tor->getNameSuggestion()).arg(err.toString())); + } + } + setupData(ddir); + + updateStatusMsg(); + + // to get rid of phantom bytes we need to take into account + // the data from downloads already in progress + try + { + Uint64 db = down->bytesDownloaded(); + Uint64 cb = down->getDownloadedBytesOfCurrentChunksFile(datadir + "current_chunks"); + istats.prev_bytes_dl = db + cb; + + // Out() << "Downloaded : " << kt::BytesToString(db) << endl; + // Out() << "current_chunks : " << kt::BytesToString(cb) << endl; + } + catch (Error & e) + { + // print out warning in case of failure + Out() << "Warning : " << e.toString() << endl; + istats.prev_bytes_dl = down->bytesDownloaded(); + } + + loadStats(); + updateStats(); + saveStats(); + stats.output_path = cman->getOutputPath(); + /* if (stats.output_path.isNull()) + { + cman->createFiles(); + stats.output_path = cman->getOutputPath(); + }*/ + Out() << "OutputPath = " << stats.output_path << endl; + } + + + + bool TorrentControl::announceAllowed() + { + if(istats.last_announce == 0) + return true; + + if (psman && psman->getNumFailures() == 0) + return bt::GetCurrentTime() - istats.last_announce >= 60 * 1000; + else + return true; + } + + void TorrentControl::updateTracker() + { + if (stats.running && announceAllowed()) + { + psman->manualUpdate(); + istats.last_announce = bt::GetCurrentTime(); + } + } + + void TorrentControl::onNewPeer(Peer* p) + { + connect(p,SIGNAL(gotPortPacket( const QString&, Uint16 )), + this,SLOT(onPortPacket( const QString&, Uint16 ))); + + if (p->getStats().fast_extensions) + { + const BitSet & bs = cman->getBitSet(); + if (bs.allOn()) + p->getPacketWriter().sendHaveAll(); + else if (bs.numOnBits() == 0) + p->getPacketWriter().sendHaveNone(); + else + p->getPacketWriter().sendBitSet(bs); + } + else + { + p->getPacketWriter().sendBitSet(cman->getBitSet()); + } + + if (!stats.completed) + p->getPacketWriter().sendInterested(); + + if (!stats.priv_torrent) + { + if (p->isDHTSupported()) + p->getPacketWriter().sendPort(Globals::instance().getDHT().getPort()); + else + // WORKAROUND so we can contact µTorrent's DHT + // They do not properly support the standard and do not turn on + // the DHT bit in the handshake, so we just ping each peer by default. + p->emitPortPacket(); + } + + // set group ID's for traffic shaping + p->setGroupIDs(upload_gid,download_gid); + + if (tmon) + tmon->peerAdded(p); + } + + void TorrentControl::onPeerRemoved(Peer* p) + { + disconnect(p,SIGNAL(gotPortPacket( const QString&, Uint16 )), + this,SLOT(onPortPacket( const QString&, Uint16 ))); + if (tmon) + tmon->peerRemoved(p); + } + + void TorrentControl::doChoking() + { + choke->update(stats.completed,stats); + } + + bool TorrentControl::changeDataDir(const QString & new_dir) + { + int pos = datadir.findRev(bt::DirSeparator(),-2); + if (pos == -1) + { + Out(SYS_GEN|LOG_DEBUG) << "Could not find torX part in " << datadir << endl; + return false; + } + + QString ndatadir = new_dir + datadir.mid(pos + 1); + + Out(SYS_GEN|LOG_DEBUG) << datadir << " -> " << ndatadir << endl; + try + { + bt::Move(datadir,ndatadir); + old_datadir = datadir; + datadir = ndatadir; + } + catch (Error & err) + { + Out(SYS_GEN|LOG_IMPORTANT) << "Could not move " << datadir << " to " << ndatadir << endl; + return false; + } + + cman->changeDataDir(datadir); + return true; + } + + bool TorrentControl::changeOutputDir(const QString & new_dir, bool moveFiles) + { + if (moving_files) + return false; + + Out(SYS_GEN|LOG_NOTICE) << "Moving data for torrent " << stats.torrent_name << " to " << new_dir << endl; + + restart_torrent_after_move_data_files = false; + + //check if torrent is running and stop it before moving data + if(stats.running) + { + restart_torrent_after_move_data_files = true; + this->stop(false); + } + + moving_files = true; + try + { + QString nd; + if (istats.custom_output_name) + { + int slash_pos = stats.output_path.findRev(bt::DirSeparator(),-2); + nd = new_dir + stats.output_path.mid(slash_pos + 1); + } + else + { + nd = new_dir + tor->getNameSuggestion(); + } + + if (stats.output_path != nd) + { + KIO::Job* j = 0; + if(moveFiles) + { + if (stats.multi_file_torrent) + j = cman->moveDataFiles(nd); + else + j = cman->moveDataFiles(new_dir); + } + + move_data_files_destination_path = nd; + if (j) + { + connect(j,SIGNAL(result(KIO::Job*)),this,SLOT(moveDataFilesJobDone(KIO::Job*))); + return true; + } + else + { + moveDataFilesJobDone(0); + } + } + else + { + Out(SYS_GEN|LOG_NOTICE) << "Source is the same as destination, so doing nothing" << endl; + } + } + catch (Error& err) + { + Out(SYS_GEN|LOG_IMPORTANT) << "Could not move " << stats.output_path << " to " << new_dir << ". Exception: " << err.toString() << endl; + moving_files = false; + return false; + } + + moving_files = false; + if (restart_torrent_after_move_data_files) + { + this->start(); + } + + return true; + } + + void TorrentControl::moveDataFilesJobDone(KIO::Job* job) + { + if (job) + cman->moveDataFilesCompleted(job); + + if (!job || (job && !job->error())) + { + cman->changeOutputPath(move_data_files_destination_path); + outputdir = stats.output_path = move_data_files_destination_path; + istats.custom_output_name = true; + + saveStats(); + Out(SYS_GEN|LOG_NOTICE) << "Data directory changed for torrent " << "'" << stats.torrent_name << "' to: " << move_data_files_destination_path << endl; + } + else if (job->error()) + { + Out(SYS_GEN|LOG_IMPORTANT) << "Could not move " << stats.output_path << " to " << move_data_files_destination_path << endl; + } + + moving_files = false; + if (restart_torrent_after_move_data_files) + { + this->start(); + } + } + + + void TorrentControl::rollback() + { + try + { + bt::Move(datadir,old_datadir); + datadir = old_datadir; + cman->changeDataDir(datadir); + } + catch (Error & err) + { + Out(SYS_GEN|LOG_IMPORTANT) << "Could not move " << datadir << " to " << old_datadir << endl; + } + } + + void TorrentControl::updateStatusMsg() + { + if (stats.stopped_by_error) + stats.status = kt::ERROR; + else if (!stats.started) + stats.status = kt::NOT_STARTED; + else if(!stats.running && !stats.user_controlled) + stats.status = kt::QUEUED; + else if (!stats.running && stats.completed && (overMaxRatio() || overMaxSeedTime())) + stats.status = kt::SEEDING_COMPLETE; + else if (!stats.running && stats.completed) + stats.status = kt::DOWNLOAD_COMPLETE; + else if (!stats.running) + stats.status = kt::STOPPED; + else if (stats.running && stats.completed) + stats.status = kt::SEEDING; + else if (stats.running) + // protocol messages are also included in speed calculation, so lets not compare with 0 + stats.status = down->downloadRate() > 100 ? + kt::DOWNLOADING : kt::STALLED; + } + + const BitSet & TorrentControl::downloadedChunksBitSet() const + { + if (cman) + return cman->getBitSet(); + else + return BitSet::null; + } + + const BitSet & TorrentControl::availableChunksBitSet() const + { + if (!pman) + return BitSet::null; + else + return pman->getAvailableChunksBitSet(); + } + + const BitSet & TorrentControl::excludedChunksBitSet() const + { + if (!cman) + return BitSet::null; + else + return cman->getExcludedBitSet(); + } + + const BitSet & TorrentControl::onlySeedChunksBitSet() const + { + if (!cman) + return BitSet::null; + else + return cman->getOnlySeedBitSet(); + } + + void TorrentControl::saveStats() + { + StatsFile st(datadir + "stats"); + + st.write("OUTPUTDIR", cman->getDataDir()); + + if (cman->getDataDir() != outputdir) + outputdir = cman->getDataDir(); + + st.write("UPLOADED", QString::number(up->bytesUploaded())); + + if (stats.running) + { + QDateTime now = QDateTime::currentDateTime(); + st.write("RUNNING_TIME_DL",QString("%1").arg(istats.running_time_dl + istats.time_started_dl.secsTo(now))); + st.write("RUNNING_TIME_UL",QString("%1").arg(istats.running_time_ul + istats.time_started_ul.secsTo(now))); + } + else + { + st.write("RUNNING_TIME_DL", QString("%1").arg(istats.running_time_dl)); + st.write("RUNNING_TIME_UL", QString("%1").arg(istats.running_time_ul)); + } + + st.write("PRIORITY", QString("%1").arg(istats.priority)); + st.write("AUTOSTART", QString("%1").arg(stats.autostart)); + st.write("IMPORTED", QString("%1").arg(stats.imported_bytes)); + st.write("CUSTOM_OUTPUT_NAME",istats.custom_output_name ? "1" : "0"); + st.write("MAX_RATIO", QString("%1").arg(stats.max_share_ratio,0,'f',2)); + st.write("MAX_SEED_TIME",QString::number(stats.max_seed_time)); + st.write("RESTART_DISK_PREALLOCATION",prealloc ? "1" : "0"); + + if(!stats.priv_torrent) + { + //save dht and pex + st.write("DHT", isFeatureEnabled(kt::DHT_FEATURE) ? "1" : "0"); + st.write("UT_PEX", isFeatureEnabled(kt::UT_PEX_FEATURE) ? "1" : "0"); + } + + st.write("UPLOAD_LIMIT",QString::number(upload_limit)); + st.write("DOWNLOAD_LIMIT",QString::number(download_limit)); + + st.writeSync(); + } + + void TorrentControl::loadStats() + { + StatsFile st(datadir + "stats"); + + Uint64 val = st.readUint64("UPLOADED"); + // stats.session_bytes_uploaded will be calculated based upon prev_bytes_ul + // seeing that this will change here, we need to save it + istats.session_bytes_uploaded = stats.session_bytes_uploaded; + istats.prev_bytes_ul = val; + up->setBytesUploaded(val); + + this->istats.running_time_dl = st.readULong("RUNNING_TIME_DL"); + this->istats.running_time_ul = st.readULong("RUNNING_TIME_UL"); + outputdir = st.readString("OUTPUTDIR").stripWhiteSpace(); + if (st.hasKey("CUSTOM_OUTPUT_NAME") && st.readULong("CUSTOM_OUTPUT_NAME") == 1) + { + istats.custom_output_name = true; + } + + setPriority(st.readInt("PRIORITY")); + stats.user_controlled = istats.priority == 0 ? true : false; + stats.autostart = st.readBoolean("AUTOSTART"); + + stats.imported_bytes = st.readUint64("IMPORTED"); + float rat = st.readFloat("MAX_RATIO"); + stats.max_share_ratio = rat; + if (st.hasKey("RESTART_DISK_PREALLOCATION")) + prealloc = st.readString("RESTART_DISK_PREALLOCATION") == "1"; + + stats.max_seed_time = st.readFloat("MAX_SEED_TIME"); + + if (!stats.priv_torrent) + { + if(st.hasKey("DHT")) + istats.dht_on = st.readBoolean("DHT"); + else + istats.dht_on = true; + + setFeatureEnabled(kt::DHT_FEATURE,istats.dht_on); + if (st.hasKey("UT_PEX")) + setFeatureEnabled(kt::UT_PEX_FEATURE,st.readBoolean("UT_PEX")); + } + + net::SocketMonitor & smon = net::SocketMonitor::instance(); + + Uint32 nl = st.readInt("UPLOAD_LIMIT"); + if (nl != upload_limit) + { + if (nl > 0) + { + if (upload_gid) + smon.setGroupLimit(net::SocketMonitor::UPLOAD_GROUP,upload_gid,nl); + else + upload_gid = smon.newGroup(net::SocketMonitor::UPLOAD_GROUP,nl); + } + else + { + smon.removeGroup(net::SocketMonitor::UPLOAD_GROUP,upload_gid); + upload_gid = 0; + } + } + upload_limit = nl; + + nl = st.readInt("DOWNLOAD_LIMIT"); + if (nl != download_limit) + { + if (nl > 0) + { + if (download_gid) + smon.setGroupLimit(net::SocketMonitor::DOWNLOAD_GROUP,download_gid,nl); + else + download_gid = smon.newGroup(net::SocketMonitor::DOWNLOAD_GROUP,nl); + } + else + { + smon.removeGroup(net::SocketMonitor::DOWNLOAD_GROUP,download_gid); + download_gid = 0; + } + } + download_limit = nl; + } + + void TorrentControl::loadOutputDir() + { + StatsFile st(datadir + "stats"); + if (!st.hasKey("OUTPUTDIR")) + return; + + outputdir = st.readString("OUTPUTDIR").stripWhiteSpace(); + if (st.hasKey("CUSTOM_OUTPUT_NAME") && st.readULong("CUSTOM_OUTPUT_NAME") == 1) + { + istats.custom_output_name = true; + } + } + + bool TorrentControl::readyForPreview(int start_chunk, int end_chunk) + { + if ( !tor->isMultimedia() && !tor->isMultiFile()) return false; + + const BitSet & bs = downloadedChunksBitSet(); + for(int i = start_chunk; i<end_chunk; ++i) + { + if ( !bs.get(i) ) return false; + } + return true; + } + + Uint32 TorrentControl::getTimeToNextTrackerUpdate() const + { + if (psman) + return psman->getTimeToNextUpdate(); + else + return 0; + } + + void TorrentControl::updateStats() + { + stats.num_chunks_downloading = down ? down->numActiveDownloads() : 0; + stats.num_peers = pman ? pman->getNumConnectedPeers() : 0; + stats.upload_rate = up && stats.running ? up->uploadRate() : 0; + stats.download_rate = down && stats.running ? down->downloadRate() : 0; + stats.bytes_left = cman ? cman->bytesLeft() : 0; + stats.bytes_left_to_download = cman ? cman->bytesLeftToDownload() : 0; + stats.bytes_uploaded = up ? up->bytesUploaded() : 0; + stats.bytes_downloaded = down ? down->bytesDownloaded() : 0; + stats.total_chunks = tor ? tor->getNumChunks() : 0; + stats.num_chunks_downloaded = cman ? cman->chunksDownloaded() : 0; + stats.num_chunks_excluded = cman ? cman->chunksExcluded() : 0; + stats.chunk_size = tor ? tor->getChunkSize() : 0; + stats.num_chunks_left = cman ? cman->chunksLeft() : 0; + stats.total_bytes_to_download = (tor && cman) ? tor->getFileLength() - cman->bytesExcluded() : 0; + + if (stats.bytes_downloaded >= istats.prev_bytes_dl) + stats.session_bytes_downloaded = stats.bytes_downloaded - istats.prev_bytes_dl; + else + stats.session_bytes_downloaded = 0; + + if (stats.bytes_uploaded >= istats.prev_bytes_ul) + stats.session_bytes_uploaded = (stats.bytes_uploaded - istats.prev_bytes_ul) + istats.session_bytes_uploaded; + else + stats.session_bytes_uploaded = istats.session_bytes_uploaded; + /* + Safety check, it is possible that stats.bytes_downloaded gets subtracted in Downloader. + Which can cause stats.bytes_downloaded to be smaller the istats.trk_prev_bytes_dl. + This can screw up your download ratio. + */ + if (stats.bytes_downloaded >= istats.trk_prev_bytes_dl) + stats.trk_bytes_downloaded = stats.bytes_downloaded - istats.trk_prev_bytes_dl; + else + stats.trk_bytes_downloaded = 0; + + if (stats.bytes_uploaded >= istats.trk_prev_bytes_ul) + stats.trk_bytes_uploaded = stats.bytes_uploaded - istats.trk_prev_bytes_ul; + else + stats.trk_bytes_uploaded = 0; + + getSeederInfo(stats.seeders_total,stats.seeders_connected_to); + getLeecherInfo(stats.leechers_total,stats.leechers_connected_to); + } + + void TorrentControl::getSeederInfo(Uint32 & total,Uint32 & connected_to) const + { + total = 0; + connected_to = 0; + if (!pman || !psman) + return; + + for (Uint32 i = 0;i < pman->getNumConnectedPeers();i++) + { + if (pman->getPeer(i)->isSeeder()) + connected_to++; + } + total = psman->getNumSeeders(); + if (total == 0) + total = connected_to; + } + + void TorrentControl::getLeecherInfo(Uint32 & total,Uint32 & connected_to) const + { + total = 0; + connected_to = 0; + if (!pman || !psman) + return; + + for (Uint32 i = 0;i < pman->getNumConnectedPeers();i++) + { + if (!pman->getPeer(i)->isSeeder()) + connected_to++; + } + total = psman->getNumLeechers(); + if (total == 0) + total = connected_to; + } + + Uint32 TorrentControl::getRunningTimeDL() const + { + if (!stats.running || stats.completed) + return istats.running_time_dl; + else + return istats.running_time_dl + istats.time_started_dl.secsTo(QDateTime::currentDateTime()); + } + + Uint32 TorrentControl::getRunningTimeUL() const + { + if (!stats.running) + return istats.running_time_ul; + else + return istats.running_time_ul + istats.time_started_ul.secsTo(QDateTime::currentDateTime()); + } + + Uint32 TorrentControl::getNumFiles() const + { + if (tor && tor->isMultiFile()) + return tor->getNumFiles(); + else + return 0; + } + + TorrentFileInterface & TorrentControl::getTorrentFile(Uint32 index) + { + if (tor) + return tor->getFile(index); + else + return TorrentFile::null; + } + + void TorrentControl::migrateTorrent(const QString & default_save_dir) + { + if (bt::Exists(datadir + "current_chunks") && bt::IsPreMMap(datadir + "current_chunks")) + { + // in case of error copy torX dir to migrate-failed-tor + QString dd = datadir; + int pos = dd.findRev("tor"); + if (pos != - 1) + { + dd = dd.replace(pos,3,"migrate-failed-tor"); + Out() << "Copying " << datadir << " to " << dd << endl; + bt::CopyDir(datadir,dd,true); + } + + bt::MigrateCurrentChunks(*tor,datadir + "current_chunks"); + if (outputdir.isNull() && bt::IsCacheMigrateNeeded(*tor,datadir + "cache")) + { + // if the output dir is NULL + if (default_save_dir.isNull()) + { + KMessageBox::information(0, + i18n("The torrent %1 was started with a previous version of KTorrent." + " To make sure this torrent still works with this version of KTorrent, " + "we will migrate this torrent. You will be asked for a location to save " + "the torrent to. If you press cancel, we will select your home directory.") + .arg(tor->getNameSuggestion())); + outputdir = KFileDialog::getExistingDirectory(QString::null, 0,i18n("Select Folder to Save To")); + if (outputdir.isNull()) + outputdir = QDir::homeDirPath(); + } + else + { + outputdir = default_save_dir; + } + + if (!outputdir.endsWith(bt::DirSeparator())) + outputdir += bt::DirSeparator(); + + bt::MigrateCache(*tor,datadir + "cache",outputdir); + } + + // delete backup + if (pos != - 1) + bt::Delete(dd); + } + } + + void TorrentControl::setPriority(int p) + { + istats.priority = p; + stats.user_controlled = p == 0 ? true : false; + if(p) + stats.status = kt::QUEUED; + else + updateStatusMsg(); + + saveStats(); + } + + void TorrentControl::setMaxShareRatio(float ratio) + { + if(ratio == 1.00f) + { + if (stats.max_share_ratio != ratio) + stats.max_share_ratio = ratio; + } + else + stats.max_share_ratio = ratio; + + if(stats.completed && !stats.running && !stats.user_controlled && (kt::ShareRatio(stats) >= stats.max_share_ratio)) + setPriority(0); //dequeue it + + saveStats(); + emit maxRatioChanged(this); + } + + void TorrentControl::setMaxSeedTime(float hours) + { + stats.max_seed_time = hours; + saveStats(); + } + + bool TorrentControl::overMaxRatio() + { + if(stats.completed && stats.bytes_uploaded != 0 && stats.bytes_downloaded != 0 && stats.max_share_ratio > 0) + { + if(kt::ShareRatio(stats) >= stats.max_share_ratio) + return true; + } + + return false; + } + + bool TorrentControl::overMaxSeedTime() + { + if(stats.completed && stats.bytes_uploaded != 0 && stats.bytes_downloaded != 0 && stats.max_seed_time > 0) + { + Uint32 dl = getRunningTimeDL(); + Uint32 ul = getRunningTimeUL(); + if ((ul - dl) / 3600.0f > stats.max_seed_time) + return true; + } + + return false; + } + + + QString TorrentControl::statusToString() const + { + switch (stats.status) + { + case kt::NOT_STARTED : + return i18n("Not started"); + case kt::DOWNLOAD_COMPLETE : + return i18n("Download completed"); + case kt::SEEDING_COMPLETE : + return i18n("Seeding completed"); + case kt::SEEDING : + return i18n("Seeding"); + case kt::DOWNLOADING: + return i18n("Downloading"); + case kt::STALLED: + return i18n("Stalled"); + case kt::STOPPED: + return i18n("Stopped"); + case kt::ERROR : + return i18n("Error: ") + getShortErrorMessage(); + case kt::ALLOCATING_DISKSPACE: + return i18n("Allocating diskspace"); + case kt::QUEUED: + return i18n("Queued"); + case kt::CHECKING_DATA: + return i18n("Checking data"); + case kt::NO_SPACE_LEFT: + return i18n("Stopped. No space left on device."); + } + return QString::null; + } + + TrackersList* TorrentControl::getTrackersList() + { + return psman; + } + + const TrackersList* TorrentControl::getTrackersList() const + { + return psman; + } + + void TorrentControl::onPortPacket(const QString & ip,Uint16 port) + { + if (Globals::instance().getDHT().isRunning() && !stats.priv_torrent) + Globals::instance().getDHT().portRecieved(ip,port); + } + + void TorrentControl::startDataCheck(bt::DataCheckerListener* lst,bool auto_import) + { + if (stats.status == kt::ALLOCATING_DISKSPACE) + return; + + + DataChecker* dc = 0; + stats.status = kt::CHECKING_DATA; + stats.num_corrupted_chunks = 0; // reset the number of corrupted chunks found + if (stats.multi_file_torrent) + dc = new MultiDataChecker(); + else + dc = new SingleDataChecker(); + + dc->setListener(lst); + + dcheck_thread = new DataCheckerThread(dc,stats.output_path,*tor,datadir + "dnd" + bt::DirSeparator()); + + // dc->check(stats.output_path,*tor,datadir + "dnd" + bt::DirSeparator()); + dcheck_thread->start(); + } + + void TorrentControl::afterDataCheck() + { + DataChecker* dc = dcheck_thread->getDataChecker(); + DataCheckerListener* lst = dc->getListener(); + + bool err = !dcheck_thread->getError().isNull(); + if (err) + { + // show a queued error message when an error has occurred + KMessageBox::queuedMessageBox(0,KMessageBox::Error,dcheck_thread->getError()); + lst->stop(); + } + + if (lst && !lst->isStopped()) + { + down->dataChecked(dc->getDownloaded()); + // update chunk manager + cman->dataChecked(dc->getDownloaded()); + if (lst->isAutoImport()) + { + down->recalcDownloaded(); + stats.imported_bytes = down->bytesDownloaded(); + if (cman->haveAllChunks()) + stats.completed = true; + } + else + { + Uint64 downloaded = stats.bytes_downloaded; + down->recalcDownloaded(); + updateStats(); + if (stats.bytes_downloaded > downloaded) + stats.imported_bytes = stats.bytes_downloaded - downloaded; + + if (cman->haveAllChunks()) + stats.completed = true; + } + } + + stats.status = kt::NOT_STARTED; + // update the status + updateStatusMsg(); + updateStats(); + if (lst) + lst->finished(); + delete dcheck_thread; + dcheck_thread = 0; + } + + bool TorrentControl::isCheckingData(bool & finished) const + { + if (dcheck_thread) + { + finished = !dcheck_thread->isRunning(); + return true; + } + return false; + } + + bool TorrentControl::hasExistingFiles() const + { + return cman->hasExistingFiles(); + } + + bool TorrentControl::hasMissingFiles(QStringList & sl) + { + return cman->hasMissingFiles(sl); + } + + void TorrentControl::recreateMissingFiles() + { + try + { + cman->recreateMissingFiles(); + prealloc = true; // set prealloc to true so files will be truncated again + down->dataChecked(cman->getBitSet()); // update chunk selector + } + catch (Error & err) + { + onIOError(err.toString()); + throw; + } + } + + void TorrentControl::dndMissingFiles() + { + try + { + cman->dndMissingFiles(); + prealloc = true; // set prealloc to true so files will be truncated again + missingFilesMarkedDND(this); + down->dataChecked(cman->getBitSet()); // update chunk selector + } + catch (Error & err) + { + onIOError(err.toString()); + throw; + } + } + + void TorrentControl::handleError(const QString & err) + { + onIOError(err); + } + + Uint32 TorrentControl::getNumDHTNodes() const + { + return tor->getNumDHTNodes(); + } + + const kt::DHTNode & TorrentControl::getDHTNode(Uint32 i) const + { + return tor->getDHTNode(i); + } + + void TorrentControl::deleteDataFiles() + { + cman->deleteDataFiles(); + } + + const bt::SHA1Hash & TorrentControl::getInfoHash() const + { + return tor->getInfoHash(); + } + + void TorrentControl::resetTrackerStats() + { + istats.trk_prev_bytes_dl = stats.bytes_downloaded, + istats.trk_prev_bytes_ul = stats.bytes_uploaded, + stats.trk_bytes_downloaded = 0; + stats.trk_bytes_uploaded = 0; + } + + void TorrentControl::trackerStatusChanged(const QString & ns) + { + stats.trackerstatus = ns; + } + + void TorrentControl::addPeerSource(kt::PeerSource* ps) + { + if (psman) + psman->addPeerSource(ps); + } + + void TorrentControl::removePeerSource(kt::PeerSource* ps) + { + if (psman) + psman->removePeerSource(ps); + } + + void TorrentControl::corrupted(Uint32 chunk) + { + // make sure we will redownload the chunk + down->corrupted(chunk); + if (stats.completed) + stats.completed = false; + + // emit signal to show a systray message + stats.num_corrupted_chunks++; + corruptedDataFound(this); + } + + Uint32 TorrentControl::getETA() + { + return m_eta->estimate(); + } + + + + const bt::PeerID & TorrentControl::getOwnPeerID() const + { + return tor->getPeerID(); + } + + + bool TorrentControl::isFeatureEnabled(TorrentFeature tf) + { + switch (tf) + { + case kt::DHT_FEATURE: + return psman->dhtStarted(); + case kt::UT_PEX_FEATURE: + return pman->isPexEnabled(); + default: + return false; + } + } + + void TorrentControl::setFeatureEnabled(TorrentFeature tf,bool on) + { + switch (tf) + { + case kt::DHT_FEATURE: + if (on) + { + if(!stats.priv_torrent) + { + psman->addDHT(); + istats.dht_on = psman->dhtStarted(); + saveStats(); + } + } + else + { + psman->removeDHT(); + istats.dht_on = false; + saveStats(); + } + break; + case kt::UT_PEX_FEATURE: + if (on) + { + if (!stats.priv_torrent && !pman->isPexEnabled()) + { + pman->setPexEnabled(true); + } + } + else + { + pman->setPexEnabled(false); + } + break; + } + } + + void TorrentControl::createFiles() + { + cman->createFiles(true); + stats.output_path = cman->getOutputPath(); + } + + bool TorrentControl::checkDiskSpace(bool emit_sig) + { + last_diskspace_check = bt::GetCurrentTime(); + + //calculate free disk space + Uint64 bytes_free = 0; + if (FreeDiskSpace(getDataDir(),bytes_free)) + { + Uint64 bytes_to_download = stats.total_bytes_to_download; + Uint64 downloaded = 0; + try + { + downloaded = cman->diskUsage(); + } + catch (bt::Error & err) + { + Out(SYS_GEN|LOG_DEBUG) << "Error : " << err.toString() << endl; + } + Uint64 remaining = 0; + if (downloaded <= bytes_to_download) + remaining = bytes_to_download - downloaded; + + if (remaining > bytes_free) + { + bool toStop = bytes_free < (Uint64) Settings::minDiskSpace() * 1024 * 1024; + + // if we don't need to stop the torrent, only emit the signal once + // so that we do bother the user continously + if (emit_sig && (toStop || !istats.diskspace_warning_emitted)) + { + emit diskSpaceLow(this, toStop); + istats.diskspace_warning_emitted = true; + } + + if (!stats.running) + { + stats.status = NO_SPACE_LEFT; + } + + return false; + } + } + + return true; + } + + void TorrentControl::setTrafficLimits(Uint32 up,Uint32 down) + { + net::SocketMonitor & smon = net::SocketMonitor::instance(); + if (up && !upload_gid) + { + // create upload group + upload_gid = smon.newGroup(net::SocketMonitor::UPLOAD_GROUP,up); + upload_limit = up; + } + else if (up && upload_gid) + { + // change existing group limit + smon.setGroupLimit(net::SocketMonitor::UPLOAD_GROUP,upload_gid,up); + upload_limit = up; + } + else if (!up && !upload_gid) + { + upload_limit = up; + } + else // !up && upload_gid + { + // remove existing group + smon.removeGroup(net::SocketMonitor::UPLOAD_GROUP,upload_gid); + upload_gid = upload_limit = 0; + } + + if (down && !download_gid) + { + // create download grodown + download_gid = smon.newGroup(net::SocketMonitor::DOWNLOAD_GROUP,down); + download_limit = down; + } + else if (down && download_gid) + { + // change existing grodown limit + smon.setGroupLimit(net::SocketMonitor::DOWNLOAD_GROUP,download_gid,down); + download_limit = down; + } + else if (!down && !download_gid) + { + download_limit = down; + } + else // !down && download_gid + { + // remove existing grodown + smon.removeGroup(net::SocketMonitor::DOWNLOAD_GROUP,download_gid); + download_gid = download_limit = 0; + } + + saveStats(); + pman->setGroupIDs(upload_gid,download_gid); + } + + void TorrentControl::getTrafficLimits(Uint32 & up,Uint32 & down) + { + up = upload_limit; + down = download_limit; + } + + const PeerManager * TorrentControl::getPeerMgr() const + { + return pman; + } +} + +#include "torrentcontrol.moc" |