feat: Introduce PowerManager on macOS (#7543)

- Use modern APIs to prevent idle system sleep.
- Consolidate all related logic.
- Make Controller class a little bit less huge.

Signed-off-by: Dzmitry Neviadomski <nevack.d@gmail.com>
This commit is contained in:
Dzmitry Neviadomski
2025-11-02 23:41:22 +03:00
committed by GitHub
parent 90b52bc65c
commit 85a325ed71
6 changed files with 236 additions and 77 deletions

View File

@@ -46,7 +46,6 @@
4D36BA780CA2F00800A63CA5 /* peer-mgr.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D36BA690CA2F00800A63CA5 /* peer-mgr.h */; };
4D36BA790CA2F00800A63CA5 /* peer-msgs.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4D36BA6A0CA2F00800A63CA5 /* peer-msgs.cc */; };
4D36BA7A0CA2F00800A63CA5 /* peer-msgs.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D36BA6B0CA2F00800A63CA5 /* peer-msgs.h */; };
4D3EA0AA08AE13C600EA10C2 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3EA0A908AE13C600EA10C2 /* IOKit.framework */; };
4D4ADFC70DA1631500A68297 /* blocklist.cc in Sources */ = {isa = PBXBuildFile; fileRef = A2D3078E0D9EC45F0051FD27 /* blocklist.cc */; };
4D8017EA10BBC073008A4AF2 /* torrent-magnet.cc in Sources */ = {isa = PBXBuildFile; fileRef = 4D8017E810BBC073008A4AF2 /* torrent-magnet.cc */; };
4D8017EB10BBC073008A4AF2 /* torrent-magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D8017E910BBC073008A4AF2 /* torrent-magnet.h */; };
@@ -481,6 +480,7 @@
EDBAAC8C29E486BC00D9495F /* ip-cache.h in Headers */ = {isa = PBXBuildFile; fileRef = EDBAAC8B29E486BC00D9495F /* ip-cache.h */; };
EDBAAC8E29E486C200D9495F /* ip-cache.cc in Sources */ = {isa = PBXBuildFile; fileRef = EDBAAC8D29E486C200D9495F /* ip-cache.cc */; };
EDBDFA9E25AFCCA60093D9C1 /* evutil_time.c in Sources */ = {isa = PBXBuildFile; fileRef = EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */; };
EDC749F92D98AE3000A12D0F /* PowerManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDC749F82D98AE2900A12D0F /* PowerManager.mm */; };
F11545ACA7C4D7A464F703AB /* block-info.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A044CBD8C049AFCBD4DB411 /* block-info.h */; settings = {ATTRIBUTES = (Project, ); }; };
F63480631E1D7274005B9E09 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F63480621E1D7274005B9E09 /* Images.xcassets */; };
/* End PBXBuildFile section */
@@ -828,7 +828,6 @@
4D36BA690CA2F00800A63CA5 /* peer-mgr.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; path = "peer-mgr.h"; sourceTree = "<group>"; };
4D36BA6A0CA2F00800A63CA5 /* peer-msgs.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "peer-msgs.cc"; sourceTree = "<group>"; };
4D36BA6B0CA2F00800A63CA5 /* peer-msgs.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; path = "peer-msgs.h"; sourceTree = "<group>"; };
4D3EA0A908AE13C600EA10C2 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
4D8017E810BBC073008A4AF2 /* torrent-magnet.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "torrent-magnet.cc"; sourceTree = "<group>"; };
4D8017E910BBC073008A4AF2 /* torrent-magnet.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; path = "torrent-magnet.h"; sourceTree = "<group>"; };
4D80185710BBC0B0008A4AF2 /* magnet-metainfo.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "magnet-metainfo.cc"; sourceTree = "<group>"; };
@@ -1474,6 +1473,8 @@
EDBAAC8B29E486BC00D9495F /* ip-cache.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = "ip-cache.h"; sourceTree = "<group>"; };
EDBAAC8D29E486C200D9495F /* ip-cache.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "ip-cache.cc"; sourceTree = "<group>"; };
EDBDFA9D25AFCCA60093D9C1 /* evutil_time.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = evutil_time.c; sourceTree = "<group>"; };
EDC749F72D98ADE200A12D0F /* PowerManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerManager.h; sourceTree = "<group>"; };
EDC749F82D98AE2900A12D0F /* PowerManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PowerManager.mm; sourceTree = "<group>"; };
F63480621E1D7274005B9E09 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Images/Images.xcassets; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -1503,7 +1504,6 @@
files = (
C87369652809984200573C90 /* UserNotifications.framework in Frameworks */,
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */,
4D3EA0AA08AE13C600EA10C2 /* IOKit.framework in Frameworks */,
4D1838DD09DEC0E80047D688 /* libtransmission.a in Frameworks */,
A24F19080A3A790800C9C145 /* Sparkle.framework in Frameworks */,
C88771B52803EEB1005C7523 /* libiconv.tbd in Frameworks */,
@@ -1789,6 +1789,8 @@
A222EA7A0E6C32C4009FB003 /* BlocklistScheduler.mm */,
ED86936D2ADAE34D00342B1A /* DefaultAppHelper.h */,
ED86936E2ADAE34D00342B1A /* DefaultAppHelper.mm */,
EDC749F72D98ADE200A12D0F /* PowerManager.h */,
EDC749F82D98AE2900A12D0F /* PowerManager.mm */,
ED9862952B979AA2002F3035 /* Utils.h */,
ED9862962B979AA2002F3035 /* Utils.mm */,
A2AB883916A399A6008FAD50 /* VDKQueue */,
@@ -1898,7 +1900,6 @@
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
29B97324FDCFA39411CA2CEA /* AppKit.framework */,
29B97325FDCFA39411CA2CEA /* Foundation.framework */,
4D3EA0A908AE13C600EA10C2 /* IOKit.framework */,
A2E669780F5B8E5A00B4251A /* Security.framework */,
A22CFB810FB66EF30009BD3E /* Carbon.framework */,
A221DCC7104B3660008A642D /* Quartz.framework */,
@@ -3556,6 +3557,7 @@
A2ED7D8F0CEF431B00970975 /* FilterButton.mm in Sources */,
45A7D32C2843B55F00F0C32A /* PriorityPopUpButtonCell.mm in Sources */,
A25892640CF1F7E800CCCDDF /* StatsWindowController.mm in Sources */,
EDC749F92D98AE3000A12D0F /* PowerManager.mm in Sources */,
A2C89D600CFCBF57004CC2BC /* ButtonToolbarItem.mm in Sources */,
A219798B0D07B78400438EA7 /* GroupToolbarItem.mm in Sources */,
4521532B29AF891F009331B0 /* GroupCell.mm in Sources */,

