mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-19 15:07:22 -06:00
The goal is to improve responsiveness of qbt when CPU resources are scarce. Instead of lowering libtorrent threads priority, it is chosen to raise main event loop thread priority to avoid getting messy with libtorrent internals. Also on Windows, threads doesn't inherit thread priority from the parent thread and it always use the default (normal) priority. PR #17278.
904 lines
30 KiB
C++
904 lines
30 KiB
C++
/*
|
|
* Bittorrent Client using Qt and libtorrent.
|
|
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
|
* Copyright (C) 2006 Christophe Dumez
|
|
*
|
|
* 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.
|
|
*
|
|
* In addition, as a special exception, the copyright holders give permission to
|
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
|
* and distribute the linked executables. You must obey the GNU General Public
|
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
|
* modify file(s), you may extend this exception to your version of the file(s),
|
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
|
* exception statement from your version.
|
|
*/
|
|
|
|
#include "application.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#ifdef DISABLE_GUI
|
|
#include <cstdio>
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <memory>
|
|
#include <Windows.h>
|
|
#include <Shellapi.h>
|
|
#elif defined(Q_OS_UNIX)
|
|
#include <sys/resource.h>
|
|
#endif
|
|
|
|
#include <QByteArray>
|
|
#include <QDebug>
|
|
#include <QLibraryInfo>
|
|
#include <QMetaObject>
|
|
#include <QProcess>
|
|
|
|
#ifndef DISABLE_GUI
|
|
#include <QMessageBox>
|
|
#include <QPixmapCache>
|
|
#ifdef Q_OS_WIN
|
|
#include <QSessionManager>
|
|
#include <QSharedMemory>
|
|
#endif // Q_OS_WIN
|
|
#ifdef Q_OS_MACOS
|
|
#include <QFileOpenEvent>
|
|
#endif // Q_OS_MACOS
|
|
#endif
|
|
|
|
#include "base/bittorrent/infohash.h"
|
|
#include "base/bittorrent/session.h"
|
|
#include "base/bittorrent/torrent.h"
|
|
#include "base/exceptions.h"
|
|
#include "base/global.h"
|
|
#include "base/iconprovider.h"
|
|
#include "base/logger.h"
|
|
#include "base/net/downloadmanager.h"
|
|
#include "base/net/geoipmanager.h"
|
|
#include "base/net/proxyconfigurationmanager.h"
|
|
#include "base/net/smtp.h"
|
|
#include "base/preferences.h"
|
|
#include "base/profile.h"
|
|
#include "base/rss/rss_autodownloader.h"
|
|
#include "base/rss/rss_session.h"
|
|
#include "base/search/searchpluginmanager.h"
|
|
#include "base/settingsstorage.h"
|
|
#include "base/torrentfileswatcher.h"
|
|
#include "base/utils/compare.h"
|
|
#include "base/utils/fs.h"
|
|
#include "base/utils/misc.h"
|
|
#include "base/version.h"
|
|
#include "applicationinstancemanager.h"
|
|
#include "filelogger.h"
|
|
|
|
#ifndef DISABLE_GUI
|
|
#include "gui/addnewtorrentdialog.h"
|
|
#include "gui/mainwindow.h"
|
|
#include "gui/shutdownconfirmdialog.h"
|
|
#include "gui/uithememanager.h"
|
|
#include "gui/utils.h"
|
|
#endif // DISABLE_GUI
|
|
|
|
#ifndef DISABLE_WEBUI
|
|
#include "webui/webui.h"
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
#define SETTINGS_KEY(name) u"Application/" name
|
|
#define FILELOGGER_SETTINGS_KEY(name) (SETTINGS_KEY(u"FileLogger/") name)
|
|
|
|
const QString LOG_FOLDER = u"logs"_qs;
|
|
const QChar PARAMS_SEPARATOR = u'|';
|
|
|
|
const Path DEFAULT_PORTABLE_MODE_PROFILE_DIR {u"profile"_qs};
|
|
|
|
const int MIN_FILELOG_SIZE = 1024; // 1KiB
|
|
const int MAX_FILELOG_SIZE = 1000 * 1024 * 1024; // 1000MiB
|
|
const int DEFAULT_FILELOG_SIZE = 65 * 1024; // 65KiB
|
|
|
|
#if !defined(DISABLE_GUI)
|
|
const int PIXMAP_CACHE_SIZE = 64 * 1024 * 1024; // 64MiB
|
|
#endif
|
|
}
|
|
|
|
Application::Application(int &argc, char **argv)
|
|
: BaseApplication(argc, argv)
|
|
, m_shutdownAct(ShutdownDialogAction::Exit)
|
|
, m_commandLineArgs(parseCommandLine(this->arguments()))
|
|
, m_storeFileLoggerEnabled(FILELOGGER_SETTINGS_KEY(u"Enabled"_qs))
|
|
, m_storeFileLoggerBackup(FILELOGGER_SETTINGS_KEY(u"Backup"_qs))
|
|
, m_storeFileLoggerDeleteOld(FILELOGGER_SETTINGS_KEY(u"DeleteOld"_qs))
|
|
, m_storeFileLoggerMaxSize(FILELOGGER_SETTINGS_KEY(u"MaxSizeBytes"_qs))
|
|
, m_storeFileLoggerAge(FILELOGGER_SETTINGS_KEY(u"Age"_qs))
|
|
, m_storeFileLoggerAgeType(FILELOGGER_SETTINGS_KEY(u"AgeType"_qs))
|
|
, m_storeFileLoggerPath(FILELOGGER_SETTINGS_KEY(u"Path"_qs))
|
|
, m_storeMemoryWorkingSetLimit(SETTINGS_KEY(u"MemoryWorkingSetLimit"_qs))
|
|
{
|
|
qRegisterMetaType<Log::Msg>("Log::Msg");
|
|
qRegisterMetaType<Log::Peer>("Log::Peer");
|
|
|
|
setApplicationName(u"qBittorrent"_qs);
|
|
setOrganizationDomain(u"qbittorrent.org"_qs);
|
|
#if !defined(DISABLE_GUI)
|
|
setDesktopFileName(u"org.qbittorrent.qBittorrent"_qs);
|
|
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
|
|
setAttribute(Qt::AA_UseHighDpiPixmaps, true); // opt-in to the high DPI pixmap support
|
|
#endif
|
|
setQuitOnLastWindowClosed(false);
|
|
QPixmapCache::setCacheLimit(PIXMAP_CACHE_SIZE);
|
|
#endif
|
|
|
|
const auto portableProfilePath = Path(QCoreApplication::applicationDirPath()) / DEFAULT_PORTABLE_MODE_PROFILE_DIR;
|
|
const bool portableModeEnabled = m_commandLineArgs.profileDir.isEmpty() && portableProfilePath.exists();
|
|
|
|
const Path profileDir = portableModeEnabled
|
|
? portableProfilePath
|
|
: m_commandLineArgs.profileDir;
|
|
Profile::initInstance(profileDir, m_commandLineArgs.configurationName,
|
|
(m_commandLineArgs.relativeFastresumePaths || portableModeEnabled));
|
|
|
|
m_instanceManager = new ApplicationInstanceManager(Profile::instance()->location(SpecialFolder::Config), this);
|
|
|
|
Logger::initInstance();
|
|
SettingsStorage::initInstance();
|
|
Preferences::initInstance();
|
|
|
|
initializeTranslation();
|
|
|
|
if (m_commandLineArgs.webUiPort > 0) // it will be -1 when user did not set any value
|
|
Preferences::instance()->setWebUiPort(m_commandLineArgs.webUiPort);
|
|
|
|
connect(this, &QCoreApplication::aboutToQuit, this, &Application::cleanup);
|
|
connect(m_instanceManager, &ApplicationInstanceManager::messageReceived, this, &Application::processMessage);
|
|
#if defined(Q_OS_WIN) && !defined(DISABLE_GUI)
|
|
connect(this, &QGuiApplication::commitDataRequest, this, &Application::shutdownCleanup, Qt::DirectConnection);
|
|
#endif
|
|
|
|
if (isFileLoggerEnabled())
|
|
m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType()));
|
|
|
|
Logger::instance()->addMessage(tr("qBittorrent %1 started", "qBittorrent v3.2.0alpha started").arg(QStringLiteral(QBT_VERSION)));
|
|
if (portableModeEnabled)
|
|
{
|
|
Logger::instance()->addMessage(tr("Running in portable mode. Auto detected profile folder at: %1").arg(profileDir.toString()));
|
|
if (m_commandLineArgs.relativeFastresumePaths)
|
|
Logger::instance()->addMessage(tr("Redundant command line flag detected: \"%1\". Portable mode implies relative fastresume.").arg(u"--relative-fastresume"_qs), Log::WARNING); // to avoid translating the `--relative-fastresume` string
|
|
}
|
|
else
|
|
{
|
|
Logger::instance()->addMessage(tr("Using config directory: %1").arg(Profile::instance()->location(SpecialFolder::Config).toString()));
|
|
}
|
|
}
|
|
|
|
Application::~Application()
|
|
{
|
|
// we still need to call cleanup()
|
|
// in case the App failed to start
|
|
cleanup();
|
|
}
|
|
|
|
#ifndef DISABLE_GUI
|
|
MainWindow *Application::mainWindow()
|
|
{
|
|
return m_window;
|
|
}
|
|
#endif
|
|
|
|
const QBtCommandLineParameters &Application::commandLineArgs() const
|
|
{
|
|
return m_commandLineArgs;
|
|
}
|
|
|
|
int Application::memoryWorkingSetLimit() const
|
|
{
|
|
return m_storeMemoryWorkingSetLimit.get(512);
|
|
}
|
|
|
|
void Application::setMemoryWorkingSetLimit(const int size)
|
|
{
|
|
if (size == memoryWorkingSetLimit())
|
|
return;
|
|
|
|
m_storeMemoryWorkingSetLimit = size;
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
applyMemoryWorkingSetLimit();
|
|
#endif
|
|
}
|
|
|
|
bool Application::isFileLoggerEnabled() const
|
|
{
|
|
return m_storeFileLoggerEnabled.get(true);
|
|
}
|
|
|
|
void Application::setFileLoggerEnabled(const bool value)
|
|
{
|
|
if (value && !m_fileLogger)
|
|
m_fileLogger = new FileLogger(fileLoggerPath(), isFileLoggerBackup(), fileLoggerMaxSize(), isFileLoggerDeleteOld(), fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType()));
|
|
else if (!value)
|
|
delete m_fileLogger;
|
|
m_storeFileLoggerEnabled = value;
|
|
}
|
|
|
|
Path Application::fileLoggerPath() const
|
|
{
|
|
return m_storeFileLoggerPath.get(specialFolderLocation(SpecialFolder::Data) / Path(LOG_FOLDER));
|
|
}
|
|
|
|
void Application::setFileLoggerPath(const Path &path)
|
|
{
|
|
if (m_fileLogger)
|
|
m_fileLogger->changePath(path);
|
|
m_storeFileLoggerPath = path;
|
|
}
|
|
|
|
bool Application::isFileLoggerBackup() const
|
|
{
|
|
return m_storeFileLoggerBackup.get(true);
|
|
}
|
|
|
|
void Application::setFileLoggerBackup(const bool value)
|
|
{
|
|
if (m_fileLogger)
|
|
m_fileLogger->setBackup(value);
|
|
m_storeFileLoggerBackup = value;
|
|
}
|
|
|
|
bool Application::isFileLoggerDeleteOld() const
|
|
{
|
|
return m_storeFileLoggerDeleteOld.get(true);
|
|
}
|
|
|
|
void Application::setFileLoggerDeleteOld(const bool value)
|
|
{
|
|
if (value && m_fileLogger)
|
|
m_fileLogger->deleteOld(fileLoggerAge(), static_cast<FileLogger::FileLogAgeType>(fileLoggerAgeType()));
|
|
m_storeFileLoggerDeleteOld = value;
|
|
}
|
|
|
|
int Application::fileLoggerMaxSize() const
|
|
{
|
|
const int val = m_storeFileLoggerMaxSize.get(DEFAULT_FILELOG_SIZE);
|
|
return std::min(std::max(val, MIN_FILELOG_SIZE), MAX_FILELOG_SIZE);
|
|
}
|
|
|
|
void Application::setFileLoggerMaxSize(const int bytes)
|
|
{
|
|
const int clampedValue = std::min(std::max(bytes, MIN_FILELOG_SIZE), MAX_FILELOG_SIZE);
|
|
if (m_fileLogger)
|
|
m_fileLogger->setMaxSize(clampedValue);
|
|
m_storeFileLoggerMaxSize = clampedValue;
|
|
}
|
|
|
|
int Application::fileLoggerAge() const
|
|
{
|
|
const int val = m_storeFileLoggerAge.get(1);
|
|
return std::min(std::max(val, 1), 365);
|
|
}
|
|
|
|
void Application::setFileLoggerAge(const int value)
|
|
{
|
|
m_storeFileLoggerAge = std::min(std::max(value, 1), 365);
|
|
}
|
|
|
|
int Application::fileLoggerAgeType() const
|
|
{
|
|
const int val = m_storeFileLoggerAgeType.get(1);
|
|
return ((val < 0) || (val > 2)) ? 1 : val;
|
|
}
|
|
|
|
void Application::setFileLoggerAgeType(const int value)
|
|
{
|
|
m_storeFileLoggerAgeType = ((value < 0) || (value > 2)) ? 1 : value;
|
|
}
|
|
|
|
void Application::processMessage(const QString &message)
|
|
{
|
|
const QStringList params = message.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts);
|
|
// If Application is not running (i.e., other
|
|
// components are not ready) store params
|
|
if (m_running)
|
|
processParams(params);
|
|
else
|
|
m_paramsQueue.append(params);
|
|
}
|
|
|
|
void Application::runExternalProgram(const BitTorrent::Torrent *torrent) const
|
|
{
|
|
QString program = Preferences::instance()->getAutoRunProgram().trimmed();
|
|
|
|
for (int i = (program.length() - 2); i >= 0; --i)
|
|
{
|
|
if (program[i] != u'%')
|
|
continue;
|
|
|
|
const ushort specifier = program[i + 1].unicode();
|
|
switch (specifier)
|
|
{
|
|
case u'C':
|
|
program.replace(i, 2, QString::number(torrent->filesCount()));
|
|
break;
|
|
case u'D':
|
|
program.replace(i, 2, torrent->savePath().toString());
|
|
break;
|
|
case u'F':
|
|
program.replace(i, 2, torrent->contentPath().toString());
|
|
break;
|
|
case u'G':
|
|
program.replace(i, 2, torrent->tags().join(u","_qs));
|
|
break;
|
|
case u'I':
|
|
program.replace(i, 2, (torrent->infoHash().v1().isValid() ? torrent->infoHash().v1().toString() : u"-"_qs));
|
|
break;
|
|
case u'J':
|
|
program.replace(i, 2, (torrent->infoHash().v2().isValid() ? torrent->infoHash().v2().toString() : u"-"_qs));
|
|
break;
|
|
case u'K':
|
|
program.replace(i, 2, torrent->id().toString());
|
|
break;
|
|
case u'L':
|
|
program.replace(i, 2, torrent->category());
|
|
break;
|
|
case u'N':
|
|
program.replace(i, 2, torrent->name());
|
|
break;
|
|
case u'R':
|
|
program.replace(i, 2, torrent->rootPath().toString());
|
|
break;
|
|
case u'T':
|
|
program.replace(i, 2, torrent->currentTracker());
|
|
break;
|
|
case u'Z':
|
|
program.replace(i, 2, QString::number(torrent->totalSize()));
|
|
break;
|
|
default:
|
|
// do nothing
|
|
break;
|
|
}
|
|
|
|
// decrement `i` to avoid unwanted replacement, example pattern: "%%N"
|
|
--i;
|
|
}
|
|
|
|
LogMsg(tr("Torrent: %1, running external program, command: %2").arg(torrent->name(), program));
|
|
|
|
#if defined(Q_OS_WIN)
|
|
const std::wstring programWStr = program.toStdWString();
|
|
|
|
// Need to split arguments manually because QProcess::startDetached(QString)
|
|
// will strip off empty parameters.
|
|
// E.g. `python.exe "1" "" "3"` will become `python.exe "1" "3"`
|
|
int argCount = 0;
|
|
std::unique_ptr<LPWSTR[], decltype(&::LocalFree)> args {::CommandLineToArgvW(programWStr.c_str(), &argCount), ::LocalFree};
|
|
|
|
QStringList argList;
|
|
for (int i = 1; i < argCount; ++i)
|
|
argList += QString::fromWCharArray(args[i]);
|
|
|
|
QProcess proc;
|
|
proc.setProgram(QString::fromWCharArray(args[0]));
|
|
proc.setArguments(argList);
|
|
proc.setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args)
|
|
{
|
|
if (Preferences::instance()->isAutoRunConsoleEnabled())
|
|
{
|
|
args->flags |= CREATE_NEW_CONSOLE;
|
|
args->flags &= ~(CREATE_NO_WINDOW | DETACHED_PROCESS);
|
|
}
|
|
else
|
|
{
|
|
args->flags |= CREATE_NO_WINDOW;
|
|
args->flags &= ~(CREATE_NEW_CONSOLE | DETACHED_PROCESS);
|
|
}
|
|
args->inheritHandles = false;
|
|
args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
|
|
::CloseHandle(args->startupInfo->hStdInput);
|
|
::CloseHandle(args->startupInfo->hStdOutput);
|
|
::CloseHandle(args->startupInfo->hStdError);
|
|
args->startupInfo->hStdInput = nullptr;
|
|
args->startupInfo->hStdOutput = nullptr;
|
|
args->startupInfo->hStdError = nullptr;
|
|
});
|
|
proc.startDetached();
|
|
#else // Q_OS_WIN
|
|
// Cannot give users shell environment by default, as doing so could
|
|
// enable command injection via torrent name and other arguments
|
|
// (especially when some automated download mechanism has been setup).
|
|
// See: https://github.com/qbittorrent/qBittorrent/issues/10925
|
|
QStringList args = QProcess::splitCommand(program);
|
|
if (args.isEmpty())
|
|
return;
|
|
|
|
const QString command = args.takeFirst();
|
|
QProcess::startDetached(command, args);
|
|
#endif
|
|
}
|
|
|
|
void Application::sendNotificationEmail(const BitTorrent::Torrent *torrent)
|
|
{
|
|
// Prepare mail content
|
|
const QString content = tr("Torrent name: %1").arg(torrent->name()) + u'\n'
|
|
+ tr("Torrent size: %1").arg(Utils::Misc::friendlyUnit(torrent->wantedSize())) + u'\n'
|
|
+ tr("Save path: %1").arg(torrent->savePath().toString()) + u"\n\n"
|
|
+ tr("The torrent was downloaded in %1.", "The torrent was downloaded in 1 hour and 20 seconds")
|
|
.arg(Utils::Misc::userFriendlyDuration(torrent->activeTime())) + u"\n\n\n"
|
|
+ tr("Thank you for using qBittorrent.") + u'\n';
|
|
|
|
// Send the notification email
|
|
const Preferences *pref = Preferences::instance();
|
|
auto *smtp = new Net::Smtp(this);
|
|
smtp->sendMail(pref->getMailNotificationSender(),
|
|
pref->getMailNotificationEmail(),
|
|
tr("[qBittorrent] '%1' has finished downloading").arg(torrent->name()),
|
|
content);
|
|
}
|
|
|
|
void Application::torrentFinished(BitTorrent::Torrent *const torrent)
|
|
{
|
|
Preferences *const pref = Preferences::instance();
|
|
|
|
// AutoRun program
|
|
if (pref->isAutoRunEnabled())
|
|
runExternalProgram(torrent);
|
|
|
|
// Mail notification
|
|
if (pref->isMailNotificationEnabled())
|
|
{
|
|
Logger::instance()->addMessage(tr("Torrent: %1, sending mail notification").arg(torrent->name()));
|
|
sendNotificationEmail(torrent);
|
|
}
|
|
}
|
|
|
|
void Application::allTorrentsFinished()
|
|
{
|
|
Preferences *const pref = Preferences::instance();
|
|
bool isExit = pref->shutdownqBTWhenDownloadsComplete();
|
|
bool isShutdown = pref->shutdownWhenDownloadsComplete();
|
|
bool isSuspend = pref->suspendWhenDownloadsComplete();
|
|
bool isHibernate = pref->hibernateWhenDownloadsComplete();
|
|
|
|
bool haveAction = isExit || isShutdown || isSuspend || isHibernate;
|
|
if (!haveAction) return;
|
|
|
|
ShutdownDialogAction action = ShutdownDialogAction::Exit;
|
|
if (isSuspend)
|
|
action = ShutdownDialogAction::Suspend;
|
|
else if (isHibernate)
|
|
action = ShutdownDialogAction::Hibernate;
|
|
else if (isShutdown)
|
|
action = ShutdownDialogAction::Shutdown;
|
|
|
|
#ifndef DISABLE_GUI
|
|
// ask confirm
|
|
if ((action == ShutdownDialogAction::Exit) && (pref->dontConfirmAutoExit()))
|
|
{
|
|
// do nothing & skip confirm
|
|
}
|
|
else
|
|
{
|
|
if (!ShutdownConfirmDialog::askForConfirmation(m_window, action)) return;
|
|
}
|
|
#endif // DISABLE_GUI
|
|
|
|
// Actually shut down
|
|
if (action != ShutdownDialogAction::Exit)
|
|
{
|
|
qDebug("Preparing for auto-shutdown because all downloads are complete!");
|
|
// Disabling it for next time
|
|
pref->setShutdownWhenDownloadsComplete(false);
|
|
pref->setSuspendWhenDownloadsComplete(false);
|
|
pref->setHibernateWhenDownloadsComplete(false);
|
|
// Make sure preferences are synced before exiting
|
|
m_shutdownAct = action;
|
|
}
|
|
|
|
qDebug("Exiting the application");
|
|
exit();
|
|
}
|
|
|
|
bool Application::sendParams(const QStringList ¶ms)
|
|
{
|
|
return m_instanceManager->sendMessage(params.join(PARAMS_SEPARATOR));
|
|
}
|
|
|
|
// As program parameters, we can get paths or urls.
|
|
// This function parse the parameters and call
|
|
// the right addTorrent function, considering
|
|
// the parameter type.
|
|
void Application::processParams(const QStringList ¶ms)
|
|
{
|
|
#ifndef DISABLE_GUI
|
|
if (params.isEmpty())
|
|
{
|
|
m_window->activate(); // show UI
|
|
return;
|
|
}
|
|
#endif
|
|
BitTorrent::AddTorrentParams torrentParams;
|
|
std::optional<bool> skipTorrentDialog;
|
|
|
|
for (QString param : params)
|
|
{
|
|
param = param.trimmed();
|
|
|
|
// Process strings indicating options specified by the user.
|
|
|
|
if (param.startsWith(u"@savePath="))
|
|
{
|
|
torrentParams.savePath = Path(param.mid(10));
|
|
continue;
|
|
}
|
|
|
|
if (param.startsWith(u"@addPaused="))
|
|
{
|
|
torrentParams.addPaused = (QStringView(param).mid(11).toInt() != 0);
|
|
continue;
|
|
}
|
|
|
|
if (param == u"@skipChecking")
|
|
{
|
|
torrentParams.skipChecking = true;
|
|
continue;
|
|
}
|
|
|
|
if (param.startsWith(u"@category="))
|
|
{
|
|
torrentParams.category = param.mid(10);
|
|
continue;
|
|
}
|
|
|
|
if (param == u"@sequential")
|
|
{
|
|
torrentParams.sequential = true;
|
|
continue;
|
|
}
|
|
|
|
if (param == u"@firstLastPiecePriority")
|
|
{
|
|
torrentParams.firstLastPiecePriority = true;
|
|
continue;
|
|
}
|
|
|
|
if (param.startsWith(u"@skipDialog="))
|
|
{
|
|
skipTorrentDialog = (QStringView(param).mid(12).toInt() != 0);
|
|
continue;
|
|
}
|
|
|
|
#ifndef DISABLE_GUI
|
|
// There are two circumstances in which we want to show the torrent
|
|
// dialog. One is when the application settings specify that it should
|
|
// be shown and skipTorrentDialog is undefined. The other is when
|
|
// skipTorrentDialog is false, meaning that the application setting
|
|
// should be overridden.
|
|
const bool showDialogForThisTorrent = !skipTorrentDialog.value_or(!AddNewTorrentDialog::isEnabled());
|
|
if (showDialogForThisTorrent)
|
|
AddNewTorrentDialog::show(param, torrentParams, m_window);
|
|
else
|
|
#endif
|
|
BitTorrent::Session::instance()->addTorrent(param, torrentParams);
|
|
}
|
|
}
|
|
|
|
int Application::exec(const QStringList ¶ms)
|
|
{
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
applyMemoryWorkingSetLimit();
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN
|
|
adjustThreadPriority();
|
|
#endif
|
|
|
|
Net::ProxyConfigurationManager::initInstance();
|
|
Net::DownloadManager::initInstance();
|
|
IconProvider::initInstance();
|
|
|
|
try
|
|
{
|
|
BitTorrent::Session::initInstance();
|
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentFinished, this, &Application::torrentFinished);
|
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::allTorrentsFinished, this, &Application::allTorrentsFinished, Qt::QueuedConnection);
|
|
|
|
Net::GeoIPManager::initInstance();
|
|
TorrentFilesWatcher::initInstance();
|
|
|
|
#ifndef DISABLE_WEBUI
|
|
m_webui = new WebUI(this);
|
|
#ifdef DISABLE_GUI
|
|
if (m_webui->isErrored())
|
|
return 1;
|
|
connect(m_webui, &WebUI::fatalError, this, []() { QCoreApplication::exit(1); });
|
|
#endif // DISABLE_GUI
|
|
#endif // DISABLE_WEBUI
|
|
|
|
new RSS::Session; // create RSS::Session singleton
|
|
new RSS::AutoDownloader; // create RSS::AutoDownloader singleton
|
|
}
|
|
catch (const RuntimeError &err)
|
|
{
|
|
#ifdef DISABLE_GUI
|
|
fprintf(stderr, "%s", qPrintable(err.message()));
|
|
#else
|
|
QMessageBox msgBox;
|
|
msgBox.setIcon(QMessageBox::Critical);
|
|
msgBox.setText(tr("Application failed to start."));
|
|
msgBox.setInformativeText(err.message());
|
|
msgBox.show(); // Need to be shown or to moveToCenter does not work
|
|
msgBox.move(Utils::Gui::screenCenter(&msgBox));
|
|
msgBox.exec();
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
#ifdef DISABLE_GUI
|
|
#ifndef DISABLE_WEBUI
|
|
const Preferences *pref = Preferences::instance();
|
|
|
|
const auto scheme = pref->isWebUiHttpsEnabled() ? u"https"_qs : u"http"_qs;
|
|
const auto url = u"%1://localhost:%2\n"_qs.arg(scheme, QString::number(pref->getWebUiPort()));
|
|
const QString mesg = u"\n******** %1 ********\n"_qs.arg(tr("Information"))
|
|
+ tr("To control qBittorrent, access the WebUI at: %1").arg(url);
|
|
printf("%s\n", qUtf8Printable(mesg));
|
|
|
|
if (pref->getWebUIPassword() == QByteArrayLiteral("ARQ77eY1NUZaQsuDHbIMCA==:0WMRkYTUWVT9wVvdDtHAjU9b3b7uB8NR1Gur2hmQCvCDpm39Q+PsJRJPaCU51dEiz+dTzh8qbPsL8WkFljQYFQ=="))
|
|
{
|
|
const QString warning = tr("The Web UI administrator username is: %1").arg(pref->getWebUiUsername()) + u'\n'
|
|
+ tr("The Web UI administrator password has not been changed from the default: %1").arg(u"adminadmin"_qs) + u'\n'
|
|
+ tr("This is a security risk, please change your password in program preferences.") + u'\n';
|
|
printf("%s", qUtf8Printable(warning));
|
|
}
|
|
#endif // DISABLE_WEBUI
|
|
#else
|
|
UIThemeManager::initInstance();
|
|
m_window = new MainWindow(this);
|
|
#endif // DISABLE_GUI
|
|
|
|
m_running = true;
|
|
|
|
// Now UI is ready to process signals from Session
|
|
BitTorrent::Session::instance()->startUpTorrents();
|
|
|
|
m_paramsQueue = params + m_paramsQueue;
|
|
if (!m_paramsQueue.isEmpty())
|
|
{
|
|
processParams(m_paramsQueue);
|
|
m_paramsQueue.clear();
|
|
}
|
|
return BaseApplication::exec();
|
|
}
|
|
|
|
bool Application::isRunning()
|
|
{
|
|
return !m_instanceManager->isFirstInstance();
|
|
}
|
|
|
|
#ifndef DISABLE_GUI
|
|
#ifdef Q_OS_MACOS
|
|
bool Application::event(QEvent *ev)
|
|
{
|
|
if (ev->type() == QEvent::FileOpen)
|
|
{
|
|
QString path = static_cast<QFileOpenEvent *>(ev)->file();
|
|
if (path.isEmpty())
|
|
// Get the url instead
|
|
path = static_cast<QFileOpenEvent *>(ev)->url().toString();
|
|
qDebug("Received a mac file open event: %s", qUtf8Printable(path));
|
|
if (m_running)
|
|
processParams(QStringList(path));
|
|
else
|
|
m_paramsQueue.append(path);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return BaseApplication::event(ev);
|
|
}
|
|
}
|
|
#endif // Q_OS_MACOS
|
|
#endif // DISABLE_GUI
|
|
|
|
void Application::initializeTranslation()
|
|
{
|
|
Preferences *const pref = Preferences::instance();
|
|
// Load translation
|
|
const QString localeStr = pref->getLocale();
|
|
|
|
if (m_qtTranslator.load((u"qtbase_" + localeStr), QLibraryInfo::location(QLibraryInfo::TranslationsPath)) ||
|
|
m_qtTranslator.load((u"qt_" + localeStr), QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
|
|
qDebug("Qt %s locale recognized, using translation.", qUtf8Printable(localeStr));
|
|
else
|
|
qDebug("Qt %s locale unrecognized, using default (en).", qUtf8Printable(localeStr));
|
|
|
|
installTranslator(&m_qtTranslator);
|
|
|
|
if (m_translator.load(u":/lang/qbittorrent_" + localeStr))
|
|
qDebug("%s locale recognized, using translation.", qUtf8Printable(localeStr));
|
|
else
|
|
qDebug("%s locale unrecognized, using default (en).", qUtf8Printable(localeStr));
|
|
installTranslator(&m_translator);
|
|
|
|
#ifndef DISABLE_GUI
|
|
if (localeStr.startsWith(u"ar") || localeStr.startsWith(u"he"))
|
|
{
|
|
qDebug("Right to Left mode");
|
|
setLayoutDirection(Qt::RightToLeft);
|
|
}
|
|
else
|
|
{
|
|
setLayoutDirection(Qt::LeftToRight);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if (!defined(DISABLE_GUI) && defined(Q_OS_WIN))
|
|
void Application::shutdownCleanup(QSessionManager &manager)
|
|
{
|
|
Q_UNUSED(manager);
|
|
|
|
// This is only needed for a special case on Windows XP.
|
|
// (but is called for every Windows version)
|
|
// If a process takes too much time to exit during OS
|
|
// shutdown, the OS presents a dialog to the user.
|
|
// That dialog tells the user that qbt is blocking the
|
|
// shutdown, it shows a progress bar and it offers
|
|
// a "Terminate Now" button for the user. However,
|
|
// after the progress bar has reached 100% another button
|
|
// is offered to the user reading "Cancel". With this the
|
|
// user can cancel the **OS** shutdown. If we don't do
|
|
// the cleanup by handling the commitDataRequest() signal
|
|
// and the user clicks "Cancel", it will result in qbt being
|
|
// killed and the shutdown proceeding instead. Apparently
|
|
// aboutToQuit() is emitted too late in the shutdown process.
|
|
cleanup();
|
|
|
|
// According to the qt docs we shouldn't call quit() inside a slot.
|
|
// aboutToQuit() is never emitted if the user hits "Cancel" in
|
|
// the above dialog.
|
|
QMetaObject::invokeMethod(qApp, &QCoreApplication::quit, Qt::QueuedConnection);
|
|
}
|
|
#endif
|
|
|
|
#ifdef QBT_USES_LIBTORRENT2
|
|
void Application::applyMemoryWorkingSetLimit() const
|
|
{
|
|
const size_t MiB = 1024 * 1024;
|
|
const QString logMessage = tr("Failed to set physical memory (RAM) usage limit. Error code: %1. Error message: \"%2\"");
|
|
|
|
#ifdef Q_OS_WIN
|
|
const SIZE_T maxSize = memoryWorkingSetLimit() * MiB;
|
|
const auto minSize = std::min<SIZE_T>((64 * MiB), (maxSize / 2));
|
|
if (!::SetProcessWorkingSetSizeEx(::GetCurrentProcess(), minSize, maxSize, QUOTA_LIMITS_HARDWS_MAX_ENABLE))
|
|
{
|
|
const DWORD errorCode = ::GetLastError();
|
|
QString message;
|
|
LPVOID lpMsgBuf = nullptr;
|
|
const DWORD msgLength = ::FormatMessageW((FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS)
|
|
, nullptr, errorCode, LANG_USER_DEFAULT, reinterpret_cast<LPWSTR>(&lpMsgBuf), 0, nullptr);
|
|
if (msgLength > 0)
|
|
{
|
|
message = QString::fromWCharArray(reinterpret_cast<LPWSTR>(lpMsgBuf)).trimmed();
|
|
::LocalFree(lpMsgBuf);
|
|
}
|
|
LogMsg(logMessage.arg(QString::number(errorCode), message), Log::WARNING);
|
|
}
|
|
#elif defined(Q_OS_UNIX)
|
|
// has no effect on linux but it might be meaningful for other OS
|
|
rlimit limit {};
|
|
|
|
if (::getrlimit(RLIMIT_RSS, &limit) != 0)
|
|
return;
|
|
|
|
limit.rlim_cur = memoryWorkingSetLimit() * MiB;
|
|
if (::setrlimit(RLIMIT_RSS, &limit) != 0)
|
|
{
|
|
const auto message = QString::fromLocal8Bit(strerror(errno));
|
|
LogMsg(logMessage.arg(QString::number(errno), message), Log::WARNING);
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN
|
|
void Application::adjustThreadPriority() const
|
|
{
|
|
// Workaround for improving responsiveness of qbt when CPU resources are scarce.
|
|
// Raise main event loop thread to be just one level higher than libtorrent threads.
|
|
// Also note that on *nix platforms there is no easy way to achieve it,
|
|
// so implementation is omitted.
|
|
|
|
::SetThreadPriority(::GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
|
|
}
|
|
#endif
|
|
|
|
void Application::cleanup()
|
|
{
|
|
// cleanup() can be called multiple times during shutdown. We only need it once.
|
|
if (!m_isCleanupRun.testAndSetAcquire(0, 1))
|
|
return;
|
|
|
|
LogMsg(tr("qBittorrent termination initiated"));
|
|
|
|
#ifndef DISABLE_GUI
|
|
if (m_window)
|
|
{
|
|
// Hide the window and don't leave it on screen as
|
|
// unresponsive. Also for Windows take the WinId
|
|
// after it's hidden, because hide() may cause a
|
|
// WinId change.
|
|
m_window->hide();
|
|
|
|
#ifdef Q_OS_WIN
|
|
const std::wstring msg = tr("Saving torrent progress...").toStdWString();
|
|
::ShutdownBlockReasonCreate(reinterpret_cast<HWND>(m_window->effectiveWinId())
|
|
, msg.c_str());
|
|
#endif // Q_OS_WIN
|
|
|
|
// Do manual cleanup in MainWindow to force widgets
|
|
// to save their Preferences, stop all timers and
|
|
// delete as many widgets as possible to leave only
|
|
// a 'shell' MainWindow.
|
|
// We need a valid window handle for Windows Vista+
|
|
// otherwise the system shutdown will continue even
|
|
// though we created a ShutdownBlockReason
|
|
m_window->cleanup();
|
|
}
|
|
#endif // DISABLE_GUI
|
|
|
|
#ifndef DISABLE_WEBUI
|
|
delete m_webui;
|
|
#endif
|
|
|
|
delete RSS::AutoDownloader::instance();
|
|
delete RSS::Session::instance();
|
|
|
|
TorrentFilesWatcher::freeInstance();
|
|
BitTorrent::Session::freeInstance();
|
|
Net::GeoIPManager::freeInstance();
|
|
Net::DownloadManager::freeInstance();
|
|
Net::ProxyConfigurationManager::freeInstance();
|
|
Preferences::freeInstance();
|
|
SettingsStorage::freeInstance();
|
|
IconProvider::freeInstance();
|
|
SearchPluginManager::freeInstance();
|
|
Utils::Fs::removeDirRecursively(Utils::Fs::tempPath());
|
|
|
|
LogMsg(tr("qBittorrent is now ready to exit"));
|
|
Logger::freeInstance();
|
|
delete m_fileLogger;
|
|
|
|
#ifndef DISABLE_GUI
|
|
if (m_window)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
::ShutdownBlockReasonDestroy(reinterpret_cast<HWND>(m_window->effectiveWinId()));
|
|
#endif // Q_OS_WIN
|
|
delete m_window;
|
|
UIThemeManager::freeInstance();
|
|
}
|
|
#endif // DISABLE_GUI
|
|
|
|
Profile::freeInstance();
|
|
|
|
if (m_shutdownAct != ShutdownDialogAction::Exit)
|
|
{
|
|
qDebug() << "Sending computer shutdown/suspend/hibernate signal...";
|
|
Utils::Misc::shutdownComputer(m_shutdownAct);
|
|
}
|
|
}
|