mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-25 21:50:08 +01:00
♻️ Reworked various startup fixes
Some further testing is required, and an improved UX is also something that needs to be considered.
This commit is contained in:
@@ -56,6 +56,10 @@
|
|||||||
033E9DF82F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
033E9DF82F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
||||||
033E9DF92F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
033E9DF92F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
||||||
033E9DFA2F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
033E9DFA2F44AD5400685F62 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DF62F44AD5200685F62 /* AppleScript.swift */; };
|
||||||
|
033E9DFC2F44D93000685F62 /* Startup+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */; };
|
||||||
|
033E9DFD2F44D93000685F62 /* Startup+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */; };
|
||||||
|
033E9DFE2F44D93000685F62 /* Startup+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */; };
|
||||||
|
033E9DFF2F44D93000685F62 /* Startup+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */; };
|
||||||
035983A12E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
035983A12E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
||||||
035983A22E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
035983A22E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
||||||
035983A32E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
035983A32E97FA9100218DC7 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
|
||||||
@@ -1046,6 +1050,7 @@
|
|||||||
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = "<group>"; };
|
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpExtensionCommand.swift; sourceTree = "<group>"; };
|
||||||
033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpExtensionManagerView+Actions.swift"; sourceTree = "<group>"; };
|
033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhpExtensionManagerView+Actions.swift"; sourceTree = "<group>"; };
|
||||||
033E9DF62F44AD5200685F62 /* AppleScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScript.swift; sourceTree = "<group>"; };
|
033E9DF62F44AD5200685F62 /* AppleScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScript.swift; sourceTree = "<group>"; };
|
||||||
|
033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Alert.swift"; sourceTree = "<group>"; };
|
||||||
034515442EC4F3A000472561 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
034515442EC4F3A000472561 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
034515452EC4F3C000472561 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
034515452EC4F3C000472561 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
034515462EC4FB9100472561 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
034515462EC4FB9100472561 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
@@ -2223,6 +2228,7 @@
|
|||||||
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
|
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
|
||||||
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
|
C4EED88827A48778006D7272 /* InterAppHandler.swift */,
|
||||||
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
C4D8016522B1584700C6DA1B /* Startup.swift */,
|
||||||
|
033E9DFB2F44D92A00685F62 /* Startup+Alert.swift */,
|
||||||
0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */,
|
0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */,
|
||||||
03BFF5262E312C39007F96FA /* Startup+Timers.swift */,
|
03BFF5262E312C39007F96FA /* Startup+Timers.swift */,
|
||||||
C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */,
|
C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */,
|
||||||
@@ -2862,6 +2868,7 @@
|
|||||||
C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */,
|
C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */,
|
||||||
C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */,
|
C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */,
|
||||||
C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,
|
C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,
|
||||||
|
033E9DFF2F44D93000685F62 /* Startup+Alert.swift in Sources */,
|
||||||
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */,
|
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */,
|
||||||
C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
|
C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
|
||||||
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */,
|
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */,
|
||||||
@@ -3259,6 +3266,7 @@
|
|||||||
C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */,
|
C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */,
|
||||||
C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */,
|
C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */,
|
||||||
C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
|
C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
|
||||||
|
033E9DFE2F44D93000685F62 /* Startup+Alert.swift in Sources */,
|
||||||
C471E7F128F9BAC70021E251 /* VersionNumber.swift in Sources */,
|
C471E7F128F9BAC70021E251 /* VersionNumber.swift in Sources */,
|
||||||
C471E7DC28F9BA8F0021E251 /* ShellProtocol.swift in Sources */,
|
C471E7DC28F9BA8F0021E251 /* ShellProtocol.swift in Sources */,
|
||||||
);
|
);
|
||||||
@@ -3350,6 +3358,7 @@
|
|||||||
C471E8C528F9BB8F0021E251 /* AddProxyVC.swift in Sources */,
|
C471E8C528F9BB8F0021E251 /* AddProxyVC.swift in Sources */,
|
||||||
C471E8C628F9BB8F0021E251 /* PMTableView.swift in Sources */,
|
C471E8C628F9BB8F0021E251 /* PMTableView.swift in Sources */,
|
||||||
C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */,
|
C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */,
|
||||||
|
033E9DFC2F44D93000685F62 /* Startup+Alert.swift in Sources */,
|
||||||
C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */,
|
C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */,
|
||||||
C46DC7A72C7B5BCA00F19D17 /* Favorites.swift in Sources */,
|
C46DC7A72C7B5BCA00F19D17 /* Favorites.swift in Sources */,
|
||||||
C471E8C928F9BB8F0021E251 /* PhpDoctorWindowController.swift in Sources */,
|
C471E8C928F9BB8F0021E251 /* PhpDoctorWindowController.swift in Sources */,
|
||||||
@@ -3713,6 +3722,7 @@
|
|||||||
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
|
C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */,
|
||||||
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
|
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */,
|
||||||
039C291D2E8AA39A007F5FAB /* TestableWebApiTest.swift in Sources */,
|
039C291D2E8AA39A007F5FAB /* TestableWebApiTest.swift in Sources */,
|
||||||
|
033E9DFD2F44D93000685F62 /* Startup+Alert.swift in Sources */,
|
||||||
C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */,
|
C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */,
|
||||||
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
|
C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */,
|
||||||
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
|
C4F780B725D80B5D000DBC97 /* App.swift in Sources */,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class Actions {
|
|||||||
+ " && "
|
+ " && "
|
||||||
+ cellarCommands.joined(separator: " && ")
|
+ cellarCommands.joined(separator: " && ")
|
||||||
|
|
||||||
try sudo(script)
|
try AppleScript.runSimpleShellAsAdmin(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Finding Config Files
|
// MARK: - Finding Config Files
|
||||||
|
|||||||
@@ -8,30 +8,62 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/**
|
class AppleScript {
|
||||||
Execute a script with administrative privileges.
|
/**
|
||||||
Returns the output of the script.
|
Execute a simple shell script with administrative privileges (as root).
|
||||||
*/
|
|
||||||
@discardableResult
|
|
||||||
func sudo(_ script: String) throws -> String {
|
|
||||||
let source = "do shell script \"\(script)\" with administrator privileges"
|
|
||||||
|
|
||||||
Log.info("Running script via AppleScript as administrator: `\(source)`")
|
@return Returns the output of the script.
|
||||||
|
*/
|
||||||
let appleScript = NSAppleScript(source: source)
|
@discardableResult
|
||||||
|
public static func runSimpleShellAsAdmin(
|
||||||
var error: NSDictionary?
|
_ script: String
|
||||||
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(&error)
|
) throws -> String {
|
||||||
|
let source = "do shell script \"\(script)\" with administrator privileges"
|
||||||
if let error = error {
|
return try runAppleScript(script: source)
|
||||||
Log.err("AppleScript error: \(error)")
|
|
||||||
throw AdminPrivilegeError(kind: .applescriptNilError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let result = eventResult else {
|
/**
|
||||||
Log.err("Unknown AppleScript error")
|
Execute a shell script with administrative privileges, but sets USER to the current user, and also adds the Homebrew `bin` folder to the PATH.
|
||||||
throw AdminPrivilegeError(kind: .applescriptNilError)
|
|
||||||
|
Using this may be necessary for certain scripts to work correctly, like `valet trust`, which may execute `which php` as part of the PHP script it runs, and thus requires knowledge about the current user and where the PHP binaries are.
|
||||||
|
|
||||||
|
@return The output of the script.
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
public static func runShellAsAdmin(
|
||||||
|
_ script: String,
|
||||||
|
asUser user: String = App.shared.container.paths.whoami,
|
||||||
|
appendToPATH append: String = App.shared.container.paths.binPath,
|
||||||
|
) throws -> String {
|
||||||
|
let script = """
|
||||||
|
export USER=\(user) && \
|
||||||
|
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:\(append) \
|
||||||
|
&& \(script)
|
||||||
|
"""
|
||||||
|
let source = "do shell script \"\(script)\" with administrator privileges"
|
||||||
|
return try runAppleScript(script: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.stringValue ?? ""
|
/**
|
||||||
|
Runs a given AppleScript.
|
||||||
|
*/
|
||||||
|
private static func runAppleScript(script: String) throws -> String {
|
||||||
|
Log.info("Running via AppleScript: `\(script)`")
|
||||||
|
let appleScript = NSAppleScript(source: script)
|
||||||
|
|
||||||
|
var error: NSDictionary?
|
||||||
|
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(&error)
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
Log.err("AppleScript error: \(error)")
|
||||||
|
throw AdminPrivilegeError(kind: .applescriptNilError)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let result = eventResult else {
|
||||||
|
Log.err("Unknown AppleScript error")
|
||||||
|
throw AdminPrivilegeError(kind: .applescriptNilError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stringValue ?? ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,24 +15,27 @@ protocol ShellProtocol {
|
|||||||
var PATH: String { get }
|
var PATH: String { get }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command synchronously. Use with caution.
|
Run a command synchronously. Use with caution!
|
||||||
|
|
||||||
Common usage:
|
Common usage:
|
||||||
```
|
```
|
||||||
let output = Shell.sync("php -v")
|
let output = Shell.sync("php -v")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@return The shell output. If the command times out, returns empty output.
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func sync(_ command: String) -> ShellOutput
|
func sync(_ command: String) -> ShellOutput
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Run a command asynchronously.
|
Run a command asynchronously.
|
||||||
Returns the most relevant output (prefers error output if it exists).
|
|
||||||
|
|
||||||
Common usage:
|
Common usage:
|
||||||
```
|
```
|
||||||
let output = await Shell.pipe("php -v")
|
let output = await Shell.pipe("php -v")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@return The shell output. If the command times out, returns empty output.
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func pipe(_ command: String) async -> ShellOutput
|
func pipe(_ command: String) async -> ShellOutput
|
||||||
@@ -43,7 +46,8 @@ protocol ShellProtocol {
|
|||||||
|
|
||||||
- Parameter command: The command to execute.
|
- Parameter command: The command to execute.
|
||||||
- Parameter timeout: Timeout in seconds. If the command exceeds this, it is terminated.
|
- Parameter timeout: Timeout in seconds. If the command exceeds this, it is terminated.
|
||||||
- Returns: The shell output. If the command times out, returns empty output.
|
|
||||||
|
@return The shell output. If the command times out, returns empty output.
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
|
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
|
||||||
@@ -56,7 +60,8 @@ protocol ShellProtocol {
|
|||||||
(Whether it is complete or not.)
|
(Whether it is complete or not.)
|
||||||
|
|
||||||
Unlike `sync`, `pipe` and `quiet`, you can capture both `stdout` and `stderr` with this mechanism.
|
Unlike `sync`, `pipe` and `quiet`, you can capture both `stdout` and `stderr` with this mechanism.
|
||||||
The end result is still the most relevant output (where error output is preferred if it exists).
|
|
||||||
|
@return A tuple, containing the `Process` and `ShellOutput` objects.
|
||||||
*/
|
*/
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func attach(
|
func attach(
|
||||||
|
|||||||
71
phpmon/Domain/App/Startup+Alert.swift
Normal file
71
phpmon/Domain/App/Startup+Alert.swift
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// Startup+Fixes.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 17/02/2026.
|
||||||
|
// Copyright © 2026 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
import NVAlert
|
||||||
|
|
||||||
|
extension Startup {
|
||||||
|
/**
|
||||||
|
The potential outcome of an environment check failure alert.
|
||||||
|
*/
|
||||||
|
enum EnvironmentAlertOutcome {
|
||||||
|
/** The automatic fix was requested, will try and continue if it worked. */
|
||||||
|
case shouldRunFix
|
||||||
|
|
||||||
|
/** No automatic fix was requested, show alert and require retry of all startup checks. */
|
||||||
|
case shouldRetryStartup
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Displays an alert for a particular check. There are two types of alerts:
|
||||||
|
- ones that require an app restart, which prompt the user to exit the app
|
||||||
|
- ones that allow the app to continue, which allow the user to retry
|
||||||
|
*/
|
||||||
|
@MainActor internal func showAlert(for check: EnvironmentCheck) -> EnvironmentAlertOutcome {
|
||||||
|
// Ensure that the timeout does not fire until we restart
|
||||||
|
Self.startupTimer?.invalidate()
|
||||||
|
|
||||||
|
if check.requiresAppRestart {
|
||||||
|
NVAlert()
|
||||||
|
.withInformation(
|
||||||
|
title: check.titleText,
|
||||||
|
subtitle: check.subtitleText,
|
||||||
|
description: check.descriptionText
|
||||||
|
)
|
||||||
|
.withPrimary(text: check.buttonText, action: { _ in
|
||||||
|
exit(1)
|
||||||
|
}).show(urgency: .bringToFront)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify if an automatic fix is available
|
||||||
|
let hasAutomaticFix = check.fixCommand != nil
|
||||||
|
|
||||||
|
// Present an alert with one or two buttons (depending on fix)
|
||||||
|
let outcome = NVAlert()
|
||||||
|
.withInformation(
|
||||||
|
title: check.titleText,
|
||||||
|
subtitle: check.subtitleText,
|
||||||
|
description: check.descriptionText
|
||||||
|
)
|
||||||
|
.withPrimary(text: hasAutomaticFix ? "startup.fix_for_me".localized : "startup.fix_manually".localized)
|
||||||
|
.withSecondary(if: hasAutomaticFix, text: "startup.fix_manually".localized)
|
||||||
|
.withTertiary(if: hasAutomaticFix, text: "", action: { _ in
|
||||||
|
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
||||||
|
})
|
||||||
|
.runModal(urgency: .bringToFront)
|
||||||
|
|
||||||
|
// If there's an automatic fix and we chose to fix it, return outcome
|
||||||
|
if hasAutomaticFix && outcome == .alertFirstButtonReturn {
|
||||||
|
return .shouldRunFix
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any other situation, we will require a retry of the startup
|
||||||
|
return .shouldRetryStartup
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ class Startup {
|
|||||||
let start = Measurement()
|
let start = Measurement()
|
||||||
if await check.succeeds() {
|
if await check.succeeds() {
|
||||||
Log.info("[PASS] \(check.name) (\(start.milliseconds) ms)")
|
Log.info("[PASS] \(check.name) (\(start.milliseconds) ms)")
|
||||||
continue
|
continue // continue to the next check!
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, something's gone wrong and the check has failed...
|
// If we get here, something's gone wrong and the check has failed...
|
||||||
@@ -48,8 +48,9 @@ class Startup {
|
|||||||
// We will present the user with an option (potentially)
|
// We will present the user with an option (potentially)
|
||||||
let outcome = await showAlert(for: check)
|
let outcome = await showAlert(for: check)
|
||||||
|
|
||||||
|
// If the user requested an automatic fix, do this
|
||||||
if outcome == .shouldRunFix {
|
if outcome == .shouldRunFix {
|
||||||
// First, validate there's a fix
|
// Verify a fix actually exists
|
||||||
guard let command = check.fixCommand else {
|
guard let command = check.fixCommand else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -57,13 +58,16 @@ class Startup {
|
|||||||
// We will try to run the fix, it may fail!
|
// We will try to run the fix, it may fail!
|
||||||
do {
|
do {
|
||||||
try await command(App.shared.container)
|
try await command(App.shared.container)
|
||||||
return await check.succeeds()
|
guard await check.succeeds() else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue // continue to the next check!
|
||||||
} catch {
|
} catch {
|
||||||
// our fix failed :(
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No fix requested, this is just a failure
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -78,58 +82,6 @@ class Startup {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EnvironmentAlertOutcome {
|
|
||||||
case shouldRunFix
|
|
||||||
case shouldRetryStartup
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Displays an alert for a particular check. There are two types of alerts:
|
|
||||||
- ones that require an app restart, which prompt the user to exit the app
|
|
||||||
- ones that allow the app to continue, which allow the user to retry
|
|
||||||
*/
|
|
||||||
@MainActor private func showAlert(for check: EnvironmentCheck) -> EnvironmentAlertOutcome {
|
|
||||||
// Ensure that the timeout does not fire until we restart
|
|
||||||
Self.startupTimer?.invalidate()
|
|
||||||
|
|
||||||
if check.requiresAppRestart {
|
|
||||||
NVAlert()
|
|
||||||
.withInformation(
|
|
||||||
title: check.titleText,
|
|
||||||
subtitle: check.subtitleText,
|
|
||||||
description: check.descriptionText
|
|
||||||
)
|
|
||||||
.withPrimary(text: check.buttonText, action: { _ in
|
|
||||||
exit(1)
|
|
||||||
}).show(urgency: .bringToFront)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if an automatic fix is available
|
|
||||||
let hasAutomaticFix = check.fixCommand != nil
|
|
||||||
|
|
||||||
// Present an alert with one or two buttons (depending on fix)
|
|
||||||
let outcome = NVAlert()
|
|
||||||
.withInformation(
|
|
||||||
title: check.titleText,
|
|
||||||
subtitle: check.subtitleText,
|
|
||||||
description: check.descriptionText
|
|
||||||
)
|
|
||||||
.withPrimary(text: hasAutomaticFix ? "startup.fix_for_me".localized : "startup.fix_manually".localized)
|
|
||||||
.withSecondary(if: hasAutomaticFix, text: "startup.fix_manually".localized)
|
|
||||||
.withTertiary(if: hasAutomaticFix, text: "", action: { _ in
|
|
||||||
NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions)
|
|
||||||
})
|
|
||||||
.runModal(urgency: .bringToFront)
|
|
||||||
|
|
||||||
// If there's an automatic fix and we chose to fix it, return outcome
|
|
||||||
if hasAutomaticFix && outcome == .alertFirstButtonReturn {
|
|
||||||
return .shouldRunFix
|
|
||||||
}
|
|
||||||
|
|
||||||
// In any other situation, we will require a retry of the startup
|
|
||||||
return .shouldRetryStartup
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Check (List)
|
// MARK: - Check (List)
|
||||||
|
|
||||||
public var groups: [EnvironmentCheckGroup] = [
|
public var groups: [EnvironmentCheckGroup] = [
|
||||||
@@ -161,6 +113,10 @@ class Startup {
|
|||||||
return await !container.shell
|
return await !container.shell
|
||||||
.pipe("ls \(container.paths.optPath) | grep php").out.contains("php")
|
.pipe("ls \(container.paths.optPath) | grep php").out.contains("php")
|
||||||
},
|
},
|
||||||
|
fix: { container in
|
||||||
|
let brew = container.paths.brew
|
||||||
|
await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) install shivammathur/php/php")
|
||||||
|
},
|
||||||
name: "`ls \(App.shared.container.paths.optPath) | grep php` returned php result",
|
name: "`ls \(App.shared.container.paths.optPath) | grep php` returned php result",
|
||||||
titleText: "startup.errors.php_opt.title".localized,
|
titleText: "startup.errors.php_opt.title".localized,
|
||||||
subtitleText: "startup.errors.php_opt.subtitle".localized(
|
subtitleText: "startup.errors.php_opt.subtitle".localized(
|
||||||
@@ -175,6 +131,11 @@ class Startup {
|
|||||||
command: { container in
|
command: { container in
|
||||||
return !container.filesystem.fileExists(container.paths.php)
|
return !container.filesystem.fileExists(container.paths.php)
|
||||||
},
|
},
|
||||||
|
fix: { container in
|
||||||
|
// See if we can't link PHP
|
||||||
|
let brew = container.paths.brew
|
||||||
|
await container.shell.pipe("\(brew) link php")
|
||||||
|
},
|
||||||
name: "`\(App.shared.container.paths.php)` exists",
|
name: "`\(App.shared.container.paths.php)` exists",
|
||||||
titleText: "startup.errors.php_binary.title".localized,
|
titleText: "startup.errors.php_binary.title".localized,
|
||||||
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
subtitleText: "startup.errors.php_binary.subtitle".localized,
|
||||||
@@ -188,6 +149,10 @@ class Startup {
|
|||||||
return await container.shell.pipe("\(container.paths.binPath)/php -v").err
|
return await container.shell.pipe("\(container.paths.binPath)/php -v").err
|
||||||
.contains("Library not loaded")
|
.contains("Library not loaded")
|
||||||
},
|
},
|
||||||
|
fix: { container in
|
||||||
|
let brew = App.shared.container.paths.brew
|
||||||
|
await container.shell.pipe("\(brew) tap shivammathur/php && \(brew) reinstall shivammathur/php/php && \(brew) link php")
|
||||||
|
},
|
||||||
name: "no `dyld` issue (`Library not loaded`) detected",
|
name: "no `dyld` issue (`Library not loaded`) detected",
|
||||||
titleText: "startup.errors.dyld_library.title".localized,
|
titleText: "startup.errors.dyld_library.title".localized,
|
||||||
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
subtitleText: "startup.errors.dyld_library.subtitle".localized(
|
||||||
@@ -219,6 +184,10 @@ class Startup {
|
|||||||
await container.phpEnvs.determinePhpAlias()
|
await container.phpEnvs.determinePhpAlias()
|
||||||
return PhpEnvironments.brewPhpAlias == nil
|
return PhpEnvironments.brewPhpAlias == nil
|
||||||
},
|
},
|
||||||
|
fix: { container in
|
||||||
|
let brew = container.paths.brew
|
||||||
|
await container.shell.pipe("\(brew) update")
|
||||||
|
},
|
||||||
name: "`brew` alias is not nil and valid",
|
name: "`brew` alias is not nil and valid",
|
||||||
titleText: "startup.errors.could_not_determine_alias.title".localized,
|
titleText: "startup.errors.could_not_determine_alias.title".localized,
|
||||||
subtitleText: "startup.errors.could_not_determine_alias.subtitle".localized,
|
subtitleText: "startup.errors.could_not_determine_alias.subtitle".localized,
|
||||||
@@ -253,7 +222,8 @@ class Startup {
|
|||||||
.out.contains(container.paths.brew)
|
.out.contains(container.paths.brew)
|
||||||
},
|
},
|
||||||
fix: { container in
|
fix: { container in
|
||||||
try sudo("export USER=\(container.paths.whoami) && export PATH=/bin:/usr/bin:\(container.paths.binPath) && valet trust")
|
let valet = container.paths.binPath.appending("/valet")
|
||||||
|
try AppleScript.runShellAsAdmin("\(valet) trust")
|
||||||
},
|
},
|
||||||
name: "`/private/etc/sudoers.d/brew` contains brew",
|
name: "`/private/etc/sudoers.d/brew` contains brew",
|
||||||
titleText: "startup.errors.sudoers_brew.title".localized,
|
titleText: "startup.errors.sudoers_brew.title".localized,
|
||||||
@@ -277,6 +247,10 @@ class Startup {
|
|||||||
command: { container in
|
command: { container in
|
||||||
return !container.filesystem.directoryExists("~/.config/valet")
|
return !container.filesystem.directoryExists("~/.config/valet")
|
||||||
},
|
},
|
||||||
|
fix: { container in
|
||||||
|
let valet = container.paths.binPath.appending("/valet")
|
||||||
|
await container.shell.pipe("\(valet) install")
|
||||||
|
},
|
||||||
name: "`.config/valet` not empty (Valet installed)",
|
name: "`.config/valet` not empty (Valet installed)",
|
||||||
titleText: "startup.errors.valet_not_installed.title".localized,
|
titleText: "startup.errors.valet_not_installed.title".localized,
|
||||||
subtitleText: "startup.errors.valet_not_installed.subtitle".localized,
|
subtitleText: "startup.errors.valet_not_installed.subtitle".localized,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class BrewPermissionFixer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let script = buildBrokenFormulaeScript()
|
let script = buildBrokenFormulaeScript()
|
||||||
try sudo(script)
|
try AppleScript.runSimpleShellAsAdmin(script)
|
||||||
|
|
||||||
Log.info("Ownership was taken of the folder(s) at: " + broken
|
Log.info("Ownership was taken of the folder(s) at: " + broken
|
||||||
.map({ $0.path })
|
.map({ $0.path })
|
||||||
|
|||||||
Reference in New Issue
Block a user