mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2026-01-10 01:22:31 -06:00
This commit implements a new "Reboot System" option that allows users to automatically reboot the computer when all downloads are complete, similar to the existing shutdown, suspend, and hibernate options. Closes #10774. PR #23525.
335 lines
14 KiB
C++
335 lines
14 KiB
C++
/*
|
|
* Bittorrent Client using Qt and libtorrent.
|
|
* Copyright (C) 2023 Mike Tzou (Chocobo1)
|
|
* Copyright (C) 2014 sledgehammer999 <hammered999@gmail.com>
|
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
|
*
|
|
* 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 "os.h"
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <algorithm>
|
|
|
|
#include <windows.h>
|
|
#include <powrprof.h>
|
|
#include <shlobj.h>
|
|
#endif // Q_OS_WIN
|
|
|
|
#ifdef Q_OS_MACOS
|
|
#include <Carbon/Carbon.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <CoreServices/CoreServices.h>
|
|
#endif // Q_OS_MACOS
|
|
|
|
#include <QScopeGuard>
|
|
|
|
#ifdef QBT_USES_DBUS
|
|
#include <QDBusInterface>
|
|
#endif // QBT_USES_DBUS
|
|
|
|
#ifdef Q_OS_WIN
|
|
#include <QCoreApplication>
|
|
#endif // Q_OS_WIN
|
|
|
|
#include "base/global.h"
|
|
#include "base/types.h"
|
|
|
|
void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &action)
|
|
{
|
|
#if defined(Q_OS_WIN)
|
|
HANDLE hToken; // handle to process token
|
|
TOKEN_PRIVILEGES tkp; // pointer to token structure
|
|
if (!::OpenProcessToken(::GetCurrentProcess(), (TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY), &hToken))
|
|
return;
|
|
// Get the LUID for shutdown privilege.
|
|
::LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0].Luid);
|
|
|
|
tkp.PrivilegeCount = 1; // one privilege to set
|
|
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
|
|
// Get shutdown privilege for this process.
|
|
|
|
::AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, NULL, 0);
|
|
|
|
// Cannot test the return value of AdjustTokenPrivileges.
|
|
|
|
if (::GetLastError() != ERROR_SUCCESS)
|
|
return;
|
|
|
|
if (action == ShutdownDialogAction::Suspend)
|
|
{
|
|
::SetSuspendState(FALSE, FALSE, FALSE);
|
|
}
|
|
else if (action == ShutdownDialogAction::Hibernate)
|
|
{
|
|
::SetSuspendState(TRUE, FALSE, FALSE);
|
|
}
|
|
else if (action == ShutdownDialogAction::Shutdown)
|
|
{
|
|
std::wstring msg = QCoreApplication::translate("misc"
|
|
, "qBittorrent will shutdown the computer now because all downloads are complete.").toStdWString();
|
|
::InitiateSystemShutdownW(nullptr, msg.data(), 10, TRUE, FALSE);
|
|
}
|
|
else if (action == ShutdownDialogAction::Reboot)
|
|
{
|
|
std::wstring msg = QCoreApplication::translate("misc"
|
|
, "qBittorrent will reboot the computer now because all downloads are complete.").toStdWString();
|
|
::InitiateSystemShutdownW(nullptr, msg.data(), 10, TRUE, TRUE);
|
|
}
|
|
|
|
// Disable shutdown privilege.
|
|
tkp.Privileges[0].Attributes = 0;
|
|
::AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, NULL, 0);
|
|
|
|
#elif defined(Q_OS_MACOS)
|
|
AEEventID EventToSend {};
|
|
if (action == ShutdownDialogAction::Suspend)
|
|
EventToSend = kAESleep;
|
|
else if (action == ShutdownDialogAction::Reboot)
|
|
EventToSend = kAERestart;
|
|
else if (action == ShutdownDialogAction::Shutdown)
|
|
EventToSend = kAEShutDown;
|
|
AEAddressDesc targetDesc;
|
|
const ProcessSerialNumber kPSNOfSystemProcess = {0, kSystemProcess};
|
|
AppleEvent eventReply = {typeNull, NULL};
|
|
AppleEvent appleEventToSend = {typeNull, NULL};
|
|
|
|
OSStatus error = ::AECreateDesc(typeProcessSerialNumber, &kPSNOfSystemProcess
|
|
, sizeof(kPSNOfSystemProcess), &targetDesc);
|
|
|
|
if (error != noErr)
|
|
return;
|
|
|
|
error = ::AECreateAppleEvent(kCoreEventClass, EventToSend, &targetDesc, kAutoGenerateReturnID
|
|
, kAnyTransactionID, &appleEventToSend);
|
|
|
|
AEDisposeDesc(&targetDesc);
|
|
if (error != noErr)
|
|
return;
|
|
|
|
error = ::AESend(&appleEventToSend, &eventReply, kAENoReply, kAENormalPriority, kAEDefaultTimeout
|
|
, NULL, NULL);
|
|
|
|
::AEDisposeDesc(&appleEventToSend);
|
|
if (error != noErr)
|
|
return;
|
|
|
|
::AEDisposeDesc(&eventReply);
|
|
|
|
#elif defined(QBT_USES_DBUS)
|
|
// Use dbus to power off / suspend the system
|
|
if ((action == ShutdownDialogAction::Suspend) || (action == ShutdownDialogAction::Hibernate))
|
|
{
|
|
// Some recent systems use systemd's logind
|
|
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
|
|
u"org.freedesktop.login1.Manager"_s, QDBusConnection::systemBus());
|
|
if (login1Iface.isValid())
|
|
{
|
|
if (action == ShutdownDialogAction::Suspend)
|
|
login1Iface.call(u"Suspend"_s, false);
|
|
else
|
|
login1Iface.call(u"Hibernate"_s, false);
|
|
return;
|
|
}
|
|
// Else, other recent systems use UPower
|
|
QDBusInterface upowerIface(u"org.freedesktop.UPower"_s, u"/org/freedesktop/UPower"_s,
|
|
u"org.freedesktop.UPower"_s, QDBusConnection::systemBus());
|
|
if (upowerIface.isValid())
|
|
{
|
|
if (action == ShutdownDialogAction::Suspend)
|
|
upowerIface.call(u"Suspend"_s);
|
|
else
|
|
upowerIface.call(u"Hibernate"_s);
|
|
return;
|
|
}
|
|
// HAL (older systems)
|
|
QDBusInterface halIface(u"org.freedesktop.Hal"_s, u"/org/freedesktop/Hal/devices/computer"_s,
|
|
u"org.freedesktop.Hal.Device.SystemPowerManagement"_s,
|
|
QDBusConnection::systemBus());
|
|
if (action == ShutdownDialogAction::Suspend)
|
|
halIface.call(u"Suspend"_s, 5);
|
|
else
|
|
halIface.call(u"Hibernate"_s);
|
|
}
|
|
else if (action == ShutdownDialogAction::Shutdown)
|
|
{
|
|
// Some recent systems use systemd's logind
|
|
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
|
|
u"org.freedesktop.login1.Manager"_s, QDBusConnection::systemBus());
|
|
if (login1Iface.isValid())
|
|
{
|
|
login1Iface.call(u"PowerOff"_s, false);
|
|
return;
|
|
}
|
|
// Else, other recent systems use ConsoleKit
|
|
QDBusInterface consolekitIface(u"org.freedesktop.ConsoleKit"_s, u"/org/freedesktop/ConsoleKit/Manager"_s,
|
|
u"org.freedesktop.ConsoleKit.Manager"_s, QDBusConnection::systemBus());
|
|
if (consolekitIface.isValid())
|
|
{
|
|
consolekitIface.call(u"Stop"_s);
|
|
return;
|
|
}
|
|
// HAL (older systems)
|
|
QDBusInterface halIface(u"org.freedesktop.Hal"_s, u"/org/freedesktop/Hal/devices/computer"_s,
|
|
u"org.freedesktop.Hal.Device.SystemPowerManagement"_s,
|
|
QDBusConnection::systemBus());
|
|
halIface.call(u"Shutdown"_s);
|
|
}
|
|
else if (action == ShutdownDialogAction::Reboot)
|
|
{
|
|
// Some recent systems use systemd's logind
|
|
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
|
|
u"org.freedesktop.login1.Manager"_s, QDBusConnection::systemBus());
|
|
if (login1Iface.isValid())
|
|
{
|
|
login1Iface.call(u"Reboot"_s, false);
|
|
return;
|
|
}
|
|
// Else, other recent systems use ConsoleKit
|
|
QDBusInterface consolekitIface(u"org.freedesktop.ConsoleKit"_s, u"/org/freedesktop/ConsoleKit/Manager"_s,
|
|
u"org.freedesktop.ConsoleKit.Manager"_s, QDBusConnection::systemBus());
|
|
if (consolekitIface.isValid())
|
|
{
|
|
consolekitIface.call(u"Restart"_s);
|
|
return;
|
|
}
|
|
// HAL (older systems)
|
|
QDBusInterface halIface(u"org.freedesktop.Hal"_s, u"/org/freedesktop/Hal/devices/computer"_s,
|
|
u"org.freedesktop.Hal.Device.SystemPowerManagement"_s,
|
|
QDBusConnection::systemBus());
|
|
halIface.call(u"Reboot"_s);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
Path Utils::OS::windowsSystemPath()
|
|
{
|
|
static const Path path = []() -> Path
|
|
{
|
|
WCHAR systemPath[MAX_PATH] = {0};
|
|
::GetSystemDirectoryW(systemPath, sizeof(systemPath) / sizeof(WCHAR));
|
|
return Path(QString::fromWCharArray(systemPath));
|
|
}();
|
|
return path;
|
|
}
|
|
#endif // Q_OS_WIN
|
|
|
|
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
|
bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
|
|
{
|
|
// Trying to apply this to a non-existent file is unacceptable,
|
|
// as it may unexpectedly create such a file.
|
|
if (!file.exists())
|
|
return false;
|
|
|
|
Q_ASSERT(url.isEmpty() || url.startsWith(u"http:") || url.startsWith(u"https:"));
|
|
|
|
#ifdef Q_OS_MACOS
|
|
// References:
|
|
// https://searchfox.org/mozilla-central/rev/ffdc4971dc18e1141cb2a90c2b0b776365650270/xpcom/io/CocoaFileUtils.mm#230
|
|
// https://github.com/transmission/transmission/blob/f62f7427edb1fd5c430e0ef6956bbaa4f03ae597/macosx/Torrent.mm#L1945-L1955
|
|
|
|
const CFStringRef fileString = file.toString().toCFString();
|
|
[[maybe_unused]] const auto fileStringGuard = qScopeGuard([&fileString] { ::CFRelease(fileString); });
|
|
const CFURLRef fileURL = ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault
|
|
, fileString, kCFURLPOSIXPathStyle, false);
|
|
[[maybe_unused]] const auto fileURLGuard = qScopeGuard([&fileURL] { ::CFRelease(fileURL); });
|
|
|
|
if (CFDictionaryRef currentProperties = nullptr;
|
|
::CFURLCopyResourcePropertyForKey(fileURL, kCFURLQuarantinePropertiesKey, ¤tProperties, NULL)
|
|
&& currentProperties)
|
|
{
|
|
[[maybe_unused]] const auto currentPropertiesGuard = qScopeGuard([¤tProperties] { ::CFRelease(currentProperties); });
|
|
|
|
if (CFStringRef quarantineType = nullptr;
|
|
::CFDictionaryGetValueIfPresent(currentProperties, kLSQuarantineTypeKey, reinterpret_cast<const void **>(&quarantineType))
|
|
&& quarantineType)
|
|
{
|
|
if (::CFStringCompare(quarantineType, kLSQuarantineTypeOtherDownload, 0) == kCFCompareEqualTo)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
CFMutableDictionaryRef properties = ::CFDictionaryCreateMutable(kCFAllocatorDefault, 0
|
|
, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
|
if (!properties)
|
|
return false;
|
|
[[maybe_unused]] const auto propertiesGuard = qScopeGuard([&properties] { ::CFRelease(properties); });
|
|
|
|
::CFDictionarySetValue(properties, kLSQuarantineTypeKey, kLSQuarantineTypeOtherDownload);
|
|
if (!url.isEmpty())
|
|
{
|
|
const CFStringRef urlCFString = url.toCFString();
|
|
[[maybe_unused]] const auto urlStringGuard = qScopeGuard([&urlCFString] { ::CFRelease(urlCFString); });
|
|
::CFDictionarySetValue(properties, kLSQuarantineDataURLKey, urlCFString);
|
|
}
|
|
|
|
const Boolean success = ::CFURLSetResourcePropertyForKey(fileURL, kCFURLQuarantinePropertiesKey
|
|
, properties, NULL);
|
|
return success;
|
|
#elif defined(Q_OS_WIN)
|
|
const QString zoneIDStream = file.toString() + u":Zone.Identifier";
|
|
|
|
HANDLE handle = ::CreateFileW(zoneIDStream.toStdWString().c_str(), (GENERIC_READ | GENERIC_WRITE)
|
|
, (FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE)
|
|
, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (handle == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
[[maybe_unused]] const auto handleGuard = qScopeGuard([&handle] { ::CloseHandle(handle); });
|
|
|
|
// 5.6.1 Zone.Identifier Stream Name
|
|
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
|
|
const QString hostURL = !url.isEmpty() ? url : u"about:internet"_s;
|
|
const QByteArray zoneID = QByteArrayLiteral("[ZoneTransfer]\r\nZoneId=3\r\n")
|
|
+ u"HostUrl=%1\r\n"_s.arg(hostURL).toUtf8();
|
|
|
|
if (LARGE_INTEGER streamSize {};
|
|
::GetFileSizeEx(handle, &streamSize) && (streamSize.QuadPart > 0))
|
|
{
|
|
const DWORD expectedReadSize = std::min<LONGLONG>(streamSize.QuadPart, 1024);
|
|
QByteArray buf {static_cast<qsizetype>(expectedReadSize), '\0'};
|
|
|
|
if (DWORD actualReadSize = 0;
|
|
::ReadFile(handle, buf.data(), expectedReadSize, &actualReadSize, nullptr) && (actualReadSize == expectedReadSize))
|
|
{
|
|
if (buf.startsWith("[ZoneTransfer]\r\n") && buf.contains("\r\nZoneId=3\r\n") && buf.contains("\r\nHostUrl="))
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!::SetFilePointerEx(handle, {}, nullptr, FILE_BEGIN))
|
|
return false;
|
|
if (!::SetEndOfFile(handle))
|
|
return false;
|
|
|
|
DWORD written = 0;
|
|
const BOOL writeResult = ::WriteFile(handle, zoneID.constData(), zoneID.size(), &written, nullptr);
|
|
return writeResult && (static_cast<qsizetype>(written) == zoneID.size());
|
|
#endif
|
|
}
|
|
#endif // Q_OS_MACOS || Q_OS_WIN
|