You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ktorrent/libktorrent/torrent/torrentcontrol.cpp

1771 lines
44 KiB

/***************************************************************************
* 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"