CloverBootloader/CloverApp/Clover/SettingsView.swift
vectorsigma72 b06c4d4d41 Clover.app v1.02 Beta
Clver.app:
Corrected a bug that was causing the installer to fail on unknown drivers.

CloverDaemonNew :
Now is registered with the Power notifications (sleep and wake), so that can clean up nvram.plist files created by third partu kexts.
At shut down it now delete the following nvram keys:
efi-backup-boot-device
efi-backup-boot-device-data
install-product-url
previous-system-uuid

Clover.app promoted to Beta.
2019-11-09 18:03:22 +01:00

664 lines
22 KiB
Swift

//
// SettingsView.swift
// Clover
//
// Created by vector sigma on 30/10/2019.
// Copyright © 2019 CloverHackyColor. All rights reserved.
//
import Cocoa
class SettingsViewController: NSViewController, NSTextFieldDelegate, URLSessionDownloadDelegate {
// MARK: Variables
@IBOutlet var currentRevField : NSTextField!
@IBOutlet var bootDeviceField : NSTextField!
@IBOutlet var configPathField : FWTextField!
@IBOutlet var disksPopUp : NSPopUpButton!
@IBOutlet var unmountButton : NSButton!
@IBOutlet var themeField : FWTextField!
@IBOutlet var soundField : FWTextField!
@IBOutlet var disbaleSleepProxyButton : NSButton!
@IBOutlet var makeRootRWButton : NSButton!
@IBOutlet var installDaemonButton : NSButton!
@IBOutlet var unInstallDaemonButton : NSButton!
@IBOutlet var timeIntervalPopUp : NSPopUpButton!
@IBOutlet var checkNowButton : NSButton!
@IBOutlet var lastUpdateCheckField : NSTextField!
@IBOutlet var runAtLoginButton : NSButton!
@IBOutlet var updateCloverButton : NSButton!
@IBOutlet var installCloverButton : NSButton!
@IBOutlet var progressBar : NSProgressIndicator!
@IBOutlet var appVersionField : NSTextField!
var lastReleaseRev : String? = nil
var lastReleaseLink : String? = nil
var currentRev : String = findCloverRevision() ?? kNotAvailable.locale
var bootDevice : String? = findBootPartitionDevice()
var timerUpdate : Timer? = nil
var timeUpdateInterval : TimeInterval = UpdateInterval.never.rawValue
var downloadTask : URLSessionDownloadTask? = nil
// MARK: View customization
override func viewDidLoad() {
super.viewDidLoad()
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String + " Beta"
self.appVersionField.stringValue = "v\(appVersion)"
localize(view: self.view)
self.view.wantsLayer = true
self.view.layer?.backgroundColor = NSColor.clear.cgColor
self.themeField.delegate = self
self.soundField.delegate = self
self.progressBar.isHidden = true
self.runAtLoginButton.state = UDs.bool(forKey: kRunAtLogin) ? .on : .off
self.unmountButton.isEnabled = false
self.setUpdateInformations()
let nvram = getNVRAM()
var nvdata = nvram?.object(forKey: "Clover.Theme") as? Data
self.themeField.placeholderString = kNotAvailable.locale
self.themeField.stringValue = (nvdata != nil) ? String(decoding: nvdata!, as: UTF8.self) : ""
self.themeField.cell?.representedObject = self.themeField.stringValue
nvdata = nvram?.object(forKey: "Clover.Sound") as? Data
self.soundField.placeholderString = kNotAvailable.locale
self.soundField.stringValue = (nvdata != nil) ? String(decoding: nvdata!, as: UTF8.self) : ""
self.soundField.cell?.representedObject = self.soundField.stringValue
nvdata = nvram?.object(forKey: "Clover.RootRW") as? Data
var value : String = String(decoding: nvdata ?? Data(), as: UTF8.self)
self.makeRootRWButton.state = (value == "true") ? .on : .off
nvdata = nvram?.object(forKey: "Clover.DisableSleepProxyClient") as? Data
value = String(decoding: nvdata ?? Data(), as: UTF8.self)
self.disbaleSleepProxyButton.state = (value == "true") ? .on : .off
let daemonExist = fm.fileExists(atPath: kDaemonPath) && fm.fileExists(atPath: kLaunchPlistPath)
self.unInstallDaemonButton.isEnabled = daemonExist
self.setUpdateButton()
self.searchESPDisks()
let itervals = ["never", "daily", "weekly", "monthly"]
self.timeIntervalPopUp.removeAllItems()
for i in itervals {
self.timeIntervalPopUp.addItem(withTitle: i.locale)
self.timeIntervalPopUp.lastItem?.representedObject = i
}
if (UDs.object(forKey: kUpdateSearchInterval) == nil) {
// search update the first time..
UDs.set("monthly", forKey: kUpdateSearchInterval)
UDs.synchronize()
}
let def = UDs.string(forKey: kUpdateSearchInterval)!
for i in self.timeIntervalPopUp.itemArray {
let c = i.representedObject as! String
if def == c {
self.timeIntervalPopUp.select(i)
break
}
}
if let date : Date = UDs.object(forKey: kLastSearchUpdateDateKey) as? Date {
let now = DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .short)
self.lastUpdateCheckField.stringValue = "\("last checked:".locale) \(now)"
} else {
self.lastUpdateCheckField.stringValue = "\("last checked:".locale) \("never".locale)"
}
self.timerUpdate = Timer.scheduledTimer(timeInterval: 60 * 60,
target: self,
selector: #selector(self.setUpdateTimer),
userInfo: nil,
repeats: true)
}
func setUpdateInformations() {
let rev = findCloverRevision(at: Bundle.main.sharedSupportPath!.addPath("CloverV2/EFI")) ?? "0000"
var title = "\("Install Clover".locale) \(rev)"
self.installCloverButton.title = title
self.bootDevice = findBootPartitionDevice()
title = "\("Boot device".locale): \(self.bootDevice ?? kNotAvailable.locale)"
self.bootDeviceField.stringValue = title
self.configPathField.stringValue = findConfigPath() ?? kNotAvailable.locale
title = "\("Current Clover revision".locale): \(self.currentRev)"
self.currentRevField.stringValue = title
let last : String = (self.lastReleaseRev == nil) ? kNotAvailable.locale : "r\(self.lastReleaseRev!)"
title = "\("Update Clover available".locale): \(last)"
self.installCloverButton.isEnabled = true
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: Disks
func searchESPDisks() {
self.unmountButton.isEnabled = false
let selected = self.disksPopUp.selectedItem?.representedObject as? String
self.disksPopUp.removeAllItems()
self.disksPopUp.addItem(withTitle: "Select a disk..".locale)
for e in getAllESPs() {
if isWritable(diskOrMtp: e) {
let fs = getFS(from: e) ?? kNotAvailable.locale
let mp = getMountPoint(from: e) ?? kNotAvailable.locale
let title : String = "\(e), \(fs), \("mount point".locale): \(mp)"
self.disksPopUp.addItem(withTitle: title)
self.disksPopUp.lastItem?.representedObject = e
if e == self.bootDevice {
let image : NSImage = NSImage(named: "NSApplicationIcon")!.copy() as! NSImage
image.size = NSMakeSize(16, 16)
self.disksPopUp.lastItem?.image = image
} else if let image : NSImage = getIconFor(volume: e) {
image.size = NSMakeSize(16, 16)
self.disksPopUp.lastItem?.image = image
}
}
}
if (selected != nil) {
for item in self.disksPopUp.itemArray {
if let d = item.representedObject as? String {
if d == selected {
self.disksPopUp.select(item)
if isMountPoint(path: d) {
self.unmountButton.isEnabled = true
}
break
}
}
}
}
}
// MARK: Mount ESPs
@IBAction func mountESP(_ sender: NSPopUpButton!) {
self.unmountButton.isEnabled = false
if let disk = sender.selectedItem?.representedObject as? String {
if !isMountPoint(path: disk) {
DispatchQueue.global(qos: .background).async {
let cmd = "diskutil mount \(disk)"
let msg = String(format: "Clover wants to mount %@", disk)
let script = "do shell script \"\(cmd)\" with prompt \"\(msg)\" with administrator privileges"
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", script]
task.terminationHandler = { t in
if t.terminationStatus == 0 {
DispatchQueue.main.async {
self.unmountButton.isEnabled = true
}
NSWorkspace.shared.openFile(getMountPoint(from: disk) ?? "")
} else {
NSSound.beep()
}
}
task.launch()
}
} else {
self.unmountButton.isEnabled = true
NSWorkspace.shared.openFile(getMountPoint(from: disk) ?? "")
}
}
}
// MARK: Umount
@IBAction func umount(_ sender: NSButton!) {
if let disk = self.disksPopUp.selectedItem?.representedObject as? String {
if isMountPoint(path: disk) {
DispatchQueue.global(qos: .background).async {
let cmd = "diskutil umount \(disk)"
let msg = String(format: "Clover wants to umount %@", disk)
let script = "do shell script \"\(cmd)\" with prompt \"\(msg)\" with administrator privileges"
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", script]
task.terminationHandler = { t in
if t.terminationStatus != 0 {
NSSound.beep()
}
DispatchQueue.main.async {
self.searchESPDisks()
}
}
task.launch()
}
}
}
}
// MARK: Controls actions
@IBAction func installClover(_ sender: NSButton!) {
if (AppSD.installerWC == nil) {
AppSD.installerWC = InstallerWindowController.loadFromNib()
}
AppSD.installerWC?.showWindow(self)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func installDaemon(_ sender: NSButton!) {
let daemonPath = Bundle.main.executablePath!.deletingLastPath.addPath("CloverDaemonNew")
DispatchQueue.global(qos: .background).async {
let task = Process()
let msg = "Install CloverDaemonNew".locale
let script = "do shell script \"\" & quoted form of \"\(daemonPath)\" & \" --install\" with prompt \"\(msg)\" with administrator privileges"
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", script]
task.terminationHandler = { t in
if t.terminationStatus != 0 {
print(t.terminationReason)
NSSound.beep()
}
DispatchQueue.main.async {
let daemonExist = fm.fileExists(atPath: kDaemonPath) && fm.fileExists(atPath: kLaunchPlistPath)
self.unInstallDaemonButton.isEnabled = daemonExist
}
}
task.launch()
}
}
@IBAction func unInstallDaemon(_ sender: NSButton!) {
let daemonPath = Bundle.main.executablePath!.deletingLastPath.addPath("CloverDaemonNew")
DispatchQueue.global(qos: .background).async {
let task = Process()
let msg = "Install CloverDaemonNew".locale
let script = "do shell script \"\" & quoted form of \"\(daemonPath)\" & \" --uninstall\" with prompt \"\(msg)\" with administrator privileges"
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", script]
task.terminationHandler = { t in
if t.terminationStatus != 0 {
print(t.terminationReason)
NSSound.beep()
}
DispatchQueue.main.async {
let daemonExist = fm.fileExists(atPath: kDaemonPath) && fm.fileExists(atPath: kLaunchPlistPath)
self.unInstallDaemonButton.isEnabled = daemonExist
}
}
task.launch()
}
}
@IBAction func setUpdateInterval(_ sender: NSPopUpButton!) {
if let selected = sender.selectedItem?.representedObject as? String {
UDs.set(selected, forKey: kUpdateSearchInterval)
self.setUpdateTimer()
}
}
@IBAction func checkNow(_ sender: NSButton!) {
self.searchUpdate()
}
// MARK: Run At Login
@IBAction func runAtLogin(_ sender: NSButton!) {
if sender.state == .on {
AppSD.setLaunchAtStartup()
} else {
AppSD.removeLaunchAtStartup()
}
// check the result
sender.state = UDs.bool(forKey: kRunAtLogin) ? .on : .off
}
// MARK: NVRAM editing
func controlTextDidEndEditing(_ obj: Notification) {
if let field = obj.object as? NSTextField {
let delete : Bool = field.stringValue.count == 0
if field == self.themeField || field == soundField {
if let old = field.cell?.representedObject as? String {
let key = (field == self.themeField) ? "Clover.Theme" : "Clover.Sound"
if old != field.stringValue {
if delete {
deleteNVRAM(key: key)
} else {
setNVRAM(key: key, stringValue: field.stringValue/*, error: &error*/)
}
let nvdata = getNVRAM()?.object(forKey: key) as? Data
field.stringValue = (nvdata != nil) ? String(decoding: nvdata!, as: UTF8.self) : ""
field.cell?.representedObject = field.stringValue
}
}
}
}
}
@IBAction func disableSleepProxy(_ sender: NSButton!) {
let key = "Clover.DisableSleepProxyClient"
if sender.state == .on {
setNVRAM(key: key, stringValue: "true"/*, error: &error*/)
} else {
deleteNVRAM(key: key)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
var value = ""
if let nvram = getNVRAM() {
let nvdata = nvram.object(forKey: "Clover.DisableSleepProxyClient") as? Data
value = String(decoding: nvdata ?? Data(), as: UTF8.self)
}
self.disbaleSleepProxyButton.state = (value == "true") ? .on : .off
}
}
@IBAction func makeRootRW(_ sender: NSButton!) {
let key = "Clover.RootRW"
if sender.state == .on {
setNVRAM(key: key, stringValue: "true"/*, error: &error*/)
} else {
deleteNVRAM(key: key)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
var value = ""
if let nvram = getNVRAM() {
let nvdata = nvram.object(forKey: "Clover.RootRW") as? Data
value = String(decoding: nvdata ?? Data(), as: UTF8.self)
}
self.disbaleSleepProxyButton.state = (value == "true") ? .on : .off
}
}
@IBAction func readDaemonLog(_ sender: NSButton!) {
DispatchQueue.global(qos: .background).async {
let cmd = "cat /Library/Logs/CloverEFI/clover.daemon.log > /tmp/clover.daemon.log && open /tmp/clover.daemon.log"
let msg = "CloverDaemon log"
let script = "do shell script \"\(cmd)\" with prompt \"\(msg)\" with administrator privileges"
let task = Process()
task.launchPath = "/usr/bin/osascript"
task.arguments = ["-e", script]
task.terminationHandler = { t in
if t.terminationStatus == 0 {
DispatchQueue.main.async {
self.unmountButton.isEnabled = true
}
NSWorkspace.shared.openFile("/tmp/clover.daemon.log")
} else {
NSSound.beep()
}
}
task.launch()
}
}
@IBAction func readbdmesg(_ sender: NSButton!) {
if let bdmesg = dumpBootlog() {
do {
try bdmesg.write(toFile: "/tmp/bdmesg.log", atomically: true, encoding: .utf8)
NSWorkspace.shared.openFile("/tmp/bdmesg.log")
} catch {
NSSound.beep()
}
} else {
NSSound.beep()
}
}
// MARK: Search update
@objc func setUpdateTimer() {
// check last date update was checked
var ti : TimeInterval = UpdateInterval.never.rawValue
let def = (UDs.value(forKey: kUpdateSearchInterval) as? String) ?? "never"
switch def {
case "never":
ti = UpdateInterval.never.rawValue
case "daily":
ti = UpdateInterval.daily.rawValue
case "weekly":
ti = UpdateInterval.weekly.rawValue
case "monthly":
ti = UpdateInterval.monthly.rawValue
default:
break
}
// Time interval is what user defines less time elapsed
if ti > 0 {
let lastCheckDate : Date = (UDs.object(forKey: kLastSearchUpdateDateKey) as? Date) ?? Date()
let secElapsed = Date().timeIntervalSinceReferenceDate - lastCheckDate.timeIntervalSinceReferenceDate
if secElapsed >= ti {
print(secElapsed)
self.searchUpdate()
}
}
}
func setUpdateButton() {
if (self.lastReleaseRev != nil && self.lastReleaseLink != nil) {
AppSD.statusItem.title = self.lastReleaseRev
self.updateCloverButton.title = String(format: "Update to %@".locale, self.lastReleaseRev!)
self.updateCloverButton.isEnabled = true
} else {
AppSD.statusItem.title = ""
self.updateCloverButton.title = kNotAvailable.locale
self.updateCloverButton.isEnabled = false
}
}
@objc func searchUpdate() {
getLatestRelease { (l, v) in
self.lastReleaseLink = l
self.lastReleaseRev = v
let currRevNum : Int = Int(self.currentRev) ?? 0
let lastRevNum : Int = Int(self.lastReleaseRev ?? "0") ?? 0
if (self.lastReleaseLink != nil && self.lastReleaseRev != nil)
&& lastRevNum > 0
&& (lastRevNum > currRevNum) {
UDs.set(self.lastReleaseLink!, forKey: kLastUpdateLink)
UDs.set(self.lastReleaseRev!, forKey: kLastUpdateRevision)
DispatchQueue.main.async {
AppSD.statusItem.button?.title = "\(lastRevNum)"
AppSD.statusItem.button?.imagePosition = .imageLeft
self.updateCloverButton.isEnabled = true
self.updateCloverButton.title = String(format: "Update to r%d".locale, lastRevNum)
}
} else {
DispatchQueue.main.async {
let ll = UDs.string(forKey: kLastUpdateLink)
let lr = UDs.string(forKey: kLastUpdateRevision)
if (ll != nil && lr != nil) {
self.lastReleaseLink = ll
self.lastReleaseRev = lr
AppSD.statusItem.button?.title = "\(lastRevNum)"
AppSD.statusItem.button?.imagePosition = .imageLeft
self.updateCloverButton.isEnabled = true
self.updateCloverButton.title = String(format: "Update to r%d".locale, lastRevNum)
} else {
AppSD.statusItem.button?.title = ""
AppSD.statusItem.button?.imagePosition = .imageOnly
self.updateCloverButton.isEnabled = false
self.updateCloverButton.title = kNotAvailable.locale
}
}
}
let date = Date()
let now = DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .short)
DispatchQueue.main.async {
self.lastUpdateCheckField.stringValue = "\("last checked:".locale) \(now)"
}
UDs.set(date, forKey: kLastSearchUpdateDateKey)
UDs.synchronize()
}
}
// MARK: Download
@IBAction func updateClover(_ sender: NSButton!) {
if AppSD.isInstalling ||
AppSD.isInstallerOpen ||
self.lastReleaseLink == nil ||
self.lastReleaseRev == nil {
NSSound.beep()
return
}
self.progressBar.isHidden = false
self.progressBar.doubleValue = 0.0
let url = URL(string: self.lastReleaseLink!)
let b = URLSessionConfiguration.default
let session = Foundation.URLSession(configuration: b, delegate: self, delegateQueue: nil)
//let session = URLSession(configuration: .default)
if (url != nil) {
self.installCloverButton.isEnabled = false
self.downloadTask = session.downloadTask(with: url!)
self.downloadTask?.resume()
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
self.progressBar.doubleValue = 100
self.progressBar.isHidden = true
}
do {
let lastPath = downloadTask.originalRequest!.url!.lastPathComponent
let data = try Data(contentsOf: location)
if lastPath.fileExtension == "zip" && lastPath.hasPrefix("CloverV2") {
// ok, We have the download completed: replace CloverV2 inside SharedSupport directory!
// Decompress the zip archive
let tempDir = NSTemporaryDirectory().addPath("CloverXXXXX")
if fm.fileExists(atPath: tempDir) {
try fm.removeItem(atPath: tempDir)
try fm.createDirectory(atPath: tempDir, withIntermediateDirectories: true, attributes: nil)
}
let file = tempDir.addPath(lastPath)
try data.write(to: URL(fileURLWithPath: file))
unzip(file: file, destination: tempDir) { (success) in
if success {
self.replaceCloverV2(with: tempDir.addPath("CloverV2"))
}
}
} else {
try data.write(to: URL(fileURLWithPath: NSHomeDirectory().addPath("Downloads/\(lastPath)")))
replaceCloverV2(with: "/Users/vectorsigma/src/CloverBootloader/CloverPackage/CloverV2")
}
} catch {
print(error)
}
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
DispatchQueue.main.async {
self.installCloverButton.isEnabled = true
}
if (error != nil) {
print(error!.localizedDescription)
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown {
DispatchQueue.main.async {
self.progressBar.isIndeterminate = true
self.progressBar.startAnimation(nil)
}
} else {
let percentage : Double = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) * 100
DispatchQueue.main.async {
self.progressBar?.isIndeterminate = false
self.progressBar?.doubleValue = percentage
}
}
}
private func replaceCloverV2(with newOne: String) {
var isDir : ObjCBool = false
if fm.fileExists(atPath: newOne, isDirectory: &isDir) {
if isDir.boolValue {
do {
try fm.removeItem(atPath: Cloverv2Path)
try fm.copyItem(atPath: newOne, toPath: Cloverv2Path)
print("CloverV2 Replaced!")
DispatchQueue.main.async {
self.lastReleaseRev = nil
self.lastReleaseLink = nil
self.setUpdateInformations()
self.setUpdateButton()
}
} catch {
print(error)
}
}
}
}
// MARK: Close
@IBAction func close(_ sender: NSButton!) {
NSApp.terminate(nil)
}
}
// MARK: Settings Window controller
class SettingsWindowController: NSWindowController, NSWindowDelegate {
class func loadFromNib() -> SettingsWindowController {
let wc = NSStoryboard(name: "Settings",
bundle: nil).instantiateController(withIdentifier: "SettingsWindowController") as! SettingsWindowController
return wc
}
}