View File

@@ -143,6 +143,8 @@ target_sources(${TR_NAME}-mac
PiecesView.mm
PortChecker.h
PortChecker.mm
PowerManager.h
PowerManager.mm
PredicateEditorRowTemplateAny.h
PredicateEditorRowTemplateAny.mm
PrefsController.h
@@ -412,7 +414,6 @@ target_link_libraries(${TR_NAME}-mac
"-framework AppKit"
"-framework Carbon"
"-framework Foundation"
"-framework IOKit"
"-framework Quartz"
"-framework Security"
"-weak_framework UserNotifications")

View File

@@ -143,8 +143,6 @@ typedef NS_ENUM(NSUInteger, AddType) { //
- (void)beginCreateFile:(NSNotification*)notification;
- (void)sleepCallback:(natural_t)messageType argument:(void*)messageArgument;
@property(nonatomic, readonly) VDKQueue* fileWatcherQueue;
- (void)torrentTableViewSelectionDidChange:(NSNotification*)notification;

View File

@@ -2,8 +2,6 @@
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
@import IOKit;
@import IOKit.pwr_mgt;
@import Carbon;
@import UserNotifications;
@@ -56,6 +54,7 @@
#import "ExpandedPathToPathTransformer.h"
#import "ExpandedPathToIconTransformer.h"
#import "VersionComparator.h"
#import "PowerManager.h"
typedef NSString* ToolbarItemIdentifier NS_TYPED_EXTENSIBLE_ENUM;
@@ -176,11 +175,6 @@ static tr_rpc_callback_status rpcCallback([[maybe_unused]] tr_session* handle, t
return TR_RPC_NOREMOVE; //we'll do the remove manually
}
static void sleepCallback(void* controller, io_service_t /*y*/, natural_t messageType, void* messageArgument)
{
[(__bridge Controller*)controller sleepCallback:messageType argument:messageArgument];
}
// 2.90 was infected with ransomware which we now check for and attempt to remove
static void removeKeRangerRansomware()
{
@@ -271,7 +265,7 @@ static void removeKeRangerRansomware()
NSLog(@"OSX.KeRanger.A ransomware removal completed, proceeding to normal operation");
}
@interface Controller ()<UNUserNotificationCenterDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate>
@interface Controller ()<UNUserNotificationCenterDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, PowerManagerDelegate>
@property(nonatomic) IBOutlet NSWindow* fWindow;
@property(nonatomic) IBOutlet NSStackView* fStackView;
@@ -311,7 +305,6 @@ static void removeKeRangerRansomware()
@property(nonatomic) DragOverlayWindow* fOverlayWindow;
@property(nonatomic) io_connect_t fRootPort;
@property(nonatomic) NSTimer* fTimer;
@property(nonatomic) StatusBarController* fStatusBar;
@@ -338,7 +331,6 @@ static void removeKeRangerRansomware()
@property(nonatomic) BOOL fGlobalPopoverShown;
@property(nonatomic) NSView* fPositioningView;
@property(nonatomic) BOOL fSoundPlaying;
@property(nonatomic) id fNoNapActivity;
@end
@@ -702,18 +694,6 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
//this must be called after showStatusBar:
[self.fStatusBar updateWithDownload:0.0 upload:0.0];
//register for sleep notifications
IONotificationPortRef notify;
io_object_t iterator;
if ((self.fRootPort = IORegisterForSystemPower((__bridge void*)(self), &notify, sleepCallback, &iterator)))
{
CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notify), kCFRunLoopCommonModes);
}
else
{
NSLog(@"Could not IORegisterForSystemPower");
}
auto* const session = self.fLib;
//load previous transfers
@@ -889,8 +869,8 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
NSApp.servicesProvider = self;
self.fNoNapActivity = [NSProcessInfo.processInfo beginActivityWithOptions:NSActivityUserInitiatedAllowingIdleSystemSleep
reason:@"No napping on the job!"];
[PowerManager.shared setDelegate:self];
[PowerManager.shared start];
//register for dock icon drags (has to be in applicationDidFinishLaunching: to work)
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleOpenContentsEvent:replyEvent:)
@@ -1038,7 +1018,7 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
{
self.fQuitting = YES;
[NSProcessInfo.processInfo endActivity:self.fNoNapActivity];
[PowerManager.shared stop];
//stop the Bonjour service
if (BonjourController.defaultControllerExists)
@@ -2378,6 +2358,8 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
{
CGFloat dlRate = 0.0, ulRate = 0.0;
BOOL anyCompleted = NO;
BOOL anyActive = NO;
for (Torrent* torrent in self.fTorrents)
{
[torrent update];
@@ -2387,8 +2369,11 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
ulRate += torrent.uploadRate;
anyCompleted |= torrent.finishedSeeding;
anyActive |= torrent.active && !torrent.stalled && !torrent.error;
}
PowerManager.shared.shouldPreventSleep = anyActive && [self.fDefaults boolForKey:@"SleepPrevent"];
if (!NSApp.hidden)
{
if (self.fWindow.visible)
@@ -5007,11 +4992,7 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
return YES;
}
- (void)sleepCallback:(natural_t)messageType argument:(void*)messageArgument
{
switch (messageType)
{
case kIOMessageSystemWillSleep:
- (void)systemWillSleep
{
//stop all transfers (since some are active) before going to sleep and remember to resume when we wake up
BOOL anyActive = NO;
@@ -5029,36 +5010,15 @@ void onTorrentCompletenessChanged(tr_torrent* tor, tr_completeness status, bool
{
sleep(15);
}
IOAllowPowerChange(self.fRootPort, (long)messageArgument);
break;
}
case kIOMessageCanSystemSleep:
if ([self.fDefaults boolForKey:@"SleepPrevent"])
- (void)systemDidWakeUp
{
//prevent idle sleep unless no torrents are active
for (Torrent* torrent in self.fTorrents)
{
if (torrent.active && !torrent.stalled && !torrent.error)
{
IOCancelPowerChange(self.fRootPort, (long)messageArgument);
return;
}
}
}
IOAllowPowerChange(self.fRootPort, (long)messageArgument);
break;
case kIOMessageSystemHasPoweredOn:
//resume sleeping transfers after we wake up
for (Torrent* torrent in self.fTorrents)
{
[torrent wakeUp];
}
break;
}
}
- (NSMenu*)applicationDockMenu:(NSApplication*)sender

26
macosx/PowerManager.h Normal file
View File

@@ -0,0 +1,26 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import <AppKit/AppKit.h>
@protocol PowerManagerDelegate<NSObject>
- (void)systemWillSleep;
- (void)systemDidWakeUp;
@end
@interface PowerManager : NSObject
@property(nonatomic, class, readonly) PowerManager* shared;
@property(nonatomic, weak) id<PowerManagerDelegate> delegate;
@property(nonatomic) BOOL shouldPreventSleep;
- (instancetype)init NS_UNAVAILABLE;
- (void)start;
- (void)stop;
@end

172
macosx/PowerManager.mm Normal file
View File

@@ -0,0 +1,172 @@
// This file Copyright © Transmission authors and contributors.
// It may be used under the MIT (SPDX: MIT) license.
// License text can be found in the licenses/ folder.
#import "PowerManager.h"
#include <os/log.h>
@interface PowerManager ()
@property(nonatomic, readonly) os_log_t log;
@property(getter=isListening) BOOL listening;
@property(nonatomic) id<NSObject> noNapActivity;
@property(nonatomic) id<NSObject> noSleepActivity;
- (void)systemWillSleep:(NSNotification*)notification;
- (void)systemDidWakeUp:(NSNotification*)notification;
- (void)powerStateDidChange:(NSNotification*)notification NS_AVAILABLE_MAC(12_0);
@end
@implementation PowerManager
+ (instancetype)shared
{
static PowerManager* sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[PowerManager alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
if ((self = [super init]))
{
_log = os_log_create("org.transmission", "power");
_listening = NO;
}
return self;
}
- (void)dealloc
{
[self stop];
}
- (void)start
{
os_log_info(self.log, "Starting power manager");
if (!self.isListening)
{
os_log_debug(self.log, "Registering sleep/wake/low power mode notifications");
[NSWorkspace.sharedWorkspace.notificationCenter addObserver:self selector:@selector(systemWillSleep:)
name:NSWorkspaceWillSleepNotification
object:nil];
[NSWorkspace.sharedWorkspace.notificationCenter addObserver:self selector:@selector(systemDidWakeUp:)
name:NSWorkspaceDidWakeNotification
object:nil];
if (@available(macOS 12.0, *))
{
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(powerStateDidChange:)
name:NSProcessInfoPowerStateDidChangeNotification
object:nil];
}
self.listening = YES;
}
if (self.noNapActivity == nil)
{
os_log_debug(self.log, "Starting no-nap activity");
self.noNapActivity = [NSProcessInfo.processInfo beginActivityWithOptions:NSActivityUserInitiatedAllowingIdleSystemSleep
reason:@"Transmission: Application is active"];
}
}
- (void)stop
{
os_log_info(self.log, "Stopping power manager");
if (self.isListening)
{
os_log_debug(self.log, "Unregistering sleep/wake/low power mode notifications");
[NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self name:NSWorkspaceWillSleepNotification object:nil];
[NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self name:NSWorkspaceDidWakeNotification object:nil];
if (@available(macOS 12.0, *))
{
[NSNotificationCenter.defaultCenter removeObserver:self name:NSProcessInfoPowerStateDidChangeNotification object:nil];
}
self.listening = NO;
}
if (self.noNapActivity != nil)
{
os_log_debug(self.log, "Ending no-nap activity");
[NSProcessInfo.processInfo endActivity:self.noNapActivity];
self.noNapActivity = nil;
}
if (self.noSleepActivity != nil)
{
os_log_debug(self.log, "Ending no-sleep activity");
[NSProcessInfo.processInfo endActivity:self.noSleepActivity];
self.noSleepActivity = nil;
}
}
- (void)systemWillSleep:(NSNotification*)notification
{
os_log_info(self.log, "System will sleep notification received");
[self.delegate systemWillSleep];
}
- (void)systemDidWakeUp:(NSNotification*)notification
{
os_log_info(self.log, "System did wake up notification received");
[self.delegate systemDidWakeUp];
}
- (void)powerStateDidChange:(NSNotification*)notification
{
os_log_info(self.log, "Power state did change notification received");
if (NSProcessInfo.processInfo.lowPowerModeEnabled)
{
os_log_info(self.log, "Low power mode enabled, disabling sleep prevention");
self.shouldPreventSleep = NO;
}
}
- (void)setShouldPreventSleep:(BOOL)shouldPreventSleep
{
if (@available(macOS 12.0, *))
{
if (shouldPreventSleep && NSProcessInfo.processInfo.lowPowerModeEnabled)
{
return;
}
}
if (shouldPreventSleep)
{
if (self.noSleepActivity != nil)
{
return;
}
os_log_info(self.log, "Starting no-sleep activity");
self.noSleepActivity = [NSProcessInfo.processInfo beginActivityWithOptions:NSActivityIdleSystemSleepDisabled
reason:@"Transmission: Active Torrents"];
}
else
{
if (self.noSleepActivity == nil)
{
return;
}
os_log_info(self.log, "Ending no-sleep activity");
[NSProcessInfo.processInfo endActivity:self.noSleepActivity];
self.noSleepActivity = nil;
}
}
- (BOOL)shouldPreventSleep
{
return self.noSleepActivity != nil;
}
@end