- update a download view

- add a progress bar to download new version view
- small refactoring in upgrader

- remove Sensors config from Stats target
This commit is contained in:
Serhiy Mytrovtsiy
2020-07-18 21:34:22 +02:00
parent e3324b2177
commit ad604789a4
6 changed files with 243 additions and 140 deletions

View File

@@ -104,9 +104,7 @@
9AE29AE2249A50640071B02D /* ModuleKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AABEADD243FB13500668CB0 /* ModuleKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9AE29AE5249A50640071B02D /* StatsKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; };
9AE29AE6249A50640071B02D /* StatsKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A0C82DA24460F7200FAE3D4 /* StatsKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9AE29AEE249A50960071B02D /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE29AEC249A50960071B02D /* Info.plist */; };
9AE29AF3249A51D70071B02D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF1249A50CD0071B02D /* main.swift */; };
9AE29AF5249A52870071B02D /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE29AF4249A52870071B02D /* config.plist */; };
9AE29AF6249A52B00071B02D /* config.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE29AF4249A52870071B02D /* config.plist */; };
9AE29AFB249A53DC0071B02D /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF9249A53780071B02D /* readers.swift */; };
9AE29AFC249A53DC0071B02D /* values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE29AF7249A53420071B02D /* values.swift */; };
@@ -1187,9 +1185,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9AE29AEE249A50960071B02D /* Info.plist in Resources */,
9A6CFC0122A1C9F5001E782D /* Assets.xcassets in Resources */,
9AE29AF5249A52870071B02D /* config.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -25,10 +25,9 @@ var modules: [Module] = [Battery(&store), Network(&store), Sensors(&store, &smc)
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Stats")
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
private let settingsWindow: SettingsWindow = SettingsWindow()
private let updateWindow: UpdateWindow = UpdateWindow()
internal let settingsWindow: SettingsWindow = SettingsWindow()
internal let updateNotification = NSUserNotification()
private let updateNotification = NSUserNotification()
private let updateActivity = NSBackgroundActivityScheduler(identifier: "eu.exelban.Stats.updateCheck")
func applicationDidFinishLaunching(_ aNotification: Notification) {
@@ -37,7 +36,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
self.parseArguments()
NSUserNotificationCenter.default.removeAllDeliveredNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(checkForNewVersion), name: .checkForUpdates, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(updateCron), name: .changeCronInterval, object: nil)
modules.forEach{ $0.mount() }
@@ -54,7 +52,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
if let uri = notification.userInfo?["url"] as? String {
os_log(.debug, log: log, "Downloading new version of app...")
if let url = URL(string: uri) {
updater.download(url)
updater.download(url, doneHandler: { path in
updater.install(path: path)
})
}
}
@@ -76,44 +76,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
return true
}
@objc internal func checkForNewVersion(_ window: Bool = false) {
updater.check() { result, error in
if error != nil {
os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)")
return
}
guard error == nil, let version: version = result else {
os_log(.error, log: log, "download error(): %s", "\(error!.localizedDescription)")
return
}
DispatchQueue.main.async(execute: {
if window {
os_log(.debug, log: log, "open update window: %s", "\(version.latest)")
self.updateWindow.open(version)
return
}
if version.newest {
os_log(.debug, log: log, "show update window because new version of app found: %s", "\(version.latest)")
self.updateNotification.identifier = "new-version-\(version.latest)"
self.updateNotification.title = "New version available"
self.updateNotification.subtitle = "Click to install the new version of Stats"
self.updateNotification.soundName = NSUserNotificationDefaultSoundName
self.updateNotification.hasActionButton = true
self.updateNotification.actionButtonTitle = "Install"
self.updateNotification.userInfo = ["url": version.url]
NSUserNotificationCenter.default.delegate = self
NSUserNotificationCenter.default.deliver(self.updateNotification)
}
})
}
}
@objc private func updateCron() {
self.updateActivity.invalidate()
self.updateActivity.repeats = true
@@ -132,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
self.updateActivity.schedule { (completion: @escaping NSBackgroundActivityScheduler.CompletionHandler) in
self.checkForNewVersion(false)
self.checkForNewVersion()
completion(NSBackgroundActivityScheduler.Result.finished)
}
}

View File

@@ -11,6 +11,7 @@
import Cocoa
import StatsKit
import os.log
class ApplicationSettings: NSView {
private let width: CGFloat = 540
@@ -23,6 +24,9 @@ class ApplicationSettings: NSView {
}
}
private var updateButton: NSButton? = nil
private let updateWindow: UpdateWindow = UpdateWindow()
init() {
super.init(frame: NSRect(x: 0, y: 0, width: width, height: height))
self.wantsLayer = true
@@ -36,6 +40,16 @@ class ApplicationSettings: NSView {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidMoveToWindow() {
if let button = self.updateButton, let version = updater.latest {
if version.newest {
button.title = "Update application"
} else {
button.title = "Check for updates"
}
}
}
private func addSettings() {
let view: NSView = NSView(frame: NSRect(x: 0, y: 1, width: self.width-1, height: self.height - self.deviceInfoHeight))
let rowHeight: CGFloat = 40
@@ -262,7 +276,8 @@ class ApplicationSettings: NSView {
button.title = "Check for updates"
button.bezelStyle = .rounded
button.target = self
button.action = #selector(checkNewVersion)
button.action = #selector(updateAction)
self.updateButton = button
rightPanel.addSubview(iconView)
rightPanel.addSubview(infoView)
@@ -274,8 +289,23 @@ class ApplicationSettings: NSView {
self.addSubview(view)
}
@objc func checkNewVersion(_ sender: NSObject) {
NotificationCenter.default.post(name: .checkForUpdates, object: nil, userInfo: nil)
@objc func updateAction(_ sender: NSObject) {
updater.check() { result, error in
if error != nil {
os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)")
return
}
guard error == nil, let version: version_s = result else {
os_log(.error, log: log, "download error(): %s", "\(error!.localizedDescription)")
return
}
DispatchQueue.main.async(execute: {
self.updateWindow.open(version)
return
})
}
}
@objc private func toggleUpdateInterval(_ sender: NSMenuItem) {

View File

@@ -20,7 +20,12 @@ class UpdateWindow: NSWindow, NSWindowDelegate {
let w = NSScreen.main!.frame.width
let h = NSScreen.main!.frame.height
super.init(
contentRect: NSMakeRect(w - self.viewController.view.frame.width, h - self.viewController.view.frame.height, self.viewController.view.frame.width, self.viewController.view.frame.height),
contentRect: NSMakeRect(
w - self.viewController.view.frame.width,
h - self.viewController.view.frame.height,
self.viewController.view.frame.width,
self.viewController.view.frame.height
),
styleMask: [.closable, .titled],
backing: .buffered,
defer: true
@@ -39,7 +44,7 @@ class UpdateWindow: NSWindow, NSWindowDelegate {
windowController.loadWindow()
}
public func open(_ v: version) {
public func open(_ v: version_s) {
if !self.isVisible {
self.setIsVisible(true)
self.makeKeyAndOrderFront(nil)
@@ -61,72 +66,80 @@ private class UpdateViewController: NSViewController {
fatalError("init(coder:) has not been implemented")
}
public func open(_ v: version) {
self.update.setVersions(v)
public func open(_ v: version_s) {
self.update.clear()
if v.newest {
self.update.newVersion(v)
return
}
self.update.noUpdates()
}
}
private class UpdateView: NSView {
private let progressBar: NSProgressIndicator = NSProgressIndicator()
private var version: version? = nil
private var informationView: NSView? = nil
private var noNew: NSView? = nil
private var currentVersion: NSTextField? = nil
private var latestVersion: NSTextField? = nil
private var version: version_s? = nil
private var path: String = ""
override init(frame: NSRect) {
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
self.wantsLayer = true
self.addProgressBar()
self.addInformation()
self.addNoNew()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addProgressBar() {
self.progressBar.isDisplayedWhenStopped = false
self.progressBar.frame = NSRect(x: (self.frame.width - 22)/2, y: (self.frame.height - 22)/2, width: 22, height: 22)
self.progressBar.style = .spinning
self.addSubview(self.progressBar)
}
private func addInformation() {
public func newVersion(_ version: version_s) {
self.version = version
let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20))
let title: NSTextField = TextView(frame: NSRect(x: 0, y: view.frame.height - 18, width: view.frame.width, height: 18))
title.font = NSFont.systemFont(ofSize: 14, weight: .bold)
let title: NSTextField = TextView(frame: NSRect(x: 0, y: view.frame.height - 20, width: view.frame.width, height: 18))
title.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
title.alignment = .center
title.stringValue = "New version available"
let currentVersion: NSTextField = TextView(frame: NSRect(x: 0, y: title.frame.origin.y - 40, width: view.frame.width, height: 16))
currentVersion.stringValue = "Current version: 0.0.0"
let currentVersionString = "Current version: \(version.current)"
let currentVersionWidth = currentVersionString.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light))
let currentVersion: NSTextField = TextView(frame: NSRect(
x: (view.frame.width-currentVersionWidth)/2,
y: title.frame.origin.y - 40,
width: currentVersionWidth,
height: 16
))
currentVersion.stringValue = currentVersionString
let latestVersion: NSTextField = TextView(frame: NSRect(x: 0, y: currentVersion.frame.origin.y - 22, width: view.frame.width, height: 16))
latestVersion.stringValue = "Latest version: 0.0.0"
let latestVersionString = "Latest version: \(version.latest)"
let latestVersionWidth = latestVersionString.widthOfString(usingFont: .systemFont(ofSize: 13, weight: .light))
let latestVersion: NSTextField = TextView(frame: NSRect(
x: (view.frame.width-currentVersionWidth)/2,
y: currentVersion.frame.origin.y - 22,
width: latestVersionWidth,
height: 16
))
latestVersion.stringValue = latestVersionString
let button: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
button.title = "Download"
button.bezelStyle = .rounded
button.action = #selector(self.download)
button.target = self
let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width/2, height: 26))
closeButton.title = "Close"
closeButton.bezelStyle = .rounded
closeButton.action = #selector(self.close)
closeButton.target = self
let downloadButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26))
downloadButton.title = "Download"
downloadButton.bezelStyle = .rounded
downloadButton.action = #selector(self.download)
downloadButton.target = self
view.addSubview(title)
view.addSubview(currentVersion)
view.addSubview(latestVersion)
view.addSubview(button)
view.isHidden = true
view.addSubview(closeButton)
view.addSubview(downloadButton)
self.addSubview(view)
self.informationView = view
self.currentVersion = currentVersion
self.latestVersion = latestVersion
}
private func addNoNew() {
public func noUpdates() {
let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20))
let title: NSTextField = TextView(frame: NSRect(x: 0, y: ((view.frame.height - 18)/2)+20, width: view.frame.width, height: 18))
@@ -143,37 +156,77 @@ private class UpdateView: NSView {
view.addSubview(button)
view.addSubview(title)
self.addSubview(view)
self.noNew = view
}
public func setVersions(_ v: version) {
self.progressBar.stopAnimation(self)
self.noNew?.isHidden = true
self.informationView?.isHidden = true
if v.newest {
self.informationView?.isHidden = false
self.version = v
currentVersion?.stringValue = "Current version: \(v.current)"
latestVersion?.stringValue = "Latest version: \(v.latest)"
return
}
self.noNew?.isHidden = false
public func clear() {
self.subviews.forEach{ $0.removeFromSuperview() }
}
@objc func close(_ sender: Any) {
self.window?.setIsVisible(false)
}
@objc func download(_ sender: Any) {
@objc private func download(_ sender: Any) {
guard let urlString = self.version?.url, let url = URL(string: urlString) else {
return
}
os_log(.debug, log: log, "start downloading new version of app from: %s", "\(url.absoluteString)")
updater.download(url)
self.progressBar.startAnimation(self)
self.informationView?.isHidden = true
self.clear()
let view: NSView = NSView(frame: NSRect(x: 10, y: 10, width: self.frame.width - 20, height: self.frame.height - 20))
let title: NSTextField = TextView(frame: NSRect(x: 0, y: view.frame.height - 28, width: view.frame.width, height: 18))
title.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
title.alignment = .center
title.stringValue = "Downloading..."
let progressBar: NSProgressIndicator = NSProgressIndicator()
progressBar.frame = NSRect(x: 20, y: 64, width: view.frame.width - 40, height: 22)
progressBar.minValue = 0
progressBar.maxValue = 1
progressBar.isIndeterminate = false
let state: NSTextField = TextView(frame: NSRect(x: 0, y: 48, width: view.frame.width, height: 18))
state.font = NSFont.systemFont(ofSize: 12, weight: .light)
state.alignment = .center
state.textColor = .secondaryLabelColor
state.stringValue = "0%"
let closeButton: NSButton = NSButton(frame: NSRect(x: 0, y: 0, width: view.frame.width, height: 26))
closeButton.title = "Close"
closeButton.bezelStyle = .rounded
closeButton.action = #selector(self.close)
closeButton.target = self
let installButton: NSButton = NSButton(frame: NSRect(x: view.frame.width/2, y: 0, width: view.frame.width/2, height: 26))
installButton.title = "Install"
installButton.bezelStyle = .rounded
installButton.action = #selector(self.install)
installButton.target = self
installButton.isHidden = true
updater.download(url, progressHandler: { progress in
DispatchQueue.main.async {
progressBar.doubleValue = progress.fractionCompleted
state.stringValue = "\(Int(progress.fractionCompleted*100))%"
}
}, doneHandler: { path in
self.path = path
DispatchQueue.main.async {
closeButton.setFrameSize(NSSize(width: view.frame.width/2, height: closeButton.frame.height))
installButton.isHidden = false
}
})
view.addSubview(title)
view.addSubview(progressBar)
view.addSubview(state)
view.addSubview(closeButton)
view.addSubview(installButton)
self.addSubview(view)
}
@objc private func close(_ sender: Any) {
self.window?.close()
}
@objc private func install(_ sender: Any) {
updater.install(path: self.path)
}
}

View File

@@ -89,7 +89,39 @@ extension AppDelegate {
}
if updateIntervals(rawValue: store.string(key: "update-interval", defaultValue: updateIntervals.atStart.rawValue)) != .never {
self.checkForNewVersion(false)
self.checkForNewVersion()
}
}
internal func checkForNewVersion() {
updater.check() { result, error in
if error != nil {
os_log(.error, log: log, "error updater.check(): %s", "\(error!.localizedDescription)")
return
}
guard error == nil, let version: version_s = result else {
os_log(.error, log: log, "download error(): %s", "\(error!.localizedDescription)")
return
}
DispatchQueue.main.async(execute: {
if version.newest {
os_log(.debug, log: log, "show update window because new version of app found: %s", "\(version.latest)")
self.updateNotification.identifier = "new-version-\(version.latest)"
self.updateNotification.title = "New version available"
self.updateNotification.subtitle = "Click to install the new version of Stats"
self.updateNotification.soundName = NSUserNotificationDefaultSoundName
self.updateNotification.hasActionButton = true
self.updateNotification.actionButtonTitle = "Install"
self.updateNotification.userInfo = ["url": version.url]
NSUserNotificationCenter.default.delegate = self
NSUserNotificationCenter.default.deliver(self.updateNotification)
}
})
}
}
}

View File

@@ -12,11 +12,18 @@
import Cocoa
import SystemConfiguration
public struct version {
public struct version_s {
public let current: String
public let latest: String
public let newest: Bool
public let url: String
public init(current: String, latest: String, newest: Bool, url: String) {
self.current = current
self.latest = latest
self.newest = newest
self.url = url
}
}
public struct Version {
@@ -32,6 +39,9 @@ public class macAppUpdater {
private let appName: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as! String
private let currentVersion: String = "v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String)"
public var latest: version_s? = nil
private var observation: NSKeyValueObservation?
private var url: String {
return "https://api.github.com/repos/\(user)/\(repo)/releases/latest"
}
@@ -41,7 +51,11 @@ public class macAppUpdater {
self.repo = repo
}
public func check(completionHandler: @escaping (_ result: version?, _ error: Error?) -> Void) {
deinit {
observation?.invalidate()
}
public func check(completionHandler: @escaping (_ result: version_s?, _ error: Error?) -> Void) {
if !isConnectedToNetwork() {
completionHandler(nil, "No internet connection")
return
@@ -62,7 +76,8 @@ public class macAppUpdater {
let lastVersion: String = result![0]
let newVersion: Bool = IsNewestVersion(currentVersion: self.currentVersion, latestVersion: lastVersion)
completionHandler(version(current: self.currentVersion, latest: lastVersion, newest: newVersion, url: downloadURL), nil)
self.latest = version_s(current: self.currentVersion, latest: lastVersion, newest: newVersion, url: downloadURL)
completionHandler(self.latest, nil)
}
}
@@ -93,13 +108,8 @@ public class macAppUpdater {
task.resume()
}
public func download(_ url: URL) {
let downloadTask = URLSession.shared.downloadTask(with: url) {
urlOrNil, responseOrNil, errorOrNil in
// check for and handle errors:
// * errorOrNil should be nil
// * responseOrNil should be an HTTPURLResponse with statusCode in 200..<299
public func download(_ url: URL, progressHandler: @escaping (_ progress: Progress) -> Void = {_ in }, doneHandler: @escaping (_ path: String) -> Void = {_ in }) {
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else { return }
do {
let downloadsURL = try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
@@ -111,27 +121,47 @@ public class macAppUpdater {
return
}
_ = syncShell("mkdir /tmp/Stats") // make sure that directory exist
let res = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen") // mount the dmg
if res.contains("is busy") { // dmg can be busy, if yes, unmount it and mount again
_ = syncShell("/usr/bin/hdiutil detach $TMPDIR/Stats")
_ = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen")
}
_ = syncShell("cp /tmp/Stats/Stats.app/Contents/Resources/Scripts/updater.sh $TMPDIR/updater.sh") // copy updater script to tmp folder
let pwd = Bundle.main.bundleURL.absoluteString.replacingOccurrences(of: "file://", with: "").replacingOccurrences(of: "Stats.app/", with: "")
_ = asyncShell("sh $TMPDIR/updater.sh --app \(pwd) --dmg \(path) >/dev/null &") // run updater script in in background
exit(0)
doneHandler(path)
}
} catch {
print ("file error: \(error)")
}
}
self.observation = downloadTask.progress.observe(\.fractionCompleted) { progress, _ in
progressHandler(progress)
}
downloadTask.resume()
}
public func install(path: String) {
print("Started new version installation...")
_ = syncShell("mkdir /tmp/Stats") // make sure that directory exist
let res = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen") // mount the dmg
print("DMG is mounted")
if res.contains("is busy") { // dmg can be busy, if yes, unmount it and mount again
print("DMG is busy, remounting")
_ = syncShell("/usr/bin/hdiutil detach $TMPDIR/Stats")
_ = syncShell("/usr/bin/hdiutil attach \(path) -mountpoint /tmp/Stats -noverify -nobrowse -noautoopen")
}
_ = syncShell("cp -rf /tmp/Stats/Stats.app/Contents/Resources/Scripts/updater.sh $TMPDIR/updater.sh") // copy updater script to tmp folder
print("Script is copied to $TMPDIR/updater.sh")
let pwd = Bundle.main.bundleURL.absoluteString.replacingOccurrences(of: "file://", with: "").replacingOccurrences(of: "Stats.app/", with: "")
_ = asyncShell("sh $TMPDIR/updater.sh --app \(pwd) --dmg \(path) >/dev/null &") // run updater script in in background
print("Run updater.sh with app: \(pwd) and dmg: \(path)")
exit(0)
}
private func copyFile(from: URL, to: URL, completionHandler: @escaping (_ path: String, _ error: Error?) -> Void) {
var toPath = to
let fileName = (URL(fileURLWithPath: to.absoluteString)).lastPathComponent