1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 20:10:08 +02:00

🏗 Remove LegacyShell entirely

This commit is contained in:
2022-10-06 22:55:05 +02:00
parent ad41e3891e
commit e2ab7f08ed
37 changed files with 359 additions and 584 deletions

View File

@ -103,8 +103,6 @@
C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nginx-site.test */; }; C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nginx-site.test */; };
C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */; }; C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */; };
C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */; }; C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */; };
C42E3BF428A9BF5100AFECFC /* LegacyShell+PATH.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E3BF328A9BF5100AFECFC /* LegacyShell+PATH.swift */; };
C42E3BF528A9BF5100AFECFC /* LegacyShell+PATH.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E3BF328A9BF5100AFECFC /* LegacyShell+PATH.swift */; };
C42F26732805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; }; C42F26732805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; };
C42F26742805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; }; C42F26742805B4B400938AC7 /* DomainListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* DomainListable.swift */; };
C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */; }; C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */; };
@ -224,8 +222,6 @@
C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; }; C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; };
C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; C4B5853E2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; };
C4B585412770FE3900DA4FBE /* LegacyShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* LegacyShell.swift */; };
C4B585422770FE3900DA4FBE /* LegacyShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853C2770FE3900DA4FBE /* LegacyShell.swift */; };
C4B585442770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; }; C4B585442770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B585452770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; }; C4B585452770FE3900DA4FBE /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* Command.swift */; };
C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B609192853AAD300C95265 /* SectionHeaderView.swift */; }; C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B609192853AAD300C95265 /* SectionHeaderView.swift */; };
@ -406,7 +402,6 @@
C42CFB1527DFDE7900862737 /* nginx-site.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site.test"; sourceTree = "<group>"; }; C42CFB1527DFDE7900862737 /* nginx-site.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site.test"; sourceTree = "<group>"; };
C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site-isolated.test"; sourceTree = "<group>"; }; C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site-isolated.test"; sourceTree = "<group>"; };
C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationTest.swift; sourceTree = "<group>"; }; C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationTest.swift; sourceTree = "<group>"; };
C42E3BF328A9BF5100AFECFC /* LegacyShell+PATH.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LegacyShell+PATH.swift"; sourceTree = "<group>"; };
C42F26722805B4B400938AC7 /* DomainListable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListable.swift; sourceTree = "<group>"; }; C42F26722805B4B400938AC7 /* DomainListable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListable.swift; sourceTree = "<group>"; };
C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy.test"; sourceTree = "<group>"; }; C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy.test"; sourceTree = "<group>"; };
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; }; C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = "<group>"; };
@ -470,7 +465,6 @@
C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; }; C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = "<group>"; };
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = "<group>"; }; C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = "<group>"; };
C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; }; C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = "<group>"; };
C4B5853C2770FE3900DA4FBE /* LegacyShell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyShell.swift; sourceTree = "<group>"; };
C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; }; C4B5853D2770FE3900DA4FBE /* Command.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = "<group>"; };
C4B609192853AAD300C95265 /* SectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = "<group>"; }; C4B609192853AAD300C95265 /* SectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = "<group>"; };
C4B6091C2853AB9700C95265 /* ServicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; }; C4B6091C2853AB9700C95265 /* ServicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = "<group>"; };
@ -897,15 +891,6 @@
path = Next; path = Next;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C46EBC4C28DB9F43007ACC74 /* Pending Removal */ = {
isa = PBXGroup;
children = (
C4B5853C2770FE3900DA4FBE /* LegacyShell.swift */,
C42E3BF328A9BF5100AFECFC /* LegacyShell+PATH.swift */,
);
path = "Pending Removal";
sourceTree = "<group>";
};
C47331A0247093AC009A0597 /* Menu */ = { C47331A0247093AC009A0597 /* Menu */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1007,7 +992,6 @@
C4B5853A2770FE2500DA4FBE /* Common */ = { C4B5853A2770FE2500DA4FBE /* Common */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C46EBC4C28DB9F43007ACC74 /* Pending Removal */,
C40C7F2127721F7300DDDCDC /* Core */, C40C7F2127721F7300DDDCDC /* Core */,
54B20EDF263AA22C00D3250E /* PHP */, 54B20EDF263AA22C00D3250E /* PHP */,
C44CCD4327AFE93300CE40E5 /* Errors */, C44CCD4327AFE93300CE40E5 /* Errors */,
@ -1383,7 +1367,6 @@
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */, C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */, C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */,
C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */,
C4B585412770FE3900DA4FBE /* LegacyShell.swift in Sources */,
C4998F0A2617633900B2526E /* PreferencesWindowController.swift in Sources */, C4998F0A2617633900B2526E /* PreferencesWindowController.swift in Sources */,
C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */, C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
@ -1509,7 +1492,6 @@
C40508AF28ADA23D008FAC1F /* NoDomainResultsView.swift in Sources */, C40508AF28ADA23D008FAC1F /* NoDomainResultsView.swift in Sources */,
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */,
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
C42E3BF428A9BF5100AFECFC /* LegacyShell+PATH.swift in Sources */,
C42337A3281F19F000459A48 /* Xdebug.swift in Sources */, C42337A3281F19F000459A48 /* Xdebug.swift in Sources */,
C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */, C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */,
@ -1563,7 +1545,6 @@
C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */,
C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */,
C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */, C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */,
C42E3BF528A9BF5100AFECFC /* LegacyShell+PATH.swift in Sources */,
C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */,
C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */,
C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F319C927B034A500AFF46F /* Stats.swift in Sources */,
@ -1668,7 +1649,6 @@
C4A6957728D23EE300A14CF8 /* EnvironmentManager.swift in Sources */, C4A6957728D23EE300A14CF8 /* EnvironmentManager.swift in Sources */,
C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4A81CA528C67101008DD9D1 /* PMTableView.swift in Sources */, C4A81CA528C67101008DD9D1 /* PMTableView.swift in Sources */,
C4B585422770FE3900DA4FBE /* LegacyShell.swift in Sources */,
C45E76152854A65300B4FE0C /* ServicesManager.swift in Sources */, C45E76152854A65300B4FE0C /* ServicesManager.swift in Sources */,
C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */, C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */,
C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */, C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */,

View File

@ -12,22 +12,22 @@ class Actions {
// MARK: - Services // MARK: - Services
public static func restartPhpFpm() { public static func restartPhpFpm() async {
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true) await brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
} }
public static func restartNginx() { public static func restartNginx() async {
brew("services restart nginx", sudo: true) await brew("services restart nginx", sudo: true)
} }
public static func restartDnsMasq() { public static func restartDnsMasq() async {
brew("services restart dnsmasq", sudo: true) await brew("services restart dnsmasq", sudo: true)
} }
public static func stopValetServices() { public static func stopValetServices() async {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true) await brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true) await brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true) await brew("services stop dnsmasq", sudo: true)
} }
public static func fixHomebrewPermissions() throws { public static func fixHomebrewPermissions() throws {
@ -65,26 +65,20 @@ class Actions {
} }
// MARK: - Third Party Services // MARK: - Third Party Services
public static func stopService(name: String, completion: @escaping () -> Void) { public static func stopService(name: String) async {
DispatchQueue.global(qos: .userInitiated).async { await brew(
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name }) "services stop \(name)",
ServicesManager.loadHomebrewServices(completed: { sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name }
DispatchQueue.main.async { )
completion() await ServicesManager.loadHomebrewServices()
}
})
}
} }
public static func startService(name: String, completion: @escaping () -> Void) { public static func startService(name: String) async {
DispatchQueue.global(qos: .userInitiated).async { await brew(
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name }) "services start \(name)",
ServicesManager.loadHomebrewServices(completed: { sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name }
DispatchQueue.main.async { )
completion() await ServicesManager.loadHomebrewServices()
}
})
}
} }
// MARK: - Finding Config Files // MARK: - Finding Config Files
@ -119,12 +113,12 @@ class Actions {
// MARK: - Other Actions // MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL { public static func createTempPhpInfoFile() async -> URL {
// Write a file called `phpmon_phpinfo.php` to /tmp // Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8) try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file // Tell php-cgi to run the PHP and output as an .html file
LegacyShell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html") await Shell.quiet("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")! return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
} }
@ -145,10 +139,12 @@ class Actions {
*/ */
public static func fixMyValet(completed: @escaping () -> Void) { public static func fixMyValet(completed: @escaping () -> Void) {
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias, completion: { InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias, completion: {
brew("services restart dnsmasq", sudo: true) Task { // restart all services and fire callback upon completion
brew("services restart php", sudo: true) await brew("services restart dnsmasq", sudo: true)
brew("services restart nginx", sudo: true) await brew("services restart php", sudo: true)
completed() await brew("services restart nginx", sudo: true)
completed()
}
}) })
} }
} }

