/* * FanControl * * Copyright (c) 2006-2012 Hendrik Holtmann * Portions Copyright (c) 2013 Michael Wilber * * FanControl.m - MacBook(Pro) FanControl application * * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #import "FanControl.h" #import "MachineDefaults.h" #import #import #import #import #import "SystemVersion.h" @interface FanControl () + (void)copyMachinesIfNecessary; @property (NS_NONATOMIC_IOSONLY, getter=isInAutoStart, readonly) BOOL inAutoStart; - (void)setStartAtLogin:(BOOL)enabled; + (void)checkRightStatus:(OSStatus)status; @end @implementation FanControl // Number of fans reported by the hardware. int g_numFans = 0; NSUserDefaults *defaults; #pragma mark **Init-Methods** +(void) initialize { //avoid Zombies when starting external app signal(SIGCHLD, SIG_IGN); [FanControl copyMachinesIfNecessary]; //check owner and suid rights [FanControl setRights]; //talk to smc [smcWrapper init]; //app in foreground for update notifications [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } +(void)copyMachinesIfNecessary { NSString *path = [[[NSFileManager defaultManager] applicationSupportDirectory] stringByAppendingPathComponent:@"Machines.plist"]; if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { [[NSFileManager defaultManager] copyItemAtPath:[[NSBundle mainBundle] pathForResource:@"Machines" ofType:@"plist"] toPath:path error:nil]; } } -(void)upgradeFavorites { //upgrade favorites NSArray *rfavorites = [FavoritesController arrangedObjects]; int j; int i; for (i=0;i<[rfavorites count];i++) { BOOL selected = NO; NSArray *fans = rfavorites[i][@"FanData"]; for (j=0;j<[fans count];j++) { if ([fans[j][@"menu"] boolValue] == YES ) { selected = YES; } } if (selected==NO) { rfavorites[i][@"FanData"][0][@"menu"] = @YES; } } } -(void) awakeFromNib { pw=[[Power alloc] init]; [pw setDelegate:self]; [pw registerForSleepWakeNotification]; [pw registerForPowerChange]; //load defaults [DefaultsController setAppliesImmediately:NO]; mdefaults=[[MachineDefaults alloc] init:nil]; self.machineDefaultsDict=[[NSMutableDictionary alloc] initWithDictionary:[mdefaults get_machine_defaults]]; NSMutableArray *favorites = [[NSMutableArray alloc] init]; NSMutableDictionary *defaultFav = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"Default", @"Title", [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[[mdefaults get_machine_defaults] objectForKey:@"Fans"]]], @"FanData",nil]; [favorites addObject:defaultFav]; NSRange range=[[MachineDefaults computerModel] rangeOfString:@"MacBook"]; if (range.length>0) { //for macbooks add a second default NSMutableDictionary *higherFav=[[NSMutableDictionary alloc] initWithObjectsAndKeys:@"Higher RPM", @"Title", [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[[mdefaults get_machine_defaults] objectForKey:@"Fans"]]], @"FanData",nil]; for (NSUInteger i=0;i<[_machineDefaultsDict[@"Fans"] count];i++) { int min_value=([[[[_machineDefaultsDict objectForKey:@"Fans"] objectAtIndex:i] objectForKey:@"Minspeed"] intValue])*2; [[[higherFav objectForKey:@"FanData"] objectAtIndex:i] setObject:[NSNumber numberWithInt:min_value] forKey:@"selspeed"]; } [favorites addObject:higherFav]; } //sync option for Macbook Pro's NSRange range_mbp=[[MachineDefaults computerModel] rangeOfString:@"MacBookPro"]; if (range_mbp.length>0 && [_machineDefaultsDict[@"Fans"] count] == 2) { [sync setHidden:NO]; } //load user defaults defaults = [NSUserDefaults standardUserDefaults]; [defaults registerDefaults: [NSMutableDictionary dictionaryWithObjectsAndKeys: @0, @"Unit", @0, @"SelDefault", @NO, @"AutoStart", @NO,@"AutomaticChange", @0,@"selbatt", @0,@"selac", @0,@"selload", @0,@"MenuBar", @"TC0D",@"TSensor", @0,@"NumLaunches", @NO,@"DonationMessageShown", [NSArchiver archivedDataWithRootObject:[NSColor blackColor]],@"MenuColor", favorites,@"Favorites", nil]]; g_numFans = [smcWrapper get_fan_num]; s_menus=[[NSMutableArray alloc] init]; int i; for(i=0;i0) { [autochange setEnabled:true]; } else { [autochange setEnabled:false]; } [faqText replaceCharactersInRange:NSMakeRange(0,0) withRTF: [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"F.A.Q" ofType:@"rtf"]]]; [self apply_settings:nil controllerindex:[[defaults objectForKey:@"SelDefault"] intValue]]; [[[[theMenu itemWithTag:1] submenu] itemAtIndex:[[defaults objectForKey:@"SelDefault"] intValue]] setState:NSOnState]; [[sliderCell dataCell] setControlSize:NSSmallControlSize]; [self changeMenu:nil]; //seting toolbar image menu_image = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"smc" ofType:@"png"]]; menu_image_alt = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"smcover" ofType:@"png"]]; if ([menu_image respondsToSelector:@selector(setTemplate:)]) { [menu_image setTemplate:YES]; [menu_image_alt setTemplate:YES]; } //add timer for reading to RunLoop _readTimer = [NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(readFanData:) userInfo:nil repeats:YES]; if ([_readTimer respondsToSelector:@selector(setTolerance:)]) { [_readTimer setTolerance:2.0]; } [_readTimer fire]; //autoapply settings if valid [self upgradeFavorites]; //autostart [[NSUserDefaults standardUserDefaults] setValue:@([self isInAutoStart]) forKey:@"AutoStart"]; NSUInteger numLaunches = [[[NSUserDefaults standardUserDefaults] objectForKey:@"NumLaunches"] integerValue]; [[NSUserDefaults standardUserDefaults] setObject:@(numLaunches+1) forKey:@"NumLaunches"]; if (numLaunches != 0 && (numLaunches % 5 == 0) && ![[[NSUserDefaults standardUserDefaults] objectForKey:@"DonationMessageShown"] boolValue]) { [self displayDonationMessage]; } } -(void)displayDonationMessage { NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Consider a donation",nil) defaultButton:NSLocalizedString(@"Donate over Paypal",nil) alternateButton:NSLocalizedString(@"Never ask me again",nil) otherButton:NSLocalizedString(@"Remind me later",nil) informativeTextWithFormat:NSLocalizedString(@"smcFanControl keeps your Mac cool since 2006.\n\nIf smcFanControl is helfpul for you and you want to support further development, a small donation over Paypal is much appreciated.",nil)]; NSModalResponse code=[alert runModal]; if (code == NSAlertDefaultReturn) { [self paypal:nil]; [[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"DonationMessageShown"]; } else if (code == NSAlertAlternateReturn) { [[NSUserDefaults standardUserDefaults] setObject:@(YES) forKey:@"DonationMessageShown"]; } } -(void)init_statusitem{ statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength: NSVariableStatusItemLength]; [statusItem setMenu: theMenu]; if ([statusItem respondsToSelector:@selector(button)]) { [statusItem.button setTitle:@"smc..."]; } else { [statusItem setEnabled: YES]; [statusItem setHighlightMode:YES]; [statusItem setTitle:@"smc..."]; } int i; for(i=0;i<[s_menus count];i++) { [theMenu insertItem:s_menus[i] atIndex:i]; }; // Sign up for menuNeedsUpdate call // so that the fan speeds in the menu can be updated // only when needed. [theMenu setDelegate:self]; } #pragma mark **Action-Methods** - (IBAction)loginItem:(id)sender{ if ([sender state]==NSOnState) { [self setStartAtLogin:YES]; } else { [self setStartAtLogin:NO]; } } - (IBAction)add_favorite:(id)sender{ [[NSApplication sharedApplication] beginSheet:newfavoritewindow modalForWindow: mainwindow modalDelegate: nil didEndSelector: nil contextInfo: nil]; } - (IBAction)close_favorite:(id)sender{ [newfavoritewindow close]; [[NSApplication sharedApplication] endSheet:newfavoritewindow]; } - (IBAction)save_favorite:(id)sender{ MachineDefaults *msdefaults=[[MachineDefaults alloc] init:nil]; if ([[newfavorite_title stringValue] length]>0) { NSMutableDictionary *toinsert=[[NSMutableDictionary alloc] initWithObjectsAndKeys:[newfavorite_title stringValue],@"Title",[msdefaults get_machine_defaults][@"Fans"],@"FanData",nil]; //default as template [toinsert setValue:@0 forKey:@"Standard"]; [FavoritesController addObject:toinsert]; [newfavoritewindow close]; [[NSApplication sharedApplication] endSheet:newfavoritewindow]; } [self upgradeFavorites]; } -(void) check_deletion:(id)combo{ if ([FavoritesController selectionIndex]==[[defaults objectForKey:combo] intValue]) { [defaults setObject:@0 forKey:combo]; } } - (void) deleteAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo; { if (returnCode==0) { //delete favorite, but resets presets before [self check_deletion:@"selbatt"]; [self check_deletion:@"selac"]; [self check_deletion:@"selload"]; [FavoritesController removeObjects:[FavoritesController selectedObjects]]; } } - (IBAction)delete_favorite:(id)sender{ NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Delete favorite",nil) defaultButton:NSLocalizedString(@"No",nil) alternateButton:NSLocalizedString(@"Yes",nil) otherButton:nil informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete the favorite %@?",nil), [FavoritesController arrangedObjects][[FavoritesController selectionIndex]][@"Title"] ]]; [alert beginSheetModalForWindow:mainwindow modalDelegate:self didEndSelector:@selector(deleteAlertDidEnd:returnCode:contextInfo:) contextInfo:NULL]; } // Called via a timer mechanism. This is where all the temp / RPM reading is done. //reads fan data and updates the gui -(void) readFanData:(id)caller{ int i = 0; //on init handling if (_machineDefaultsDict==nil) { return; } // Determine what data is actually needed to keep the energy impact // as low as possible. bool bNeedTemp = false; bool bNeedRpm = false; const int menuBarSetting = [[defaults objectForKey:@"MenuBar"] intValue]; switch (menuBarSetting) { default: case 1: bNeedTemp = true; bNeedRpm = true; break; case 2: bNeedTemp = true; bNeedRpm = true; break; case 3: bNeedTemp = true; bNeedRpm = false; break; case 4: bNeedTemp = false; bNeedRpm = true; break; } NSString *temp = nil; NSString *fan = nil; float c_temp = 0.0f; int selectedRpm = 0; if (bNeedRpm == true) { // Read the current fan speed for the desired fan and format text for display in the menubar. NSArray *fans = [FavoritesController arrangedObjects][[FavoritesController selectionIndex]][@"FanData"]; for (i=0; i1 ) { if (bind==YES) { [[FanController arrangedObjects][1] bind:@"selspeed" toObject:[FanController arrangedObjects][0] withKeyPath:@"selspeed" options:nil]; [[FanController arrangedObjects][0] bind:@"selspeed" toObject:[FanController arrangedObjects][1] withKeyPath:@"selspeed" options:nil]; } else { [[FanController arrangedObjects][1] unbind:@"selspeed"]; [[FanController arrangedObjects][0] unbind:@"selspeed"]; } } } #pragma mark **Power Watchdog-Methods** - (void)systemDidWakeFromSleep:(id)sender{ [self apply_settings:nil controllerindex:[[defaults objectForKey:@"SelDefault"] intValue]]; } - (void)powerChangeToBattery:(id)sender{ if ([[defaults objectForKey:@"AutomaticChange"] boolValue]==YES) { [self apply_settings:nil controllerindex:[[defaults objectForKey:@"selbatt"] intValue]]; } } - (void)powerChangeToAC:(id)sender{ if ([[defaults objectForKey:@"AutomaticChange"] boolValue]==YES) { [self apply_settings:nil controllerindex:[[defaults objectForKey:@"selac"] intValue]]; } } - (void)powerChangeToACLoading:(id)sender{ if ([[defaults objectForKey:@"AutomaticChange"] boolValue]==YES) { [self apply_settings:nil controllerindex:[[defaults objectForKey:@"selload"] intValue]]; } } #pragma mark - #pragma mark Start-at-login control - (BOOL)isInAutoStart { BOOL found = NO; LSSharedFileListRef loginItems = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, /*options*/ NULL); NSString *path = [[NSBundle mainBundle] bundlePath]; CFURLRef URLToToggle = (__bridge CFURLRef)[NSURL fileURLWithPath:path]; //LSSharedFileListItemRef existingItem = NULL; UInt32 seed = 0U; NSArray *currentLoginItems = CFBridgingRelease(LSSharedFileListCopySnapshot(loginItems, &seed)); for (id itemObject in currentLoginItems) { LSSharedFileListItemRef item = (__bridge LSSharedFileListItemRef)itemObject; UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes; CFURLRef URL = NULL; OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL); if (err == noErr) { Boolean foundIt = CFEqual(URL, URLToToggle); CFRelease(URL); if (foundIt) { //existingItem = item; found = YES; break; } } } return found; } - (void) setStartAtLogin:(BOOL)enabled { LSSharedFileListRef loginItems = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, /*options*/ NULL); NSString *path = [[NSBundle mainBundle] bundlePath]; OSStatus status; CFURLRef URLToToggle = (__bridge CFURLRef)[NSURL fileURLWithPath:path]; LSSharedFileListItemRef existingItem = NULL; UInt32 seed = 0U; NSArray *currentLoginItems = CFBridgingRelease(LSSharedFileListCopySnapshot(loginItems, &seed)); for (id itemObject in currentLoginItems) { LSSharedFileListItemRef item = (__bridge LSSharedFileListItemRef)itemObject; UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes; CFURLRef URL = NULL; OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL); if (err == noErr) { Boolean foundIt = CFEqual(URL, URLToToggle); CFRelease(URL); if (foundIt) { existingItem = item; break; } } } if (enabled && (existingItem == NULL)) { NSString *displayName = [[NSFileManager defaultManager] displayNameAtPath:path]; IconRef icon = NULL; FSRef ref; Boolean gotRef = CFURLGetFSRef(URLToToggle, &ref); if (gotRef) { status = GetIconRefFromFileInfo(&ref, /*fileNameLength*/ 0, /*fileName*/ NULL, kFSCatInfoNone, /*catalogInfo*/ NULL, kIconServicesNormalUsageFlag, &icon, /*outLabel*/ NULL); if (status != noErr) icon = NULL; } LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst, (__bridge CFStringRef)displayName, icon, URLToToggle, /*propertiesToSet*/ NULL, /*propertiesToClear*/ NULL); } else if (!enabled && (existingItem != NULL)) LSSharedFileListItemRemove(loginItems, existingItem); } +(void) checkRightStatus:(OSStatus) status { if (status != errAuthorizationSuccess) { NSAlert *alert = [NSAlert alertWithMessageText:@"Authorization failed" defaultButton:@"Quit" alternateButton:nil otherButton:nil informativeTextWithFormat:[NSString stringWithFormat:@"Authorization failed with code %d",status]]; [alert setAlertStyle:2]; NSInteger result = [alert runModal]; if (result == NSAlertDefaultReturn) { [[NSApplication sharedApplication] terminate:self]; } } } #pragma mark **SMC-Binary Owner/Right Check** //TODO: It looks like this function is called inefficiently. //call smc binary with sudo rights and apply +(void)setRights{ NSString *smcpath = [[NSBundle mainBundle] pathForResource:@"smc" ofType:@""]; NSFileManager *fmanage=[NSFileManager defaultManager]; NSDictionary *fdic = [fmanage attributesOfItemAtPath:smcpath error:nil]; if ([[fdic valueForKey:@"NSFileOwnerAccountName"] isEqualToString:@"root"] && [[fdic valueForKey:@"NSFileGroupOwnerAccountName"] isEqualToString:@"admin"] && ([[fdic valueForKey:@"NSFilePosixPermissions"] intValue]==3437)) { // If the SMC binary has already been modified to run as root, then do nothing. return; } //TODO: Is the usage of commPipe safe? FILE *commPipe; AuthorizationRef authorizationRef; AuthorizationItem gencitem = { "system.privilege.admin", 0, NULL, 0 }; AuthorizationRights gencright = { 1, &gencitem }; int flags = kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed; OSStatus status = AuthorizationCreate(&gencright, kAuthorizationEmptyEnvironment, flags, &authorizationRef); [self checkRightStatus:status]; NSString *tool=@"/usr/sbin/chown"; NSArray *argsArray = @[@"root:admin",smcpath]; int i; char *args[255]; for(i = 0;i < [argsArray count];i++){ args[i] = (char *)[argsArray[i]cString]; } args[i] = NULL; status=AuthorizationExecuteWithPrivileges(authorizationRef,[tool UTF8String],0,args,&commPipe); [self checkRightStatus:status]; //second call for suid-bit tool=@"/bin/chmod"; argsArray = @[@"6555",smcpath]; for(i = 0;i < [argsArray count];i++){ args[i] = (char *)[argsArray[i]cString]; } args[i] = NULL; status=AuthorizationExecuteWithPrivileges(authorizationRef,[tool UTF8String],0,args,&commPipe); [self checkRightStatus:status]; } @end @implementation NSNumber (NumberAdditions) - (NSString*) tohex{ return [NSString stringWithFormat:@"%0.4x",[self intValue]<<2]; } - (NSNumber*) celsius_fahrenheit{ float celsius=[self floatValue]; float fahrenheit=(celsius*9)/5+32; return @(fahrenheit); } @end