1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 11:10:08 +01:00

♻️ Make ValetServicesManager more crash-resistant

This commit is contained in:
2025-11-07 13:35:30 +01:00
parent 155096839d
commit 21b669a97e
3 changed files with 61 additions and 28 deletions

View File

@@ -3920,7 +3920,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1681; CURRENT_PROJECT_VERSION = 1682;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG = YES; DEBUG = YES;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
@@ -3964,7 +3964,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1681; CURRENT_PROJECT_VERSION = 1682;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG = NO; DEBUG = NO;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
@@ -4146,7 +4146,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1681; CURRENT_PROJECT_VERSION = 1682;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG = YES; DEBUG = YES;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
@@ -4339,7 +4339,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1681; CURRENT_PROJECT_VERSION = 1682;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG = NO; DEBUG = NO;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;

View File

@@ -62,7 +62,7 @@ func grepContains(
Attempts to introduce sleep for a particular duration. Use with caution. Attempts to introduce sleep for a particular duration. Use with caution.
*/ */
func delay(seconds: Double) async { func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
} }
/** /**

View File

@@ -34,49 +34,45 @@ class ValetServicesManager: ServicesManager {
This method allows us to reload the Homebrew services, but we run this command This method allows us to reload the Homebrew services, but we run this command
twice (once for user services, and once for root services). Please note that twice (once for user services, and once for root services). Please note that
these two commands are executed concurrently. these two commands are executed concurrently.
If this fails, question marks will be displayed in the menu bar and we will
try one more time to reload the services.
*/ */
override func reloadServicesStatus() async { override func reloadServicesStatus() async {
await reloadServicesStatus(isRetry: false)
}
private func reloadServicesStatus(isRetry: Bool) async {
if !Valet.installed { if !Valet.installed {
return Log.info("Not reloading services because running in Standalone Mode.") return Log.info("Not reloading services because running in Standalone Mode.")
} }
await withTaskGroup(of: [HomebrewService].self, body: { group in await withTaskGroup(of: [HomebrewService].self, body: { group in
// First, retrieve the status of the formulae that run as root // Retrieve the status of the formulae that run as root
group.addTask { group.addTask {
let rootServiceNames = self.formulae await self.fetchHomebrewServices(elevated: true)
.filter { $0.elevated }
.map { $0.name }
let rootJson = await self.container.shell
.pipe("sudo \(self.container.paths.brew) services info --all --json")
.out.data(using: .utf8)!
return try! JSONDecoder()
.decode([HomebrewService].self, from: rootJson)
.filter({ return rootServiceNames.contains($0.name) })
} }
// At the same time, retrieve the status of the formulae that run as user // At the same time, retrieve the status of the formulae that run as user
group.addTask { group.addTask {
let userServiceNames = self.formulae await self.fetchHomebrewServices(elevated: false)
.filter { !$0.elevated }
.map { $0.name }
let normalJson = await self.container.shell
.pipe("\(self.container.paths.brew) services info --all --json")
.out.data(using: .utf8)!
return try! JSONDecoder()
.decode([HomebrewService].self, from: normalJson)
.filter({ return userServiceNames.contains($0.name) })
} }
// Ensure that Homebrew services' output is stored // Ensure that Homebrew services' output is stored
self.homebrewServices = [] self.homebrewServices = []
for await services in group { for await services in group {
homebrewServices.append(contentsOf: services) homebrewServices.append(contentsOf: services)
} }
// If we didn't get any service data and this isn't a retry, try again
if self.homebrewServices.isEmpty && !isRetry {
Log.warn("Failed to retrieve any Homebrew services data. Retrying once in 2 seconds...")
await delay(seconds: 2)
await self.reloadServicesStatus(isRetry: true)
return
}
// Dispatch the update of the new service wrappers // Dispatch the update of the new service wrappers
Task { @MainActor in Task { @MainActor in
// Ensure both commands complete (but run concurrently) // Ensure both commands complete (but run concurrently)
@@ -95,6 +91,43 @@ class ValetServicesManager: ServicesManager {
}) })
} }
/**
Fetches Homebrew services information for either elevated (root) or user services.
- Parameter elevated: Whether to fetch services running as root (true) or user (false)
- Returns: Array of HomebrewService objects, or empty array if fetching fails
*/
private func fetchHomebrewServices(elevated: Bool) async -> [HomebrewService] {
// Check which formulae we are supposed to be looking for
let serviceNames = self.formulae
.filter { $0.elevated == elevated }
.map { $0.name }
// Determine which command to run
let command = elevated
? "sudo \(self.container.paths.brew) services info --all --json"
: "\(self.container.paths.brew) services info --all --json"
// Run and get the output of the command
let output = await self.container.shell.pipe(command).out
// Attempt to parse the output
guard let jsonData = output.data(using: .utf8) else {
Log.err("Failed to convert \(elevated ? "root" : "user") services output to UTF-8 data. Output: \(output)")
return []
}
// Attempt to decode the JSON output. In certain situations the output may not be valid and this prevents a crash
do {
return try JSONDecoder()
.decode([HomebrewService].self, from: jsonData)
.filter { serviceNames.contains($0.name) }
} catch {
Log.err("Failed to decode \(elevated ? "root" : "user") services JSON: \(error). Output: \(output)")
return []
}
}
override func toggleService(named: String) async { override func toggleService(named: String) async {
guard let wrapper = self[named] else { guard let wrapper = self[named] else {
return Log.err("The wrapper for '\(named)' is missing.") return Log.err("The wrapper for '\(named)' is missing.")