View File

@ -11,21 +11,21 @@
/** /**
Runs a `valet` command. Defaults to running as superuser. Runs a `valet` command. Defaults to running as superuser.
*/ */
func valet(_ command: String, sudo: Bool = true) -> String { func valet(_ command: String, sudo: Bool = true) async -> String {
return LegacyShell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true) return await Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)").out
} }
/** /**
Runs a `brew` command. Can run as superuser. Runs a `brew` command. Can run as superuser.
*/ */
func brew(_ command: String, sudo: Bool = false) { func brew(_ command: String, sudo: Bool = false) async {
LegacyShell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") await Shell.quiet("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
} }
/** /**
Runs `sed` in order to replace all occurrences of a string in a specific file with another. Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/ */
func sed(file: String, original: String, replacement: String) { func sed(file: String, original: String, replacement: String) async {
// Escape slashes (or `sed` won't work) // Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/") let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
@ -33,19 +33,19 @@ func sed(file: String, original: String, replacement: String) {
// Check if gsed exists; it is able to follow symlinks, // Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension // which we want to do to toggle the extension
if Filesystem.fileExists("\(Paths.binPath)/gsed") { if Filesystem.fileExists("\(Paths.binPath)/gsed") {
LegacyShell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") await Shell.quiet("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else { } else {
LegacyShell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") await Shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
} }
} }
/** /**
Uses `grep` to determine whether a particular query string can be found in a particular file. Uses `grep` to determine whether a particular query string can be found in a particular file.
*/ */
func grepContains(file: String, query: String) -> Bool { func grepContains(file: String, query: String) async -> Bool {
return LegacyShell.pipe(""" return await Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""") """).out
.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES") .contains("YES")
} }

View File

@ -21,11 +21,11 @@ public class Paths {
init() { init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr baseDir = App.architecture != "x86_64" ? .opt : .usr
}
Task { public func loadUser() async {
let output = await Shell.pipe("id -un").out let output = await Shell.pipe("id -un").out
userName = String(output.split(separator: "\n")[0]) userName = String(output.split(separator: "\n")[0])
}
} }
public func detectBinaryPaths() { public func detectBinaryPaths() {

View File

@ -125,11 +125,12 @@ class ActivePhpInstallation {
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly. that means that Valet won't work properly.
*/ */
func checkPhpFpmStatus() -> Bool { func checkPhpFpmStatus() async -> Bool {
if self.version.short == "5.6" { if self.version.short == "5.6" {
// The main PHP config file should contain `valet.sock` and then we're probably fine? // The main PHP config file should contain `valet.sock` and then we're probably fine?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return LegacyShell.pipe("cat \(fileName)").contains("valet.sock") return await Shell.pipe("cat \(fileName)").out
.contains("valet.sock")
} }
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)

View File

@ -16,17 +16,15 @@ class PhpEnv {
self.currentInstall = ActivePhpInstallation() self.currentInstall = ActivePhpInstallation()
} }
func determinePhpAlias() { func determinePhpAlias() async {
Task { let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode( self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)! from: brewPhpAlias.data(using: .utf8)!
).first! ).first!
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!") Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
}
} }
// MARK: - Properties // MARK: - Properties
@ -80,8 +78,8 @@ class PhpEnv {
return InternalSwitcher() return InternalSwitcher()
} }
public static func detectPhpVersions() { public static func detectPhpVersions() async {
Task { await Self.shared.detectPhpVersions() } _ = await Self.shared.detectPhpVersions()
} }
/** /**
@ -90,7 +88,7 @@ class PhpEnv {
public func detectPhpVersions() async -> [String] { public func detectPhpVersions() async -> [String] {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n")) var versionsOnly = await extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected // Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0` // The user may have `php` installed, but not e.g. `php@8.0`
@ -128,7 +126,7 @@ class PhpEnv {
from versions: [String], from versions: [String],
checkBinaries: Bool = true, checkBinaries: Bool = true,
generateHelpers: Bool = true generateHelpers: Bool = true
) -> [String] { ) async -> [String] {
var output: [String] = [] var output: [String] = []
var supported = Constants.SupportedPhpVersions var supported = Constants.SupportedPhpVersions
@ -153,7 +151,9 @@ class PhpEnv {
} }
if generateHelpers { if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) } for item in output {
await PhpHelper.generate(for: item)
}
} }
return output return output

View File

@ -12,7 +12,7 @@ class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor." static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) { public static func generate(for version: String) async {
// Take the PHP version (e.g. "7.2") and generate a dotless version // Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "") let dotless = version.replacingOccurrences(of: ".", with: "")
@ -20,79 +20,81 @@ class PhpHelper {
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)" let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH // Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = LegacyShell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin") let inPath = Shell.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable) // Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/") let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
do { Task { // Create the appropriate folders and check if the files exist
Task { await Shell.quiet("mkdir -p ~/.config/phpmon/bin") } do {
await Shell.quiet("mkdir -p ~/.config/phpmon/bin")
if Filesystem.fileExists(destination) { if Filesystem.fileExists(destination) {
let contents = try String(contentsOfFile: destination) let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) { if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor " Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.") + "(or is unreadable). Not updating this file.")
return return
} }
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Write to the destination
try script.write(
to: URL(fileURLWithPath: destination),
atomically: true,
encoding: String.Encoding.utf8
)
// Make sure the file is executable
LegacyShell.run("chmod +x \(destination)")
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
} }
// Write the symlink // Let's follow the symlink to the PHP binary folder
self.createSymlink(dotless) let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Write to the destination
try script.write(
to: URL(fileURLWithPath: destination),
atomically: true,
encoding: String.Encoding.utf8
)
// Make sure the file is executable
await Shell.quiet("chmod +x \(destination)")
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
}
// Write the symlink
await self.createSymlink(dotless)
}
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
} }
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
} }
} }
private static func createSymlink(_ dotless: String) { private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)" let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)" let destination = "/usr/local/bin/pm\(dotless)"
if !Filesystem.fileExists(destination) { if !Filesystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)") Log.info("Creating new symlink: \(destination)")
LegacyShell.run("ln -s \(source) \(destination)") await Shell.quiet("ln -s \(source) \(destination)")
return return
} }
if !Filesystem.fileIsSymlink(destination) { if !Filesystem.fileIsSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)") Log.info("Overwriting existing file with new symlink: \(destination)")
LegacyShell.run("ln -fs \(source) \(destination)") await Shell.quiet("ln -fs \(source) \(destination)")
return return
} }

View File

@ -75,14 +75,14 @@ class PhpExtension {
This simply toggles the extension in the .ini file. This simply toggles the extension in the .ini file.
You may need to restart the other services in order for this change to apply. You may need to restart the other services in order for this change to apply.
*/ */
func toggle() { func toggle() async {
let newLine = enabled let newLine = enabled
// DISABLED: Commented out line // DISABLED: Commented out line
? "; \(line)" ? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed // ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "") : line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine) await sed(file: file, original: line, replacement: newLine)
enabled.toggle() enabled.toggle()
} }

View File

@ -19,6 +19,8 @@ class InternalSwitcher: PhpSwitcher {
Please note that depending on which version is installed, Please note that depending on which version is installed,
the version that is switched to may or may not be identical to `php` the version that is switched to may or may not be identical to `php`
(without @version). (without @version).
TODO: Use `async` and use structured concurrency: https://www.hackingwithswift.com/swift/5.5/structured-concurrency
*/ */
func performSwitch(to version: String, completion: @escaping () -> Void) { func performSwitch(to version: String, completion: @escaping () -> Void) {
Log.info("Switching to \(version), unlinking all versions...") Log.info("Switching to \(version), unlinking all versions...")
@ -30,26 +32,28 @@ class InternalSwitcher: PhpSwitcher {
PhpEnv.shared.availablePhpVersions.forEach { (available) in PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter() group.enter()
DispatchQueue.global(qos: .userInitiated).async { Task { // TODO: Use structured concurrency
self.disableDefaultPhpFpmPool(available) await self.disableDefaultPhpFpmPool(available)
self.stopPhpVersion(available) await self.stopPhpVersion(available)
group.leave() group.leave()
} }
} }
group.notify(queue: .global(qos: .userInitiated)) { group.notify(queue: .global(qos: .userInitiated)) {
Log.info("All versions have been unlinked!") Task { // TODO: Use structured concurrency
Log.info("Linking the new version!") Log.info("All versions have been unlinked!")
Log.info("Linking the new version!")
for formula in versions { for formula in versions {
self.startPhpVersion(formula, primary: (version == formula)) await self.startPhpVersion(formula, primary: (version == formula))
}
Log.info("Restarting nginx, just to be sure!")
await brew("services restart nginx", sudo: true)
Log.info("The new version(s) have been linked!")
completion()
} }
Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true)
Log.info("The new version(s) have been linked!")
completion()
} }
} }
@ -74,7 +78,7 @@ class InternalSwitcher: PhpSwitcher {
return FileManager.default.fileExists(atPath: pool) return FileManager.default.fileExists(atPath: pool)
} }
func disableDefaultPhpFpmPool(_ version: String) { func disableDefaultPhpFpmPool(_ version: String) async {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) { if FileManager.default.fileExists(atPath: pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).") Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
@ -94,28 +98,28 @@ class InternalSwitcher: PhpSwitcher {
} }
} }
func stopPhpVersion(_ version: String) { func stopPhpVersion(_ version: String) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
brew("unlink \(formula)") await brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true) await brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)") Log.info("Unlinked and stopped services for \(formula)")
} }
func startPhpVersion(_ version: String, primary: Bool) { func startPhpVersion(_ version: String, primary: Bool) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)" let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
if primary { if primary {
Log.info("\(formula) is the primary formula, linking and starting services...") Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force") await brew("link \(formula) --overwrite --force")
} else { } else {
Log.info("\(formula) is an isolated PHP version, starting services only...") Log.info("\(formula) is an isolated PHP version, starting services only...")
} }
brew("services start \(formula)", sudo: true) await brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary { if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "") let socketVersion = version.replacingOccurrences(of: ".", with: "")
LegacyShell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock") await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).") Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
} }

View File

@ -1,33 +0,0 @@
//
// Shell+PATH.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension LegacyShell {
var PATH: String {
let task = Process()
task.launchPath = "/bin/zsh"
let command = Filesystem.fileExists("~/.zshrc")
// source the user's .zshrc file if it exists to complete $PATH
? ". ~/.zshrc && echo $PATH"
// otherwise, non-interactive mode is sufficient
: "echo $PATH"
task.arguments = ["--login", "-lc", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: String.Encoding.utf8) ?? ""
}
}

View File

@ -1,175 +0,0 @@
//
// Shell.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
// TODO: Enable this to see where deprecations and replacements are needed.
@available(*, deprecated, message: "Use the new replacement `Shell` instead")
public class LegacyShell {
// MARK: - Invoke static functions
public static func run(
_ command: String,
requiresPath: Bool = false
) {
LegacyShell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return LegacyShell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
public var shell: String = "/bin/sh"
/** Additional exports that are sent if `requiresPath` is set to true. */
public var exports: String = ""
/**
Singleton to access a user shell (with --login)
*/
public static let user = LegacyShell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func run(
_ command: String,
requiresPath: Bool = false
) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = LegacyShell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
*/
public func executeSynchronously(
_ command: String,
requiresPath: Bool = false
) -> LegacyShell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
let output = LegacyShell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
task: task
)
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
}
/**
Creates a new process with the correct PATH and shell.
*/
public func createTask(for command: String, requiresPath: Bool) -> Process {
var completeCommand = ""
Log.info("LEGACY COMMAND: \(command)")
if requiresPath {
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
// Put additional exports in between
if !self.exports.isEmpty {
completeCommand += "\(self.exports) && "
}
}
completeCommand += command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
return task
}
/**
Verbose logging for PHP Monitor's synchronous shell output.
*/
private func log(task: Process, output: Output) {
Log.info("")
Log.info("==== COMMAND ====")
Log.info("")
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
Log.info("")
Log.info("==== OUTPUT ====")
Log.info("")
dump(output)
Log.info("")
Log.info("==== END OUTPUT ====")
Log.info("")
}
public class Output {
public let standardOutput: String
public let errorOutput: String
public let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
}
}
}

View File

@ -33,15 +33,17 @@ extension AppDelegate {
} }
@IBAction func reloadDomainListPressed(_ sender: Any) { @IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.domainListWindowController? Task { // Reload domains
.window?.contentViewController as? DomainListVC let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
if vc != nil { if vc != nil {
// If the view exists, directly reload the list of sites. // If the view exists, directly reload the list of sites.
vc!.reloadDomains() await vc!.reloadDomains()
} else { } else {
// If the view does not exist, reload the cached data that was populated when the app initially launched. // If the view does not exist, reload the cached data that was populated when the app launched.
Valet.shared.reloadSites() await Valet.shared.reloadSites()
}
} }
} }

View File

@ -13,13 +13,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: - Variables // MARK: - Variables
/**
The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application.
*/
let sharedShell: LegacyShell
/** /**
The App singleton contains information about the state of The App singleton contains information about the state of
the application and global variables. the application and global variables.
@ -68,7 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
logger.verbosity = .performance logger.verbosity = .performance
// TODO: Enable to fake broken setup during testing // TODO: Enable to fake broken setup during testing
ActiveShell.useTestable(Testables.working.shellOutput) // ActiveShell.useTestable(Testables.working.shellOutput)
#endif #endif
if CommandLine.arguments.contains("--v") { if CommandLine.arguments.contains("--v") {
@ -81,7 +74,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
Log.info("Version \(App.version)") Log.info("Version \(App.version)")
Log.separator(as: .info) Log.separator(as: .info)
self.sharedShell = LegacyShell.user
self.state = App.shared self.state = App.shared
self.menu = MainMenu.shared self.menu = MainMenu.shared
self.paths = Paths.shared self.paths = Paths.shared
@ -103,8 +95,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
// Make sure notifications will work // Make sure notifications will work
setupNotifications() setupNotifications()
// Make sure the menu performs its initial checks Task { // Make sure the menu performs its initial checks
Task { await menu.startup() } await paths.loadUser()
await menu.startup()
}
} }
} }

View File

@ -21,7 +21,7 @@ class AppUpdateChecker {
public static func retrieveVersionFromCask( public static func retrieveVersionFromCask(
_ initiatedFromBackground: Bool = true _ initiatedFromBackground: Bool = true
) -> String { ) async -> String {
let caskFile = App.version.contains("-dev") let caskFile = App.version.contains("-dev")
? Constants.Urls.DevBuildCaskFile.absoluteString ? Constants.Urls.DevBuildCaskFile.absoluteString
: Constants.Urls.StableBuildCaskFile.absoluteString : Constants.Urls.StableBuildCaskFile.absoluteString
@ -32,14 +32,14 @@ class AppUpdateChecker {
command = "curl -s --max-time 5" command = "curl -s --max-time 5"
} }
return LegacyShell.pipe( return await Shell.pipe(
"\(command) '\(caskFile)' | grep version" "\(command) '\(caskFile)' | grep version"
) ).out
} }
public static func checkIfNewerVersionIsAvailable( public static func checkIfNewerVersionIsAvailable(
initiatedFromBackground: Bool = true initiatedFromBackground: Bool = true
) { ) async {
if initiatedFromBackground { if initiatedFromBackground {
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) { if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Automatic updates are disabled. No check will be performed.") Log.info("Automatic updates are disabled. No check will be performed.")
@ -49,7 +49,7 @@ class AppUpdateChecker {
Log.info("Automatic updates are enabled, a check will be performed.") Log.info("Automatic updates are enabled, a check will be performed.")
} }
let versionString = retrieveVersionFromCask(initiatedFromBackground) let versionString = await retrieveVersionFromCask(initiatedFromBackground)
guard let onlineVersion = AppVersion.from(versionString) else { guard let onlineVersion = AppVersion.from(versionString) else {
Log.err("We couldn't check for updates!") Log.err("We couldn't check for updates!")

View File

@ -26,10 +26,14 @@ class InterApp {
DomainListVC.show() DomainListVC.show()
}), }),
InterApp.Action(command: "services/stop", action: { _ in InterApp.Action(command: "services/stop", action: { _ in
MainMenu.shared.stopValetServices() Task { // Stopping services as standalone task
await MainMenu.shared.stopValetServices()
}
}), }),
InterApp.Action(command: "services/restart/all", action: { _ in InterApp.Action(command: "services/restart/all", action: { _ in
MainMenu.shared.restartValetServices() Task { // Restarting services as standalone task
await MainMenu.shared.restartValetServices()
}
}), }),
InterApp.Action(command: "services/restart/nginx", action: { _ in InterApp.Action(command: "services/restart/nginx", action: { _ in
MainMenu.shared.restartNginx() MainMenu.shared.restartNginx()

View File

@ -16,53 +16,44 @@ class ServicesManager: ObservableObject {
@Published var rootServices: [String: HomebrewService] = [:] @Published var rootServices: [String: HomebrewService] = [:]
@Published var userServices: [String: HomebrewService] = [:] @Published var userServices: [String: HomebrewService] = [:]
public static func loadHomebrewServices(completed: (() -> Void)? = nil) { public static func loadHomebrewServices() async {
let rootServiceNames = [ let rootServiceNames = [
PhpEnv.phpInstall.formula, PhpEnv.phpInstall.formula,
"nginx", "nginx",
"dnsmasq" "dnsmasq"
] ]
DispatchQueue.global(qos: .background).async { let normalJson = await Shell
let data = LegacyShell .pipe("sudo \(Paths.brew) services info --all --json")
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true) .out
.data(using: .utf8)! .data(using: .utf8)!
let services = try! JSONDecoder() let normalServices = try! JSONDecoder()
.decode([HomebrewService].self, from: data) .decode([HomebrewService].self, from: normalJson)
.filter({ return rootServiceNames.contains($0.name) }) .filter({ return rootServiceNames.contains($0.name) })
DispatchQueue.main.async { DispatchQueue.main.async {
ServicesManager.shared.rootServices = Dictionary( ServicesManager.shared.rootServices = Dictionary(
uniqueKeysWithValues: services.map { ($0.name, $0) } uniqueKeysWithValues: normalServices.map { ($0.name, $0) }
) )
}
} }
guard let userServiceNames = Preferences.custom.services else { guard let userServiceNames = Preferences.custom.services else {
return return
} }
DispatchQueue.global(qos: .background).async { let rootJson = await Shell
let data = LegacyShell .pipe("\(Paths.brew) services info --all --json")
.pipe("\(Paths.brew) services info --all --json", requiresPath: true) .out
.data(using: .utf8)! .data(using: .utf8)!
let services = try! JSONDecoder() let rootServices = try! JSONDecoder()
.decode([HomebrewService].self, from: data) .decode([HomebrewService].self, from: rootJson)
.filter({ return userServiceNames.contains($0.name) }) .filter({ return userServiceNames.contains($0.name) })
DispatchQueue.main.async { ServicesManager.shared.userServices = Dictionary(
ServicesManager.shared.userServices = Dictionary( uniqueKeysWithValues: rootServices.map { ($0.name, $0) }
uniqueKeysWithValues: services.map { ($0.name, $0) } )
)
completed?()
}
}
}
func loadData() {
Self.loadHomebrewServices()
} }
/** /**

View File

@ -71,9 +71,9 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
App.shared.domainListWindowController?.contentVC.setUIBusy() App.shared.domainListWindowController?.contentVC.setUIBusy()
DispatchQueue.global(qos: .userInitiated).async { Task {
LegacyShell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true) await Shell.quiet("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)")
Actions.restartNginx() await Actions.restartNginx()
DispatchQueue.main.async { DispatchQueue.main.async {
App.shared.domainListWindowController?.contentVC.setUINotBusy() App.shared.domainListWindowController?.contentVC.setUINotBusy()

View File

@ -51,7 +51,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlet Interactions // MARK: - Outlet Interactions
@IBAction func pressedCreateLink(_ sender: Any) { @IBAction func pressedCreateLink(_ sender: Any) async {
let path = pathControl.url!.path let path = pathControl.url!.path
let name = inputDomainName.stringValue let name = inputDomainName.stringValue
@ -71,7 +71,9 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// Adding `valet links` is a workaround for Valet malforming the config.json file // Adding `valet links` is a workaround for Valet malforming the config.json file
// TODO: I will have to investigate and report this behaviour if possible // TODO: I will have to investigate and report this behaviour if possible
LegacyShell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true) Task {
await Shell.quiet("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links")
}
dismissView(outcome: .OK) dismissView(outcome: .OK)
@ -81,7 +83,7 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
.searchField.stringValue = "" .searchField.stringValue = ""
// Add the new item and scrolls to it // Add the new item and scrolls to it
App.shared.domainListWindowController? await App.shared.domainListWindowController?
.contentVC .contentVC
.addedNewSite( .addedNewSite(
name: name, name: name,

View File

@ -25,15 +25,14 @@ extension DomainListVC {
self.waitAndExecute { self.waitAndExecute {
// 1. Remove the original proxy // 1. Remove the original proxy
LegacyShell.run("\(Paths.valet) unproxy \(selectedProxy.domain)", requiresPath: true) await Shell.quiet("\(Paths.valet) unproxy \(selectedProxy.domain)")
// 2. Add a new proxy, which is either secured/unsecured // 2. Add a new proxy, which is either secured/unsecured
let secure = originalSecureStatus ? "" : " --secure" let secure = originalSecureStatus ? "" : " --secure"
LegacyShell.run("\(Paths.valet) proxy \(selectedProxy.domain) \(selectedProxy.target)\(secure)", await Shell.quiet("\(Paths.valet) proxy \(selectedProxy.domain) \(selectedProxy.target)\(secure)")
requiresPath: true)
// 3. Restart nginx // 3. Restart nginx
Actions.restartNginx() await Actions.restartNginx()
// 4. Reload site list // 4. Reload site list
DispatchQueue.main.async { DispatchQueue.main.async {
@ -50,7 +49,7 @@ extension DomainListVC {
let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;" let command = "cd '\(selectedSite.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
waitAndExecute { waitAndExecute {
LegacyShell.run(command, requiresPath: true) await Shell.quiet(command)
} completion: { [self] in } completion: { [self] in
selectedSite.determineSecured() selectedSite.determineSecured()
if selectedSite.secured == originalSecureStatus { if selectedSite.secured == originalSecureStatus {
@ -99,12 +98,12 @@ extension DomainListVC {
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
@objc func openInFinder() { @objc func openInFinder() async {
LegacyShell.run("open '\(selectedSite!.absolutePath)'") await Shell.quiet("open '\(selectedSite!.absolutePath)'")
} }
@objc func openInTerminal() { @objc func openInTerminal() async {
LegacyShell.run("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") await Shell.quiet("open -b com.apple.terminal '\(selectedSite!.absolutePath)'")
} }
@objc func openWithEditor(sender: EditorMenuItem) async { @objc func openWithEditor(sender: EditorMenuItem) async {
@ -157,9 +156,9 @@ extension DomainListVC {
style: .critical, style: .critical,
onFirstButtonPressed: { onFirstButtonPressed: {
self.waitAndExecute { self.waitAndExecute {
LegacyShell.run("valet unlink '\(site.name)'", requiresPath: true) Task { await Shell.quiet("valet unlink '\(site.name)'") }
} completion: { } completion: {
self.reloadDomains() Task { await self.reloadDomains() }
} }
} }
) )
@ -179,9 +178,9 @@ extension DomainListVC {
style: .critical, style: .critical,
onFirstButtonPressed: { onFirstButtonPressed: {
self.waitAndExecute { self.waitAndExecute {
LegacyShell.run("valet unproxy '\(proxy.domain)'", requiresPath: true) Task { await Shell.quiet("valet unproxy '\(proxy.domain)'") }
} completion: { } completion: {
self.reloadDomains() Task { await self.reloadDomains() }
} }
} }
) )
@ -191,7 +190,7 @@ extension DomainListVC {
let rowToReload = tableView.selectedRow let rowToReload = tableView.selectedRow
waitAndExecute { waitAndExecute {
LegacyShell.run(command, requiresPath: true) await Shell.quiet(command)
} completion: { [self] in } completion: { [self] in
beforeCellReload() beforeCellReload()
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4]) tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])

View File

@ -97,7 +97,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
domains = Valet.getDomainListable() domains = Valet.getDomainListable()
searchedFor(text: lastSearchedFor) searchedFor(text: lastSearchedFor)
} else { } else {
reloadDomains() Task { await reloadDomains() }
} }
} }
@ -107,7 +107,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
Disables the UI so the user cannot interact with it. Disables the UI so the user cannot interact with it.
Also shows a spinner to indicate that we're busy. Also shows a spinner to indicate that we're busy.
*/ */
public func setUIBusy() { @MainActor public func setUIBusy() {
// If it takes more than 0.5s to set the UI to not busy, show a spinner // If it takes more than 0.5s to set the UI to not busy, show a spinner
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { _ in
self.progressIndicator.startAnimation(true) self.progressIndicator.startAnimation(true)
@ -121,7 +121,7 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
/** /**
Re-enables the UI so the user can interact with it. Re-enables the UI so the user can interact with it.
*/ */
public func setUINotBusy() { @MainActor public func setUINotBusy() {
timer?.invalidate() timer?.invalidate()
progressIndicator.stopAnimation(nil) progressIndicator.stopAnimation(nil)
tableView.alphaValue = 1.0 tableView.alphaValue = 1.0
@ -136,12 +136,11 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
- Parameter execute: Callback of the work that needs to happen. - Parameter execute: Callback of the work that needs to happen.
- Parameter completion: Callback that is fired when the work is done. - Parameter completion: Callback that is fired when the work is done.
*/ */
internal func waitAndExecute(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {}) { internal func waitAndExecute(_ execute: @escaping () async -> Void, completion: @escaping () -> Void = {}) {
setUIBusy() Task {
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in setUIBusy()
execute() await execute()
// For a smoother animation, expect at least a 0.2 second delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
completion() completion()
setUINotBusy() setUINotBusy()
@ -151,9 +150,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
// MARK: - Site Data Loading // MARK: - Site Data Loading
func reloadDomains() { func reloadDomains() async {
waitAndExecute { waitAndExecute {
Valet.shared.reloadSites() await Valet.shared.reloadSites()
} completion: { [self] in } completion: { [self] in
domains = Valet.shared.sites domains = Valet.shared.sites
searchedFor(text: lastSearchedFor) searchedFor(text: lastSearchedFor)
@ -177,9 +176,9 @@ class DomainListVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource
self.domains = descriptor.ascending ? sorted.reversed() : sorted self.domains = descriptor.ascending ? sorted.reversed() : sorted
} }
func addedNewSite(name: String, secure: Bool) { func addedNewSite(name: String, secure: Bool) async {
waitAndExecute { waitAndExecute {
Valet.shared.reloadSites() await Valet.shared.reloadSites()
} completion: { [self] in } completion: { [self] in
find(name, secure) find(name, secure)
} }

View File

@ -51,7 +51,9 @@ class DomainListWindowController: PMWindowController, NSSearchFieldDelegate, NST
// MARK: - Reload functionality // MARK: - Reload functionality
@IBAction func pressedReload(_ sender: Any?) { @IBAction func pressedReload(_ sender: Any?) {
contentVC.reloadDomains() Task {
await contentVC.reloadDomains()
}
} }
@IBAction func pressedAddLink(_ sender: Any?) { @IBAction func pressedAddLink(_ sender: Any?) {

View File

@ -21,10 +21,9 @@ import Foundation
self.completion = completion self.completion = completion
Paths.shared.detectBinaryPaths() Paths.shared.detectBinaryPaths()
if Paths.composer == nil { if Paths.composer == nil {
DispatchQueue.main.async { self.presentMissingAlert()
self.presentMissingAlert()
}
return return
} }
@ -39,7 +38,9 @@ import Foundation
window?.setType(info: true) window?.setType(info: true)
Task { await performComposerUpdate() } Task { // Start the Composer global update as a separate task
await performComposerUpdate()
}
} }
private func performComposerUpdate() async { private func performComposerUpdate() async {

View File

@ -42,7 +42,7 @@ class HomebrewDiagnostics {
This check only needs to be performed if the `shivammathur/php` tap is active. This check only needs to be performed if the `shivammathur/php` tap is active.
*/ */
public static func checkForCaskConflict() { public static func checkForCaskConflict() {
Task { Task { // Check if there's a conflict
if await hasAliasConflict() { if await hasAliasConflict() {
presentAlertAboutConflict() presentAlertAboutConflict()
} }
@ -72,9 +72,11 @@ class HomebrewDiagnostics {
} }
versions.forEach { version in versions.forEach { version in
switcher.disableDefaultPhpFpmPool(version) Task { // Fix each pool concurrently (but perform the tasks sequentially)
switcher.stopPhpVersion(version) await switcher.disableDefaultPhpFpmPool(version)
switcher.startPhpVersion(version, primary: version == primary) await switcher.stopPhpVersion(version)
await switcher.startPhpVersion(version, primary: version == primary)
}
} }
} }

View File

@ -116,15 +116,15 @@ class Valet {
Starts the preload of sites. In order to make sure PHP Monitor can correctly Starts the preload of sites. In order to make sure PHP Monitor can correctly
handle all PHP versions including isolation, it needs to know about all sites. handle all PHP versions including isolation, it needs to know about all sites.
*/ */
public func startPreloadingSites() { public func startPreloadingSites() async {
self.reloadSites() await self.reloadSites()
} }
/** /**
Reloads the list of sites, assuming that the list isn't being reloaded at the time. Reloads the list of sites, assuming that the list isn't being reloaded at the time.
(We don't want to do duplicate or parallel work!) (We don't want to do duplicate or parallel work!)
*/ */
public func reloadSites() { public func reloadSites() async {
loadConfiguration() loadConfiguration()
if isBusy { if isBusy {

View File

@ -42,17 +42,17 @@ extension MainMenu {
} }
@objc func restartPhpFpm() { @objc func restartPhpFpm() {
asyncExecution { Task { // Simple restart service
Actions.restartPhpFpm() await Actions.restartPhpFpm()
} }
} }
@objc func restartValetServices() { @MainActor @objc func restartValetServices() {
asyncExecution { Task { // Restart services and show notification
Actions.restartDnsMasq() await Actions.restartDnsMasq()
Actions.restartPhpFpm() await Actions.restartPhpFpm()
Actions.restartNginx() await Actions.restartNginx()
} success: {
LocalNotification.send( LocalNotification.send(
title: "notification.services_restarted".localized, title: "notification.services_restarted".localized,
subtitle: "notification.services_restarted_desc".localized, subtitle: "notification.services_restarted_desc".localized,
@ -61,10 +61,10 @@ extension MainMenu {
} }
} }
@objc func stopValetServices() { @MainActor @objc func stopValetServices() {
asyncExecution { Task { // Stop services and show notification
Actions.stopValetServices() await Actions.stopValetServices()
} success: {
LocalNotification.send( LocalNotification.send(
title: "notification.services_stopped".localized, title: "notification.services_stopped".localized,
subtitle: "notification.services_stopped_desc".localized, subtitle: "notification.services_stopped_desc".localized,
@ -74,14 +74,14 @@ extension MainMenu {
} }
@objc func restartNginx() { @objc func restartNginx() {
asyncExecution { Task {
Actions.restartNginx() await Actions.restartNginx()
} }
} }
@objc func restartDnsMasq() { @objc func restartDnsMasq() {
asyncExecution { Task {
Actions.restartDnsMasq() await Actions.restartDnsMasq()
} }
} }
@ -134,18 +134,18 @@ extension MainMenu {
} }
@objc func toggleExtension(sender: ExtensionMenuItem) { @objc func toggleExtension(sender: ExtensionMenuItem) {
asyncExecution { Task {
sender.phpExtension?.toggle() await sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) { if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
Actions.restartPhpFpm() await Actions.restartPhpFpm()
} }
} }
} }
private func performRollback() { private func performRollback() {
asyncExecution { Task {
PresetHelper.rollbackPreset?.apply() await PresetHelper.rollbackPreset?.apply()
PresetHelper.rollbackPreset = nil PresetHelper.rollbackPreset = nil
MainMenu.shared.rebuild() MainMenu.shared.rebuild()
} }
@ -171,8 +171,8 @@ extension MainMenu {
} }
@objc func togglePreset(sender: PresetMenuItem) { @objc func togglePreset(sender: PresetMenuItem) {
asyncExecution { Task {
sender.preset?.apply() await sender.preset?.apply()
} }
} }
@ -191,12 +191,11 @@ extension MainMenu {
} }
@objc func openPhpInfo() { @objc func openPhpInfo() {
var url: URL?
asyncWithBusyUI { asyncWithBusyUI {
url = Actions.createTempPhpInfoFile() Task {
} completion: { let url = await Actions.createTempPhpInfoFile()
if url != nil { NSWorkspace.shared.open(url!) } NSWorkspace.shared.open(url)
}
} }
} }

View File

@ -74,7 +74,7 @@ extension MainMenu {
} }
if behaviours.contains(.broadcastServicesUpdate) { if behaviours.contains(.broadcastServicesUpdate) {
ServicesManager.shared.loadData() Task { await ServicesManager.loadHomebrewServices() }
} }
if error != nil { if error != nil {

View File

@ -21,18 +21,18 @@ extension MainMenu {
await App.shared.environment.process() await App.shared.environment.process()
if await Startup().checkEnvironment() { if await Startup().checkEnvironment() {
self.onEnvironmentPass() await self.onEnvironmentPass()
} else { } else {
self.onEnvironmentFail() await self.onEnvironmentFail()
} }
} }
/** /**
When the environment is all clear and the app can run, let's go. When the environment is all clear and the app can run, let's go.
*/ */
private func onEnvironmentPass() { private func onEnvironmentPass() async {
// Determine what the `php` formula is aliased to // Determine what the `php` formula is aliased to
PhpEnv.shared.determinePhpAlias() await PhpEnv.shared.determinePhpAlias()
// Determine install method // Determine install method
Log.info(HomebrewDiagnostics.customCaskInstalled Log.info(HomebrewDiagnostics.customCaskInstalled
@ -49,7 +49,7 @@ extension MainMenu {
Valet.shared.validateVersion() Valet.shared.validateVersion()
// Actually detect the PHP versions // Actually detect the PHP versions
PhpEnv.detectPhpVersions() await PhpEnv.detectPhpVersions()
// Check for an alias conflict // Check for an alias conflict
HomebrewDiagnostics.checkForCaskConflict() HomebrewDiagnostics.checkForCaskConflict()
@ -79,7 +79,7 @@ extension MainMenu {
App.shared.loadGlobalHotkey() App.shared.loadGlobalHotkey()
// Preload sites // Preload sites
Valet.shared.startPreloadingSites() await Valet.shared.startPreloadingSites()
// After preloading sites, check for PHP-FPM pool conflicts // After preloading sites, check for PHP-FPM pool conflicts
HomebrewDiagnostics.checkForPhpFpmPoolConflicts() HomebrewDiagnostics.checkForPhpFpmPoolConflicts()
@ -88,7 +88,7 @@ extension MainMenu {
Valet.notifyAboutUnsupportedTLD() Valet.notifyAboutUnsupportedTLD()
// Find out which services are active // Find out which services are active
ServicesManager.shared.loadData() await ServicesManager.loadHomebrewServices()
// Start the background refresh timer // Start the background refresh timer
startSharedTimer() startSharedTimer()
@ -111,9 +111,7 @@ extension MainMenu {
} }
// Check for updates // Check for updates
DispatchQueue.global(qos: .utility).async { await AppUpdateChecker.checkIfNewerVersionIsAvailable()
AppUpdateChecker.checkIfNewerVersionIsAvailable()
}
// We are ready! // We are ready!
Log.info("PHP Monitor is ready to serve!") Log.info("PHP Monitor is ready to serve!")
@ -122,9 +120,8 @@ extension MainMenu {
/** /**
When the environment is not OK, present an alert to inform the user. When the environment is not OK, present an alert to inform the user.
*/ */
private func onEnvironmentFail() { private func onEnvironmentFail() async {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
BetterAlert() BetterAlert()
.withInformation( .withInformation(
title: "alert.cannot_start.title".localized, title: "alert.cannot_start.title".localized,

View File

@ -19,36 +19,38 @@ extension MainMenu {
PhpEnv.shared.isBusy = false PhpEnv.shared.isBusy = false
// Reload the site list // Reload the site list
self.reloadDomainListData() Task {
await self.reloadDomainListData()
// Perform UI updates on main thread // Perform UI updates on main thread
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
updatePhpVersionInStatusBar() updatePhpVersionInStatusBar()
rebuild() rebuild()
if !PhpEnv.shared.validate(version) { if !PhpEnv.shared.validate(version) {
self.suggestFixMyValet(failed: version) self.suggestFixMyValet(failed: version)
return return
}
// Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
ComposerWindow().updateGlobalDependencies(
notify: false,
completion: { _ in
self.notifyAboutVersionChange(to: version)
}
)
} else {
self.notifyAboutVersionChange(to: version)
}
// Check if Valet still works correctly
self.checkForPlatformIssues()
// Update stats
Stats.incrementSuccessfulSwitchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
} }
// Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
ComposerWindow().updateGlobalDependencies(
notify: false,
completion: { _ in
self.notifyAboutVersionChange(to: version)
}
)
} else {
self.notifyAboutVersionChange(to: version)
}
// Check if Valet still works correctly
self.checkForPlatformIssues()
// Update stats
Stats.incrementSuccessfulSwitchCount()
Stats.evaluateSponsorMessageShouldBeDisplayed()
} }
} }
@ -102,25 +104,21 @@ extension MainMenu {
.show() .show()
} }
private func reloadDomainListData() { private func reloadDomainListData() async {
if let window = App.shared.domainListWindowController { if let window = App.shared.domainListWindowController {
DispatchQueue.main.async { await window.contentVC.reloadDomains()
window.contentVC.reloadDomains()
}
} else { } else {
Valet.shared.reloadSites() await Valet.shared.reloadSites()
} }
} }
private func notifyAboutVersionChange(to version: String) { @MainActor private func notifyAboutVersionChange(to version: String) {
DispatchQueue.main.async { LocalNotification.send(
LocalNotification.send( title: String(format: "notification.version_changed_title".localized, version),
title: String(format: "notification.version_changed_title".localized, version), subtitle: String(format: "notification.version_changed_desc".localized, version),
subtitle: String(format: "notification.version_changed_desc".localized, version), preference: .notifyAboutVersionChange
preference: .notifyAboutVersionChange )
)
PhpEnv.phpInstall.notifyAboutBrokenPhpFpm() Task { await PhpEnv.phpInstall.notifyAboutBrokenPhpFpm() }
}
} }
} }

View File

@ -103,11 +103,11 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
Reloads the menu in the foreground. Reloads the menu in the foreground.
This mimics the exact behaviours of `asyncExecution` as set in the method below. This mimics the exact behaviours of `asyncExecution` as set in the method below.
*/ */
@objc func reloadPhpMonitorMenuInForeground() { @MainActor @objc func reloadPhpMonitorMenuInForeground() async {
refreshActiveInstallation() refreshActiveInstallation()
refreshIcon() refreshIcon()
rebuild(async: false) rebuild(async: false)
ServicesManager.shared.loadData() await ServicesManager.loadHomebrewServices()
} }
/** /**
@ -185,10 +185,8 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
NSApplication.shared.terminate(nil) NSApplication.shared.terminate(nil)
} }
@objc func checkForUpdates() { @objc func checkForUpdates() async {
DispatchQueue.global(qos: .userInitiated).async { await AppUpdateChecker.checkIfNewerVersionIsAvailable(initiatedFromBackground: false)
AppUpdateChecker.checkIfNewerVersionIsAvailable(initiatedFromBackground: false)
}
} }
// MARK: - Menu Delegate // MARK: - Menu Delegate
@ -196,7 +194,9 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
func menuWillOpen(_ menu: NSMenu) { func menuWillOpen(_ menu: NSMenu) {
// Make sure the shortcut key does not trigger this when the menu is open // Make sure the shortcut key does not trigger this when the menu is open
App.shared.shortcutHotkey?.isPaused = true App.shared.shortcutHotkey?.isPaused = true
ServicesManager.shared.loadData() Task {
await ServicesManager.loadHomebrewServices()
}
} }
func menuDidClose(_ menu: NSMenu) { func menuDidClose(_ menu: NSMenu) {

View File

@ -74,5 +74,4 @@ class BetterAlertVC: NSViewController {
self.view.window?.close() self.view.window?.close()
NSApplication.shared.stopModal(withCode: code) NSApplication.shared.stopModal(withCode: code)
} }
} }

View File

@ -19,7 +19,13 @@ extension ActivePhpInstallation {
This method actively presents a modal if said checks fails, so don't call this method too many times. This method actively presents a modal if said checks fails, so don't call this method too many times.
*/ */
public func notifyAboutBrokenPhpFpm() { public func notifyAboutBrokenPhpFpm() {
if !self.checkPhpFpmStatus() { Task {
let fpmStatusConfiguredCorrectly = await self.checkPhpFpmStatus()
if fpmStatusConfiguredCorrectly {
return
}
DispatchQueue.main.async { DispatchQueue.main.async {
BetterAlert() BetterAlert()
.withInformation( .withInformation(

View File

@ -26,7 +26,7 @@ struct CustomPrefs: Decodable {
return self.environmentVariables != nil && !self.environmentVariables!.keys.isEmpty return self.environmentVariables != nil && !self.environmentVariables!.keys.isEmpty
} }
@available(*, deprecated, message: "Use `setCustomEnvironmentVariables` instead") // TODO: Rework this
public func getEnvironmentVariables() -> String { public func getEnvironmentVariables() -> String {
return self.environmentVariables!.map { (key, value) in return self.environmentVariables!.map { (key, value) in
return "export \(key)=\(value)" return "export \(key)=\(value)"
@ -42,12 +42,12 @@ struct CustomPrefs: Decodable {
} }
extension Preferences { extension Preferences {
func loadCustomPreferences() { func loadCustomPreferences() async {
// Ensure the configuration directory is created if missing // Ensure the configuration directory is created if missing
LegacyShell.run("mkdir -p ~/.config/phpmon") await Shell.quiet("mkdir -p ~/.config/phpmon")
// Move the legacy file // Move the legacy file
moveOutdatedConfigurationFile() await moveOutdatedConfigurationFile()
// Attempt to load the file if it exists // Attempt to load the file if it exists
let url = URL(fileURLWithPath: "\(Paths.homePath)/.config/phpmon/config.json") let url = URL(fileURLWithPath: "\(Paths.homePath)/.config/phpmon/config.json")
@ -60,10 +60,10 @@ extension Preferences {
} }
} }
func moveOutdatedConfigurationFile() { func moveOutdatedConfigurationFile() async {
if Filesystem.fileExists("~/.phpmon.conf.json") && !Filesystem.fileExists("~/.config/phpmon/config.json") { if Filesystem.fileExists("~/.phpmon.conf.json") && !Filesystem.fileExists("~/.config/phpmon/config.json") {
Log.info("An outdated configuration file was found. Moving it...") Log.info("An outdated configuration file was found. Moving it...")
LegacyShell.run("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json") await Shell.quiet("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
Log.info("The configuration file was copied successfully!") Log.info("The configuration file was copied successfully!")
} }
} }
@ -87,7 +87,9 @@ extension Preferences {
if customPreferences.hasEnvironmentVariables() { if customPreferences.hasEnvironmentVariables() {
Log.info("Configuring the additional exports...") Log.info("Configuring the additional exports...")
LegacyShell.user.exports = customPreferences.getEnvironmentVariables() if let shell = Shell as? SystemShell {
shell.exports = customPreferences.getEnvironmentVariables()
}
} }
} catch { } catch {
Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.") Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.")

View File

@ -27,7 +27,10 @@ class Preferences {
services: [], services: [],
environmentVariables: [:] environmentVariables: [:]
) )
loadCustomPreferences()
Task {
await loadCustomPreferences()
}
} }
// MARK: - First Time Run // MARK: - First Time Run

View File

@ -67,19 +67,19 @@ struct Preset: Codable, Equatable {
/** /**
Applies a given preset. Applies a given preset.
*/ */
public func apply() { public func apply() async {
Task { Task {
// Was this a rollback? // Was this a rollback?
let wasRollback = (self.name == "AutomaticRevertSnapshot") let wasRollback = (self.name == "AutomaticRevertSnapshot")
// Save the preset that would revert this preset // Save the preset that would revert this preset
self.persistRevert() await self.persistRevert()
// Apply the PHP version if is considered a valid version // Apply the PHP version if is considered a valid version
if self.version != nil { if self.version != nil {
if await !switchToPhpVersionIfValid() { if await !switchToPhpVersionIfValid() {
PresetHelper.rollbackPreset = nil PresetHelper.rollbackPreset = nil
Actions.restartPhpFpm() await Actions.restartPhpFpm()
return return
} }
} }
@ -94,7 +94,7 @@ struct Preset: Codable, Equatable {
for foundExt in PhpEnv.phpInstall.extensions for foundExt in PhpEnv.phpInstall.extensions
where foundExt.name == ext.key && foundExt.enabled != ext.value { where foundExt.name == ext.key && foundExt.enabled != ext.value {
Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)")
foundExt.toggle() await foundExt.toggle()
break break
} }
} }
@ -103,7 +103,7 @@ struct Preset: Codable, Equatable {
PresetHelper.loadRollbackPresetFromFile() PresetHelper.loadRollbackPresetFromFile()
// Restart PHP FPM process (also reloads menu, which will show the preset rollback) // Restart PHP FPM process (also reloads menu, which will show the preset rollback)
Actions.restartPhpFpm() await Actions.restartPhpFpm()
// Show the correct notification // Show the correct notification
if wasRollback { if wasRollback {
@ -257,10 +257,10 @@ struct Preset: Codable, Equatable {
/** /**
Persists the revert as a JSON file, so it can be read from a file after restarting PHP Monitor. Persists the revert as a JSON file, so it can be read from a file after restarting PHP Monitor.
*/ */
private func persistRevert() { private func persistRevert() async {
let data = try! JSONEncoder().encode(self.revertSnapshot) let data = try! JSONEncoder().encode(self.revertSnapshot)
LegacyShell.run("mkdir -p ~/.config/phpmon") await Shell.quiet("mkdir -p ~/.config/phpmon")
try! String(data: data, encoding: .utf8)! try! String(data: data, encoding: .utf8)!
.write( .write(

View File

@ -91,15 +91,13 @@ struct CheckmarkView: View {
return nil return nil
} }
public func toggleService() { public func toggleService() async {
if active()! { if active()! {
Actions.stopService(name: serviceName, completion: { await Actions.stopService(name: serviceName)
busy = false busy = false
})
} else { } else {
Actions.startService(name: serviceName, completion: { await Actions.startService(name: serviceName)
busy = false busy = false
})
} }
} }
@ -121,7 +119,7 @@ struct CheckmarkView: View {
} else { } else {
Button { Button {
busy = true busy = true
toggleService() Task { await toggleService() }
} label: { } label: {
Image(systemName: active()! ? "checkmark" : "xmark") Image(systemName: active()! ? "checkmark" : "xmark")
.resizable() .resizable()

View File

@ -25,7 +25,7 @@ class SystemShell: Shellable {
Exports are additional environment variables set by the user via the custom configuration. Exports are additional environment variables set by the user via the custom configuration.
These are populated when the configuration file is being loaded. These are populated when the configuration file is being loaded.
*/ */
private(set) var exports: String = "" var exports: String = ""
/** Retrieves the user's PATH by opening an interactive shell and echoing $PATH. */ /** Retrieves the user's PATH by opening an interactive shell and echoing $PATH. */
private static func getPath() -> String { private static func getPath() -> String {

View File

@ -33,6 +33,8 @@ public class TestableShell: Shellable {
didReceiveOutput: @escaping (String, ShellStream) -> Void, didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) { ) async throws -> (Process, ShellOutput) {
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
guard let expectation = expectations[command] else { guard let expectation = expectations[command] else {
return (Process(), .err("No Expected Output")) return (Process(), .err("No Expected Output"))
} }