From d7cdd66a625f8db14042de29867b855bb4f419e7 Mon Sep 17 00:00:00 2001 From: Cody Schrank Date: Fri, 20 Apr 2018 13:41:08 -0700 Subject: [PATCH] v1.6.0 fixed multiple bugs: Discrete only is not always enforced, Does not correctly handle external displays, and refactored large parts of code to make it much safer. --- gSwitch/AppDelegate.swift | 26 +++++----- gSwitch/BossyWindow.swift | 2 +- gSwitch/Constants.swift | 6 +++ gSwitch/GPUListener.swift | 41 +++++++++++----- gSwitch/GPUManager.swift | 55 +++++++++++---------- gSwitch/GPUViewController.swift | 2 +- gSwitch/GSProcess.m | 2 - gSwitch/ProcessManager.swift | 37 +++++++------- gSwitch/StatusMenuController.swift | 67 ++++++++++++++++++-------- gSwitch/UserNotificationManager.swift | 69 ++++++++++++++++++++------- 10 files changed, 202 insertions(+), 105 deletions(-) diff --git a/gSwitch/AppDelegate.swift b/gSwitch/AppDelegate.swift index 610a929..dd11191 100644 --- a/gSwitch/AppDelegate.swift +++ b/gSwitch/AppDelegate.swift @@ -31,6 +31,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { DistributedNotificationCenter.default().postNotificationName(.KILLME, object: Bundle.main.bundleIdentifier, userInfo: nil, options: DistributedNotificationCenter.Options.deliverImmediately) } + /** I like logs */ + let console = ConsoleDestination() + let file = FileDestination() + file.logFileURL = URL(fileURLWithPath: "swiftybeaver.log") //logs to container/*/swiftybeaver.log + log.addDestination(console) + log.addDestination(file) + /** If we cant connect to gpu there is no point in continuing */ do { try manager.connect() @@ -42,28 +49,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { NSApplication.shared.terminate(self) } - /** Juicy logs */ - let console = ConsoleDestination() - let file = FileDestination() - file.logFileURL = URL(fileURLWithPath: "swiftybeaver.log") //logs to container/*/swiftybeaver.log - log.addDestination(console) - log.addDestination(file) - /** GPU Names are good */ manager.setGPUNames() /** Lets listen to changes! */ listener.listen(manager: manager, processor: processer) - /** - TODO: Could add in ability to select mode with args here instead of hard .SetDynamic - */ - /** Lets set dynamic on startup */ if(manager.GPUMode(mode: .SetDynamic)) { - log.info("Initial Set Dynamic") + log.info("Initially set as Dynamic") } else { - log.warning("Could not set dynamic") + //if it could connect but couldnt set idk if thats possible? + //if it is possible this should quit the program here and report error + log.error("Could not set dynamic") } /** Get current state so current gpu name exists for use in menu */ @@ -72,7 +70,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { /** Are there any hungry processes off the bat? Updates menu if so */ processer.updateProcessMenuList() - /** NotificationCenter wants to check the gpu too */ + /** UserNotificationManager likes the gpu too */ notifications.inject(manager: manager) /** Default prefs so shit works */ diff --git a/gSwitch/BossyWindow.swift b/gSwitch/BossyWindow.swift index a74ae07..7c401a3 100644 --- a/gSwitch/BossyWindow.swift +++ b/gSwitch/BossyWindow.swift @@ -11,7 +11,7 @@ import Foundation import SwiftyBeaver class BossyWindow: NSWindowController { - let log = SwiftyBeaver.self + internal let log = SwiftyBeaver.self public func pushToFront() { self.window?.center() diff --git a/gSwitch/Constants.swift b/gSwitch/Constants.swift index 501895f..9e656d1 100644 --- a/gSwitch/Constants.swift +++ b/gSwitch/Constants.swift @@ -5,6 +5,10 @@ // Created by Cody Schrank on 4/14/18. // Copyright © 2018 CodySchrank. All rights reserved. // +// GPUState, DispatchSelectors, and Features is from gfxCardStatus +// Copyright (c) 2010-2012, Cody Krieger +// All rights reserved. +// import Foundation @@ -94,6 +98,7 @@ struct Constants { static let NOTIFICATION_QUEUE = "com.CodySchrank.GSwitch.GPUChangeNotificationQueue" static let kCGDisplaySetModeFlag = (1 << 3) static let kCGDisplayAddFlag = (1 << 4) + static let kCGDisplayRemoveFlag = (1 << 5) static let launcherApplicationIdentifier = "com.CodySchrank.gSwitchLauncher" static let GPU_CHANGE_NOTIFICATIONS = "gpuChangeNotifications" static let APP_LOGIN_START = "appLoginStart" @@ -104,6 +109,7 @@ extension Notification.Name { static let checkForHungryProcesses = Notification.Name("checkForHungryProcesses") static let updateProcessListInMenu = Notification.Name("updateProcessListInMenu") static let probableGPUChange = Notification.Name("probableGPUChange") + static let externalDisplayConnect = Notification.Name("externalDisplayConnect") static let startPolling = Notification.Name("startPolling") static let stopPolling = Notification.Name("stopPolling") static let KILLME = Notification.Name("killme") diff --git a/gSwitch/GPUListener.swift b/gSwitch/GPUListener.swift index 39f8bb6..2cf47b8 100644 --- a/gSwitch/GPUListener.swift +++ b/gSwitch/GPUListener.swift @@ -5,20 +5,25 @@ // Created by Cody Schrank on 4/15/18. // Copyright © 2018 CodySchrank. All rights reserved. // +// some logic is from gfxCardStatus +// https://github.com/codykrieger/gfxCardStatus/blob/master/LICENSE @ Jun 17, 2012 +// Copyright (c) 2010-2012, Cody Krieger +// All rights reserved. +// import Foundation import CoreGraphics import SwiftyBeaver class GPUListener { - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self - var _notificationQueue: DispatchQueue? - var _manager: GPUManager? - var _processor: ProcessManager? + private var notificationQueue: DispatchQueue? + private var _manager: GPUManager? + private var _processor: ProcessManager? init() { - self._notificationQueue = DispatchQueue.init(label: Constants.NOTIFICATION_QUEUE) + self.notificationQueue = DispatchQueue.init(label: Constants.NOTIFICATION_QUEUE) } public func listen(manager: GPUManager, processor: ProcessManager) { @@ -38,34 +43,48 @@ class GPUListener { NotificationCenter.default.post(name: .checkForHungryProcesses, object: nil) if(this._manager!.CheckGPUStateAndisUsingIntegratedGPU() && this._manager!.requestedMode == SwitcherMode.ForceIntergrated) { - //calls .checkGPUState - this.log.info("NOTIFY?: Switched from desired integrated to discrete") + // calls .checkGPUState + this.log.info("Switched from desired integrated to discrete") } if Int(flags.rawValue) & Constants.kCGDisplaySetModeFlag > 0 { + /** + Hungry apps usually call the gpu when they start and when they exit + If a user is on integrated only this forces it back from discrete. + */ + this.log.info("Dedicated Graphics Card Called") _ = this._processor?.getHungryProcesses() - this._notificationQueue?.async(execute: { + this.notificationQueue?.async(execute: { sleep(1) - //calls .checkGPUState + // calls .checkGPUState let isUsingIntegrated = this._manager!.CheckGPUStateAndisUsingIntegratedGPU() let requestedMode = this._manager!.requestedMode if(!isUsingIntegrated && requestedMode == SwitcherMode.ForceIntergrated) { if(this._manager!.GPUMode(mode: .ForceIntergrated)) { - this.log.info("NOTIFY?: Forced integrated GPU From dedicated GPU") + this.log.info("Forced integrated GPU From dedicated GPU") } } else { // usually gets called when change but sometimes gets called when no change? - this.log.info("NOTIFY: GPU maybe Changed") + this.log.verbose("NOTIFY: GPU maybe Changed") NotificationCenter.default.post(name: .probableGPUChange, object: this._manager?.currentGPU) } }) } + + if Int(flags.rawValue) & Constants.kCGDisplayRemoveFlag > 0 { + /** + usually gets called when switched. If I could get a flag that only triggered + when the display was disconnected I could save the last desired state that + the user selected and put them back on it. + (because dynamic is forced when a display is connected) + */ + } } diff --git a/gSwitch/GPUManager.swift b/gSwitch/GPUManager.swift index 054a078..1db3b84 100644 --- a/gSwitch/GPUManager.swift +++ b/gSwitch/GPUManager.swift @@ -5,19 +5,25 @@ // Created by Cody Schrank on 4/14/18. // Copyright © 2018 CodySchrank. All rights reserved. // +// some logic is from gfxCardStatus +// https://github.com/codykrieger/gfxCardStatus/blob/master/LICENSE @ Jun 17, 2012 +// Copyright (c) 2010-2012, Cody Krieger +// All rights reserved. +// import Foundation import IOKit import SwiftyBeaver class GPUManager { - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self + + public var integratedName: String? + public var discreteName: String? + public var currentGPU: String? + public var requestedMode: SwitcherMode? - var _connect: io_connect_t = IO_OBJECT_NULL; - var integratedName: String? - var discreteName: String? - var currentGPU: String? - var requestedMode: SwitcherMode? + private var _connect: io_connect_t = IO_OBJECT_NULL; public func setGPUNames() { let gpus = getGpuNames() @@ -112,27 +118,28 @@ class GPUManager { let integrated = CheckGPUStateAndisUsingIntegratedGPU() log.info("Requesting integrated, are we integrated? \(integrated)") - if (mode == .ForceIntergrated && !integrated) { + if !integrated { status = SwitchGPU(connect: connect) } case .ForceDiscrete: - let discrete = !CheckGPUStateAndisUsingIntegratedGPU() - log.info("Requesting discrete, are we discrete? \(discrete)") + log.info("Requesting discrete") - /** - Why not use the way macos designed it? - And leaves the user the ability to change back via system pref - */ + /** Essientialy ticks and unticks the box in system prefs, which by design forces discrete */ - if (mode == .ForceDiscrete && !discrete) { - _ = setFeatureInfo(connect: connect, feature: Features.Policy, enabled: true) - _ = setSwitchPolicy(connect: connect) - - status = setDynamicSwitching(connect: connect, enabled: false) - } + _ = setFeatureInfo(connect: connect, feature: Features.Policy, enabled: true) + _ = setSwitchPolicy(connect: connect) + + status = setDynamicSwitching(connect: connect, enabled: true) + + // give the gpu a second to switch + sleep(1) + + status = setDynamicSwitching(connect: connect, enabled: false) case .SetDynamic: - // Set switch policy back, make the MBP think it's an auto switching one once again + log.info("Requesting Dynamic") + + /** Set switch policy back, makes it think its on auto switching */ _ = setFeatureInfo(connect: connect, feature: Features.Policy, enabled: true) _ = setSwitchPolicy(connect: connect) @@ -210,9 +217,9 @@ class GPUManager { ); if kernResult == KERN_SUCCESS { - log.info("Modified state with \(state)") + log.verbose("Modified state with \(state)") } else { - log.info("ERROR: Set state returned \(kernResult)") + log.error("ERROR: Set state returned \(kernResult)") } return kernResult == KERN_SUCCESS @@ -245,9 +252,9 @@ class GPUManager { ); if kernResult == KERN_SUCCESS { - log.info("Successfully got state, count \(outputCount), value \(output)") + log.verbose("Successfully got state, count \(outputCount), value \(output)") } else { - log.info("ERROR: Get state returned \(kernResult)") + log.error("ERROR: Get state returned \(kernResult)") } return output diff --git a/gSwitch/GPUViewController.swift b/gSwitch/GPUViewController.swift index 34af333..0eda5dd 100644 --- a/gSwitch/GPUViewController.swift +++ b/gSwitch/GPUViewController.swift @@ -13,7 +13,7 @@ class GPUView: NSView { /** We use a hidden view to poll for hungry processes or possibly other information like vram */ - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) diff --git a/gSwitch/GSProcess.m b/gSwitch/GSProcess.m index c097def..998e345 100755 --- a/gSwitch/GSProcess.m +++ b/gSwitch/GSProcess.m @@ -6,8 +6,6 @@ // Copyright (c) 2010-2012, Cody Krieger // All rights reserved. // -// Original task list functionality Copyright 2010 Thierry Coppey. -// #import "GSProcess.h" diff --git a/gSwitch/ProcessManager.swift b/gSwitch/ProcessManager.swift index e99ccde..8ef796f 100644 --- a/gSwitch/ProcessManager.swift +++ b/gSwitch/ProcessManager.swift @@ -22,14 +22,14 @@ struct Process { } class ProcessManager { - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self /** At this time not doing any polling but its possible. - Maybe poll for more useful information like vram or gpu usage? + Maybe poll for more useful information like vram or gpu usage when the menu is open */ - var pollTimer: Timer? + private var pollTimer: Timer? init() { NotificationCenter.default.addObserver(self, selector: #selector(_updateProcessMenuList(notification:)), name: .checkForHungryProcesses, object: nil) @@ -62,24 +62,29 @@ class ProcessManager { self.updateProcessMenuList() } - /** Need to start on main thread */ + /** Not currently used */ @objc private func startPoll(notification: NSNotification) { - if self.pollTimer == nil { - log.info("Starting Poll") - self.pollTimer = Timer.scheduledTimer( - timeInterval: 2, - target : self, - selector : #selector(_updateProcessMenuList(notification:)), - userInfo : nil, - repeats : false) + DispatchQueue.main.async { + if self.pollTimer == nil { + self.log.info("Starting Poll") + self.pollTimer = Timer.scheduledTimer( + timeInterval: 2, + target : self, + selector : #selector(self._updateProcessMenuList(notification:)), + userInfo : nil, + repeats : false) + } } } + /** Not currently used */ @objc private func stopPoll(notification: NSNotification) { - if pollTimer != nil { - log.info("Stopping Poll") - pollTimer?.invalidate() - pollTimer = nil + DispatchQueue.main.async { + if self.pollTimer != nil { + self.log.info("Stopping Poll") + self.pollTimer?.invalidate() + self.pollTimer = nil + } } } } diff --git a/gSwitch/StatusMenuController.swift b/gSwitch/StatusMenuController.swift index c9ca9c5..630e6ed 100644 --- a/gSwitch/StatusMenuController.swift +++ b/gSwitch/StatusMenuController.swift @@ -26,14 +26,14 @@ class StatusMenuController: NSViewController { @IBOutlet weak var GPUViewController: GPUView! - var preferencesWindow: PreferencesWindow! - var aboutWindow: AboutWindow! + private var preferencesWindow: PreferencesWindow! + private var aboutWindow: AboutWindow! - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self - let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - var appDelegate: AppDelegate? + private var appDelegate: AppDelegate? override func awakeFromNib() { appDelegate = (NSApplication.shared.delegate as! AppDelegate) @@ -69,21 +69,17 @@ class StatusMenuController: NSViewController { } - /** According to gfx we cant do this */ + /** According to gfx we cant do this. Idk in testing it seems like I can do force it. + How do i find out if the dgpu is still on? */ let hungryProcesses = appDelegate?.processer.getHungryProcesses() if(hungryProcesses!.count > 0) { - - /** TODO: If external display is connected and it is the only hungry process switch to dynamic */ - /** TODO: Instead of showing warning present window with the offending processes and the option to delete them */ log.warning("SHOW: Can't switch to integrated only, because of \(String(describing: hungryProcesses))") return } - IntegratedOnlyItem.state = .on - DiscreteOnlyItem.state = .off - DynamicSwitchingItem.state = .off + changeGPUButtonToCorrectState(state: .ForceIntergrated) _ = appDelegate?.manager.GPUMode(mode: .ForceIntergrated) log.info("NOTIFY?: Set Force Integrated") @@ -94,9 +90,7 @@ class StatusMenuController: NSViewController { return //already set } - IntegratedOnlyItem.state = .off - DiscreteOnlyItem.state = .on - DynamicSwitchingItem.state = .off + changeGPUButtonToCorrectState(state: .ForceDiscrete) _ = appDelegate?.manager.GPUMode(mode: .ForceDiscrete) log.info("NOTIFY?: Set Force Discrete") @@ -107,9 +101,7 @@ class StatusMenuController: NSViewController { return //already set } - IntegratedOnlyItem.state = .off - DiscreteOnlyItem.state = .off - DynamicSwitchingItem.state = .on + changeGPUButtonToCorrectState(state: .SetDynamic) _ = appDelegate?.manager.GPUMode(mode: .SetDynamic) log.info("NOTIFY?: Set Dynamic") @@ -174,6 +166,21 @@ class StatusMenuController: NSViewController { } } + for process in hungry { + if process.name.contains("External Display") + && appDelegate?.manager.requestedMode != SwitcherMode.SetDynamic { + + if (appDelegate?.manager.GPUMode(mode: SwitcherMode.SetDynamic))! { + log.warning("External display connected, going back to dynamic") + + NotificationCenter.default.post(name: .externalDisplayConnect, object: nil) + + changeGPUButtonToCorrectState(state: .SetDynamic) + return + } + } + } + if hungry.count > 0 { Dependencies.isHidden = false @@ -190,7 +197,10 @@ class StatusMenuController: NSViewController { statusMenu.insertItem(seperator, at: 4) for process in hungry { - let title = "\t\(process.name) (\(process.pid))" + var title = "\t\(process.name)" + if process.pid != "" { + title += "(\(process.pid))" + } let newDependency = NSMenuItem(title: title, action: nil, keyEquivalent: "") newDependency.isEnabled = false newDependency.tag = 10 // so its easy to find when we delete @@ -203,4 +213,23 @@ class StatusMenuController: NSViewController { Dependencies.title = "Dependencies" } } + + private func changeGPUButtonToCorrectState(state: SwitcherMode) { + switch state { + case .ForceIntergrated: + IntegratedOnlyItem.state = .on + DynamicSwitchingItem.state = .off + DiscreteOnlyItem.state = .off + + case .SetDynamic: + IntegratedOnlyItem.state = .off + DynamicSwitchingItem.state = .on + DiscreteOnlyItem.state = .off + + case .ForceDiscrete: + IntegratedOnlyItem.state = .off + DynamicSwitchingItem.state = .off + DiscreteOnlyItem.state = .on + } + } } diff --git a/gSwitch/UserNotificationManager.swift b/gSwitch/UserNotificationManager.swift index d8f4e44..7b40470 100644 --- a/gSwitch/UserNotificationManager.swift +++ b/gSwitch/UserNotificationManager.swift @@ -11,18 +11,21 @@ import Cocoa import SwiftyBeaver class UserNotificationManager : NSObject, NSUserNotificationCenterDelegate { - let notificationCenter = NSUserNotificationCenter.default - let log = SwiftyBeaver.self + private let log = SwiftyBeaver.self - var _manager: GPUManager? - var lastGPU: String? + private let notificationCenter = NSUserNotificationCenter.default - var isGoingToCleanNotifications = false + private var _manager: GPUManager? + private var lastGPU: String? + + private var isGoingToCleanNotifications = false override init() { super.init() NotificationCenter.default.addObserver(self, selector: #selector(_showNotification(notification:)), name: .probableGPUChange, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(_showExternalDisplayNotification(notification:)), name: .externalDisplayConnect, object: nil) } func userNotificationCenter(_ center: NSUserNotificationCenter, @@ -36,6 +39,27 @@ class UserNotificationManager : NSObject, NSUserNotificationCenterDelegate { lastGPU = manager.currentGPU } + public func showExternalDisplayNotification() { + if UserDefaults.standard.integer(forKey: Constants.GPU_CHANGE_NOTIFICATIONS) == 0 { + return + } + + DispatchQueue.main.async { + sleep(3) + + self.log.info("Showing external display notification") + + let notification = NSUserNotification() + notification.title = "External Display Connected" + notification.informativeText = "Mode returned to Dynamic Switching" + notification.actionButtonTitle = "action" + + self.notificationCenter.deliver(notification) + + self.checkIfMaidOnTheWay() + } + } + public func showNotification(currentGPU: String?) { if UserDefaults.standard.integer(forKey: Constants.GPU_CHANGE_NOTIFICATIONS) == 0 { return @@ -47,35 +71,46 @@ class UserNotificationManager : NSObject, NSUserNotificationCenterDelegate { notificationCenter.deliver(notification) - if !isGoingToCleanNotifications { - log.info("Called the maid") - - DispatchQueue.main.async { - /** Removes notifications in approx 15 seconds */ - Timer.scheduledTimer(timeInterval: 15, target: self, selector: #selector(self.cleanUp), userInfo: nil, repeats: false) - self.isGoingToCleanNotifications = true - } - } + checkIfMaidOnTheWay() } - @objc public func cleanUp() { + public func cleanUp() { log.info("CLEAN: Notifications are gross") notificationCenter.removeAllDeliveredNotifications() isGoingToCleanNotifications = false } + @objc private func _showExternalDisplayNotification(notification: NSNotification) { + self.showExternalDisplayNotification() + } + @objc private func _showNotification(notification: NSNotification) { guard let currentGPU = notification.object as? String, let lastGPU = lastGPU else { return } - print("\(currentGPU) vs \(lastGPU)") - if(currentGPU != lastGPU) { + log.info("GPU did change") self.lastGPU = currentGPU showNotification(currentGPU: currentGPU) } } + private func checkIfMaidOnTheWay() { + if !isGoingToCleanNotifications { + log.info("Called the maid") + + DispatchQueue.main.async { + /** Removes notifications in approx 30 seconds */ + self.isGoingToCleanNotifications = true + + Timer.scheduledTimer(withTimeInterval: 30, repeats: false, block: { + (Timer) in + self.cleanUp() + }) + } + } + } + }