2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2023 Mike Tzou (Chocobo1)
4 * Copyright (C) 2014 sledgehammer999 <hammered999@gmail.com>
5 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * In addition, as a special exception, the copyright holders give permission to
22 * link this program with the OpenSSL project's "OpenSSL" library (or with
23 * modified versions of it that use the same license as the "OpenSSL" library),
24 * and distribute the linked executables. You must obey the GNU General Public
25 * License in all respects for all of the code used other than "OpenSSL". If you
26 * modify file(s), you may extend this exception to your version of the file(s),
27 * but you are not obligated to do so. If you do not wish to do so, delete this
28 * exception statement from your version.
42 #include <Carbon/Carbon.h>
43 #include <CoreFoundation/CoreFoundation.h>
44 #include <CoreServices/CoreServices.h>
47 #include <QScopeGuard>
50 #include <QDBusInterface>
51 #endif // QBT_USES_DBUS
54 #include <QCoreApplication>
57 #include "base/global.h"
58 #include "base/types.h"
60 void Utils::OS::shutdownComputer([[maybe_unused
]] const ShutdownDialogAction
&action
)
63 HANDLE hToken
; // handle to process token
64 TOKEN_PRIVILEGES tkp
; // pointer to token structure
65 if (!::OpenProcessToken(::GetCurrentProcess(), (TOKEN_ADJUST_PRIVILEGES
| TOKEN_QUERY
), &hToken
))
67 // Get the LUID for shutdown privilege.
68 ::LookupPrivilegeValue(NULL
, SE_SHUTDOWN_NAME
, &tkp
.Privileges
[0].Luid
);
70 tkp
.PrivilegeCount
= 1; // one privilege to set
71 tkp
.Privileges
[0].Attributes
= SE_PRIVILEGE_ENABLED
;
73 // Get shutdown privilege for this process.
75 ::AdjustTokenPrivileges(hToken
, FALSE
, &tkp
, 0, NULL
, 0);
77 // Cannot test the return value of AdjustTokenPrivileges.
79 if (::GetLastError() != ERROR_SUCCESS
)
82 if (action
== ShutdownDialogAction::Suspend
)
84 ::SetSuspendState(FALSE
, FALSE
, FALSE
);
86 else if (action
== ShutdownDialogAction::Hibernate
)
88 ::SetSuspendState(TRUE
, FALSE
, FALSE
);
92 std::wstring msg
= QCoreApplication::translate("misc"
93 , "qBittorrent will shutdown the computer now because all downloads are complete.").toStdWString();
94 ::InitiateSystemShutdownW(nullptr, msg
.data(), 10, TRUE
, FALSE
);
97 // Disable shutdown privilege.
98 tkp
.Privileges
[0].Attributes
= 0;
99 ::AdjustTokenPrivileges(hToken
, FALSE
, &tkp
, 0, NULL
, 0);
101 #elif defined(Q_OS_MACOS)
102 AEEventID EventToSend
;
103 if (action
!= ShutdownDialogAction::Shutdown
)
104 EventToSend
= kAESleep
;
106 EventToSend
= kAEShutDown
;
107 AEAddressDesc targetDesc
;
108 const ProcessSerialNumber kPSNOfSystemProcess
= {0, kSystemProcess
};
109 AppleEvent eventReply
= {typeNull
, NULL
};
110 AppleEvent appleEventToSend
= {typeNull
, NULL
};
112 OSStatus error
= ::AECreateDesc(typeProcessSerialNumber
, &kPSNOfSystemProcess
113 , sizeof(kPSNOfSystemProcess
), &targetDesc
);
118 error
= ::AECreateAppleEvent(kCoreEventClass
, EventToSend
, &targetDesc
, kAutoGenerateReturnID
119 , kAnyTransactionID
, &appleEventToSend
);
121 AEDisposeDesc(&targetDesc
);
125 error
= ::AESend(&appleEventToSend
, &eventReply
, kAENoReply
, kAENormalPriority
, kAEDefaultTimeout
128 ::AEDisposeDesc(&appleEventToSend
);
132 ::AEDisposeDesc(&eventReply
);
134 #elif defined(QBT_USES_DBUS)
135 // Use dbus to power off / suspend the system
136 if (action
!= ShutdownDialogAction::Shutdown
)
138 // Some recent systems use systemd's logind
139 QDBusInterface
login1Iface(u
"org.freedesktop.login1"_s
, u
"/org/freedesktop/login1"_s
,
140 u
"org.freedesktop.login1.Manager"_s
, QDBusConnection::systemBus());
141 if (login1Iface
.isValid())
143 if (action
== ShutdownDialogAction::Suspend
)
144 login1Iface
.call(u
"Suspend"_s
, false);
146 login1Iface
.call(u
"Hibernate"_s
, false);
149 // Else, other recent systems use UPower
150 QDBusInterface
upowerIface(u
"org.freedesktop.UPower"_s
, u
"/org/freedesktop/UPower"_s
,
151 u
"org.freedesktop.UPower"_s
, QDBusConnection::systemBus());
152 if (upowerIface
.isValid())
154 if (action
== ShutdownDialogAction::Suspend
)
155 upowerIface
.call(u
"Suspend"_s
);
157 upowerIface
.call(u
"Hibernate"_s
);
160 // HAL (older systems)
161 QDBusInterface
halIface(u
"org.freedesktop.Hal"_s
, u
"/org/freedesktop/Hal/devices/computer"_s
,
162 u
"org.freedesktop.Hal.Device.SystemPowerManagement"_s
,
163 QDBusConnection::systemBus());
164 if (action
== ShutdownDialogAction::Suspend
)
165 halIface
.call(u
"Suspend"_s
, 5);
167 halIface
.call(u
"Hibernate"_s
);
171 // Some recent systems use systemd's logind
172 QDBusInterface
login1Iface(u
"org.freedesktop.login1"_s
, u
"/org/freedesktop/login1"_s
,
173 u
"org.freedesktop.login1.Manager"_s
, QDBusConnection::systemBus());
174 if (login1Iface
.isValid())
176 login1Iface
.call(u
"PowerOff"_s
, false);
179 // Else, other recent systems use ConsoleKit
180 QDBusInterface
consolekitIface(u
"org.freedesktop.ConsoleKit"_s
, u
"/org/freedesktop/ConsoleKit/Manager"_s
,
181 u
"org.freedesktop.ConsoleKit.Manager"_s
, QDBusConnection::systemBus());
182 if (consolekitIface
.isValid())
184 consolekitIface
.call(u
"Stop"_s
);
187 // HAL (older systems)
188 QDBusInterface
halIface(u
"org.freedesktop.Hal"_s
, u
"/org/freedesktop/Hal/devices/computer"_s
,
189 u
"org.freedesktop.Hal.Device.SystemPowerManagement"_s
,
190 QDBusConnection::systemBus());
191 halIface
.call(u
"Shutdown"_s
);
199 const CFStringRef torrentExtension
= CFSTR("torrent");
200 const CFStringRef magnetUrlScheme
= CFSTR("magnet");
203 bool Utils::OS::isTorrentFileAssocSet()
206 const CFStringRef torrentId
= ::UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension
, torrentExtension
, NULL
);
207 if (torrentId
!= NULL
)
209 const CFStringRef defaultHandlerId
= ::LSCopyDefaultRoleHandlerForContentType(torrentId
, kLSRolesViewer
);
210 if (defaultHandlerId
!= NULL
)
212 const CFStringRef myBundleId
= ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
213 if (myBundleId
!= NULL
)
214 isSet
= ::CFStringCompare(myBundleId
, defaultHandlerId
, 0) == kCFCompareEqualTo
;
215 ::CFRelease(defaultHandlerId
);
217 ::CFRelease(torrentId
);
222 void Utils::OS::setTorrentFileAssoc()
224 if (isTorrentFileAssocSet())
227 const CFStringRef torrentId
= ::UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension
, torrentExtension
, NULL
);
228 if (torrentId
!= NULL
)
230 const CFStringRef myBundleId
= ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
231 if (myBundleId
!= NULL
)
232 ::LSSetDefaultRoleHandlerForContentType(torrentId
, kLSRolesViewer
, myBundleId
);
233 ::CFRelease(torrentId
);
237 bool Utils::OS::isMagnetLinkAssocSet()
240 const CFStringRef defaultHandlerId
= ::LSCopyDefaultHandlerForURLScheme(magnetUrlScheme
);
241 if (defaultHandlerId
!= NULL
)
243 const CFStringRef myBundleId
= ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
244 if (myBundleId
!= NULL
)
245 isSet
= ::CFStringCompare(myBundleId
, defaultHandlerId
, 0) == kCFCompareEqualTo
;
246 ::CFRelease(defaultHandlerId
);
251 void Utils::OS::setMagnetLinkAssoc()
253 if (isMagnetLinkAssocSet())
256 const CFStringRef myBundleId
= ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
257 if (myBundleId
!= NULL
)
258 ::LSSetDefaultHandlerForURLScheme(magnetUrlScheme
, myBundleId
);
263 Path
Utils::OS::windowsSystemPath()
265 static const Path path
= []() -> Path
267 WCHAR systemPath
[MAX_PATH
] = {0};
268 ::GetSystemDirectoryW(systemPath
, sizeof(systemPath
) / sizeof(WCHAR
));
269 return Path(QString::fromWCharArray(systemPath
));
275 #if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
276 bool Utils::OS::applyMarkOfTheWeb(const Path
&file
, const QString
&url
)
278 // Trying to apply this to a non-existent file is unacceptable,
279 // as it may unexpectedly create such a file.
283 Q_ASSERT(url
.isEmpty() || url
.startsWith(u
"http:") || url
.startsWith(u
"https:"));
287 // https://searchfox.org/mozilla-central/rev/ffdc4971dc18e1141cb2a90c2b0b776365650270/xpcom/io/CocoaFileUtils.mm#230
288 // https://github.com/transmission/transmission/blob/f62f7427edb1fd5c430e0ef6956bbaa4f03ae597/macosx/Torrent.mm#L1945-L1955
290 const CFStringRef fileString
= file
.toString().toCFString();
291 [[maybe_unused
]] const auto fileStringGuard
= qScopeGuard([&fileString
] { ::CFRelease(fileString
); });
292 const CFURLRef fileURL
= ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault
293 , fileString
, kCFURLPOSIXPathStyle
, false);
294 [[maybe_unused
]] const auto fileURLGuard
= qScopeGuard([&fileURL
] { ::CFRelease(fileURL
); });
296 if (CFDictionaryRef currentProperties
= nullptr;
297 ::CFURLCopyResourcePropertyForKey(fileURL
, kCFURLQuarantinePropertiesKey
, ¤tProperties
, NULL
)
298 && currentProperties
)
300 [[maybe_unused
]] const auto currentPropertiesGuard
= qScopeGuard([¤tProperties
] { ::CFRelease(currentProperties
); });
302 if (CFStringRef quarantineType
= nullptr;
303 ::CFDictionaryGetValueIfPresent(currentProperties
, kLSQuarantineTypeKey
, reinterpret_cast<const void **>(&quarantineType
))
306 if (::CFStringCompare(quarantineType
, kLSQuarantineTypeOtherDownload
, 0) == kCFCompareEqualTo
)
311 CFMutableDictionaryRef properties
= ::CFDictionaryCreateMutable(kCFAllocatorDefault
, 0
312 , &kCFTypeDictionaryKeyCallBacks
, &kCFTypeDictionaryValueCallBacks
);
315 [[maybe_unused
]] const auto propertiesGuard
= qScopeGuard([&properties
] { ::CFRelease(properties
); });
317 ::CFDictionarySetValue(properties
, kLSQuarantineTypeKey
, kLSQuarantineTypeOtherDownload
);
319 ::CFDictionarySetValue(properties
, kLSQuarantineDataURLKey
, url
.toCFString());
321 const Boolean success
= ::CFURLSetResourcePropertyForKey(fileURL
, kCFURLQuarantinePropertiesKey
324 #elif defined(Q_OS_WIN)
325 const QString zoneIDStream
= file
.toString() + u
":Zone.Identifier";
327 HANDLE handle
= ::CreateFileW(zoneIDStream
.toStdWString().c_str(), (GENERIC_READ
| GENERIC_WRITE
)
328 , (FILE_SHARE_DELETE
| FILE_SHARE_READ
| FILE_SHARE_WRITE
)
329 , nullptr, OPEN_ALWAYS
, FILE_ATTRIBUTE_NORMAL
, nullptr);
330 if (handle
== INVALID_HANDLE_VALUE
)
332 [[maybe_unused
]] const auto handleGuard
= qScopeGuard([&handle
] { ::CloseHandle(handle
); });
334 // 5.6.1 Zone.Identifier Stream Name
335 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
336 const QString hostURL
= !url
.isEmpty() ? url
: u
"about:internet"_s
;
337 const QByteArray zoneID
= QByteArrayLiteral("[ZoneTransfer]\r\nZoneId=3\r\n")
338 + u
"HostUrl=%1\r\n"_s
.arg(hostURL
).toUtf8();
340 if (LARGE_INTEGER streamSize
= {0};
341 ::GetFileSizeEx(handle
, &streamSize
) && (streamSize
.QuadPart
> 0))
343 const DWORD expectedReadSize
= std::min
<LONGLONG
>(streamSize
.QuadPart
, 1024);
344 QByteArray buf
{expectedReadSize
, '\0'};
346 if (DWORD actualReadSize
= 0;
347 ::ReadFile(handle
, buf
.data(), expectedReadSize
, &actualReadSize
, nullptr) && (actualReadSize
== expectedReadSize
))
349 if (buf
.startsWith("[ZoneTransfer]\r\n") && buf
.contains("\r\nZoneId=3\r\n") && buf
.contains("\r\nHostUrl="))
354 if (!::SetFilePointerEx(handle
, {0}, nullptr, FILE_BEGIN
))
356 if (!::SetEndOfFile(handle
))
360 const BOOL writeResult
= ::WriteFile(handle
, zoneID
.constData(), zoneID
.size(), &written
, nullptr);
361 return writeResult
&& (written
== zoneID
.size());
364 #endif // Q_OS_MACOS || Q_OS_WIN