mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-11-04 20:20:05 +01:00
♻️ Lint tests, add background update check
This commit is contained in:
@@ -3,13 +3,25 @@ disabled_rules:
|
||||
- identifier_name
|
||||
- force_try
|
||||
- force_cast
|
||||
|
||||
- private_over_fileprivate
|
||||
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
|
||||
included:
|
||||
- phpmon
|
||||
- phpmon-tests
|
||||
- phpmon-updater
|
||||
- tests
|
||||
|
||||
excluded:
|
||||
- phpmon/Vendor
|
||||
|
||||
line_length:
|
||||
ignores_function_declarations: true
|
||||
ignores_comments: true
|
||||
ignores_urls: true
|
||||
warning: 120
|
||||
error: 200
|
||||
|
||||
analyzer_rules:
|
||||
- unused_import
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
|
||||
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
|
||||
031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
|
||||
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
|
||||
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
|
||||
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
|
||||
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
|
||||
033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
|
||||
033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
|
||||
033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */; };
|
||||
@@ -957,6 +961,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = "<group>"; };
|
||||
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = "<group>"; };
|
||||
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = "<group>"; };
|
||||
0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallPhpExtensionCommand.swift; sourceTree = "<group>"; };
|
||||
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = "<group>"; };
|
||||
@@ -2058,6 +2063,7 @@
|
||||
C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */,
|
||||
C40FE736282ABA4F00A302C2 /* AppVersion.swift */,
|
||||
C409349C298EE8E900D25014 /* AppUpdater.swift */,
|
||||
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */,
|
||||
);
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
@@ -2679,6 +2685,7 @@
|
||||
C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */,
|
||||
C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */,
|
||||
C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */,
|
||||
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
|
||||
0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */,
|
||||
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
|
||||
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */,
|
||||
@@ -3006,6 +3013,7 @@
|
||||
C471E81A28F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */,
|
||||
C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */,
|
||||
C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */,
|
||||
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
|
||||
C471E80A28F9BADC0021E251 /* CreatedFromFile.swift in Sources */,
|
||||
C471E80528F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */,
|
||||
C471E80628F9BAD40021E251 /* PhpInstallation.swift in Sources */,
|
||||
@@ -3286,6 +3294,7 @@
|
||||
C471E80F28F9BAE80021E251 /* NSMenuExtension.swift in Sources */,
|
||||
C471E80B28F9BAE80021E251 /* XibLoadable.swift in Sources */,
|
||||
C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */,
|
||||
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
|
||||
C471E7CB28F9BA5B0021E251 /* TestableCommand.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -3344,6 +3353,7 @@
|
||||
54D9E0B527E4F51E003B9AD9 /* Key.swift in Sources */,
|
||||
C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */,
|
||||
C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */,
|
||||
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
|
||||
C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */,
|
||||
C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */,
|
||||
C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */,
|
||||
|
||||
@@ -31,14 +31,25 @@ struct Constants {
|
||||
/**
|
||||
The interval between automatic background update checks.
|
||||
*/
|
||||
static let AutomaticUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 * 24 // 24 hours
|
||||
static let AutomaticUpdateCheckInterval: TimeInterval = 15.0 // 60.0 * 60 * 24 // 24 hours
|
||||
|
||||
/**
|
||||
The minimum interval that must pass before allowing another
|
||||
automatic update check. This prevents excessive checking
|
||||
on frequent app restarts (due to crashes or bad config).
|
||||
*/
|
||||
static let MinimumUpdateCheckInterval: TimeInterval = 60 // 60.0 * 60 // 60 minutes
|
||||
static let MinimumUpdateCheckInterval: TimeInterval = 5.0 // 60.0 * 60 // 60 minutes
|
||||
|
||||
/**
|
||||
Retry intervals for failed automatic update checks.
|
||||
Uses exponential backoff: 5 min → 15 min → 1 hr → 3 hrs before falling back to normal schedule.
|
||||
*/
|
||||
static let UpdateCheckRetryIntervals: [TimeInterval] = [
|
||||
300, // 5 minutes
|
||||
900, // 15 minutes
|
||||
3600, // 1 hour
|
||||
10800 // 3 hours (final attempt)
|
||||
]
|
||||
|
||||
/**
|
||||
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||
|
||||
@@ -10,17 +10,24 @@ import Foundation
|
||||
import Cocoa
|
||||
import NVAlert
|
||||
|
||||
enum UpdateCheckResult {
|
||||
case success
|
||||
case networkError
|
||||
case parseError
|
||||
case disabled
|
||||
}
|
||||
|
||||
class AppUpdater {
|
||||
var caskFile: CaskFile!
|
||||
var latestVersionOnline: AppVersion!
|
||||
var interactive: Bool = false
|
||||
|
||||
public func checkForUpdates(userInitiated: Bool) async {
|
||||
public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult {
|
||||
self.interactive = userInitiated
|
||||
|
||||
if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
|
||||
Log.info("Skipping automatic update check due to user preference.")
|
||||
return
|
||||
return .disabled
|
||||
}
|
||||
|
||||
Log.info("The app will search for updates...")
|
||||
@@ -29,7 +36,8 @@ class AppUpdater {
|
||||
|
||||
guard let caskFile = await CaskFile.from(url: caskUrl) else {
|
||||
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
|
||||
return presentCouldNotRetrieveUpdateIfInteractive()
|
||||
presentCouldNotRetrieveUpdateIfInteractive()
|
||||
return .networkError
|
||||
}
|
||||
|
||||
self.caskFile = caskFile
|
||||
@@ -38,7 +46,8 @@ class AppUpdater {
|
||||
|
||||
guard let onlineVersion = AppVersion.from(caskFile.version) else {
|
||||
Log.err("The version string from the CaskFile could not be read.")
|
||||
return presentCouldNotRetrieveUpdateIfInteractive()
|
||||
presentCouldNotRetrieveUpdateIfInteractive()
|
||||
return .parseError
|
||||
}
|
||||
|
||||
latestVersionOnline = onlineVersion
|
||||
@@ -49,6 +58,8 @@ class AppUpdater {
|
||||
} else if interactive {
|
||||
presentNoNewerVersionAvailableAlert()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
private func presentCouldNotRetrieveUpdateIfInteractive() {
|
||||
|
||||
113
phpmon/Domain/App/UpdateScheduler.swift
Normal file
113
phpmon/Domain/App/UpdateScheduler.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// UpdateScheduler.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 26/09/2025.
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
class UpdateScheduler {
|
||||
static let shared = UpdateScheduler()
|
||||
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
Start the automatic update checking process.
|
||||
This should be called once during app startup.
|
||||
*/
|
||||
func startAutomaticUpdateChecking() async {
|
||||
await performUpdateCheck()
|
||||
}
|
||||
|
||||
/**
|
||||
Perform an automatic update check and schedule the next one.
|
||||
*/
|
||||
private func performUpdateCheck() async {
|
||||
guard isNotThrottled() else {
|
||||
// If we are throttled, just schedule a regular check the regular time from now!
|
||||
scheduleTimer()
|
||||
return
|
||||
}
|
||||
|
||||
// This check will be aborted if the preference disallows it in AppUpdater
|
||||
let result = await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
// Reset failure count and record successful check
|
||||
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
|
||||
scheduleTimer()
|
||||
Log.info("Update check completed successfully. Next check scheduled in \(Constants.AutomaticUpdateCheckInterval) seconds.")
|
||||
|
||||
case .disabled:
|
||||
// User disabled automatic checks, don't schedule another
|
||||
Log.info("Automatic update checks are disabled. No further checks will be scheduled.")
|
||||
|
||||
case .networkError, .parseError:
|
||||
// Handle failures with exponential backoff
|
||||
handleFailure(result: result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle update check failures with exponential backoff retry logic.
|
||||
*/
|
||||
private func handleFailure(result: UpdateCheckResult) {
|
||||
let currentFailureCount = UserDefaults.standard.integer(
|
||||
forKey: PersistentAppState.updateCheckFailureCount.rawValue
|
||||
)
|
||||
let newFailureCount = currentFailureCount + 1
|
||||
|
||||
UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
|
||||
let retryInterval: TimeInterval
|
||||
if newFailureCount <= Constants.UpdateCheckRetryIntervals.count {
|
||||
// Use exponential backoff
|
||||
retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1]
|
||||
Log.info("Update check failed (\(result)). Retry attempt \(newFailureCount) scheduled in \(retryInterval) seconds.")
|
||||
} else {
|
||||
// Exceeded max retries, fall back to normal schedule and reset counter
|
||||
retryInterval = Constants.AutomaticUpdateCheckInterval
|
||||
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
Log.info("Update check failed (\(result)). Max retries exceeded. Falling back to normal schedule in \(retryInterval) seconds.")
|
||||
}
|
||||
|
||||
scheduleTimer(after: retryInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
Determine whether another automatic update check should occur based on the last check timestamp.
|
||||
Returns true if a check should happen, false otherwise.
|
||||
*/
|
||||
private func isNotThrottled() -> Bool {
|
||||
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
|
||||
let lastCheckTime = UserDefaults.standard.object(
|
||||
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
|
||||
) as? Date
|
||||
|
||||
// If no previous check or last check was > minimum time frame, should check now
|
||||
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
|
||||
}
|
||||
|
||||
/**
|
||||
Schedule a timer to perform an update check after the specified interval.
|
||||
*/
|
||||
private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) {
|
||||
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
|
||||
Task {
|
||||
Log.info("Performing scheduled update check after \(interval) seconds.")
|
||||
await self.performUpdateCheck()
|
||||
}
|
||||
}
|
||||
|
||||
Log.info("A new update check will occur in \(interval) seconds from now.")
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ extension MainMenu {
|
||||
}
|
||||
} else {
|
||||
// Check for updates
|
||||
await performAutomaticUpdateCheck()
|
||||
await UpdateScheduler.shared.startAutomaticUpdateChecking()
|
||||
|
||||
// Check if the linked version has changed between launches of phpmon
|
||||
await PhpGuard().compareToLastGlobalVersion()
|
||||
@@ -205,60 +205,4 @@ extension MainMenu {
|
||||
|
||||
Log.info("Detected applications: \(appNames)")
|
||||
}
|
||||
|
||||
/**
|
||||
Perform an automatic update check and schedule the next one.
|
||||
*/
|
||||
private func performAutomaticUpdateCheck() async {
|
||||
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
|
||||
// The user has chosen not to receive update notifications
|
||||
return
|
||||
}
|
||||
|
||||
guard automaticUpdateCheckIsNotThrottled() else {
|
||||
// If we are throttled, just schedule a regular check 24 hours from now
|
||||
scheduleUpdateCheckTimer()
|
||||
return
|
||||
}
|
||||
|
||||
await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
|
||||
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
|
||||
|
||||
scheduleUpdateCheckTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
Determine whether another automatic update check should occur based on the last check timestamp.
|
||||
Returns true if a check should happen, false otherwise.
|
||||
*/
|
||||
private func automaticUpdateCheckIsNotThrottled() -> Bool {
|
||||
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
|
||||
let lastCheckTime = UserDefaults.standard.object(
|
||||
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
|
||||
) as? Date
|
||||
|
||||
// If no previous check or last check was > minimum time frame, should check now
|
||||
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
|
||||
}
|
||||
|
||||
/**
|
||||
Schedule a timer to perform an update check after the specified interval.
|
||||
*/
|
||||
private func scheduleUpdateCheckTimer() {
|
||||
let interval = Constants.AutomaticUpdateCheckInterval
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
|
||||
Task {
|
||||
Log.info("Performing scheduled update check after \(interval) seconds.")
|
||||
await self.performAutomaticUpdateCheck()
|
||||
}
|
||||
}
|
||||
|
||||
Log.info("A new update check will occur in \(interval) seconds from now.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ enum PersistentAppState: String {
|
||||
case wasLaunchedBefore = "launched_before"
|
||||
case lastAutomaticUpdateCheck = "last_automatic_update_check"
|
||||
case userFavorites = "user_favorites"
|
||||
case updateCheckFailureCount = "update_check_failure_count"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -50,7 +50,6 @@ class AppearancePreferencesVC: GenericPreferenceVC {
|
||||
|
||||
class MenuStructurePreferencesVC: GenericPreferenceVC {
|
||||
|
||||
// swiftlint:disable line_length
|
||||
public static func fromStoryboard() -> GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
@@ -67,7 +66,6 @@ class MenuStructurePreferencesVC: GenericPreferenceVC {
|
||||
.addView(when: true, vc.displayFeature("prefs.display_misc", .displayMisc))
|
||||
.addView(when: true, vc.displayFeature("prefs.display_driver", .displayDriver))
|
||||
}
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
|
||||
class NotificationPreferencesVC: GenericPreferenceVC {
|
||||
|
||||
126
phpmon/Domain/Services/AutomaticUpdateService.swift
Normal file
126
phpmon/Domain/Services/AutomaticUpdateService.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// AutomaticUpdateService.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 26/09/2025.
|
||||
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
actor UpdateScheduler {
|
||||
static let shared = UpdateScheduler()
|
||||
|
||||
private var currentTimer: Timer?
|
||||
|
||||
private init() {}
|
||||
|
||||
/**
|
||||
Start the automatic update checking process. This should be called once during app startup.
|
||||
*/
|
||||
func startAutomaticUpdateChecking() async {
|
||||
await performUpdateCheck()
|
||||
}
|
||||
|
||||
/**
|
||||
Perform an automatic update check and schedule the next one.
|
||||
*/
|
||||
private func performUpdateCheck() async {
|
||||
guard isNotThrottled() else {
|
||||
// If we are throttled, just schedule a regular check 24 hours from now.
|
||||
scheduleTimer()
|
||||
return
|
||||
}
|
||||
|
||||
let result = await AppUpdater().checkForUpdates(userInitiated: false)
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
// Reset failure count and record successful check
|
||||
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
UserDefaults.standard.set(Date(), forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue)
|
||||
scheduleTimer()
|
||||
Log.info("Update check succeeded. Next check in \(Constants.AutomaticUpdateCheckInterval)s.")
|
||||
|
||||
case .disabled:
|
||||
// User disabled automatic checks, don't schedule another
|
||||
Log.info("Automatic update checks disabled. No further checks scheduled.")
|
||||
|
||||
case .networkError, .parseError:
|
||||
// Handle failures with exponential backoff
|
||||
handleFailure(result: result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle update check failures with exponential backoff retry logic.
|
||||
*/
|
||||
private func handleFailure(result: UpdateCheckResult) {
|
||||
let currentFailureCount = UserDefaults.standard.integer(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
let newFailureCount = currentFailureCount + 1
|
||||
|
||||
UserDefaults.standard.set(newFailureCount, forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
|
||||
let retryInterval: TimeInterval
|
||||
if newFailureCount <= Constants.UpdateCheckRetryIntervals.count {
|
||||
// Use exponential backoff
|
||||
retryInterval = Constants.UpdateCheckRetryIntervals[newFailureCount - 1]
|
||||
Log.info("Update check failed (\(result)). Retry \(newFailureCount) in \(retryInterval)s.")
|
||||
} else {
|
||||
// Exceeded max retries, fall back to normal schedule and reset counter
|
||||
retryInterval = Constants.AutomaticUpdateCheckInterval
|
||||
UserDefaults.standard.removeObject(forKey: PersistentAppState.updateCheckFailureCount.rawValue)
|
||||
Log.info("Update check failed (\(result)). Max retries exceeded. Normal schedule in \(retryInterval)s.")
|
||||
}
|
||||
|
||||
scheduleTimer(after: retryInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
Determine whether another automatic update check should occur based on the last check timestamp.
|
||||
Returns true if a check should happen, false otherwise.
|
||||
*/
|
||||
private func isNotThrottled() -> Bool {
|
||||
guard Preferences.isEnabled(.automaticBackgroundUpdateCheck) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let minimumTimeAgo = Date().addingTimeInterval(-Constants.MinimumUpdateCheckInterval)
|
||||
let lastCheckTime = UserDefaults.standard.object(
|
||||
forKey: PersistentAppState.lastAutomaticUpdateCheck.rawValue
|
||||
) as? Date
|
||||
|
||||
// If no previous check or last check was > minimum time frame, should check now
|
||||
return lastCheckTime == nil || lastCheckTime! < minimumTimeAgo
|
||||
}
|
||||
|
||||
/**
|
||||
Schedule a timer to perform an update check after the specified interval.
|
||||
*/
|
||||
private func scheduleTimer(after interval: TimeInterval = Constants.AutomaticUpdateCheckInterval) {
|
||||
// Invalidate any existing timer
|
||||
currentTimer?.invalidate()
|
||||
|
||||
// Ensure timer is scheduled on main run loop since actors run on background threads
|
||||
Task { @MainActor in
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
|
||||
Task {
|
||||
Log.info("Performing scheduled update check after \(interval)s.")
|
||||
await self.performUpdateCheck()
|
||||
}
|
||||
}
|
||||
|
||||
// Store timer reference back in actor
|
||||
await self.setCurrentTimer(timer)
|
||||
}
|
||||
|
||||
Log.info("Next update check scheduled in \(interval)s.")
|
||||
}
|
||||
|
||||
/**
|
||||
Set the current timer reference. Used to store timer from main thread back to actor.
|
||||
*/
|
||||
private func setCurrentTimer(_ timer: Timer) {
|
||||
currentTimer = timer
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable colon
|
||||
class TestableConfigurations {
|
||||
/** A functional, working system setup that is compatible with PHP Monitor. */
|
||||
static var working: TestableConfiguration {
|
||||
@@ -179,7 +180,9 @@ class TestableConfigurations {
|
||||
: .delayed(0.2, "OK"),
|
||||
"ln -sF ~/.config/valet/valet84.sock ~/.config/valet/valet.sock"
|
||||
: .instant("OK"),
|
||||
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae": .delayed(2.0, """
|
||||
"/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae"
|
||||
: .delayed(2.0,
|
||||
"""
|
||||
{
|
||||
"formulae": [
|
||||
{
|
||||
@@ -199,7 +202,7 @@ class TestableConfigurations {
|
||||
commandOutput: [
|
||||
"/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M",
|
||||
"/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M",
|
||||
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M",
|
||||
"/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M"
|
||||
],
|
||||
preferenceOverrides: [
|
||||
.automaticBackgroundUpdateCheck: false
|
||||
@@ -223,6 +226,7 @@ class TestableConfigurations {
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
// swiftlint:enable colon
|
||||
|
||||
class ShellStrings {
|
||||
static var shared = ShellStrings()
|
||||
|
||||
@@ -17,7 +17,7 @@ class FeatureTestCase: XCTestCase {
|
||||
return fs as! TestableFileSystem
|
||||
}
|
||||
|
||||
fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem` to use the fake filesystem.")
|
||||
fatalError("The active filesystem is not a TestableFileSystem. Please use `ActiveFileSystem`.")
|
||||
}
|
||||
|
||||
public func assertFileSystemHas(
|
||||
@@ -44,6 +44,4 @@ class FeatureTestCase: XCTestCase {
|
||||
) {
|
||||
XCTAssertEqual(contents, fakeFileSystem.files[path]?.content, file: file, line: line)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ final class DomainsListTest: UITestCase {
|
||||
searchField.click()
|
||||
searchField.typeText("non-existent thing")
|
||||
Thread.sleep(forTimeInterval: 0.2)
|
||||
XCTAssertTrue(window.tables.tableRows.count == 0)
|
||||
XCTAssertTrue(window.tables.tableRows.count == 0) // swiftlint:disable:this empty_count
|
||||
|
||||
searchField.clearText()
|
||||
searchField.click()
|
||||
|
||||
@@ -26,7 +26,7 @@ final class StartupTest: UITestCase {
|
||||
assertAllExist([
|
||||
app.dialogs["generic.notice".localized],
|
||||
app.staticTexts["startup.errors.php_binary.title".localized],
|
||||
app.buttons["generic.ok".localized],
|
||||
app.buttons["generic.ok".localized]
|
||||
])
|
||||
click(app.buttons["generic.ok".localized])
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ final class ExtensionEnumeratorTest: XCTestCase {
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.1.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.2.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.3.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>"),
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-extensions/Formula/xdebug@8.4.rb": .fake(.text, "<test>")
|
||||
])
|
||||
}
|
||||
|
||||
@@ -37,5 +37,4 @@ final class ExtensionEnumeratorTest: XCTestCase {
|
||||
XCTAssertEqual(formulae["8.3"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.3")])
|
||||
XCTAssertEqual(formulae["8.4"], [BrewPhpExtension(path: "/", name: "xdebug", phpVersion: "8.4")])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ class ValetRcTest: XCTestCase {
|
||||
.url(forResource: "valetrc", withExtension: "broken")!
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Tests
|
||||
|
||||
func test_can_extract_fields_from_valetrc_file() throws {
|
||||
|
||||
@@ -71,8 +71,6 @@ class RealFileSystemTest: XCTestCase {
|
||||
return executablePath
|
||||
}
|
||||
|
||||
|
||||
|
||||
func test_can_read_file_as_text() {
|
||||
let temporaryDirectory = self.createUniqueTemporaryDirectory()
|
||||
let executable = self.createTestBinaryFile(temporaryDirectory)
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestableShellTest: XCTestCase {
|
||||
|
||||
XCTAssertEqual("Hello world\nGoodbye world", output.out)
|
||||
}
|
||||
|
||||
|
||||
func test_fake_shell_synchronous_output() {
|
||||
let greeting = BatchFakeShellOutput(items: [
|
||||
.instant("Hello world\n"),
|
||||
|
||||
@@ -12,7 +12,7 @@ class TestableConfigurationTest: XCTestCase {
|
||||
func test_configuration_can_be_saved_as_json() async {
|
||||
// WORKING
|
||||
var configuration = TestableConfigurations.working
|
||||
|
||||
|
||||
try! configuration.toJson().write(
|
||||
toFile: NSHomeDirectory() + "/.phpmon_fconf_working.json",
|
||||
atomically: true,
|
||||
@@ -38,4 +38,3 @@ class TestableConfigurationTest: XCTestCase {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import XCTest
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
// swiftlint:disable type_body_length file_length
|
||||
class PhpVersionNumberTest: XCTestCase {
|
||||
|
||||
func test_can_deconstruct_php_version() throws {
|
||||
@@ -51,7 +51,6 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
XCTAssertEqual(version!.minor, 0)
|
||||
}
|
||||
|
||||
|
||||
func test_can_check_wildcard_version_constraint() throws {
|
||||
// Wildcard for patch only
|
||||
XCTAssertEqual(
|
||||
@@ -408,3 +407,4 @@ class PhpVersionNumberTest: XCTestCase {
|
||||
)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable type_body_length file_length
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Testing
|
||||
|
||||
@Suite("Commands")
|
||||
@Suite("Commands")
|
||||
struct CommandTest {
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,7 +18,7 @@ struct ValetConfigurationTest {
|
||||
)!
|
||||
}
|
||||
|
||||
@Test("Can load config file")
|
||||
@Test("Can load config file")
|
||||
func can_load_config_file() throws {
|
||||
let json = try? String(
|
||||
contentsOf: Self.jsonConfigFileUrl,
|
||||
|
||||
Reference in New Issue
Block a user