1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-31 08:50:07 +02:00

🚀 Version 25.10

This commit is contained in:
2025-11-07 13:56:32 +01:00
164 changed files with 3275 additions and 1826 deletions

View File

@@ -79,7 +79,11 @@ You can enable marketing mode by setting the `PHPMON_MARKETING_MODE` environment
## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.
The easiest way to symbolicate crashes is to simply rename the file to `.crash`, and drag it into Xcode.
Starting with PHP Monitor 25.10, opt-in automatic crash reporting is now included with `PLCrashReporter` and a custom API endpoint. These crash logs can also be symbolicated in exactly the same way.
If you have an archived build of the app and exported the DSYM, it is possible to manually symbolicate `.ips` crash logs.
For example, given the following crash (from an .ips file):

View File

@@ -8,14 +8,26 @@
/* Begin PBXBuildFile section */
0309E6672B0D4B2F002AC007 /* BrewExtensionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */; };
0310B17A2EB8F3FF00A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B1792EB8F3FF00A8B140 /* CrashReporter */; };
0310B17C2EB8F40100A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B17B2EB8F40100A8B140 /* CrashReporter */; };
0310B17E2EB8F40400A8B140 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 0310B17D2EB8F40400A8B140 /* CrashReporter */; };
031E2B692B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6A2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031E2B6C2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */; };
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24812EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
031F24832EA1071A00CFB8D9 /* Container+Fake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031F247F2EA1071700CFB8D9 /* Container+Fake.swift */; };
03263A382E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A3A2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03263A372E86D5E800BD0415 /* UpdateScheduler.swift */; };
0329A9A12E92A2AA00A62A12 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A02E92A2A800A62A12 /* Container.swift */; };
0329A9A32E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; };
0329A9A42E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; };
0329A9A52E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; };
0329A9A62E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */; };
032DAC282E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; };
032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; };
032DAC2A2E8BEB5B0018E01C /* RealApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032DAC272E8BEB590018E01C /* RealApi.swift */; };
@@ -33,6 +45,9 @@
033D45A02B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; };
033D45A12B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */; };
033D45A32B0D531D00070080 /* PhpExtensionManagerView+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033D45A22B0D531D00070080 /* PhpExtensionManagerView+Actions.swift */; };
035983A12E97FA9100218DC7 /* 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 */; };
036C39022E5C883B008DAEDF /* Packagist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39012E5C883A008DAEDF /* Packagist.swift */; };
036C39032E5C883B008DAEDF /* Packagist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39012E5C883A008DAEDF /* Packagist.swift */; };
036C39042E5C883B008DAEDF /* Packagist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39012E5C883A008DAEDF /* Packagist.swift */; };
@@ -48,6 +63,14 @@
036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C390E2E5C8D3B008DAEDF /* PackagistError.swift */; };
036C39142E5CB822008DAEDF /* TestBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036C39132E5CB820008DAEDF /* TestBundle.swift */; };
036C3A212E5CBBAA008DAEDF /* ValetConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */; };
0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */; };
0392CDEB2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; };
0392CDEC2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; };
0392CDED2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; };
0392CDEE2EB25371009176DA /* SecurePopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0392CDEA2EB25371009176DA /* SecurePopoverView.swift */; };
0396160D2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
0396160F2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */; };
@@ -65,6 +88,10 @@
039E1D7A2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
039E1D7C2E5F0F300072D13D /* ValetUpgrader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */; };
03B675E92EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03B675EA2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03B675EB2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03B675EC2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */; };
03BFF5272E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
03BFF5282E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
03BFF5292E312C3D007F96FA /* Startup+Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF5262E312C39007F96FA /* Startup+Timers.swift */; };
@@ -73,6 +100,10 @@
03BFF52D2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; };
03BFF52E2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; };
03BFF52F2E313244007F96FA /* StatusMenu+Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */; };
03C099442EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; };
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; };
03C099462EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; };
03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C099432EA15C8B00B76D43 /* Container+Real.swift */; };
03CC1FE52E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; };
03CC1FE62E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; };
03CC1FE72E3D22120050FC18 /* InstallHomebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */; };
@@ -81,8 +112,19 @@
03CC1FF52E3D23130050FC18 /* ZshRunCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FF32E3D230B0050FC18 /* ZshRunCommand.swift */; };
03CC1FF62E3D23130050FC18 /* ZshRunCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FF32E3D230B0050FC18 /* ZshRunCommand.swift */; };
03CC1FF72E3D23130050FC18 /* ZshRunCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CC1FF32E3D230B0050FC18 /* ZshRunCommand.swift */; };
03E36FE728D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; };
03E36FE828D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; };
03D846252EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */; };
03D846262EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */; };
03D846272EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */; };
03D846282EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */; };
03D8462B2EB6418F006EFE3C /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 03D8462A2EB6418F006EFE3C /* CrashReporter */; };
03D846322EB64E39006EFE3C /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846312EB64E35006EFE3C /* CrashReporter.swift */; };
03D846332EB64E39006EFE3C /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846312EB64E35006EFE3C /* CrashReporter.swift */; };
03D846342EB64E39006EFE3C /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846312EB64E35006EFE3C /* CrashReporter.swift */; };
03D846352EB64E39006EFE3C /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D846312EB64E35006EFE3C /* CrashReporter.swift */; };
03DAD3A62EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */; };
03DAD3A72EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */; };
03DAD3A82EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */; };
03DAD3A92EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */; };
03FE39E72E81682800B7B5AC /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E52E81682800B7B5AC /* AppIcon.icon */; };
03FE39E82E81682800B7B5AC /* AppIconEAP.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E62E81682800B7B5AC /* AppIconEAP.icon */; };
03FE39EA2E81694500B7B5AC /* AppIconUD.icon in Resources */ = {isa = PBXBuildFile; fileRef = 03FE39E92E81694500B7B5AC /* AppIconUD.icon */; };
@@ -377,7 +419,6 @@
C470150B2C46D81E0069AAE7 /* NVAlert in Frameworks */ = {isa = PBXBuildFile; productRef = C470150A2C46D81E0069AAE7 /* NVAlert */; };
C470150D2C46D83E0069AAE7 /* NVAlert in Frameworks */ = {isa = PBXBuildFile; productRef = C470150C2C46D83E0069AAE7 /* NVAlert */; };
C4709CA228524B3400088BB8 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4709CA128524B3400088BB8 /* StatsView.swift */; };
C471E79328F9B21F0021E251 /* ActiveFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */; };
C471E79428F9B23B0021E251 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; };
C471E79528F9B2420021E251 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; };
C471E7BF28F9B90F0021E251 /* StartupTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471E7BE28F9B90F0021E251 /* StartupTest.swift */; };
@@ -387,12 +428,8 @@
C471E7CC28F9BA5B0021E251 /* TestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4928DB966A007ACC74 /* TestableShell.swift */; };
C471E7CD28F9BA600021E251 /* ShellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4328DB95F0007ACC74 /* ShellProtocol.swift */; };
C471E7CE28F9BA600021E251 /* RealShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4628DB9644007ACC74 /* RealShell.swift */; };
C471E7CF28F9BA600021E251 /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; };
C471E7D028F9BA630021E251 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; };
C471E7D128F9BA630021E251 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; };
C471E7D228F9BA630021E251 /* ActiveFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */; };
C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; };
C471E7D428F9BA8F0021E251 /* ActiveFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */; };
C471E7D528F9BA8F0021E251 /* TestableConfigurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40F505428ECA64E004AD45B /* TestableConfigurations.swift */; };
C471E7D628F9BA8F0021E251 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; };
C471E7D728F9BA8F0021E251 /* TestableFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AD38B128ECD9D300FA8D83 /* TestableFileSystem.swift */; };
@@ -404,9 +441,7 @@
C471E7DD28F9BAA30021E251 /* CommandProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */; };
C471E7DE28F9BAA30021E251 /* CommandProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */; };
C471E7DF28F9BAAB0021E251 /* RealCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* RealCommand.swift */; };
C471E7E028F9BAAB0021E251 /* ActiveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE628F764050026AC4E /* ActiveCommand.swift */; };
C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* RealCommand.swift */; };
C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE628F764050026AC4E /* ActiveCommand.swift */; };
C471E7E328F9BAC20021E251 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; };
C471E7E428F9BAC20021E251 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; };
C471E7E528F9BAC20021E251 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; };
@@ -788,7 +823,6 @@
C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; };
C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; };
C4C8900528F0E3D100CE5E97 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; };
C4C8900728F0E3EF00CE5E97 /* ActiveFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */; };
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; };
C4C8E81B276F54E5003AC782 /* ConfigWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */; };
@@ -874,8 +908,6 @@
C4E2E86A28FC3002003B070C /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; };
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.swift */; };
C4E49DE728F764050026AC4E /* ActiveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE628F764050026AC4E /* ActiveCommand.swift */; };
C4E49DE828F764050026AC4E /* ActiveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE628F764050026AC4E /* ActiveCommand.swift */; };
C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */; };
C4E49DEB28F7643D0026AC4E /* CommandProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */; };
C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DEC28F764A00026AC4E /* TestableCommand.swift */; };
@@ -978,7 +1010,11 @@
/* Begin PBXFileReference section */
0309E6662B0D4B2F002AC007 /* BrewExtensionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewExtensionsObservable.swift; sourceTree = "<group>"; };
031E2B682B1525A7007C29E1 /* BrewPhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPhpExtension.swift; sourceTree = "<group>"; };
031F247F2EA1071700CFB8D9 /* Container+Fake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+Fake.swift"; sourceTree = "<group>"; };
031F24842EA1132300CFB8D9 /* PHP Monitor.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "PHP Monitor.xctestplan"; sourceTree = "<group>"; };
03263A372E86D5E800BD0415 /* UpdateScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateScheduler.swift; sourceTree = "<group>"; };
0329A9A02E92A2A800A62A12 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; };
0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WarningManager+Evaluations.swift"; sourceTree = "<group>"; };
032DAC272E8BEB590018E01C /* RealApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealApi.swift; sourceTree = "<group>"; };
032DAC2C2E8BEB690018E01C /* ApiProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiProtocol.swift; sourceTree = "<group>"; };
0336CAAF2B0D0CDA009A1034 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -990,16 +1026,22 @@
036C39092E5C8CBD008DAEDF /* PackagistP2Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistP2Response.swift; sourceTree = "<group>"; };
036C390E2E5C8D3B008DAEDF /* PackagistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackagistError.swift; sourceTree = "<group>"; };
036C39132E5CB820008DAEDF /* TestBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundle.swift; sourceTree = "<group>"; };
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
0392CDEA2EB25371009176DA /* SecurePopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurePopoverView.swift; sourceTree = "<group>"; };
0396160C2E74A61B002DD7F6 /* LoggableEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggableEvent.swift; sourceTree = "<group>"; };
039C29122E8AA15F007F5FAB /* ActiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveApi.swift; sourceTree = "<group>"; };
039C29172E8AA311007F5FAB /* TestableApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApi.swift; sourceTree = "<group>"; };
039C291C2E8AA399007F5FAB /* TestableApiTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableApiTest.swift; sourceTree = "<group>"; };
039E1D782E5F0F2C0072D13D /* ValetUpgrader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetUpgrader.swift; sourceTree = "<group>"; };
03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSImageExtension.swift; sourceTree = "<group>"; };
03BFF5262E312C39007F96FA /* Startup+Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Timers.swift"; sourceTree = "<group>"; };
03BFF52B2E313240007F96FA /* StatusMenu+Driver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Driver.swift"; sourceTree = "<group>"; };
03C099432EA15C8B00B76D43 /* Container+Real.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+Real.swift"; sourceTree = "<group>"; };
03CC1FE42E3D220F0050FC18 /* InstallHomebrew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallHomebrew.swift; sourceTree = "<group>"; };
03CC1FF32E3D230B0050FC18 /* ZshRunCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZshRunCommand.swift; sourceTree = "<group>"; };
03E36FE628D9219000636F7F /* ActiveShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveShell.swift; sourceTree = "<group>"; };
03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+Window.swift"; sourceTree = "<group>"; };
03D846312EB64E35006EFE3C /* CrashReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporter.swift; sourceTree = "<group>"; };
03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DomainListVC+Certs.swift"; sourceTree = "<group>"; };
03FE39E52E81682800B7B5AC /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
03FE39E62E81682800B7B5AC /* AppIconEAP.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconEAP.icon; sourceTree = "<group>"; };
03FE39E92E81694500B7B5AC /* AppIconUD.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIconUD.icon; sourceTree = "<group>"; };
@@ -1206,7 +1248,6 @@
C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = "<group>"; };
C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemProtocol.swift; sourceTree = "<group>"; };
C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealFileSystem.swift; sourceTree = "<group>"; };
C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveFileSystem.swift; sourceTree = "<group>"; };
C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = "<group>"; };
C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigWatchManager.swift; sourceTree = "<group>"; };
C4CB250429B28BB800CA4492 /* MainMenuTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTest.swift; sourceTree = "<group>"; };
@@ -1240,7 +1281,6 @@
C4E2E85B28FC282B003B070C /* TestableConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableConfiguration.swift; sourceTree = "<group>"; };
C4E2E86328FC2F1B003B070C /* XCPMApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCPMApplication.swift; sourceTree = "<group>"; };
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.swift; sourceTree = "<group>"; };
C4E49DE628F764050026AC4E /* ActiveCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCommand.swift; sourceTree = "<group>"; };
C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandProtocol.swift; sourceTree = "<group>"; };
C4E49DEC28F764A00026AC4E /* TestableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableCommand.swift; sourceTree = "<group>"; };
C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewTapFormulae.swift; sourceTree = "<group>"; };
@@ -1289,6 +1329,7 @@
buildActionMask = 2147483647;
files = (
C47014FF2C46D57C0069AAE7 /* NVAlert in Frameworks */,
03D8462B2EB6418F006EFE3C /* CrashReporter in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1297,6 +1338,7 @@
buildActionMask = 2147483647;
files = (
C470150B2C46D81E0069AAE7 /* NVAlert in Frameworks */,
0310B17C2EB8F40100A8B140 /* CrashReporter in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1305,6 +1347,7 @@
buildActionMask = 2147483647;
files = (
C470150D2C46D83E0069AAE7 /* NVAlert in Frameworks */,
0310B17E2EB8F40400A8B140 /* CrashReporter in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1313,6 +1356,7 @@
buildActionMask = 2147483647;
files = (
C47015072C46D8180069AAE7 /* NVAlert in Frameworks */,
0310B17A2EB8F3FF00A8B140 /* CrashReporter in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1336,6 +1380,18 @@
path = "PHP Versions";
sourceTree = "<group>";
};
036575C62EA12E2200BA41BF /* Versions */ = {
isa = PBXGroup;
children = (
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */,
C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */,
);
path = Versions;
sourceTree = "<group>";
};
036C38FB2E5C8827008DAEDF /* Packagist */ = {
isa = PBXGroup;
children = (
@@ -1355,18 +1411,6 @@
path = Integration;
sourceTree = "<group>";
};
036C3A222E5CBC33008DAEDF /* SwiftTestMigrated */ = {
isa = PBXGroup;
children = (
039C291E2E8AA39B007F5FAB /* Api */,
C4C1019927C65A4D001FACC2 /* Commands */,
036C39062E5C8890008DAEDF /* Integration */,
036C3A232E5CBC57008DAEDF /* Parsers */,
03D53E902E8AE089001B1671 /* Testables */,
);
path = SwiftTestMigrated;
sourceTree = "<group>";
};
036C3A232E5CBC57008DAEDF /* Parsers */ = {
isa = PBXGroup;
children = (
@@ -1419,6 +1463,16 @@
path = Provision;
sourceTree = "<group>";
};
03C099422EA156C100B76D43 /* Container */ = {
isa = PBXGroup;
children = (
0329A9A02E92A2A800A62A12 /* Container.swift */,
03C099432EA15C8B00B76D43 /* Container+Real.swift */,
031F247F2EA1071700CFB8D9 /* Container+Fake.swift */,
);
path = Container;
sourceTree = "<group>";
};
03D53E902E8AE089001B1671 /* Testables */ = {
isa = PBXGroup;
children = (
@@ -1612,6 +1666,7 @@
C41C1B3522B0097F00E7CF16 /* phpmon */ = {
isa = PBXGroup;
children = (
03C099422EA156C100B76D43 /* Container */,
C4B5853A2770FE2500DA4FBE /* Common */,
C41E181722CB61EB0072CF09 /* Domain */,
54D9E0BE27E4F5C0003B9AD9 /* Vendor */,
@@ -1808,6 +1863,7 @@
isa = PBXGroup;
children = (
C47699EE28A2F2A30060FEB8 /* WarningManager.swift */,
0329A9A22E92A68B00A62A12 /* WarningManager+Evaluations.swift */,
C47699F028A2F3150060FEB8 /* Warning.swift */,
C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */,
);
@@ -1841,6 +1897,8 @@
children = (
C464ADAB275A7A3F003FCD53 /* DomainListWindowController.swift */,
C464ADAE275A7A69003FCD53 /* DomainListVC.swift */,
03D846242EB6344A006EFE3C /* DomainListVC+Window.swift */,
03DAD3A52EB3B08A003417BD /* DomainListVC+Certs.swift */,
C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */,
C41CA5EC2774F8EE00A2C80E /* DomainListVC+Actions.swift */,
C4FE011028084FC200D1DE6D /* SelectionVC.swift */,
@@ -1968,6 +2026,7 @@
C471E79628F9B4260021E251 /* tests */ = {
isa = PBXGroup;
children = (
031F24842EA1132300CFB8D9 /* PHP Monitor.xctestplan */,
C4E2E86828FC2FF2003B070C /* Shared */,
C4F7807A25D7F84B000DBC97 /* unit */,
C471E7AE28F9B4940021E251 /* feature */,
@@ -2094,6 +2153,7 @@
C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */,
C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */,
C415D3E72770F692005EF286 /* AppDelegate+InterApp.swift */,
03D846312EB64E35006EFE3C /* CrashReporter.swift */,
C4811D2322D70A4700B5F6B3 /* App.swift */,
C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */,
C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */,
@@ -2111,8 +2171,8 @@
C4B5853A2770FE2500DA4FBE /* Common */ = {
isa = PBXGroup;
children = (
039C29112E8AA159007F5FAB /* Http */,
C4F787A728EF812600790735 /* Testables */,
039C29112E8AA159007F5FAB /* Http */,
C4F787A628EF811000790735 /* Shell */,
C4C8900128F0E27900CE5E97 /* Filesystem */,
C4E49DE528F763E20026AC4E /* Command */,
@@ -2153,6 +2213,7 @@
C4B609182853AAA700C95265 /* Domains */ = {
isa = PBXGroup;
children = (
0392CDEA2EB25371009176DA /* SecurePopoverView.swift */,
C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */,
);
path = Domains;
@@ -2174,6 +2235,7 @@
C4E4404527C56F4700D225E1 /* ValetSite.swift */,
C41C02A827E61A65009F26CB /* FakeValetSite.swift */,
C4BB39382981AFC700F8E797 /* PhpVersionSource.swift */,
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */,
);
path = Sites;
sourceTree = "<group>";
@@ -2195,18 +2257,6 @@
path = Nginx;
sourceTree = "<group>";
};
C4C1019827C65A1A001FACC2 /* Versions */ = {
isa = PBXGroup;
children = (
C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */,
C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */,
C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */,
C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */,
C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */,
);
path = Versions;
sourceTree = "<group>";
};
C4C1019927C65A4D001FACC2 /* Commands */ = {
isa = PBXGroup;
children = (
@@ -2218,7 +2268,6 @@
C4C8900128F0E27900CE5E97 /* Filesystem */ = {
isa = PBXGroup;
children = (
C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */,
C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */,
C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */,
);
@@ -2286,7 +2335,6 @@
C4E49DE528F763E20026AC4E /* Command */ = {
isa = PBXGroup;
children = (
C4E49DE628F764050026AC4E /* ActiveCommand.swift */,
C4B5853D2770FE3900DA4FBE /* RealCommand.swift */,
C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */,
);
@@ -2336,8 +2384,12 @@
isa = PBXGroup;
children = (
C40C7F1C27720E1400DDDCDC /* Test Files */,
036C3A222E5CBC33008DAEDF /* SwiftTestMigrated */,
C4C1019827C65A1A001FACC2 /* Versions */,
039C291E2E8AA39B007F5FAB /* Api */,
C4C1019927C65A4D001FACC2 /* Commands */,
036C39062E5C8890008DAEDF /* Integration */,
036C3A232E5CBC57008DAEDF /* Parsers */,
03D53E902E8AE089001B1671 /* Testables */,
036575C62EA12E2200BA41BF /* Versions */,
);
path = unit;
sourceTree = "<group>";
@@ -2345,7 +2397,6 @@
C4F787A628EF811000790735 /* Shell */ = {
isa = PBXGroup;
children = (
03E36FE628D9219000636F7F /* ActiveShell.swift */,
C46EBC4628DB9644007ACC74 /* RealShell.swift */,
C46EBC4328DB95F0007ACC74 /* ShellProtocol.swift */,
);
@@ -2367,6 +2418,7 @@
C4F8C0A222D4F100002EFE61 /* Extensions */ = {
isa = PBXGroup;
children = (
03B675E82EBA30D200EE04A9 /* NSImageExtension.swift */,
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */,
C46FA23E246C358E00944F05 /* StringExtension.swift */,
C48D0C9225CC804200CC7490 /* XibLoadable.swift */,
@@ -2422,6 +2474,7 @@
name = "PHP Monitor";
packageProductDependencies = (
C47014FE2C46D57C0069AAE7 /* NVAlert */,
03D8462A2EB6418F006EFE3C /* CrashReporter */,
);
productName = phpmon;
productReference = C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */;
@@ -2443,6 +2496,7 @@
name = "Feature Tests";
packageProductDependencies = (
C470150A2C46D81E0069AAE7 /* NVAlert */,
0310B17B2EB8F40100A8B140 /* CrashReporter */,
);
productName = "Feature Tests";
productReference = C471E7AD28F9B4940021E251 /* Feature Tests.xctest */;
@@ -2464,6 +2518,7 @@
name = "UI Tests";
packageProductDependencies = (
C470150C2C46D83E0069AAE7 /* NVAlert */,
0310B17D2EB8F40400A8B140 /* CrashReporter */,
);
productName = "UI Tests";
productReference = C471E7BC28F9B90F0021E251 /* UI Tests.xctest */;
@@ -2485,6 +2540,7 @@
name = "Unit Tests";
packageProductDependencies = (
C47015062C46D8180069AAE7 /* NVAlert */,
0310B1792EB8F3FF00A8B140 /* CrashReporter */,
);
productName = "phpmon-tests";
productReference = C4F7807925D7F84B000DBC97 /* Unit Tests.xctest */;
@@ -2498,7 +2554,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 2600;
LastUpgradeCheck = 2610;
ORGANIZATIONNAME = "Nico Verbruggen";
TargetAttributes = {
C406A5EF298AD2CE00B5B85A = {
@@ -2539,6 +2595,7 @@
packageReferences = (
C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */,
C47014FD2C46D57C0069AAE7 /* XCRemoteSwiftPackageReference "NVAlert" */,
03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */,
);
productRefGroup = C41C1B3422B0097F00E7CF16 /* Products */;
projectDirPath = "";
@@ -2689,7 +2746,6 @@
C47DF1AF299D5A3B0007055D /* LoginItemManager.swift in Sources */,
C4292D542B023F61004F0D2A /* PhpExtensionManagerWindowController.swift in Sources */,
C4D3661A291173EA006BD146 /* DictionaryExtension.swift in Sources */,
C4C8900728F0E3EF00CE5E97 /* ActiveFileSystem.swift in Sources */,
C409349D298EE8E900D25014 /* AppUpdater.swift in Sources */,
C4D8016622B1584700C6DA1B /* Startup.swift in Sources */,
C43931CA29C4C03F0069165B /* Brew.swift in Sources */,
@@ -2715,11 +2771,11 @@
C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */,
C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */,
C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */,
C4E49DE728F764050026AC4E /* ActiveCommand.swift in Sources */,
C43B8FD52BA9BAD3000C02BE /* UnavailableContentView.swift in Sources */,
032DAC2A2E8BEB5B0018E01C /* RealApi.swift in Sources */,
54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */,
C450C8C628C919EC002A2B4B /* PreferenceName.swift in Sources */,
0392CDEB2EB25371009176DA /* SecurePopoverView.swift in Sources */,
C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */,
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C4D9F24B280B69E100DCD39A /* AddProxyVC.swift in Sources */,
@@ -2734,6 +2790,7 @@
039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */,
C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,
C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */,
C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */,
C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */,
C4E9D2C02878B336008FFDAD /* OnboardingView.swift in Sources */,
@@ -2746,10 +2803,12 @@
C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */,
03BFF5282E312C3D007F96FA /* Startup+Timers.swift in Sources */,
C44A874828905BB000498BC4 /* ProgressVC.swift in Sources */,
0329A9A12E92A2AA00A62A12 /* Container.swift in Sources */,
C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */,
C456A0C62AA614BD0080144F /* PhpPreference.swift in Sources */,
C4B585442770FE3900DA4FBE /* RealCommand.swift in Sources */,
C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */,
0392CDE62EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C40C5C9C2846A40600E28255 /* Preset.swift in Sources */,
C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */,
C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */,
@@ -2794,17 +2853,19 @@
033D45982B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C47699F128A2F3150060FEB8 /* Warning.swift in Sources */,
54D9E0B227E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
0329A9A52E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
036C39102E5C8D42008DAEDF /* PackagistError.swift in Sources */,
C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */,
C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */,
03B675EA2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewDecodable.swift in Sources */,
03BFF52E2E313244007F96FA /* StatusMenu+Driver.swift in Sources */,
03E36FE728D9219000636F7F /* ActiveShell.swift in Sources */,
C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */,
C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */,
C4D5857C2A7038DB00DDBB63 /* ByteLimitView.swift in Sources */,
C4D4CB3729C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */,
03DAD3A72EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */,
C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
C441CC562AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
@@ -2813,6 +2874,7 @@
C417DC74277614690015E6EE /* Helpers.swift in Sources */,
C415D3E82770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
03D846352EB64E39006EFE3C /* CrashReporter.swift in Sources */,
C42759672627662800093CAE /* NSMenuExtension.swift in Sources */,
C422DDAA28A2C49900CEAC97 /* PhpDoctorView.swift in Sources */,
C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */,
@@ -2851,6 +2913,7 @@
C485707028BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */,
C40D725A2A018ACC0054A067 /* BusyStatus.swift in Sources */,
031F24802EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */,
C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */,
C4415E8D2B0287E90035F520 /* BrewFormulaeObservable.swift in Sources */,
@@ -2871,6 +2934,7 @@
C42106662AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
C40D725F2A018AE30054A067 /* BrewFormula+UI.swift in Sources */,
03D846282EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */,
03CC1FE62E3D22120050FC18 /* InstallHomebrew.swift in Sources */,
C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */,
C43BCD4429FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */,
@@ -2895,6 +2959,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
03D846322EB64E39006EFE3C /* CrashReporter.swift in Sources */,
036C39122E5C8D42008DAEDF /* PackagistError.swift in Sources */,
C471E82D28F9BB650021E251 /* AlertableError.swift in Sources */,
C471E82E28F9BB650021E251 /* Errors.swift in Sources */,
@@ -2917,18 +2982,21 @@
C471E83928F9BB650021E251 /* ValetSite.swift in Sources */,
C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */,
C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */,
0392CDE82EB23B8F009176DA /* CertificateValidator.swift in Sources */,
033D459A2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C4E2E86928FC3002003B070C /* Utility.swift in Sources */,
C471E83D28F9BB650021E251 /* FakeDomainScanner.swift in Sources */,
C471E83F28F9BB650021E251 /* AppDelegate.swift in Sources */,
C471E84028F9BB650021E251 /* AppDelegate+MenuOutlets.swift in Sources */,
C4D36603291132B7006BD146 /* ValetScanners.swift in Sources */,
03C099442EA15C8E00B76D43 /* Container+Real.swift in Sources */,
C471E84128F9BB650021E251 /* AppDelegate+Notifications.swift in Sources */,
C471E84228F9BB650021E251 /* AppDelegate+InterApp.swift in Sources */,
C471E84328F9BB650021E251 /* App.swift in Sources */,
C4E2E85E28FC282B003B070C /* TestableConfiguration.swift in Sources */,
C45E2A7529199248005C7CFD /* InternalSwitcherTest.swift in Sources */,
C471E84428F9BB650021E251 /* App+ActivationPolicy.swift in Sources */,
035983A22E97FA9100218DC7 /* Container.swift in Sources */,
C471E84528F9BB650021E251 /* App+GlobalHotkey.swift in Sources */,
C4513F922B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */,
C471E84628F9BB650021E251 /* InterAppHandler.swift in Sources */,
@@ -2958,6 +3026,7 @@
C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */,
C471E85B28F9BB650021E251 /* DomainListKindCell.swift in Sources */,
C4611E5E2AEAD2FB0010BE24 /* ConfigManagerView.swift in Sources */,
031F24822EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C4BF56AD2949381100379603 /* FakeValetInteractor.swift in Sources */,
C471E85C28F9BB650021E251 /* DomainListWindowController.swift in Sources */,
C471E85D28F9BB650021E251 /* DomainListVC.swift in Sources */,
@@ -2997,6 +3066,7 @@
C471E86F28F9BB650021E251 /* Stats.swift in Sources */,
C4CE7F9829683B43000102CF /* PhpVersionNumberCollection.swift in Sources */,
C4EA3C492BA4F947007B0BA7 /* CustomButtonStyles.swift in Sources */,
03B675E92EBA30D800EE04A9 /* NSImageExtension.swift in Sources */,
C471E87028F9BB650021E251 /* GlobalKeybindPreference.swift in Sources */,
C471E87228F9BB650021E251 /* CheckboxPreferenceView.swift in Sources */,
C471E87428F9BB650021E251 /* SelectPreferenceView.swift in Sources */,
@@ -3017,6 +3087,7 @@
C471E88528F9BB650021E251 /* ServicesView.swift in Sources */,
C471E88628F9BB650021E251 /* StatsView.swift in Sources */,
C451AFF82969E40F0078E617 /* HelpButton.swift in Sources */,
03D846272EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */,
C471E88728F9BB650021E251 /* SectionHeaderView.swift in Sources */,
C471E88828F9BB650021E251 /* HeaderView.swift in Sources */,
C471E88928F9BB650021E251 /* SwiftUIHelper.swift in Sources */,
@@ -3044,7 +3115,6 @@
C471E7D728F9BA8F0021E251 /* TestableFileSystem.swift in Sources */,
C471E81A28F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */,
C471E7E128F9BAAB0021E251 /* RealCommand.swift in Sources */,
C471E7E228F9BAAB0021E251 /* ActiveCommand.swift in Sources */,
03263A392E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
C471E80A28F9BADC0021E251 /* CreatedFromFile.swift in Sources */,
C471E80528F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */,
@@ -3077,12 +3147,12 @@
C471E7D928F9BA8F0021E251 /* TestableShell.swift in Sources */,
C471E81428F9BAE80021E251 /* NSWindowExtension.swift in Sources */,
C43BCD4629FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */,
C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */,
C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */,
C471E7DE28F9BAA30021E251 /* CommandProtocol.swift in Sources */,
C471E82928F9BB330021E251 /* Valet.swift in Sources */,
C471E80728F9BAD40021E251 /* PhpConfigurationFile.swift in Sources */,
0329A9A32E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */,
C471E7D528F9BA8F0021E251 /* TestableConfigurations.swift in Sources */,
C436B39F29F3C42500B6A64E /* PreferencesTabs.swift in Sources */,
03CC1FE82E3D22120050FC18 /* InstallHomebrew.swift in Sources */,
@@ -3094,7 +3164,6 @@
C489E0BD2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */,
C471E82028F9BB290021E251 /* NginxConfigurationFile.swift in Sources */,
C471E7D428F9BA8F0021E251 /* ActiveFileSystem.swift in Sources */,
032DAC2D2E8BEB6B0018E01C /* ApiProtocol.swift in Sources */,
C4513F902B13E2E6001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
C471E81528F9BAE80021E251 /* ArrayExtension.swift in Sources */,
@@ -3103,7 +3172,9 @@
C471E7D628F9BA8F0021E251 /* RealFileSystem.swift in Sources */,
C471E81728F9BAE80021E251 /* NSMenuExtension.swift in Sources */,
C40D725C2A018ACC0054A067 /* BusyStatus.swift in Sources */,
03DAD3A92EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C4821C5C2C2DEDE200357A68 /* AppMenu.swift in Sources */,
0392CDED2EB25371009176DA /* SecurePopoverView.swift in Sources */,
C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */,
C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */,
C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
@@ -3150,9 +3221,11 @@
C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */,
C471E8A628F9BB8F0021E251 /* App.swift in Sources */,
0392CDE72EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C4513F912B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */,
C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */,
C45B914C295607F400F4EC78 /* Service.swift in Sources */,
03DAD3A82EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C471E8A828F9BB8F0021E251 /* App+GlobalHotkey.swift in Sources */,
C471E8A928F9BB8F0021E251 /* InterAppHandler.swift in Sources */,
C471E8AA28F9BB8F0021E251 /* Startup.swift in Sources */,
@@ -3213,6 +3286,7 @@
C471E8D128F9BB8F0021E251 /* MenuBarIcons.swift in Sources */,
C471E8D228F9BB8F0021E251 /* Stats.swift in Sources */,
C471E8D328F9BB8F0021E251 /* GlobalKeybindPreference.swift in Sources */,
03C099462EA15C8E00B76D43 /* Container+Real.swift in Sources */,
039C29152E8AA163007F5FAB /* ActiveApi.swift in Sources */,
C471E8D528F9BB8F0021E251 /* CheckboxPreferenceView.swift in Sources */,
C471E8D728F9BB8F0021E251 /* SelectPreferenceView.swift in Sources */,
@@ -3221,6 +3295,7 @@
C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */,
C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */,
C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */,
03D846262EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */,
C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */,
C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */,
C471E8DF28F9BB8F0021E251 /* ConfigWatchManager.swift in Sources */,
@@ -3257,9 +3332,9 @@
03CC1FE72E3D22120050FC18 /* InstallHomebrew.swift in Sources */,
C4CE7F9929683B43000102CF /* PhpVersionNumberCollection.swift in Sources */,
C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */,
C471E7CF28F9BA600021E251 /* ActiveShell.swift in Sources */,
C4BB393C2981AFC700F8E797 /* PhpVersionSource.swift in Sources */,
C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */,
031F24812EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
039E1D7B2E5F0F300072D13D /* ValetUpgrader.swift in Sources */,
C471E7EE28F9BAC30021E251 /* Constants.swift in Sources */,
C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */,
@@ -3272,7 +3347,6 @@
C471E81228F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */,
C471E7DF28F9BAAB0021E251 /* RealCommand.swift in Sources */,
C469E701294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */,
C471E7E028F9BAAB0021E251 /* ActiveCommand.swift in Sources */,
C40175BB2903108900763A68 /* ValetInteractor.swift in Sources */,
C471E80928F9BADC0021E251 /* CreatedFromFile.swift in Sources */,
C471E80128F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */,
@@ -3290,13 +3364,15 @@
C471E80428F9BAD40021E251 /* PhpExtension.swift in Sources */,
C43931C829C4BD610069165B /* PhpVersionManagerView.swift in Sources */,
C471E7F728F9BACB0021E251 /* PhpSwitcher.swift in Sources */,
0392CDEE2EB25371009176DA /* SecurePopoverView.swift in Sources */,
03B675EB2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */,
03D846342EB64E39006EFE3C /* CrashReporter.swift in Sources */,
C4463FCF29804BCB007B93D5 /* RCFile.swift in Sources */,
C471E82C28F9BB340021E251 /* ValetListable.swift in Sources */,
C471E82828F9BB310021E251 /* BrewDiagnostics.swift in Sources */,
C43BCD4729FBEF40001547BC /* ModifyPhpVersionCommand.swift in Sources */,
C44E985F29B23EBF0059F773 /* UpdateCheckTest.swift in Sources */,
C4513F8E2B13E2E5001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
C471E7D228F9BA630021E251 /* ActiveFileSystem.swift in Sources */,
C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */,
C471E7F528F9BAC80021E251 /* PhpEnvironments.swift in Sources */,
C471E7ED28F9BAC30021E251 /* Process.swift in Sources */,
@@ -3324,12 +3400,14 @@
C4513F972B13E338001AD760 /* PhpExtensionManagerView+Actions.swift in Sources */,
C4D3661D291173EA006BD146 /* DictionaryExtension.swift in Sources */,
C471E80D28F9BAE80021E251 /* ArrayExtension.swift in Sources */,
035983A32E97FA9100218DC7 /* Container.swift in Sources */,
C471E7CD28F9BA600021E251 /* ShellProtocol.swift in Sources */,
C471E7EC28F9BAC30021E251 /* Events.swift in Sources */,
C471E7CE28F9BA600021E251 /* RealShell.swift in Sources */,
C469E706294CFDF700A82AB2 /* DomainsListTest.swift in Sources */,
C471E80F28F9BAE80021E251 /* NSMenuExtension.swift in Sources */,
C471E80B28F9BAE80021E251 /* XibLoadable.swift in Sources */,
0329A9A62E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */,
C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */,
03263A3B2E86D5EC00BD0415 /* UpdateScheduler.swift in Sources */,
C471E7CB28F9BA5B0021E251 /* TestableCommand.swift in Sources */,
@@ -3352,6 +3430,7 @@
C42F26742805B4B400938AC7 /* ValetListable.swift in Sources */,
C46EBC4528DB95F0007ACC74 /* ShellProtocol.swift in Sources */,
C44B3A4728E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
035983A12E97FA9100218DC7 /* Container.swift in Sources */,
C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */,
C471E79528F9B2420021E251 /* RealFileSystem.swift in Sources */,
54FCFD2B276C8AA4004CE748 /* CheckboxPreferenceView.swift in Sources */,
@@ -3359,6 +3438,7 @@
54B48B60275F66AE006D90C5 /* Application.swift in Sources */,
039C291A2E8AA314007F5FAB /* TestableApi.swift in Sources */,
C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */,
03C099472EA15C8E00B76D43 /* Container+Real.swift in Sources */,
C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */,
C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */,
C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */,
@@ -3368,6 +3448,7 @@
C485707528BF454F00539B36 /* StatsView.swift in Sources */,
C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */,
54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */,
03D846332EB64E39006EFE3C /* CrashReporter.swift in Sources */,
C485707328BF454300539B36 /* OnboardingView.swift in Sources */,
C485707728BF455300539B36 /* HeaderView.swift in Sources */,
C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */,
@@ -3400,6 +3481,7 @@
C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */,
C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */,
C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */,
0392CDE92EB23B8F009176DA /* CertificateValidator.swift in Sources */,
C4821C5B2C2DEDE200357A68 /* AppMenu.swift in Sources */,
C463E381284930EE00422731 /* PresetHelper.swift in Sources */,
C441CC572AE8249400DDFACD /* ConfigFSNotifier.swift in Sources */,
@@ -3423,6 +3505,7 @@
C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */,
C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */,
C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */,
031F24832EA1071A00CFB8D9 /* Container+Fake.swift in Sources */,
C40D725B2A018ACC0054A067 /* BusyStatus.swift in Sources */,
032DAC292E8BEB5B0018E01C /* RealApi.swift in Sources */,
C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */,
@@ -3457,6 +3540,7 @@
C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */,
036C39082E5C88A7008DAEDF /* PackagistTest.swift in Sources */,
C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */,
03B675EC2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */,
C4611E612AEAD3110010BE24 /* ByteLimitView.swift in Sources */,
@@ -3469,16 +3553,14 @@
C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */,
5489625928313231004F647A /* CreatedFromFile.swift in Sources */,
C4513F932B13E2FB001AD760 /* PhpExtensionManagerView.swift in Sources */,
C471E79328F9B21F0021E251 /* ActiveFileSystem.swift in Sources */,
54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */,
03E36FE828D9219000636F7F /* ActiveShell.swift in Sources */,
0396160E2E74A61E002DD7F6 /* LoggableEvent.swift in Sources */,
C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */,
C4E2E86528FC2F1B003B070C /* XCPMApplication.swift in Sources */,
C4E49DE828F764050026AC4E /* ActiveCommand.swift in Sources */,
C489E0BC2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */,
036C390B2E5C8CC5008DAEDF /* PackagistP2Response.swift in Sources */,
C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */,
03DAD3A62EB3B08F003417BD /* DomainListVC+Certs.swift in Sources */,
C4B79ECC29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */,
C43B8FD62BA9C689000C02BE /* UnavailableContentView.swift in Sources */,
C4FD87AA29AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */,
@@ -3521,6 +3603,8 @@
039C29162E8AA163007F5FAB /* ActiveApi.swift in Sources */,
C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */,
C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */,
03D846252EB6344E006EFE3C /* DomainListVC+Window.swift in Sources */,
0329A9A42E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */,
033D45992B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */,
C40B24F227A310770018C7D2 /* Events.swift in Sources */,
@@ -3552,6 +3636,7 @@
C485706F28BF452300539B36 /* PhpDoctorWindowController.swift in Sources */,
C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */,
C43931CB29C4C03F0069165B /* Brew.swift in Sources */,
0392CDEC2EB25371009176DA /* SecurePopoverView.swift in Sources */,
C41C02AB27E61CB3009F26CB /* FakeValetSite.swift in Sources */,
C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */,
C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */,
@@ -3835,7 +3920,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1605;
CURRENT_PROJECT_VERSION = 1685;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
ENABLE_APP_SANDBOX = NO;
@@ -3854,7 +3939,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 25.09;
MARKETING_VERSION = 25.10;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_MODULE_NAME = PHP_Monitor;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -3879,7 +3964,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1605;
CURRENT_PROJECT_VERSION = 1685;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
ENABLE_APP_SANDBOX = NO;
@@ -3898,7 +3983,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 25.09;
MARKETING_VERSION = 25.10;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_MODULE_NAME = PHP_Monitor;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -4061,7 +4146,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1605;
CURRENT_PROJECT_VERSION = 1685;
DEAD_CODE_STRIPPING = YES;
DEBUG = YES;
ENABLE_APP_SANDBOX = NO;
@@ -4080,7 +4165,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 25.09;
MARKETING_VERSION = 25.10;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
PRODUCT_MODULE_NAME = PHP_Monitor;
PRODUCT_NAME = "$(TARGET_NAME) EAP";
@@ -4254,7 +4339,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1605;
CURRENT_PROJECT_VERSION = 1685;
DEAD_CODE_STRIPPING = YES;
DEBUG = NO;
ENABLE_APP_SANDBOX = NO;
@@ -4273,7 +4358,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.5;
MARKETING_VERSION = 25.09;
MARKETING_VERSION = 25.10;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
PRODUCT_MODULE_NAME = PHP_Monitor;
PRODUCT_NAME = "$(TARGET_NAME) EAP";
@@ -4490,6 +4575,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/microsoft/plcrashreporter.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.12.0;
};
};
C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nicoverbruggen/NVAppUpdater";
@@ -4502,13 +4595,33 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/nicoverbruggen/NVAlert";
requirement = {
branch = main;
kind = branch;
kind = exactVersion;
version = 1.1.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
0310B1792EB8F3FF00A8B140 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
0310B17B2EB8F40100A8B140 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
0310B17D2EB8F40400A8B140 /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
03D8462A2EB6418F006EFE3C /* CrashReporter */ = {
isa = XCSwiftPackageProductDependency;
package = 03D846292EB6418F006EFE3C /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
C47014FB2C46D31B0069AAE7 /* NVAppUpdater */ = {
isa = XCSwiftPackageProductDependency;
package = C47014FA2C46D31B0069AAE7 /* XCRemoteSwiftPackageReference "NVAppUpdater" */;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
LastUpgradeVersion = "2610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -30,7 +30,7 @@
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "NO"
testExecutionOrdering = "random">
<BuildableReference
@@ -53,7 +53,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
@@ -67,8 +67,8 @@
</TestAction>
<LaunchAction
buildConfiguration = "Debug.EA"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
LastUpgradeVersion = "2610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
LastUpgradeVersion = "2610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,11 +26,16 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:tests/PHP Monitor.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
@@ -40,7 +45,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
@@ -65,8 +70,8 @@
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -111,6 +116,11 @@
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SKIP_UPDATE_CHECK"
value = ""
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
LastUpgradeVersion = "2610"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -10,7 +10,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
disableMainThreadChecker = "YES">
<Testables>
<TestableReference
skipped = "NO">

View File

@@ -593,15 +593,17 @@ If you would like to know more, consult [this issue](https://github.com/nicoverb
<details>
<summary><strong>The app has crashed!</strong></summary>
Please get in touch and open an issue. PHP Monitor shouldn't crash... (unless you are actually removing PHP *while* the app is running, thats considered normal behaviour!)
When you launch PHP Monitor again, a crash report can be submitted automatically. This is really helpful, please consider doing this!
If you would like to report a crash, please include the associated **log files** so I can find out what exactly went wrong.
However, if you'd like to help out more, you can. You can also get in touch and open an issue with additional info.
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
- First, you need the **crash report**. To find the crash report, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor". If you've accepted the automatic crash report, I should already have received this, but if you want to report a bug, please include it again.
Additionally, you can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
- Additionally, you can help me figure out even more information by sending me your **verbose log for your latest session of PHP Monitor**. Since logging is disabled by default, you will need to turn it on. You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. (This is only relevant if you have something crash that you can reliably reproduce.)
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it to the relevant bug report.
- If you can, in fact, easily reproduce the issue, providing me the **reproduction steps** is very helpful. I may ask you additional questions to help resolve the issue.
Any further information besides the crash report may help shed light on what's causing things to go wrong.
</details>

View File

@@ -0,0 +1,36 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.362",
"green" : "0.371",
"red" : "0.362"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "extended-gray",
"components" : {
"alpha" : "1.000",
"white" : "0.792"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.180",
"green" : "0.500",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.426",
"green" : "0.655",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,25 +0,0 @@
//
// ActiveCommand.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var Command: CommandProtocol {
return ActiveCommand.shared
}
class ActiveCommand {
static var shared: CommandProtocol = RealCommand()
public static func useTestable(_ output: [String: String]) {
Self.shared = TestableCommand(commands: output)
}
public static func useSystem() {
Self.shared = RealCommand()
}
}

View File

@@ -8,7 +8,6 @@
import Cocoa
public class RealCommand: CommandProtocol {
public func execute(
path: String,
arguments: [String],
@@ -52,5 +51,4 @@ public class RealCommand: CommandProtocol {
withStandardError: false
)
}
}

View File

@@ -10,52 +10,70 @@ import AppKit
class Actions {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Variables
var formulae: HomebrewFormulae {
return HomebrewFormulae(App.shared.container)
}
var paths: Paths {
return container.paths
}
// MARK: - Services
public static func linkPhp() async {
await brew("link php --overwrite --force")
public func linkPhp() async {
await brew(container, "link php --overwrite --force")
}
public static func restartPhpFpm() async {
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
public func restartPhpFpm() async {
await brew(container, "services restart \(formulae.php)", sudo: formulae.php.elevated)
}
public static func restartPhpFpm(version: String) async {
public func restartPhpFpm(version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
await brew(container, "services restart \(formula)", sudo: formulae.php.elevated)
}
public static func restartNginx() async {
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
public func restartNginx() async {
await brew(container, "services restart \(formulae.nginx)", sudo: formulae.nginx.elevated)
}
public static func restartDnsMasq() async {
await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
public func restartDnsMasq() async {
await brew(container, "services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
}
public static func stopValetServices() async {
await brew("services stop \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
await brew("services stop \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
await brew("services stop \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
public func stopValetServices() async {
await brew(container, "services stop \(formulae.php)", sudo: formulae.php.elevated)
await brew(container, "services stop \(formulae.nginx)", sudo: formulae.nginx.elevated)
await brew(container, "services stop \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
}
public static func fixHomebrewPermissions() throws {
public func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop \(HomebrewFormulae.nginx)",
"\(Paths.brew) services stop \(HomebrewFormulae.dnsmasq)"
"\(paths.brew) services stop \(formulae.nginx)",
"\(paths.brew) services stop \(formulae.dnsmasq)"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.dnsmasq)"
"chown -R \(paths.whoami):admin \(paths.cellarPath)/\(formulae.nginx)",
"chown -R \(paths.whoami):admin \(paths.cellarPath)/\(formulae.dnsmasq)"
]
PhpEnvironments.shared.availablePhpVersions.forEach { version in
App.shared.container.phpEnvs.availablePhpVersions.forEach { version in
let formula = version == PhpEnvironments.brewPhpAlias
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
servicesCommands.append("\(paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(paths.whoami):admin \(paths.cellarPath)/\(formula)")
}
let script =
@@ -77,38 +95,38 @@ class Actions {
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
public func openGenericPhpConfigFolder() {
let files = [NSURL(fileURLWithPath: "\(paths.etcPath)/php")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
public func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(paths.etcPath)/php/\(version)/php.ini")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder() {
public func openGlobalComposerFolder() {
let file = URL(string: "file://~/.composer/composer.json".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openValetConfigFolder() {
public func openValetConfigFolder() {
let file = URL(string: "file://~/.config/valet".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpMonitorConfigFile() {
public func openPhpMonitorConfigFile() {
let file = URL(string: "file://~/.config/phpmon".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() async -> URL {
try! FileSystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
public func createTempPhpInfoFile() async -> URL {
try! container.filesystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
// Tell php-cgi to run the PHP and output as an .html file
await Shell.quiet("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
await container.shell.quiet("\(paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
@@ -127,10 +145,10 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias)
await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated)
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
public func fixMyValet() async {
await InternalSwitcher(container).performSwitch(to: PhpEnvironments.brewPhpAlias)
await brew(container, "services restart \(formulae.dnsmasq)", sudo: formulae.dnsmasq.elevated)
await brew(container, "services restart \(formulae.php)", sudo: formulae.php.elevated)
await brew(container, "services restart \(formulae.nginx)", sudo: formulae.nginx.elevated)
}
}

View File

@@ -63,7 +63,7 @@ struct Constants {
will be displayed to let them know that certain operations
will not work correctly and that they need to update their app.
The cutoff date is always a few days after GA of the latest
It always takes a few days for a new update after GA of the latest
release, as it often takes a while for Homebrew to make the
new release available and not everyone uses a separate tap.
*/
@@ -154,8 +154,14 @@ struct Constants {
static let EarlyAccessChangelog = url("https://phpmon.app/early-access/release-notes")
// API endpoints (via api.phpmon.app)
static let UpdateCheckEndpoint = url("https://api.phpmon.app/api/v1/update-check")
// API endpoints
#if DEBUG
static let UpdateCheckEndpoint = url("https://api.phpmon.test/api/v1/update-check")
static let CrashReportingEndpoint = url("https://api.phpmon.test/api/v1/report-crash")
#else
static let UpdateCheckEndpoint = url("https://api.phpmon.app/api/v1/update-check")
static let CrashReportingEndpoint = url("https://api.phpmon.app/api/v1/report-crash")
#endif
// GitHub URLs (do not alias these)
static let GitHubReleases = url("https://github.com/nicoverbruggen/phpmon/releases")

View File

@@ -13,32 +13,45 @@ import Foundation
/**
Runs a `brew` command. Can run as superuser.
*/
func brew(_ command: String, sudo: Bool = false) async {
await Shell.quiet("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
func brew(
_ container: Container,
_ command: String,
sudo: Bool = false,
) async {
await container.shell.quiet("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
}
/**
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) async {
func sed(
_ container: Container,
file: String,
original: String,
replacement: String
) async {
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if FileSystem.fileExists("\(Paths.binPath)/gsed") {
await Shell.quiet("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
if container.filesystem.fileExists("\(container.paths.binPath)/gsed") {
await container.shell.quiet("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {
await Shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
await container.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.
*/
func grepContains(file: String, query: String) async -> Bool {
return await Shell.pipe("""
func grepContains(
shell: ShellProtocol,
file: String,
query: String
) async -> Bool {
return await shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""").out
.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -49,7 +62,7 @@ func grepContains(file: String, query: String) async -> Bool {
Attempts to introduce sleep for a particular duration. Use with caution.
*/
func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}
/**

View File

@@ -9,25 +9,36 @@
import Foundation
struct HomebrewFormulae {
static var php: HomebrewFormula {
if PhpEnvironments.shared.homebrewPackage == nil {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Variables
var php: HomebrewFormula {
if container.phpEnvs.homebrewPackage == nil {
return HomebrewFormula("php", elevated: true)
}
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
return HomebrewFormula("php", elevated: true)
}
return HomebrewFormula(install.formula, elevated: true)
}
static var nginx: HomebrewFormula {
return BrewDiagnostics.usesNginxFullFormula
var nginx: HomebrewFormula {
return BrewDiagnostics.shared.usesNginxFullFormula
? HomebrewFormula("nginx-full", elevated: true)
: HomebrewFormula("nginx", elevated: true)
}
static var dnsmasq: HomebrewFormula {
var dnsmasq: HomebrewFormula {
return HomebrewFormula("dnsmasq", elevated: true)
}
}

View File

@@ -9,7 +9,6 @@
import Foundation
class Log {
static var shared = Log()
var logFilePath = "~/.config/phpmon/last_session.log"
@@ -33,7 +32,7 @@ class Log {
system_quiet("mkdir -p ~/.config/phpmon 2> /dev/null")
system_quiet("rm ~/.config/phpmon/last_session.log 2> /dev/null")
system_quiet("touch ~/.config/phpmon/last_session.log 2> /dev/null")
self.logExists = FileSystem.fileExists(self.logFilePath)
self.logExists = App.shared.container.filesystem.fileExists(self.logFilePath)
}
}

View File

@@ -12,21 +12,19 @@ import Foundation
The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/
public class Paths {
public static let shared = Paths()
internal let container: Container
internal var baseDir: Paths.HomebrewDir
private var userName: String
private var preferredShell: String
init() {
init(container: Container) {
// Assume the default directory is correct
baseDir = App.architecture != "x86_64" ? .opt : .usr
// Ensure that if a different location is used, it takes precendence
if baseDir == .usr
&& FileSystem.directoryExists("/usr/local/homebrew")
&& !FileSystem.directoryExists("/usr/local/Cellar") {
&& container.filesystem.directoryExists("/usr/local/homebrew")
&& !container.filesystem.directoryExists("/usr/local/Cellar") {
Log.warn("Using /usr/local/homebrew as base directory!")
baseDir = .usr_hb
}
@@ -38,6 +36,8 @@ public class Paths {
Log.info("The current username is `\(userName)`.")
Log.info("The user's shell is `\(preferredShell)`.")
}
self.container = container
}
public func detectBinaryPaths() {
@@ -46,76 +46,76 @@ public class Paths {
// - MARK: Binaries
public static var valet: String {
public var valet: String {
return "\(binPath)/valet"
}
public static var brew: String {
public var brew: String {
return "\(binPath)/brew"
}
public static var php: String {
public var php: String {
return "\(binPath)/php"
}
public static var phpConfig: String {
public var phpConfig: String {
return "\(binPath)/php-config"
}
// - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String?
public var composer: String?
// - MARK: Paths
public static var whoami: String {
return shared.userName
public var whoami: String {
return userName
}
public static var homePath: String {
if FileSystem is RealFileSystem {
public var homePath: String {
if container.filesystem is RealFileSystem {
return NSHomeDirectory()
}
if FileSystem is TestableFileSystem {
let fs = FileSystem as! TestableFileSystem
if container.filesystem is TestableFileSystem {
let fs = container.filesystem as! TestableFileSystem
return fs.homeDirectory
}
fatalError("A valid FileSystem must be allowed to return the home path")
}
public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar"
public var cellarPath: String {
return "\(baseDir.rawValue)/Cellar"
}
public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin"
public var binPath: String {
return "\(baseDir.rawValue)/bin"
}
public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt"
public var optPath: String {
return "\(baseDir.rawValue)/opt"
}
public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc"
public var etcPath: String {
return "\(baseDir.rawValue)/etc"
}
public static var tapPath: String {
if shared.baseDir == .usr {
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
public var tapPath: String {
if baseDir == .usr {
return "\(baseDir.rawValue)/homebrew/Library/Taps"
}
return "\(shared.baseDir.rawValue)/Library/Taps"
return "\(baseDir.rawValue)/Library/Taps"
}
public static var caskroomPath: String {
return "\(shared.baseDir.rawValue)/Caskroom/phpmon"
public var caskroomPath: String {
return "\(baseDir.rawValue)/Caskroom/phpmon"
}
public static var shell: String {
return shared.preferredShell
public var shell: String {
return preferredShell
}
// MARK: - Flexible Binaries
@@ -123,14 +123,14 @@ public class Paths {
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if FileSystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
} else if FileSystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else if FileSystem.fileExists("/usr/local/homebrew/bin/composer") {
Paths.composer = "/usr/local/homebrew/bin/composer"
if container.filesystem.fileExists("/usr/local/bin/composer") {
composer = "/usr/local/bin/composer"
} else if container.filesystem.fileExists("/opt/homebrew/bin/composer") {
composer = "/opt/homebrew/bin/composer"
} else if container.filesystem.fileExists("/usr/local/homebrew/bin/composer") {
composer = "/usr/local/homebrew/bin/composer"
} else {
Paths.composer = nil
composer = nil
Log.warn("Composer was not found.")
}
}

View File

@@ -0,0 +1,23 @@
//
// NSImageExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Cocoa
extension NSImage {
func resized(to newSize: NSSize) -> NSImage {
let newImage = NSImage(size: newSize)
newImage.lockFocus()
self.draw(in: NSRect(origin: .zero, size: newSize),
from: NSRect(origin: .zero, size: self.size),
operation: .sourceOver,
fraction: 1.0)
newImage.unlockFocus()
newImage.isTemplate = self.isTemplate
return newImage
}
}

View File

@@ -9,6 +9,10 @@ import SwiftUI
struct Localization {
static var preferredLanguage: String? {
if Preferences.shared == nil {
return nil
}
guard let language = Preferences.preferences[.languageOverride] as? String else {
return nil
}
@@ -61,12 +65,7 @@ extension String {
return NSLocalizedString(self, bundle: bundle, comment: "")
}
// Ensure that on more recent versions of macOS, "Preferences" is replaced with "Settings"
if #available(macOS 13, *) {
return string.replacingOccurrences(of: "Preferences", with: "Settings")
}
return string
return string.replacingOccurrences(of: "Preferences", with: "Settings")
}
var localizedForSwiftUI: LocalizedStringKey {

View File

@@ -1,26 +0,0 @@
//
// FS.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var FileSystem: FileSystemProtocol {
return ActiveFileSystem.shared
}
class ActiveFileSystem {
static var shared: FileSystemProtocol = RealFileSystem()
/** Note: Intermediate directories are not automatically inferred and have to be manually declared. */
public static func useTestable(_ files: [String: FakeFile]) {
Self.shared = TestableFileSystem(files: files)
}
public static func useSystem() {
Self.shared = RealFileSystem()
}
}

View File

@@ -10,12 +10,26 @@ import Foundation
extension String {
var replacingTildeWithHomeDirectory: String {
return self.replacingOccurrences(of: "~", with: Paths.homePath)
// Try and check if there's a shared container
if let paths = App.shared.container.paths {
return self.replacingOccurrences(of: "~", with: paths.homePath)
}
// TODO: Come up with some other way to handle this when the app container is not available, especially for tests
return self
}
}
class RealFileSystem: FileSystemProtocol {
// MARK: - Container
var container: Container
init(container: Container) {
self.container = container
}
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) {
@@ -64,7 +78,7 @@ class RealFileSystem: FileSystemProtocol {
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
_ = container.shell.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks

View File

@@ -17,6 +17,12 @@ class Application {
case editor, browser, git_gui, terminal, user_supplied
}
// MARK: - Container
var container: Container
// MARK: - Variables
/// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String
@@ -24,7 +30,8 @@ class Application {
let type: AppType
/// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) {
init(_ container: Container, _ name: String, _ type: AppType) {
self.container = container
self.name = name
self.type = type
}
@@ -34,19 +41,19 @@ class Application {
(This will open the app if it isn't open yet.)
*/
@objc public func openDirectory(file: String) {
Task { await Shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") }
Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") }
}
/** Checks if the app is installed. */
func isInstalled() async -> Bool {
let (process, output) = try! await Shell.attach(
let (process, output) = try! await container.shell.attach(
"/usr/bin/open -Ra \"\(name)\"",
didReceiveOutput: { _, _ in },
withTimeout: 2.0
)
if Shell is TestableShell {
if container.shell is TestableShell {
// When testing, check the error output (must not be empty)
return !output.hasError
} else {
@@ -58,15 +65,17 @@ class Application {
/**
Detect which apps are available to open a specific directory.
*/
static public func detectPresetApplications() async -> [Application] {
static public func detectPresetApplications(
_ container: Container
) async -> [Application] {
var detected: [Application] = []
let detectable = [
Application("PhpStorm", .editor),
Application("Visual Studio Code", .editor),
Application("Sublime Text", .editor),
Application("Sublime Merge", .git_gui),
Application("iTerm", .terminal)
Application(container, "PhpStorm", .editor),
Application(container, "Visual Studio Code", .editor),
Application(container, "Sublime Text", .editor),
Application(container, "Sublime Merge", .git_gui),
Application(container, "iTerm", .terminal)
]
for app in detectable where await app.isInstalled() {

View File

@@ -7,5 +7,5 @@
//
protocol ApiProtocol {
}

View File

@@ -16,36 +16,45 @@ import Foundation
- Note: Each installation has a separate version number.
Using `version.short` is advisable if you want to interact with Homebrew.
*/
class ActivePhpInstallation {
// MARK: - Container
var container: Container
// MARK: - Variables
var version: VersionNumber!
var limits: Limits!
var iniFiles: [PhpConfigurationFile] = []
var hasErrorState: Bool = false
// MARK: - Computed
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
}
}
// MARK: - Computed
var formula: String {
return (version.short == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
public static func load() -> ActivePhpInstallation? {
if !FileSystem.fileExists(Paths.phpConfig) {
public static func load(_ container: Container) -> ActivePhpInstallation? {
if !container.filesystem.fileExists(container.paths.phpConfig) {
return nil
}
return ActivePhpInstallation()
return ActivePhpInstallation(container)
}
init() {
init(_ container: Container) {
self.container = container
// Show information about the current version
do {
try determineVersion()
@@ -69,14 +78,14 @@ class ActivePhpInstallation {
post_max_size: getByteCount(key: "post_max_size")
)
let paths = ActiveShell.shared
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
let paths = container.shell
.sync("\(container.paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
.split(separator: "\n")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file)
}
}
@@ -87,7 +96,11 @@ class ActivePhpInstallation {
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private func determineVersion() throws {
let output = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
let output = container.command.execute(
path: container.paths.phpConfig,
arguments: ["--version"],
trimNewlines: true
)
self.hasErrorState = (output == "" || output.contains("Warning") || output.contains("Error"))
@@ -110,7 +123,11 @@ class ActivePhpInstallation {
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"], trimNewlines: false)
let value = container.command.execute(
path: container.paths.php,
arguments: ["-r", "echo ini_get('\(key)');"],
trimNewlines: false
)
// Check if the value is unlimited
if value == "-1" {

View File

@@ -11,12 +11,22 @@ import Cocoa
class Xdebug {
public static var enabled: Bool {
return PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") != nil
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
public static var activeModes: [String] {
guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else {
// MARK: - Variables
public var enabled: Bool {
return container.phpEnvs.getConfigFile(forKey: "xdebug.mode") != nil
}
public var activeModes: [String] {
guard let file = container.phpEnvs.getConfigFile(forKey: "xdebug.mode") else {
return []
}
@@ -24,15 +34,26 @@ class Xdebug {
return []
}
return value.components(separatedBy: ",").filter { self.modes.contains($0) }
return value.components(separatedBy: ",").filter { self.availableModes.contains($0) }
}
public static func asMenuItems() -> [NSMenuItem] {
public var availableModes: [String] {
return [
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
// MARK: - Methods
public func asMenuItems() -> [NSMenuItem] {
var items: [NSMenuItem] = []
let activeModes = Self.activeModes
for mode in Self.modes {
for mode in availableModes {
let item = XdebugMenuItem(
title: mode,
action: #selector(MainMenu.toggleXdebugMode(sender:)),
@@ -47,15 +68,4 @@ class Xdebug {
return items
}
public static var modes: [String] {
return [
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
}

View File

@@ -9,28 +9,23 @@
import Foundation
class PhpEnvironments {
var container: Container
// MARK: - Initializer
/**
Loads the currently active PHP installation upon startup. May be empty.
*/
init() {
self.currentInstall = ActivePhpInstallation.load()
}
/**
Creates the shared instance. Called when starting the app.
*/
static func prepare() {
_ = Self.shared
init(container: Container) {
self.container = container
self.currentInstall = ActivePhpInstallation.load(container)
}
/**
Determine which PHP version the `php` formula is aliased to.
*/
@MainActor func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
let brewPhpAlias = await container.shell.pipe("\(container.paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
@@ -41,9 +36,9 @@ class PhpEnvironments {
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
// Check if that version actually corresponds to an older version
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
let phpConfigExecutablePath = "\(container.paths.optPath)/php/bin/php-config"
if container.filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = container.command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"],
trimNewlines: false
@@ -66,9 +61,6 @@ class PhpEnvironments {
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static instance. Accessible at any time. */
static let shared = PhpEnvironments()
/** Whether the switcher is busy performing any actions. */
@MainActor var isBusy: Bool = false {
didSet {
@@ -110,23 +102,23 @@ class PhpEnvironments {
/**
It's possible for the alias to be newer than the actual installed version of PHP.
*/
static var homebrewBrewPhpAlias: String {
if PhpEnvironments.shared.homebrewPackage == nil {
var homebrewBrewPhpAlias: String {
if homebrewPackage == nil {
// For UI testing and as a fallback, determine this version by using (fake) php-config
let version = Command.execute(path: "/opt/homebrew/bin/php-config",
let version = App.shared.container.command.execute(path: "/opt/homebrew/bin/php-config",
arguments: ["--version"],
trimNewlines: true)
return try! VersionNumber.parse(version).short
}
return PhpEnvironments.shared.homebrewPackage.version
return homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation? {
return Self.shared.currentInstall
var phpInstall: ActivePhpInstallation? {
return currentInstall
}
/**
@@ -142,16 +134,11 @@ class PhpEnvironments {
but currently this is no longer needed.
*/
public static var switcher: PhpSwitcher {
return InternalSwitcher()
return InternalSwitcher(App.shared.container)
}
/**
Alias that detects which versions of PHP are installed.
See also: `detectPhpVersions()`. Please note that this method
does *not* return the set of PHP versions that are supported.
*/
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
public func reloadPhpVersions() async {
_ = await self.detectPhpVersions()
}
/**
@@ -162,7 +149,7 @@ class PhpEnvironments {
Returns a `Set<String>` of installations that are considered valid.
*/
public func detectPhpVersions() async -> Set<String> {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
let files = await container.shell.pipe("ls \(container.paths.optPath) | grep php@").out
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
@@ -182,8 +169,8 @@ class PhpEnvironments {
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
let phpAliasInstall = PhpInstallation(phpAlias)
if !supportedVersions.contains(phpAlias) && container.filesystem.fileExists("\(container.paths.optPath)/php/bin/php") {
let phpAliasInstall = PhpInstallation(container, phpAlias)
// Before inserting, ensure that the actual output matches the alias
// if that isn't the case, our formula remains out-of-date
if !phpAliasInstall.isMissingBinary {
@@ -203,7 +190,7 @@ class PhpEnvironments {
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
mappedVersions[version] = PhpInstallation(container, version)
}
cachedPhpInstallations = mappedVersions
@@ -235,14 +222,14 @@ class PhpEnvironments {
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& supported.contains(version)
&& (checkBinaries ? FileSystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
&& (checkBinaries ? container.filesystem.fileExists("\(container.paths.optPath)/php@\(version)/bin/php") : true) {
output.insert(version)
}
}
if generateHelpers {
for item in output {
await PhpHelper.generate(for: item)
await PhpHelper.generate(container, for: item)
}
}
@@ -265,7 +252,7 @@ class PhpEnvironments {
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
guard let install = PhpEnvironments.phpInstall else {
guard let install = self.phpInstall else {
Log.info("It appears as if no PHP installation is currently active.")
return false
}
@@ -286,7 +273,7 @@ class PhpEnvironments {
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
guard let install = PhpEnvironments.phpInstall else {
guard let install = self.phpInstall else {
return nil
}

View File

@@ -9,34 +9,36 @@
import Foundation
class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) async {
public static func generate(
_ container: Container,
for version: String
) async {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
// Determine the dotless name for this PHP version
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "\(container.paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = Shell.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
let inPath = container.shell.PATH.contains("\(container.paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileSystem.isWriteableFile("/usr/local/bin/")
let canWriteSymlinks = container.filesystem.isWriteableFile("/usr/local/bin/")
Task { // Create the appropriate folders and check if the files exist
do {
if !FileSystem.directoryExists("~/.config/phpmon/bin") {
if !container.filesystem.directoryExists("~/.config/phpmon/bin") {
Task { @MainActor in
try FileSystem.createDirectory(
try container.filesystem.createDirectory(
"~/.config/phpmon/bin",
withIntermediateDirectories: true
)
}
}
if FileSystem.fileExists(destination) {
if container.filesystem.fileExists(destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
@@ -46,19 +48,19 @@ class PhpHelper {
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
let path = URL(fileURLWithPath: "\(container.paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// Check if the user uses Fish
let script = Paths.shell.contains("/fish")
? fishScript(path, keyPhrase, version, dotless)
: zshScript(path, keyPhrase, version, dotless)
let script = container.paths.shell.contains("/fish")
? fishScript(container, path, keyPhrase, version, dotless)
: zshScript(container, path, keyPhrase, version, dotless)
Task { @MainActor in
try FileSystem.writeAtomicallyToFile(destination, content: script)
try container.filesystem.writeAtomicallyToFile(destination, content: script)
if !FileSystem.isExecutableFile(destination) {
try FileSystem.makeExecutable(destination)
if !container.filesystem.isExecutableFile(destination) {
try container.filesystem.makeExecutable(destination)
}
}
@@ -71,7 +73,7 @@ class PhpHelper {
}
// Write the symlink
await self.createSymlink(dotless)
await self.createSymlink(container, dotless)
}
} catch {
Log.err(error)
@@ -81,6 +83,7 @@ class PhpHelper {
}
private static func zshScript(
_ container: Container,
_ path: String,
_ keyPhrase: String,
_ version: String,
@@ -99,13 +102,14 @@ class PhpHelper {
}
private static func fishScript(
_ container: Container,
_ path: String,
_ keyPhrase: String,
_ version: String,
_ dotless: String
_ dotless: String,
) -> String {
return """
#!\(Paths.binPath)/fish
#!\(container.paths.binPath)/fish
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
@@ -114,19 +118,22 @@ class PhpHelper {
"""
}
private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
private static func createSymlink(
_ container: Container,
_ dotless: String,
) async {
let source = "\(container.paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
if !FileSystem.fileExists(destination) {
if !container.filesystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
await Shell.quiet("ln -s \(source) \(destination)")
await container.shell.quiet("ln -s \(source) \(destination)")
return
}
if !FileSystem.isSymlink(destination) {
if !App.shared.container.filesystem.isSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
await Shell.quiet("ln -fs \(source) \(destination)")
await container.shell.quiet("ln -fs \(source) \(destination)")
return
}

View File

@@ -9,6 +9,7 @@
import Foundation
class PhpConfigurationFile: CreatedFromFile {
var container: Container
struct ConfigValue {
let lineIndex: Int
@@ -31,22 +32,26 @@ class PhpConfigurationFile: CreatedFromFile {
var lines: [String]
/** Resolves a PHP configuration file (.ini) */
static func from(filePath: String) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
static func from(
_ container: Container,
filePath: String
) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath)
do {
let fileContents = try FileSystem.getStringFromFile(path)
return Self.init(path: path, contents: fileContents)
let fileContents = try container.filesystem.getStringFromFile(path)
return Self.init(container, path: path, contents: fileContents)
} catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")
return nil
}
}
required init(path: String, contents: String) {
required init(_ container: Container, path: String, contents: String) {
self.container = container
self.filePath = path
self.lines = contents.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: path)
self.extensions = PhpExtension.from(container, lines, filePath: path)
self.content = Self.parseConfig(lines: lines)
}
@@ -113,7 +118,7 @@ class PhpConfigurationFile: CreatedFromFile {
public func reload() {
self.lines = try! String(contentsOfFile: self.filePath)
.components(separatedBy: "\n")
self.extensions = PhpExtension.from(lines, filePath: self.filePath)
self.extensions = PhpExtension.from(container, lines, filePath: self.filePath)
self.content = Self.parseConfig(lines: lines)
}

View File

@@ -17,6 +17,12 @@ import Foundation
*/
class PhpExtension {
// MARK: - Container
var container: Container
// MARK: - Variables
/// The file where this extension was located.
var file: String
@@ -54,7 +60,9 @@ class PhpExtension {
/**
When registering an extension, we do that based on the line found inside the .ini file.
*/
init(_ line: String, file: String) {
init(_ container: Container, _ line: String, file: String) {
self.container = container
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)!
@@ -82,7 +90,7 @@ class PhpExtension {
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
await sed(file: file, original: line, replacement: newLine)
await sed(container, file: file, original: line, replacement: newLine)
self.enabled = !newLine.starts(with: ";")
self.line = newLine
@@ -96,15 +104,15 @@ class PhpExtension {
// MARK: - Static Methods
static func from(_ lines: [String], filePath: String) -> [PhpExtension] {
static func from(_ container: Container, _ lines: [String], filePath: String) -> [PhpExtension] {
return lines.filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
}.map {
return PhpExtension($0, file: filePath)
return PhpExtension(container, $0, file: filePath)
}
}
static func from(filePath: String) -> [PhpExtension] {
static func from(_ container: Container, filePath: String) -> [PhpExtension] {
let file = try? String(contentsOfFile: filePath)
if file == nil {
@@ -113,6 +121,7 @@ class PhpExtension {
}
return Self.from(
container,
file!.components(separatedBy: "\n"),
filePath: filePath
)

View File

@@ -10,6 +10,12 @@ import Foundation
class PhpInstallation {
// MARK: - Container
var container: Container
// MARK: - Variables
var versionNumber: VersionNumber
var iniFiles: [PhpConfigurationFile] = []
@@ -34,13 +40,17 @@ class PhpInstallation {
return "php@\(self.versionNumber.short)"
}
// MARK: - Methods
/**
In order to determine details about a PHP installation,
well simply run `php-config --version` in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
init(_ container: Container, _ version: String) {
self.container = container
let phpConfigExecutablePath = "\(container.paths.optPath)/php@\(version)/bin/php-config",
phpExecutablePath = "\(container.paths.optPath)/php@\(version)/bin/php"
versionNumber = VersionNumber.make(from: version)!
@@ -54,8 +64,8 @@ class PhpInstallation {
}
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
if container.filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = container.command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"],
trimNewlines: false
@@ -76,8 +86,8 @@ class PhpInstallation {
}
private func determineHealth(_ phpExecutablePath: String) {
if FileSystem.fileExists(phpExecutablePath) {
let testCommand = Command.execute(
if container.filesystem.fileExists(phpExecutablePath) {
let testCommand = container.command.execute(
path: phpExecutablePath,
arguments: ["-v"],
trimNewlines: false,
@@ -94,14 +104,14 @@ class PhpInstallation {
}
private func determineIniFiles(_ phpExecutablePath: String) {
let paths = ActiveShell.shared
let paths = container.shell
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
.split(separator: "\n")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
if let file = PhpConfigurationFile.from(container, filePath: iniFilePath) {
iniFiles.append(file)
}
}

View File

@@ -9,7 +9,6 @@
import Foundation
extension InternalSwitcher {
typealias FixApplied = Bool
public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied {
@@ -30,19 +29,19 @@ extension InternalSwitcher {
// MARK: - Corrections
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let pool = "\(container.paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileSystem.fileExists(pool) {
if container.filesystem.fileExists(pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
let existing = "\(container.paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let new = "\(container.paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
do {
if FileSystem.fileExists(new) {
if container.filesystem.fileExists(new) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ "cleaning up so the newer `www.conf` can be moved again.")
try FileSystem.remove(new)
try container.filesystem.remove(new)
}
try FileSystem.move(from: existing, to: new)
try container.filesystem.move(from: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
return true
} catch {
@@ -59,7 +58,7 @@ extension InternalSwitcher {
// For each of the files, attempt to fix anything that is wrong
let outcomes = files.map { file in
let configFileExists = FileSystem.fileExists("\(Paths.etcPath)/php/\(version)/" + file.destination)
let configFileExists = container.filesystem.fileExists("\(container.paths.etcPath)/php/\(version)/" + file.destination)
if configFileExists {
return false
@@ -72,14 +71,14 @@ extension InternalSwitcher {
}
do {
var contents = try FileSystem.getStringFromFile("~/.composer/vendor/laravel/valet" + file.source)
var contents = try container.filesystem.getStringFromFile("~/.composer/vendor/laravel/valet" + file.source)
for (original, replacement) in file.replacements {
contents = contents.replacingOccurrences(of: original, with: replacement)
}
try FileSystem.writeAtomicallyToFile(
"\(Paths.etcPath)/php/\(version)" + file.destination,
try container.filesystem.writeAtomicallyToFile(
"\(container.paths.etcPath)/php/\(version)" + file.destination,
content: contents
)
} catch {
@@ -102,7 +101,7 @@ extension InternalSwitcher {
destination: "/php-fpm.d/valet-fpm.conf",
source: "/cli/stubs/etc-phpfpm-valet.conf",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_USER": container.paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
],
@@ -112,7 +111,7 @@ extension InternalSwitcher {
destination: "/conf.d/error_log.ini",
source: "/cli/stubs/etc-phpfpm-error_log.ini",
replacements: [
"VALET_USER": Paths.whoami,
"VALET_USER": container.paths.whoami,
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
],
applies: { return true }

View File

@@ -10,6 +10,16 @@ import Foundation
class InternalSwitcher: PhpSwitcher {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Switcher
/**
Switching to a new PHP version involves:
- unlinking the current version
@@ -25,7 +35,7 @@ class InternalSwitcher: PhpSwitcher {
let versions = getVersionsToBeHandled(version)
await withTaskGroup(of: String.self, body: { group in
for available in PhpEnvironments.shared.availablePhpVersions {
for available in container.phpEnvs.availablePhpVersions {
group.addTask {
await self.unlinkAndStopPhpVersion(available)
return available
@@ -52,7 +62,7 @@ class InternalSwitcher: PhpSwitcher {
if Valet.installed {
Log.info("Restarting nginx, just to be sure!")
await brew("services restart nginx", sudo: true)
await brew(container, "services restart nginx", sudo: true)
}
Log.info("The new version(s) have been linked!")
@@ -77,10 +87,10 @@ class InternalSwitcher: PhpSwitcher {
func unlinkAndStopPhpVersion(_ version: String) async {
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)")
await brew(container, "unlink \(formula)")
if Valet.installed {
await brew("services stop \(formula)", sudo: true)
await brew(container, "services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
} else {
Log.info("Unlinked \(formula)")
@@ -92,17 +102,17 @@ class InternalSwitcher: PhpSwitcher {
if primary {
Log.info("\(formula) is the primary formula, linking...")
await brew("link \(formula) --overwrite --force")
await brew(container, "link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, not linking!")
}
if Valet.installed {
await brew("services start \(formula)", sudo: true)
await brew(container, "services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
await container.shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}
}

View File

@@ -9,7 +9,5 @@
import Foundation
protocol CreatedFromFile {
static func from(filePath: String) -> Self?
static func from(_ container: Container, filePath: String) -> Self?
}

View File

@@ -1,32 +0,0 @@
//
// Shell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 20/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var Shell: ShellProtocol {
return ActiveShell.shared
}
class ActiveShell {
static var shared: ShellProtocol = RealShell()
public static func reload() {
if shared is RealShell {
// Start a new shell, this will re-populate the PATH
shared = RealShell()
}
}
public static func useTestable(_ expectations: [String: BatchFakeShellOutput]) {
Self.shared = TestableShell(expectations: expectations)
}
public static func useSystem() {
Self.shared = RealShell()
}
}

View File

@@ -9,6 +9,12 @@
import Foundation
class RealShell: ShellProtocol {
var container: Container
init(container: Container) {
self.container = container
}
/**
The launch path of the terminal in question that is used.
On macOS, we use /bin/sh since it's pretty fast.
@@ -53,7 +59,7 @@ class RealShell: ShellProtocol {
var completeCommand = ""
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
completeCommand += "export PATH=\(container.paths.binPath):$PATH && "
// Put additional exports (as defined by the user) in between
if !self.exports.isEmpty {
@@ -84,7 +90,7 @@ class RealShell: ShellProtocol {
// MARK: - Shellable Protocol
func sync(_ command: String) -> ShellOutput {
let task = getShellProcess(for: command)
let process = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
@@ -93,23 +99,23 @@ class RealShell: ShellProtocol {
sleep(3)
}
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
process.standardOutput = outputPipe
process.standardError = errorPipe
process.launch()
process.waitUntilExit()
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
if Log.shared.verbosity == .cli {
log(task: task, stdOut: stdOut, stdErr: stdErr)
log(process: process, stdOut: stdOut, stdErr: stdErr)
}
return .out(stdOut, stdErr)
}
func pipe(_ command: String) async -> ShellOutput {
let task = getShellProcess(for: command)
let process = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
@@ -119,23 +125,27 @@ class RealShell: ShellProtocol {
await delay(seconds: 3.0)
}
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
process.standardOutput = outputPipe
process.standardError = errorPipe
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
return await withCheckedContinuation { continuation in
process.terminationHandler = { [weak self] _ in
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
if Log.shared.verbosity == .cli {
log(task: task, stdOut: stdOut, stdErr: stdErr)
if Log.shared.verbosity == .cli {
self?.log(process: process, stdOut: stdOut, stdErr: stdErr)
}
continuation.resume(returning: .out(stdOut, stdErr))
}
process.launch()
}
return .out(stdOut, stdErr)
}
private func log(task: Process, stdOut: String, stdErr: String) {
var args = task.arguments ?? []
private func log(process: Process, stdOut: String, stdErr: String) {
var args = process.arguments ?? []
let last = "\"" + (args.popLast() ?? "") + "\""
var log = """
@@ -171,18 +181,16 @@ class RealShell: ShellProtocol {
withTimeout timeout: TimeInterval = 5.0
) async throws -> (Process, ShellOutput) {
let process = getShellProcess(for: command)
let outputPipe = Pipe(), errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
let output = ShellOutput.empty()
process.listen { incoming in
output.out += incoming; didReceiveOutput(incoming, .stdOut)
} didReceiveStandardErrorData: { incoming in
output.err += incoming; didReceiveOutput(incoming, .stdErr)
}
return try await withCheckedThrowingContinuation({ continuation in
let task = Task {
try await Task.sleep(nanoseconds: timeout.nanoseconds)
let timeoutTask = Task {
try? await Task.sleep(nanoseconds: timeout.nanoseconds)
// Only terminate if the process is still running
if process.isRunning {
process.terminationHandler = nil
@@ -191,10 +199,44 @@ class RealShell: ShellProtocol {
}
}
process.terminationHandler = { [output] process in
task.cancel()
// Set up background reading for stdout
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
let data = fileHandle.availableData
if !data.isEmpty, let string = String(data: data, encoding: .utf8) {
output.out += string
didReceiveOutput(string, .stdOut)
}
}
process.haltListening()
// Set up background reading for stderr
errorPipe.fileHandleForReading.readabilityHandler = { fileHandle in
let data = fileHandle.availableData
if !data.isEmpty, let string = String(data: data, encoding: .utf8) {
output.err += string
didReceiveOutput(string, .stdErr)
}
}
process.terminationHandler = { process in
timeoutTask.cancel()
// Clean up readability handlers
outputPipe.fileHandleForReading.readabilityHandler = nil
errorPipe.fileHandleForReading.readabilityHandler = nil
// Read any remaining data
let remainingOut = outputPipe.fileHandleForReading.readDataToEndOfFile()
let remainingErr = errorPipe.fileHandleForReading.readDataToEndOfFile()
if !remainingOut.isEmpty, let string = String(data: remainingOut, encoding: .utf8) {
output.out += string
didReceiveOutput(string, .stdOut)
}
if !remainingErr.isEmpty, let string = String(data: remainingErr, encoding: .utf8) {
output.err += string
didReceiveOutput(string, .stdErr)
}
if !output.err.isEmpty {
continuation.resume(returning: (process, .err(output.err)))
@@ -204,9 +246,12 @@ class RealShell: ShellProtocol {
}
process.launch()
process.waitUntilExit()
})
}
func reload() {
container.shell = RealShell(container: container)
}
}
extension TimeInterval {

View File

@@ -56,6 +56,11 @@ protocol ShellProtocol {
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput)
/**
Reloads the shell instance, which also reloads the PATH.
*/
func reload()
}
enum ShellStream: Codable {

View File

@@ -121,12 +121,12 @@ public struct TestableConfiguration: Codable {
Log.separator()
Log.info("USING TESTABLE CONFIGURATION...")
Log.separator()
Log.info("Applying fake shell...")
ActiveShell.useTestable(shellOutput)
Log.info("Applying fake filesystem...")
ActiveFileSystem.useTestable(filesystem)
Log.info("Applying fake commands...")
ActiveCommand.useTestable(commandOutput)
Log.info("Applying to container...")
let container = App.shared.container
container.overrideWith(config: self)
Preferences.shared = Preferences(container)
Log.info("Applying temporary preference overrides...")
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
Preferences.shared.cachedPreferences[key] = value
@@ -136,7 +136,7 @@ public struct TestableConfiguration: Codable {
Log.info("Applying fake scanner...")
ValetScanner.useFake()
Log.info("Applying fake services manager...")
ServicesManager.useFake()
ServicesManager.useFake(container)
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
}

View File

@@ -64,6 +64,10 @@ public class TestableShell: ShellProtocol {
return (Process(), output)
}
func reload() {
// does nothing
}
}
struct FakeShellOutput: Codable {

View File

@@ -0,0 +1,62 @@
//
// Container+Fake.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
extension Container {
/**
Manually specify what overrides need to be active for the container.
*/
private func overrideFake(
shellExpectations: [String: BatchFakeShellOutput] = [:],
fileSystemFiles: [String: FakeFile] = [:],
commands: [String: String] = [:]
) {
self.shell = TestableShell(expectations: shellExpectations)
self.filesystem = TestableFileSystem(files: fileSystemFiles)
self.command = TestableCommand(commands: commands)
}
/**
Use a `TestableConfiguration` as the basis for shell, filesystem and more.
This is used for testing scenarios to avoid needing to have a specific system configuration.
Ideal for feature or UI tests, where a complete "computer configuration" needs to be mimicked.
*/
public func overrideWith(config: TestableConfiguration) {
self.overrideFake(
shellExpectations: config.shellOutput,
fileSystemFiles: config.filesystem,
commands: config.commandOutput
)
}
/**
Create a new DI `Container` with fake shell responses, filesystem structure and given commands.
Ideal for testing without a complex TestableConfiguration, so great for unit tests that
require injecting a new `Container` instance without requiring a complex setup process.
*/
public static func fake(
shell: [String: BatchFakeShellOutput] = [:],
files: [String: FakeFile] = [:],
commands: [String: String] = [:]
) -> Container {
// Create a new container
let container = Container()
// Fill the container with production (real) components
container.prepare()
// Replace the key ones with fake ones, so we don't touch the tester's OS, filesystem, etc.
container.overrideFake(
shellExpectations: shell,
fileSystemFiles: files,
commands: commands
)
// Return the newly created container
return container
}
}

View File

@@ -0,0 +1,15 @@
//
// Container+Real.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
extension Container {
public static func real() -> Container {
let container = Container()
container.prepare()
return container
}
}

View File

@@ -0,0 +1,46 @@
//
// Container.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 05/10/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
class Container {
// Core abstractions
var shell: ShellProtocol!
var filesystem: FileSystemProtocol!
var command: CommandProtocol!
// Extra abstractions
var paths: Paths!
var phpEnvs: PhpEnvironments!
var favorites: Favorites!
var warningManager: WarningManager! // pending rename?
///
/// The initializer is empty. You must call `prepare` to enable the container.
/// To avoid issues with unsafe access, the actual objects are set in `prepare`.
/// `self` is not available in this constructor, after all. The alternative
/// is to use lazy variables here, but I don't think it's that clean, especially
/// given the other initializers available via the extensions.
///
init() {}
///
/// Creates new instances belonging to the container, while referencing
/// the container itself and passing the reference on to each component that needs it.
///
public func prepare() {
// Core
self.shell = RealShell(container: self)
self.filesystem = RealFileSystem(container: self)
self.command = RealCommand()
// Extra
self.paths = Paths(container: self)
self.phpEnvs = PhpEnvironments(container: self)
self.favorites = Favorites()
self.warningManager = WarningManager(container: self)
}
}

View File

@@ -16,7 +16,7 @@
<p><b>Do you enjoy using the app? Is it helping you save time?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Bluesky or Mastodon.</b> Give me a <a href="https://bsky.app/profile/nicoverbruggen.be">follow on Bluesky</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
<p><b>Get the latest on social media.</b> Follow me on <a href="https://x.com/nicoverbruggen">Twitter (X)</a>, <a href="https://bsky.app/profile/nicoverbruggen.be"> Bluesky</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adibnoh, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @ash-jc-allen, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @casenxu, @caseyalee, @cgreuling, @cjcox17, @clescuyer, @codelinde, @designhammer, @Diewy, @drfraker, @driftingly, @duellsy, @e9li, @edalzell, @EYOND, @faithfm, @frankmichel, @gekich, @gpluess, @gwleuverink, @hopkins385, @incon, @intrepidws, @israaraujo, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @jorisnoo, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @kholisabdullah, @Laravel-Backpack, @leganz, @lucianvacaroiu,@martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @megabubbleteam, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rastitkac, @rderimay, @renecum, @richardhulbert, @richardtape, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @slaFFik, @spatie, @SRWieZ, @stefanbauer, @stefanzweifel, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @vintagesucks, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(This is a historical list of sponsors, not current sponsors. Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
<p><b>Localization credits:</b></br>

View File

@@ -70,6 +70,12 @@ class App {
// MARK: Variables
/**
The dependency container.
This is supposed to be injected, so direct access is discouraged.
*/
var container: Container = Container()
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
@@ -97,12 +103,6 @@ class App {
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** Favorites storage, which keeps track of favorited domains. */
var favorites = Favorites.shared
/** The warning manager, responsible for keeping track of warnings. */
var warnings = WarningManager.shared
/** Timer that will periodically reload info about the user's PHP installation. */
var timer: Timer?

View File

@@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/
let state: App
/**
The paths singleton that determines where Homebrew is installed,
and where to look for binaries.
*/
let paths: Paths
/**
The Valet singleton that determines all information
about Valet and its current configuration.
@@ -60,10 +54,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
When the application initializes, create all singletons.
*/
override init() {
// Prepare the container with the defaults
self.state = App.shared
self.state.container.prepare()
#if DEBUG
logger.verbosity = .performance
if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) {
Self.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: ""))
AppDelegate.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: ""))
}
#endif
@@ -77,7 +75,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
Log.info("Extra CLI mode has been activated via --cli flag.")
}
if FileSystem.fileExists("~/.config/phpmon/verbose") {
if state.container.filesystem.fileExists("~/.config/phpmon/verbose") {
logger.verbosity = .cli
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
}
@@ -89,15 +87,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
Log.separator(as: .info)
}
self.state = App.shared
self.paths = Paths.shared
// Initialize the crash reporter
CrashReporter.initialize()
// Set up final singletons
self.valet = Valet.shared
self.brew = Brew.shared
super.init()
}
func initializeSwitcher() {
self.phpEnvironments = PhpEnvironments.shared
self.phpEnvironments = App.shared.container.phpEnvs
}
static func initializeTestingProfile(_ path: String) {

View File

@@ -28,7 +28,7 @@ class AppUpdater {
let caskUrl = Constants.Urls.UpdateCheckEndpoint
guard let caskFile = await CaskFile.from(url: caskUrl) else {
guard let caskFile = await CaskFile.from(App.shared.container, url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
presentCouldNotRetrieveUpdateIfInteractive()
return .networkError
@@ -75,7 +75,7 @@ class AppUpdater {
.localized(latestVersionOnline.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle"
.localized,
description: BrewDiagnostics.customCaskInstalled
description: BrewDiagnostics.shared.customCaskInstalled
? "updater.installation_source.brew".localized(command)
: "updater.installation_source.direct".localized
)
@@ -152,7 +152,7 @@ class AppUpdater {
system_quiet("cp -R \"\(updater)\" \"\(updaterDirectory)/PHP Monitor Self-Updater.app\"")
try! FileSystem.writeAtomicallyToFile(
try! App.shared.container.filesystem.writeAtomicallyToFile(
"\(updaterDirectory)/update.json",
content: "{ \"url\": \"\(caskFile.url)\", \"sha256\": \"\(caskFile.sha256)\" }"
)
@@ -166,12 +166,12 @@ class AppUpdater {
}
private func cleanupCaskroom() {
let path = Paths.caskroomPath
let path = App.shared.container.paths.caskroomPath
if FileSystem.directoryExists(path) {
if App.shared.container.filesystem.directoryExists(path) {
Log.info("Removing the Caskroom directory for PHP Monitor...")
do {
try FileSystem.remove(path)
try App.shared.container.filesystem.remove(path)
Log.info("Removed the Caskroom directory at `\(path)`.")
} catch {
Log.err("Automatically removing the Caskroom directory at `\(path)` failed.")
@@ -183,7 +183,7 @@ class AppUpdater {
public static func checkIfUpdateWasPerformed() {
// Cleanup the upgrade.success file
if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
if App.shared.container.filesystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
Task { @MainActor in
if App.identifier.contains(".phpmon.eap") {
LocalNotification.send(
@@ -201,13 +201,13 @@ class AppUpdater {
}
Log.info("The `upgrade.success` file was found! An update was installed. Cleaning up...")
try? FileSystem.remove("~/.config/phpmon/updater/upgrade.success")
try? App.shared.container.filesystem.remove("~/.config/phpmon/updater/upgrade.success")
}
// Cleanup the previous updater
if FileSystem.anyExists("~/.config/phpmon/updater/PHP Monitor Self-Updater.app") {
if App.shared.container.filesystem.anyExists("~/.config/phpmon/updater/PHP Monitor Self-Updater.app") {
Log.info("A remnant of the self-updater must still be removed...")
try? FileSystem.remove("~/.config/phpmon/updater/PHP Monitor Self-Updater.app")
try? App.shared.container.filesystem.remove("~/.config/phpmon/updater/PHP Monitor Self-Updater.app")
}
}
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
@@ -436,7 +436,7 @@
</toolbarItem>
<searchToolbarItem implicitItemIdentifier="7C834FBE-7118-4082-A09F-7CBECEC1356A" label="Search" paletteLabel="Search" visibilityPriority="1001" id="G2g-jS-RVc">
<nil key="toolTip"/>
<searchField key="view" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
<searchField key="view" focusRingType="none" verticalHuggingPriority="750" textCompletion="NO" id="0gE-Yr-MLy">
<rect key="frame" x="0.0" y="0.0" width="100" height="21"/>
<autoresizingMask key="autoresizingMask"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" sendsSearchStringImmediately="YES" id="vp9-vH-goQ">
@@ -491,194 +491,24 @@
</objects>
<point key="canvasLocation" x="-374" y="1137"/>
</scene>
<!--Window Controller-->
<scene sceneID="BD0-La-ygq">
<objects>
<windowController storyboardIdentifier="noticeWindow" id="nfT-AN-9ZW" sceneMemberID="viewController">
<window key="window" title="Notice" separatorStyle="none" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="alertPanel" frameAutosaveName="" titlebarAppearsTransparent="YES" toolbarStyle="unified" titleVisibility="hidden" id="AoF-SN-xB0">
<windowStyleMask key="styleMask" titled="YES" fullSizeContentView="YES"/>
<rect key="contentRect" x="425" y="462" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<view key="contentView" id="Src-7L-4Z4">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask"/>
</view>
<connections>
<outlet property="delegate" destination="nfT-AN-9ZW" id="8dd-JR-bQG"/>
</connections>
</window>
<connections>
<segue destination="hkw-9V-NxP" kind="relationship" relationship="window.shadowedContentViewController" id="eob-YS-ACy"/>
</connections>
</windowController>
<customObject id="i3j-z8-nxv" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-374" y="2267"/>
</scene>
<!--AlertVC-->
<scene sceneID="y9E-bB-wIG">
<objects>
<viewController storyboardIdentifier="noticeVC" id="hkw-9V-NxP" customClass="NVAlertVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="UPH-hV-Naz">
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView blendingMode="behindWindow" material="popover" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="JVG-5w-fPd">
<rect key="frame" x="0.0" y="0.0" width="500" height="212"/>
<subviews>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8zu-cF-KCX">
<rect key="frame" x="383" y="13" width="104" height="32"/>
<buttonCell key="cell" type="push" title="Primary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="F26-vf-hFH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="4Uf-fh-jWJ"/>
</constraints>
<connections>
<action selector="primaryButtonAction:" target="hkw-9V-NxP" id="W7d-3b-pZT"/>
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TCp-nS-HN2">
<rect key="frame" x="281" y="13" width="104" height="32"/>
<buttonCell key="cell" type="push" title="Secondary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="eCk-FC-9Zr">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="90" id="QWZ-BA-0g9"/>
</constraints>
<connections>
<action selector="secondaryButtonAction:" target="hkw-9V-NxP" id="YJs-Hu-lFP"/>
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="n5T-nn-k3j">
<rect key="frame" x="13" y="13" width="81" height="32"/>
<buttonCell key="cell" type="push" title="Tertiary" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="mzA-Uu-gyf">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="tertiaryButtonAction:" target="hkw-9V-NxP" id="o1C-av-ifx"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="0DC-rd-xbu"/>
<constraint firstItem="8zu-cF-KCX" firstAttribute="leading" secondItem="TCp-nS-HN2" secondAttribute="trailing" constant="12" symbolic="YES" id="8tH-KO-fjY"/>
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="L6V-KQ-nj3"/>
<constraint firstItem="n5T-nn-k3j" firstAttribute="leading" secondItem="JVG-5w-fPd" secondAttribute="leading" constant="20" symbolic="YES" id="QLS-fE-1PM"/>
<constraint firstAttribute="trailing" secondItem="8zu-cF-KCX" secondAttribute="trailing" constant="20" symbolic="YES" id="RRS-WO-6KO"/>
<constraint firstAttribute="bottom" secondItem="n5T-nn-k3j" secondAttribute="bottom" constant="20" symbolic="YES" id="Scj-z1-AdN"/>
<constraint firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" constant="20" symbolic="YES" id="fYa-HG-gmL"/>
<constraint firstItem="n5T-nn-k3j" firstAttribute="bottom" secondItem="TCp-nS-HN2" secondAttribute="bottom" id="lOI-ZI-wCd"/>
<constraint firstItem="TCp-nS-HN2" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="n5T-nn-k3j" secondAttribute="trailing" constant="12" symbolic="YES" id="rUC-0t-9H9"/>
<constraint firstAttribute="bottom" secondItem="8zu-cF-KCX" secondAttribute="bottom" constant="20" symbolic="YES" id="wIl-uw-y3p"/>
</constraints>
</visualEffectView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="U1c-qS-cIm">
<rect key="frame" x="98" y="153" width="384" height="19"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="380" id="WgB-hj-d4P"/>
</constraints>
<textFieldCell key="cell" selectable="YES" title="This is the title of the notice window." id="bzW-MI-jXb">
<font key="font" metaFont="systemBold" size="15"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yI6-qf-htf">
<rect key="frame" x="98" y="127" width="384" height="16"/>
<textFieldCell key="cell" selectable="YES" title="This is a slightly more expanded explanation." id="rY3-Nd-Iit">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0rX-Ss-3Xd">
<rect key="frame" x="12" y="144" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="Uib-R1-GEx">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QkM-5D-ZEQ">
<rect key="frame" x="20" y="111" width="64" height="64"/>
<constraints>
<constraint firstAttribute="height" constant="64" id="VhJ-fI-IKC"/>
<constraint firstAttribute="width" constant="64" id="a2d-Gn-Oor"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="7eT-Hw-EL9"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hml-dl-Cah">
<rect key="frame" x="98" y="70" width="384" height="42"/>
<textFieldCell key="cell" selectable="YES" id="7iW-Lc-DqO">
<font key="font" metaFont="smallSystem"/>
<string key="title">Sometimes you need a really long explanation and in that case you can get a really, really long description here, along with, for example, various steps you can take. This allows for a lot of text to be displayed, yay!</string>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="hml-dl-Cah" firstAttribute="leading" secondItem="yI6-qf-htf" secondAttribute="leading" id="1Lh-ve-rwK"/>
<constraint firstItem="U1c-qS-cIm" firstAttribute="leading" secondItem="QkM-5D-ZEQ" secondAttribute="trailing" constant="16" id="2xX-Ma-iQZ"/>
<constraint firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" constant="20" symbolic="YES" id="39u-Uk-TDI"/>
<constraint firstItem="8zu-cF-KCX" firstAttribute="top" secondItem="hml-dl-Cah" secondAttribute="bottom" constant="30" id="7fT-EZ-cAO"/>
<constraint firstItem="JVG-5w-fPd" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" id="CE7-54-G09"/>
<constraint firstItem="hml-dl-Cah" firstAttribute="trailing" secondItem="yI6-qf-htf" secondAttribute="trailing" id="DBa-7d-sS3"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="trailing" secondItem="U1c-qS-cIm" secondAttribute="trailing" id="NvJ-vf-wEl"/>
<constraint firstItem="hml-dl-Cah" firstAttribute="top" secondItem="yI6-qf-htf" secondAttribute="bottom" constant="15" id="Pmz-I9-2up"/>
<constraint firstItem="U1c-qS-cIm" firstAttribute="top" secondItem="UPH-hV-Naz" secondAttribute="top" constant="40" id="Uqt-sc-vxn"/>
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="top" constant="-3" id="WAj-rw-srg"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="leading" secondItem="U1c-qS-cIm" secondAttribute="leading" id="bng-pH-jSG"/>
<constraint firstAttribute="trailing" secondItem="JVG-5w-fPd" secondAttribute="trailing" id="dRt-Pq-6n0"/>
<constraint firstItem="JVG-5w-fPd" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" id="ejC-of-zjN"/>
<constraint firstAttribute="bottom" secondItem="JVG-5w-fPd" secondAttribute="bottom" id="hGp-DD-cKr"/>
<constraint firstItem="QkM-5D-ZEQ" firstAttribute="leading" secondItem="UPH-hV-Naz" secondAttribute="leading" constant="20" symbolic="YES" id="jG8-dt-l4x"/>
<constraint firstItem="yI6-qf-htf" firstAttribute="top" secondItem="U1c-qS-cIm" secondAttribute="bottom" constant="10" id="kqR-yg-zdG"/>
</constraints>
</view>
<connections>
<outlet property="buttonPrimary" destination="8zu-cF-KCX" id="MT5-Px-K97"/>
<outlet property="buttonSecondary" destination="TCp-nS-HN2" id="nPn-OX-Z4m"/>
<outlet property="buttonTertiary" destination="n5T-nn-k3j" id="UnB-8x-s3D"/>
<outlet property="imageView" destination="QkM-5D-ZEQ" id="zsW-l7-eH0"/>
<outlet property="labelDescription" destination="hml-dl-Cah" id="ehw-zs-EPc"/>
<outlet property="labelSubtitle" destination="yI6-qf-htf" id="m7A-bX-HE8"/>
<outlet property="labelTitle" destination="U1c-qS-cIm" id="oM3-kl-PL8"/>
<outlet property="primaryButtonTopMargin" destination="7fT-EZ-cAO" id="r6u-1l-dbl"/>
</connections>
</viewController>
<customObject id="5Ts-EZ-bJh" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="230" y="2267"/>
</scene>
<!--Add SiteVC-->
<scene sceneID="6JC-H6-u4K">
<objects>
<viewController storyboardIdentifier="newSiteLink" id="glS-wF-sEU" customClass="AddSiteVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="JJJ-T9-Yuv">
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="252"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="js9-OW-xzC">
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="252"/>
<view key="contentView" id="HRC-RT-LxR">
<rect key="frame" x="0.0" y="0.0" width="480" height="245"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="252"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PVw-cM-qAB">
<rect key="frame" x="326" y="13" width="141" height="32"/>
<rect key="frame" x="329" y="20" width="131" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Create Link" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WwW-Wv-I8s">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -691,7 +521,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SwS-o8-pbl">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<rect key="frame" x="20" y="20" width="104" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="WHE-HW-jwp">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -706,8 +536,8 @@ Gw
<action selector="pressedCancel:" target="glS-wF-sEU" id="q0L-YZ-F3J"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="150" width="440" height="21"/>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZX9-s1-23i">
<rect key="frame" x="20" y="154" width="440" height="24"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="NFa-1D-Bi4">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -717,8 +547,8 @@ Gw
<outlet property="delegate" destination="glS-wF-sEU" id="Dyf-0M-Gwj"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VzR-5a-cmT">
<rect key="frame" x="18" y="132" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="bJr-s6-tdP">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@@ -726,7 +556,7 @@ Gw
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KZf-b0-9cm">
<rect key="frame" x="18" y="95" width="266" height="18"/>
<rect key="frame" x="20" y="100" width="262" height="16"/>
<buttonCell key="cell" type="check" title="[i18n] Secure this domain after creation" bezelStyle="regularSquare" imagePosition="left" inset="2" id="vFv-Of-2yZ">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -735,8 +565,8 @@ Gw
<action selector="pressedSecure:" target="glS-wF-sEU" id="OIj-Pz-5Ea"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mmQ-7e-dlb">
<rect key="frame" x="18" y="64" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="4gd-KM-5Fu">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@@ -744,22 +574,22 @@ Gw
</textFieldCell>
</textField>
<pathControl verticalHuggingPriority="750" allowsExpansionToolTips="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6JT-Vt-3q0">
<rect key="frame" x="20" y="179" width="440" height="22"/>
<rect key="frame" x="20" y="186" width="440" height="22"/>
<pathCell key="cell" selectable="YES" refusesFirstResponder="YES" alignment="left" id="m8d-XF-kh9">
<font key="font" metaFont="system"/>
<url key="url" string="file:///Users/"/>
</pathCell>
</pathControl>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="209" width="128" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P0B-Ht-R8n">
<rect key="frame" x="18" y="216" width="128" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Link a Folder" id="S4j-ZC-ddT">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="140" y="23" width="180" height="14"/>
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="900-Z2-tID">
<rect key="frame" x="136" y="25" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="jOt-n6-TQf">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
@@ -829,11 +659,11 @@ Gw
<scrollView borderType="none" horizontalLineScroll="54" horizontalPageScroll="10" verticalLineScroll="54" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="p0j-eB-I2i">
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="6IL-DW-37w">
<rect key="frame" x="0.0" y="0.0" width="611" height="294"/>
<rect key="frame" x="0.0" y="0.0" width="626" height="309"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" multipleSelection="NO" autosaveName="phpmon-sitelist-columns" rowHeight="54" headerView="xUg-Mq-OSh" viewBased="YES" id="cp3-34-pQj" customClass="PMTableView" customModule="PHP_Monitor" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="611" height="266"/>
<rect key="frame" x="0.0" y="0.0" width="609" height="264"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@@ -857,21 +687,27 @@ Gw
<rect key="frame" x="18" y="0.0" width="34" height="55"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Wel-Ho-Kpp">
<rect key="frame" x="7" y="18" width="20" height="20"/>
<button translatesAutoresizingMaskIntoConstraints="NO" id="3fp-xd-haK">
<rect key="frame" x="3" y="14" width="28" height="28"/>
<buttonCell key="cell" type="bevel" title=" " bezelStyle="regularSquare" image="Lock" imagePosition="overlaps" alignment="center" lineBreakMode="truncatingTail" state="on" imageScaling="proportionallyDown" inset="2" id="qVw-Oq-NJW">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" constant="20" id="7mC-me-Nse"/>
<constraint firstAttribute="height" constant="20" id="AjD-xX-suI"/>
<constraint firstAttribute="width" constant="28" id="B8B-Bs-ZMG"/>
<constraint firstAttribute="height" constant="28" id="Ssm-kd-GFd"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="Lock" id="gK0-Mh-K9Y"/>
</imageView>
<connections>
<action selector="pressedPhpVersion:" target="hft-M4-nWb" id="7IB-z6-CA4"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="Wel-Ho-Kpp" firstAttribute="centerY" secondItem="hft-M4-nWb" secondAttribute="centerY" id="L6B-iA-BCQ"/>
<constraint firstItem="Wel-Ho-Kpp" firstAttribute="centerX" secondItem="hft-M4-nWb" secondAttribute="centerX" id="jAF-AV-EeX"/>
<constraint firstItem="3fp-xd-haK" firstAttribute="centerY" secondItem="hft-M4-nWb" secondAttribute="centerY" id="9yb-Ve-hE1"/>
<constraint firstItem="3fp-xd-haK" firstAttribute="centerX" secondItem="hft-M4-nWb" secondAttribute="centerX" id="tQh-hX-llf"/>
</constraints>
<connections>
<outlet property="imageViewLock" destination="Wel-Ho-Kpp" id="iji-uw-8we"/>
<outlet property="buttonLockStatus" destination="3fp-xd-haK" id="RFv-pZ-6fX"/>
</connections>
</tableCellView>
</prototypeCellViews>
@@ -893,7 +729,7 @@ Gw
<rect key="frame" x="69" y="0.0" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="XJL-Uw-frD">
<rect key="frame" x="3" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="SGC-Gm-Mxd">
<font key="font" metaFont="systemSemibold" size="13"/>
@@ -901,7 +737,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-Q9-CpO">
<rect key="frame" x="3" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="fe7-Ha-mR9">
<font key="font" metaFont="smallSystem"/>
@@ -927,7 +763,7 @@ Gw
<rect key="frame" x="69" y="54" width="200" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="aot-FJ-HIk">
<rect key="frame" x="33" y="26" width="145" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="my-domain-name.test" id="LHu-UF-QlC">
<font key="font" metaFont="systemSemibold" size="13"/>
@@ -935,7 +771,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="GNH-l8-oki">
<rect key="frame" x="33" y="12" width="75" height="14"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="~/path/to/site" id="LNw-Ju-0Ot">
<font key="font" metaFont="smallSystem"/>
@@ -1078,7 +914,7 @@ Gw
<rect key="frame" x="470" y="0.0" width="97" height="54"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ljl-8B-key">
<rect key="frame" x="6" y="26" width="93" height="14"/>
<textFieldCell key="cell" alignment="left" title="Laravel" id="0lu-L6-oKr">
<font key="font" metaFont="smallSystem"/>
@@ -1086,7 +922,7 @@ Gw
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aPK-Xc-J4B">
<rect key="frame" x="6" y="15" width="93" height="11"/>
<textFieldCell key="cell" alignment="left" title="PHP 8.0" id="puf-Jh-ham">
<font key="font" metaFont="miniSystem"/>
@@ -1124,15 +960,15 @@ Gw
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="620" id="iRQ-sz-oyv"/>
</constraints>
<scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="TDE-ff-DQT">
<rect key="frame" x="0.0" y="294" width="611" height="15"/>
<rect key="frame" x="0.0" y="292" width="609" height="17"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="wFn-93-f10">
<rect key="frame" x="611" y="28" width="15" height="266"/>
<rect key="frame" x="609" y="28" width="17" height="264"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<tableHeaderView key="headerView" wantsLayer="YES" id="xUg-Mq-OSh">
<rect key="frame" x="0.0" y="0.0" width="611" height="28"/>
<rect key="frame" x="0.0" y="0.0" width="609" height="28"/>
<autoresizingMask key="autoresizingMask"/>
</tableHeaderView>
</scrollView>
@@ -1153,7 +989,7 @@ Gw
<constraint firstAttribute="height" constant="30" id="lfW-dB-Eu3"/>
</constraints>
</progressIndicator>
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xoy-5Y-WDT">
<rect key="frame" x="15" y="14" width="71" height="13"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="PLEASE WAIT" id="tMX-Ky-caT">
<font key="font" metaFont="system" size="10"/>
@@ -1200,17 +1036,17 @@ Gw
<objects>
<viewController storyboardIdentifier="newProxyLink" id="dwh-CF-6iv" customClass="AddProxyVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="U5U-QR-YXS">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="540" height="296"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="kkd-UV-SnA">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="540" height="296"/>
<view key="contentView" id="IXW-35-8NJ">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="540" height="296"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="196" width="500" height="21"/>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="203" width="500" height="24"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -1220,24 +1056,24 @@ Gw
<outlet property="delegate" destination="dwh-CF-6iv" id="lNE-OI-G93"/>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<rect key="frame" x="18" y="221" width="325" height="14"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uib-vA-HRc">
<rect key="frame" x="18" y="231" width="325" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Proxy subject (usually: protocol, IP address and port)" id="G1Z-3f-BhL">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<rect key="frame" x="18" y="172" width="112" height="14"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mlA-Zt-Hu8">
<rect key="frame" x="18" y="179" width="112" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Domain name" id="dQs-oZ-80e">
<font key="font" metaFont="systemMedium" size="11"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="147" width="500" height="21"/>
<textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="151" width="500" height="24"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@@ -1264,7 +1100,7 @@ Gw
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Vi-cN-ude">
<rect key="frame" x="377" y="13" width="150" height="32"/>
<rect key="frame" x="380" y="20" width="140" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Create Proxy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="H2Z-c5-5Vk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -1277,7 +1113,7 @@ DQ
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nC0-dk-QaF">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<rect key="frame" x="20" y="20" width="104" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="D8g-GE-7TU">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -1292,8 +1128,8 @@ Gw
<action selector="pressedCancel:" target="dwh-CF-6iv" id="J2T-Zj-A0j"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="128" width="504" height="14"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="132" width="504" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
</constraints>
@@ -1304,7 +1140,7 @@ Gw
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rJa-yg-nCn">
<rect key="frame" x="18" y="95" width="170" height="18"/>
<rect key="frame" x="20" y="100" width="166" height="16"/>
<buttonCell key="cell" type="check" title="[i18n] Secure this proxy" bezelStyle="regularSquare" imagePosition="left" inset="2" id="5LI-lt-Asl">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -1313,24 +1149,24 @@ Gw
<action selector="pressedSecure:" target="dwh-CF-6iv" id="b74-8T-AzO"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="60" width="504" height="28"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="64" width="504" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<rect key="frame" x="18" y="250" width="123" height="16"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="DAh-br-Dfx">
<rect key="frame" x="18" y="260" width="123" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="[i18n] Add a Proxy" id="AZ1-04-kUl">
<font key="font" textStyle="headline" name=".SFNS-Bold"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="191" y="23" width="180" height="14"/>
<textField hidden="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="187" y="25" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
@@ -1416,14 +1252,14 @@ Gw
<objects>
<viewController storyboardIdentifier="addDomainChoice" id="gOD-Gu-zDG" customClass="SelectionVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="ysc-sm-sli">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<rect key="frame" x="0.0" y="0.0" width="540" height="181"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView blendingMode="behindWindow" material="toolTip" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="F37-zt-gM3">
<rect key="frame" x="0.0" y="0.0" width="540" height="177"/>
<rect key="frame" x="0.0" y="0.0" width="540" height="181"/>
<subviews>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="FhN-AM-SkI">
<rect key="frame" x="13" y="13" width="114" height="32"/>
<rect key="frame" x="20" y="20" width="104" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LxP-t4-H2W">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -1439,10 +1275,10 @@ Gw
</connections>
</button>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pYe-Qu-qnK">
<rect key="frame" x="187" y="20" width="333" height="20"/>
<rect key="frame" x="167" y="20" width="353" height="24"/>
<subviews>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="L5n-Gw-J27">
<rect key="frame" x="-7" y="-7" width="172" height="32"/>
<rect key="frame" x="0.0" y="0.0" width="168" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Link" bezelStyle="rounded" image="IconLinked" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8UP-Sw-TP6">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -1453,7 +1289,7 @@ Gw
</connections>
</button>
<button wantsLayer="YES" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="01Z-IV-hv1">
<rect key="frame" x="159" y="-7" width="181" height="32"/>
<rect key="frame" x="176" y="0.0" width="177" height="24"/>
<buttonCell key="cell" type="push" title="[i18n] Create a Proxy" bezelStyle="rounded" image="IconProxy" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="bJ4-q8-1Ej">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -1473,16 +1309,16 @@ Gw
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<rect key="frame" x="18" y="138" width="504" height="19"/>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJK-Ke-IK3">
<rect key="frame" x="18" y="142" width="504" height="19"/>
<textFieldCell key="cell" selectable="YES" alignment="left" title="[i18n] What kind of domain would you like to set up?" id="agk-Nj-FLd">
<font key="font" metaFont="systemBold" size="15"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField wantsLayer="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<rect key="frame" x="18" y="60" width="504" height="70"/>
<textField wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="urj-Xq-TrJ">
<rect key="frame" x="18" y="64" width="504" height="70"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="tbl-AV-4qB"/>
</constraints>
@@ -1510,7 +1346,7 @@ Gw
</constraints>
</visualEffectView>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cNh-Wc-ADk">
<rect key="frame" x="200" y="109" width="0.0" height="48"/>
<rect key="frame" x="200" y="113" width="0.0" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="OQ5-hX-qai">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>

View File

@@ -0,0 +1,150 @@
//
// CrashReporter.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
import CrashReporter
import NVAlert
import AppKit
class CrashReporter {
/**
Initializes the crash reporting toolkit. Keep in mind that this crash reporter only keeps track of crashes,
it does not automatically send information. I have my own API for my crash report ingest system.
*/
static func initialize() {
if CrashReporter.isDebuggerAttached() {
Log.err("[CrashReporter] The debugger is attached, won't start crash reporting.")
return
}
let config = PLCrashReporterConfig(signalHandlerType: .mach, symbolicationStrategy: [])
guard let crashReporter = PLCrashReporter(configuration: config) else {
Log.err("[CrashReporter] Could not create an instance of PLCrashReporter.")
return
}
do {
try crashReporter.enableAndReturnError()
} catch let error {
Log.err("[CrashReporter] Could not enable crash reporter: \(error). Crashes will not be reported.")
}
if crashReporter.hasPendingCrashReport() {
Task { @MainActor in
CrashReporter.requestSendingCrashReport(crashReporter)
}
}
}
/**
If a pending crash report can be sent, show an alert to the user.
*/
@MainActor static func requestSendingCrashReport(_ crashReporter: PLCrashReporter) {
do {
let data = try crashReporter.loadPendingCrashReportDataAndReturnError()
let report = try PLCrashReport(data: data)
if let text = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS) {
// Ask the user to submit the crash report
let response = NVAlert().withInformation(
title: "crash_reporter.title".localized,
subtitle: "crash_reporter.subtitle".localized,
description: "crash_reporter.description".localized
)
.withTertiary(text: "", action: { _ in
try? text.write(toFile: "/tmp/pm_crash_log.txt", atomically: true, encoding: .utf8)
let fileUrl = URL(string: "file:///private/tmp/pm_crash_log.txt")!
NSWorkspace.shared.open(fileUrl)
})
.withSecondary(text: "crash_reporter.do_not_send".localized, action: { alert in
alert.close(with: .abort)
})
.withPrimary(text: "crash_reporter.send_report".localized, action: { alert in
alert.close(with: .OK)
}).runModal()
// Check the outcome of what the user chose
if response == .abort {
Log.warn("[CrashReporter] The user has chosen not to send the report.")
crashReporter.purgePendingCrashReport()
}
if response == .OK {
submitCrashReportToApi(text)
crashReporter.purgePendingCrashReport()
}
} else {
Log.err("[CrashReporter] Could not convert report to text.")
crashReporter.purgePendingCrashReport()
}
} catch let error {
Log.err("[CrashReporter] Failed to load and parse with error: \(error)")
crashReporter.purgePendingCrashReport()
}
}
/**
Submits the crash report to the API. Does this with high priority on the main thread
and we wait for completion (w/ a DispatchSemaphore) before continuing boot.
*/
private static func submitCrashReportToApi(_ text: String) {
let timeout = TimeInterval.seconds(10)
var request = URLRequest(url: Constants.Urls.CrashReportingEndpoint)
request.httpMethod = "POST"
request.setValue("text/crash", forHTTPHeaderField: "Content-Type")
request.setValue("phpmon-crashrep/1.0", forHTTPHeaderField: "User-Agent")
request.httpBody = text.data(using: .utf8)
// Send the request synchronously, we want the report to be sent before anything else
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = timeout
let session = URLSession(configuration: config)
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request) { _, response, error in
defer { semaphore.signal() }
if let error = error {
Log.err("[CrashReporter] Failed to send crash report: \(error.localizedDescription)")
return
}
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200...299:
Log.info("[CrashReporter] Crash report sent successfully!")
case 400...499:
Log.err("[CrashReporter] Client error when sending crash report: \(httpResponse.statusCode)")
case 500...599:
Log.err("[CrashReporter] Server error when sending crash report: \(httpResponse.statusCode)")
default:
Log.err("[CrashReporter] Unexpected response code: \(httpResponse.statusCode)")
}
}
}
task.resume()
_ = semaphore.wait(timeout: .now() + timeout)
}
/**
Determines whether a debugger is attached.
*/
private static func isDebuggerAttached() -> Bool {
var info = kinfo_proc()
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
var size = MemoryLayout<kinfo_proc>.stride
let result = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
return result == 0 && (info.kp_proc.p_flag & P_TRACED) != 0
}
}

View File

@@ -13,7 +13,7 @@ import Foundation
Checks that require an app restart will always lead to an alert and app termination shortly after.
*/
struct EnvironmentCheck {
let command: () async -> Bool
let command: (_ container: Container) async -> Bool
let name: String
let titleText: String
let subtitleText: String
@@ -22,13 +22,13 @@ struct EnvironmentCheck {
let requiresAppRestart: Bool
init(
command: @escaping () async -> Bool,
command: @escaping (_ container: Container) async -> Bool,
name: String,
titleText: String,
subtitleText: String,
descriptionText: String = "",
buttonText: String = "OK",
requiresAppRestart: Bool = false
requiresAppRestart: Bool = false,
) {
self.command = command
self.name = name
@@ -40,7 +40,7 @@ struct EnvironmentCheck {
}
public func succeeds() async -> Bool {
return await !self.command()
return await !self.command(App.shared.container)
}
}

View File

@@ -12,14 +12,17 @@ class FakeServicesManager: ServicesManager {
var fixedFormulae: [String] = []
var fixedStatus: Service.Status = .active
override init() {}
override init(_ container: Container) {
super.init(container)
}
init(
_ container: Container,
formulae: [String] = ["php", "nginx", "dnsmasq"],
status: Service.Status = .active,
loading: Bool = false
) {
super.init()
super.init(container)
Log.warn("A fake services manager is being used, so Homebrew formula resolver is set to act in fake mode.")
Log.warn("If you do not want this behaviour, do not make use of a `FakeServicesManager`!")

View File

@@ -11,19 +11,49 @@ import SwiftUI
class ServicesManager: ObservableObject {
@ObservedObject static var shared: ServicesManager = ValetServicesManager()
var container: Container
@ObservedObject static var shared: ServicesManager = ValetServicesManager(App.shared.container)
@Published var services = [Service]()
@Published var firstRunComplete: Bool = false
public static func useFake() {
init(_ container: Container) {
self.container = container
Log.info("The services manager will determine which Valet services exist on this system.")
services = formulae.map {
Service(formula: $0)
}
}
public static func useFake(_ container: Container) {
ServicesManager.shared = FakeServicesManager.init(
container,
formulae: ["php", "nginx", "dnsmasq", "mysql"],
status: .active
)
}
var formulae: [HomebrewFormula] {
let f = HomebrewFormulae(container)
var formulae = [
f.php,
f.nginx,
f.dnsmasq
]
let additionalFormulae = (Preferences.custom.services ?? []).map({ item in
return HomebrewFormula(item, elevated: false)
})
formulae.append(contentsOf: additionalFormulae)
return formulae
}
/**
The order of services is important, so easy access is accomplished
without much fanfare through subscripting.
@@ -104,28 +134,4 @@ class ServicesManager: ObservableObject {
self.objectWillChange.send()
}
}
var formulae: [HomebrewFormula] {
var formulae = [
HomebrewFormulae.php,
HomebrewFormulae.nginx,
HomebrewFormulae.dnsmasq
]
let additionalFormulae = (Preferences.custom.services ?? []).map({ item in
return HomebrewFormula(item, elevated: false)
})
formulae.append(contentsOf: additionalFormulae)
return formulae
}
init() {
Log.info("The services manager will determine which Valet services exist on this system.")
services = formulae.map {
Service(formula: $0)
}
}
}

View File

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

View File

@@ -101,15 +101,17 @@ class Startup {
// The Homebrew binary must exist.
// =================================================================================
EnvironmentCheck(
command: { return !FileSystem.fileExists(Paths.brew) },
name: "`\(Paths.brew)` exists",
command: { container in
return !container.filesystem.fileExists(container.paths.brew)
},
name: "`\(App.shared.container.paths.brew)` exists",
titleText: "alert.homebrew_missing.title".localized,
subtitleText: "alert.homebrew_missing.subtitle".localized,
descriptionText: "alert.homebrew_missing.info".localized(
App.architecture
.replacingOccurrences(of: "x86_64", with: "Intel")
.replacingOccurrences(of: "arm64", with: "Apple Silicon"),
Paths.brew
App.shared.container.paths.brew
),
buttonText: "alert.homebrew_missing.quit".localized,
requiresAppRestart: true
@@ -118,13 +120,14 @@ class Startup {
// Make sure we can detect one or more PHP installations.
// =================================================================================
EnvironmentCheck(
command: {
return await !Shell.pipe("ls \(Paths.optPath) | grep php").out.contains("php")
command: { container in
return await !container.shell
.pipe("ls \(container.paths.optPath) | grep php").out.contains("php")
},
name: "`ls \(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,
subtitleText: "startup.errors.php_opt.subtitle".localized(
Paths.optPath
App.shared.container.paths.optPath
),
descriptionText: "startup.errors.php_opt.desc".localized
)
@@ -134,24 +137,26 @@ class Startup {
// The PHP binary must exist.
// =================================================================================
EnvironmentCheck(
command: { return !FileSystem.fileExists(Paths.php) },
name: "`\(Paths.php)` exists",
command: { container in
return !container.filesystem.fileExists(container.paths.php)
},
name: "`\(App.shared.container.paths.php)` exists",
titleText: "startup.errors.php_binary.title".localized,
subtitleText: "startup.errors.php_binary.subtitle".localized,
descriptionText: "startup.errors.php_binary.desc".localized(Paths.php)
descriptionText: "startup.errors.php_binary.desc".localized(App.shared.container.paths.php)
),
// =================================================================================
// Ensure that the main PHP installation is not broken.
// =================================================================================
EnvironmentCheck(
command: {
return await Shell.pipe("\(Paths.binPath)/php -v").err
command: { container in
return await container.shell.pipe("\(container.paths.binPath)/php -v").err
.contains("Library not loaded")
},
name: "no `dyld` issue (`Library not loaded`) detected",
titleText: "startup.errors.dyld_library.title".localized,
subtitleText: "startup.errors.dyld_library.subtitle".localized(
Paths.optPath
App.shared.container.paths.optPath
),
descriptionText: "startup.errors.dyld_library.desc".localized
),
@@ -159,15 +164,15 @@ class Startup {
// The Valet binary must exist.
// =================================================================================
EnvironmentCheck(
command: {
return !(FileSystem.fileExists(Paths.valet)
|| FileSystem.fileExists("~/.composer/vendor/bin/valet"))
command: { container in
return !(container.filesystem.fileExists(container.paths.valet)
|| container.filesystem.fileExists("~/.composer/vendor/bin/valet"))
},
name: "`valet` binary exists",
titleText: "startup.errors.valet_executable.title".localized,
subtitleText: "startup.errors.valet_executable.subtitle".localized,
descriptionText: "startup.errors.valet_executable.desc".localized(
Paths.valet
App.shared.container.paths.valet
)
),
// =================================================================================
@@ -176,14 +181,21 @@ class Startup {
// functioning correctly. Let the user know that they need to run `valet trust`.
// =================================================================================
EnvironmentCheck(
command: { return await !Shell.pipe("cat /private/etc/sudoers.d/brew").out.contains(Paths.brew) },
command: { container in
return await !container.shell
.pipe("cat /private/etc/sudoers.d/brew")
.out.contains(container.paths.brew)
},
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
descriptionText: "startup.errors.sudoers_brew.desc".localized
),
EnvironmentCheck(
command: { return await !Shell.pipe("cat /private/etc/sudoers.d/valet").out.contains(Paths.valet) },
command: { container in
return await !container.shell
.pipe("cat /private/etc/sudoers.d/valet").out.contains(container.paths.valet)
},
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
@@ -193,8 +205,8 @@ class Startup {
// Determine that Valet is installed
// =================================================================================
EnvironmentCheck(
command: {
return !FileSystem.directoryExists("~/.config/valet")
command: { container in
return !container.filesystem.directoryExists("~/.config/valet")
},
name: "`.config/valet` not empty (Valet installed)",
titleText: "startup.errors.valet_not_installed.title".localized,
@@ -205,9 +217,9 @@ class Startup {
// Determine that the Valet configuration JSON file is valid.
// =================================================================================
EnvironmentCheck(
command: {
command: { container in
// Detect additional binaries (e.g. Composer)
Paths.shared.detectBinaryPaths()
container.paths.detectBinaryPaths()
// Load the configuration file (config.json)
Valet.shared.loadConfiguration()
// This check fails when the config is nil
@@ -222,11 +234,11 @@ class Startup {
// Verify if the Homebrew services are running (as root).
// =================================================================================
EnvironmentCheck(
command: {
await BrewDiagnostics.loadInstalledTaps()
return await BrewDiagnostics.cannotLoadService("dnsmasq")
command: { _ in
await BrewDiagnostics.shared.loadInstalledTaps()
return await BrewDiagnostics.shared.cannotLoadService("dnsmasq")
},
name: "`sudo \(Paths.brew) services info` JSON loaded",
name: "`sudo \(App.shared.container.paths.brew) services info` JSON loaded",
titleText: "startup.errors.services_json_error.title".localized,
subtitleText: "startup.errors.services_json_error.subtitle".localized,
descriptionText: "startup.errors.services_json_error.desc".localized
@@ -235,10 +247,10 @@ class Startup {
// Check for `which` alias issue
// =================================================================================
EnvironmentCheck(
command: {
let nodePath = await Shell.pipe("which node").out
command: { container in
let nodePath = await container.shell.pipe("which node").out
return App.architecture == "x86_64"
&& FileSystem.fileExists("/usr/local/bin/which")
&& container.filesystem.fileExists("/usr/local/bin/which")
&& nodePath.contains("env: node: No such file or directory")
},
name: "`env: node` issue does not apply",
@@ -250,7 +262,7 @@ class Startup {
// Determine that Laravel Herd is not running (may cause conflicts)
// =================================================================================
EnvironmentCheck(
command: {
command: { _ in
return NSWorkspace.shared.runningApplications.contains(where: { app in
return app.bundleIdentifier == "de.beyondco.herd"
})
@@ -264,8 +276,8 @@ class Startup {
// Determine that Valet works correctly (no issues in platform detected)
// =================================================================================
EnvironmentCheck(
command: {
return await Shell.pipe("valet --version").out
command: { container in
return await container.shell.pipe("valet --version").out
.contains("Composer detected issues in your platform")
},
name: "no global composer issues",
@@ -277,7 +289,7 @@ class Startup {
// Determine the Valet version and ensure it isn't unknown.
// =================================================================================
EnvironmentCheck(
command: {
command: { _ in
await Valet.shared.updateVersionNumber()
return Valet.shared.version == nil
},
@@ -290,7 +302,7 @@ class Startup {
// Ensure the Valet version is supported.
// =================================================================================
EnvironmentCheck(
command: {
command: { _ in
// We currently support Valet 2, 3 or 4. Any other version should get an alert.
return ![2, 3, 4].contains(Valet.shared.version?.major)
},

View File

@@ -10,10 +10,23 @@ import Foundation
import NVAlert
@MainActor class ComposerWindow {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Variables
private var shouldNotify: Bool! = nil
private var completion: ((Bool) -> Void)! = nil
private var window: TerminalProgressWindowController?
// MARK: - Methods
/**
Updates the global dependencies and runs the completion callback when done.
*/
@@ -21,14 +34,14 @@ import NVAlert
self.shouldNotify = notify
self.completion = completion
Paths.shared.detectBinaryPaths()
container.paths.detectBinaryPaths()
if Paths.composer == nil {
if container.paths.composer == nil {
self.presentMissingAlert()
return
}
PhpEnvironments.shared.isBusy = true
container.phpEnvs.isBusy = true
window = TerminalProgressWindowController.display(
title: "alert.composer_progress.title".localized,
@@ -51,11 +64,11 @@ import NVAlert
}
private func runComposerUpdateShellCommand() async throws {
let command = "\(Paths.composer!) global update"
let command = "\(container.paths.composer!) global update"
self.window?.addToConsole("\(command)\n")
let (process, _) = try await Shell.attach(
let (process, _) = try await container.shell.attach(
command,
didReceiveOutput: { [weak self] (incoming, _) in
guard let window = self?.window else { return }
@@ -111,7 +124,7 @@ import NVAlert
// MARK: Main Menu Update
private func removeBusyStatus() {
PhpEnvironments.shared.isBusy = false
container.phpEnvs.isBusy = false
}
// MARK: Alert

View File

@@ -50,7 +50,7 @@ struct ProjectTypeDetection {
public static func detectFallbackDependency(_ basePath: String) -> String? {
for entry in Self.FileMapping {
let found = entry.value
.map { path in return FileSystem.anyExists(basePath + path) }
.map { path in return App.shared.container.filesystem.anyExists(basePath + path) }
.contains(true)
if found {

View File

@@ -10,6 +10,16 @@ import Foundation
class BrewPermissionFixer {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Variables
var broken: [DueOwnershipFormula] = []
/**
@@ -54,15 +64,15 @@ class BrewPermissionFixer {
whether the Homebrew binary directory for the given PHP version is owned by root.
*/
private func determineBrokenFormulae() async {
let formulae = PhpEnvironments.shared.cachedPhpInstallations.keys
let formulae = container.phpEnvs.cachedPhpInstallations.keys
for formula in formulae {
let realFormula = formula == PhpEnvironments.brewPhpAlias
? "php"
: "php@\(formula)"
let binFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/bin")
let sbinFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/sbin")
let binFolderOwned = isOwnedByRoot(path: "\(container.paths.optPath)/\(realFormula)/bin")
let sbinFolderOwned = isOwnedByRoot(path: "\(container.paths.optPath)/\(realFormula)/sbin")
if binFolderOwned || sbinFolderOwned {
Log.warn("\(formula) is owned by root")
@@ -70,14 +80,14 @@ class BrewPermissionFixer {
if binFolderOwned {
broken.append(DueOwnershipFormula(
formula: realFormula,
path: "\(Paths.optPath)/\(realFormula)/bin"
path: "\(container.paths.optPath)/\(realFormula)/bin"
))
}
if sbinFolderOwned {
broken.append(DueOwnershipFormula(
formula: realFormula,
path: "\(Paths.optPath)/\(realFormula)/sbin"
path: "\(container.paths.optPath)/\(realFormula)/sbin"
))
}
}
@@ -92,8 +102,8 @@ class BrewPermissionFixer {
return broken
.map { b in
return """
\(Paths.brew) services stop \(b.formula) \
&& chown -R \(Paths.whoami):admin \(b.path)
\(container.paths.brew) services stop \(b.formula) \
&& chown -R \(container.paths.whoami):admin \(b.path)
"""
}
.joined(

View File

@@ -9,7 +9,18 @@
import Foundation
class Brew {
static let shared = Brew()
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Variables
static let shared = Brew(App.shared.container)
/// Formulae that can be observed.
var formulae = BrewFormulaeObservable()
@@ -19,7 +30,7 @@ class Brew {
/// Determine which version of Homebrew is installed.
public func determineVersion() async {
let output = await Shell.pipe("\(Paths.brew) --version")
let output = await container.shell.pipe("\(container.paths.brew) --version")
self.version = try? VersionNumber.parse(output.out)
if let version = version {

View File

@@ -10,17 +10,38 @@ import Foundation
import NVAlert
class BrewDiagnostics {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Static Instance
public static let shared = BrewDiagnostics(App.shared.container)
// MARK: - Variables
var filesystem: FileSystemProtocol {
return container.filesystem
}
/**
Determines the Homebrew taps the user has installed.
*/
public static var installedTaps: [String] = []
public var installedTaps: [String] = []
// MARK: - Methods
/**
Load which taps are installed.
*/
public static func loadInstalledTaps() async {
installedTaps = await Shell
.pipe("\(Paths.brew) tap")
public func loadInstalledTaps() async {
installedTaps = await container.shell
.pipe("\(container.paths.brew) tap")
.out
.split(separator: "\n")
.map { string in
@@ -31,13 +52,13 @@ class BrewDiagnostics {
/**
Logs a bunch of useful information during startup.
*/
public static func logBootInformation() {
Log.info(BrewDiagnostics.customCaskInstalled
public func logBootInformation() {
Log.info(customCaskInstalled
? "[BREW] The app has been installed via Homebrew Cask."
: "[BREW] The app has been installed directly (optimal)."
)
Log.info(BrewDiagnostics.usesNginxFullFormula
Log.info(usesNginxFullFormula
? "[BREW] The app will be using the `nginx-full` formula."
: "[BREW] The app will be using the `nginx` formula."
)
@@ -46,21 +67,21 @@ class BrewDiagnostics {
/**
Determines whether the PHP Monitor Cask is installed.
*/
public static var customCaskInstalled: Bool = {
public var customCaskInstalled: Bool {
return installedTaps.contains("nicoverbruggen/cask")
&& FileSystem.directoryExists(Paths.caskroomPath)
}()
&& filesystem.directoryExists(container.paths.caskroomPath)
}
/**
Determines whether to use the regular `nginx` or `nginx-full` formula.
*/
public static var usesNginxFullFormula: Bool = {
guard let destination = try? FileManager.default
.destinationOfSymbolicLink(atPath: "\(Paths.binPath)/nginx") else { return false }
public var usesNginxFullFormula: Bool {
guard let destination = try? filesystem
.getDestinationOfSymlink("\(container.paths.binPath)/nginx") else { return false }
// Verify that the `nginx` binary is symlinked to a directory that includes `nginx-full`.
return destination.contains("/nginx-full/")
}()
}
/**
It is possible to have outdated symlinks for PHP installations. This can mean that certain PHP installations
@@ -68,12 +89,12 @@ class BrewDiagnostics {
To ensure this does not cause issues, PHP Monitor will automatically remove all incorrect PHP symlinks.
*/
public static func checkForOutdatedPhpInstallationSymlinks() async {
public func checkForOutdatedPhpInstallationSymlinks() async {
// Set up a regular expression
let regex = try! NSRegularExpression(pattern: "^php@[0-9]+\\.[0-9]+$", options: .caseInsensitive)
// Check for incorrect versions
if let contents = try? FileSystem.getShallowContentsOfDirectory("\(Paths.optPath)")
if let contents = try? filesystem.getShallowContentsOfDirectory("\(container.paths.optPath)")
.filter({
let range = NSRange($0.startIndex..., in: $0)
return regex.firstMatch(in: $0, options: [], range: range) != nil
@@ -81,19 +102,19 @@ class BrewDiagnostics {
for symlink in contents {
let version = symlink.replacingOccurrences(of: "php@", with: "")
if let destination = try? FileSystem.getDestinationOfSymlink("\(Paths.optPath)/\(symlink)") {
if let destination = try? filesystem.getDestinationOfSymlink("\(container.paths.optPath)/\(symlink)") {
if !destination.contains("Cellar/php/\(version)")
&& !destination.contains("Cellar/php@\(version)") {
Log.err("Symlink for \(symlink) is incorrect. Removing...")
do {
try FileSystem.remove("\(Paths.optPath)/\(symlink)")
try filesystem.remove("\(container.paths.optPath)/\(symlink)")
Log.info("Incorrect symlink for \(symlink) has been successfully removed.")
} catch {
Log.err("Symlink for \(symlink) was incorrect but could not be removed!")
}
}
} else {
Log.warn("Could not read symlink at: \(Paths.optPath)/\(symlink)! Symlink check skipped.")
Log.warn("Could not read symlink at: \(container.paths.optPath)/\(symlink)! Symlink check skipped.")
}
}
}
@@ -106,7 +127,7 @@ class BrewDiagnostics {
This check only needs to be performed if the `shivammathur/php` tap is active.
*/
public static func checkForCaskConflict() async {
public func checkForCaskConflict() async {
if await hasAliasConflict() {
presentAlertAboutConflict()
}
@@ -116,10 +137,10 @@ class BrewDiagnostics {
It is possible to upgrade PHP, but forget running `valet install`.
This results in a scenario where a rogue www.conf file exists.
*/
public static func checkForValetMisconfiguration() async {
public func checkForValetMisconfiguration() async {
Log.info("Checking for PHP-FPM issues with Valet...")
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
Log.info("Will skip check for issues if no PHP version is linked.")
return
}
@@ -128,7 +149,7 @@ class BrewDiagnostics {
let primary = install.version.short
// Versions to be handled
let switcher = InternalSwitcher()
let switcher = InternalSwitcher(container)
for version in switcher.getVersionsToBeHandled(primary)
where await switcher.ensureValetConfigurationIsValidForPhpVersion(version) {
@@ -138,7 +159,7 @@ class BrewDiagnostics {
}
}
public static func verifyThirdPartyTaps() async {
public func verifyThirdPartyTaps() async {
let requiredTaps = [
"shivammathur/php",
"shivammathur/extensions"
@@ -157,8 +178,8 @@ class BrewDiagnostics {
/**
Check if the alias conflict as documented in `checkForCaskConflict` actually occurred.
*/
private static func hasAliasConflict() async -> Bool {
let tapAlias = await Shell.pipe("brew info shivammathur/php/php --json").out
private func hasAliasConflict() async -> Bool {
let tapAlias = await container.shell.pipe("brew info shivammathur/php/php --json").out
if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") || tapAlias.isEmpty {
Log.info("The user does not appear to have tapped: shivammathur/php")
@@ -177,8 +198,10 @@ class BrewDiagnostics {
+ "This could be a problem!")
Log.info("Determining whether both of these versions are installed...")
let bothInstalled = PhpEnvironments.shared.availablePhpVersions.contains(tapPhp.version)
&& PhpEnvironments.shared.availablePhpVersions.contains(PhpEnvironments.brewPhpAlias)
let availablePhpVersions = container.phpEnvs.availablePhpVersions
let bothInstalled = availablePhpVersions.contains(tapPhp.version)
&& availablePhpVersions.contains(PhpEnvironments.brewPhpAlias)
if bothInstalled {
Log.warn("Both conflicting aliases seem to be installed, warning the user!")
@@ -198,7 +221,7 @@ class BrewDiagnostics {
/**
Show this alert in case the tapped Cask does cause issues because of the conflict.
*/
private static func presentAlertAboutConflict() {
private func presentAlertAboutConflict() {
Task { @MainActor in
NVAlert()
.withInformation(
@@ -214,9 +237,9 @@ class BrewDiagnostics {
In order to see if we support the --json syntax, we'll query nginx.
If the JSON response cannot be parsed, Homebrew is probably out of date.
*/
public static func cannotLoadService(_ name: String) async -> Bool {
let nginxJson = await Shell
.pipe("sudo \(Paths.brew) services info \(name) --json")
public func cannotLoadService(_ name: String) async -> Bool {
let nginxJson = await container.shell
.pipe("sudo \(container.paths.brew) services info \(name) --json")
.out
let serviceInfo = try? JSONDecoder().decode(

View File

@@ -30,20 +30,22 @@ struct BrewPhpExtension: Hashable, Comparable {
return "\(name)@\(phpVersion)"
}
init(path: String, name: String, phpVersion: String) {
init(_ container: Container, path: String, name: String, phpVersion: String) {
self.path = path
self.name = name
self.phpVersion = phpVersion
self.isInstalled = BrewPhpExtension.hasInstallationReceipt(
for: "\(name)@\(phpVersion)"
container, for: "\(name)@\(phpVersion)"
)
self.dependencies = BrewPhpExtension.extractDependencies(from: path)
self.dependencies = BrewPhpExtension.extractDependencies(
container, from: path
)
}
var hasAlternativeInstall: Bool {
guard let php = PhpEnvironments.shared.cachedPhpInstallations[self.phpVersion] else {
guard let php = App.shared.container.phpEnvs.cachedPhpInstallations[self.phpVersion] else {
return false
}
@@ -58,8 +60,8 @@ struct BrewPhpExtension: Hashable, Comparable {
.first { $0.dependencies.contains("shivammathur/extensions/\(self.formulaName)") }
}
static func hasInstallationReceipt(for formulaName: String) -> Bool {
return FileSystem.fileExists("\(Paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
static func hasInstallationReceipt(_ container: Container, for formulaName: String) -> Bool {
return container.filesystem.fileExists("\(container.paths.optPath)/\(formulaName)/INSTALL_RECEIPT.json")
}
static func < (lhs: BrewPhpExtension, rhs: BrewPhpExtension) -> Bool {
@@ -70,11 +72,11 @@ struct BrewPhpExtension: Hashable, Comparable {
return lhs.name == rhs.name
}
private static func extractDependencies(from path: String) -> [String] {
private static func extractDependencies(_ container: Container, from path: String) -> [String] {
let regexPattern = #"depends_on "(.*)""#
var dependencies: [String] = []
guard let content = try? FileSystem.getStringFromFile(path) else {
guard let content = try? container.filesystem.getStringFromFile(path) else {
return []
}

View File

@@ -9,6 +9,10 @@
import Foundation
struct BrewPhpFormula: Equatable {
/// The dependency container.
let container: Container
/// Name of the formula.
let name: String
@@ -33,12 +37,14 @@ struct BrewPhpFormula: Equatable {
}
init(
_ container: Container,
name: String,
displayName: String,
installedVersion: String?,
upgradeVersion: String?,
prerelease: Bool = false
) {
self.container = container
self.name = name
self.displayName = displayName
self.installedVersion = installedVersion
@@ -54,8 +60,8 @@ struct BrewPhpFormula: Equatable {
/// Whether this formula alias is different.
var hasUpgradedFormulaAlias: Bool {
return self.shortVersion == PhpEnvironments.homebrewBrewPhpAlias
&& PhpEnvironments.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
return self.shortVersion == container.phpEnvs.homebrewBrewPhpAlias
&& container.phpEnvs.homebrewBrewPhpAlias != PhpEnvironments.brewPhpAlias
}
var unavailableAfterUpgrade: Bool {
@@ -77,7 +83,7 @@ struct BrewPhpFormula: Equatable {
.replacingOccurrences(of: "shivammathur/php/", with: "")
.replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php")
return "\(Paths.optPath)/\(resolved)/bin"
return "\(App.shared.container.paths.optPath)/\(resolved)/bin"
}
/// The short version associated with this formula, if installed.
@@ -104,8 +110,8 @@ struct BrewPhpFormula: Equatable {
return false
}
return FileSystem.fileExists(
"\(Paths.tapPath)/shivammathur/homebrew-php/Formula/php@\(version).rb"
return container.filesystem.fileExists(
"\(container.paths.tapPath)/shivammathur/homebrew-php/Formula/php@\(version).rb"
.replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php")
)
}
@@ -119,7 +125,16 @@ struct BrewPhpFormula: Equatable {
return nil
}
return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?
return container.phpEnvs.cachedPhpInstallations[shortVersion]?
.isHealthy ?? nil
}
static func == (lhs: BrewPhpFormula, rhs: BrewPhpFormula) -> Bool {
return lhs.name == rhs.name
&& lhs.displayName == rhs.displayName
&& lhs.installedVersion == rhs.installedVersion
&& lhs.upgradeVersion == rhs.upgradeVersion
&& lhs.prerelease == rhs.prerelease
&& lhs.hasFormulaFile == rhs.hasFormulaFile
}
}

View File

@@ -17,23 +17,33 @@ extension HandlesBrewPhpFormulae {
public func refreshPhpVersions(loadOutdated: Bool) async {
let items = await loadPhpVersions(loadOutdated: loadOutdated)
Task { @MainActor in
await PhpEnvironments.shared.determinePhpAlias()
await App.shared.container.phpEnvs.determinePhpAlias()
Brew.shared.formulae.phpVersions = items
}
}
}
class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Methods
public func loadPhpVersions(loadOutdated: Bool) async -> [BrewPhpFormula] {
var outdated: [OutdatedFormula]?
if loadOutdated {
let command = """
\(Paths.brew) update >/dev/null && \
\(Paths.brew) outdated --json --formulae
\(container.paths.brew) update >/dev/null && \
\(container.paths.brew) outdated --json --formulae
"""
let rawJsonText = await Shell.pipe(command).out
let rawJsonText = await container.shell.pipe(command).out
.data(using: .utf8)!
outdated = try? JSONDecoder().decode(
OutdatedFormulae.self,
@@ -48,7 +58,7 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
var upgradeVersion: String?
var isPrerelease: Bool = Constants.ExperimentalPhpVersions.contains(version)
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
if let install = container.phpEnvs.cachedPhpInstallations[version] {
fullVersion = install.versionNumber.text
fullVersion = install.isPreRelease ? "\(fullVersion!)-dev" : fullVersion
@@ -61,6 +71,7 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
}
return BrewPhpFormula(
container,
name: formula,
displayName: "PHP \(version)",
installedVersion: fullVersion,

View File

@@ -9,10 +9,10 @@
import Foundation
class BrewTapFormulae {
public static func from(tap: String) -> [String: [BrewPhpExtension]] {
let directory = "\(Paths.tapPath)/\(tap)/Formula"
public static func from(_ container: Container, tap: String) -> [String: [BrewPhpExtension]] {
let directory = "\(container.paths.tapPath)/\(tap)/Formula"
let files = try? FileSystem.getShallowContentsOfDirectory(directory)
let files = try? container.filesystem.getShallowContentsOfDirectory(directory)
var availableExtensions = [String: [BrewPhpExtension]]()
@@ -35,7 +35,8 @@ class BrewTapFormulae {
// Create a new BrewPhpExtension object (determines if installed)
let phpExtension = BrewPhpExtension(
path: "\(Paths.tapPath)/\(tap)/Formula/\(file)",
container,
path: "\(container.paths.tapPath)/\(tap)/Formula/\(file)",
name: phpExtensionName,
phpVersion: phpVersion
)

View File

@@ -24,11 +24,11 @@ struct CaskFile {
return self.properties["version"]!
}
private static func loadFromApi(_ url: URL) async -> String {
if App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
return await Shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
private static func loadFromApi(_ container: Container, _ url: URL) async -> String {
if isRunningTests || App.hasLoadedTestableConfiguration || url.absoluteString.contains("https://raw.githubusercontent.com") {
return await container.shell.pipe("curl -s --max-time 10 '\(url.absoluteString)'").out
} else {
return await Shell.pipe("""
return await container.shell.pipe("""
curl -s --max-time 10 \
-H "User-Agent: phpmon-curl/1.0" \
-H "X-phpmon-version: \(App.shortVersion) (\(App.bundleVersion))" \
@@ -39,13 +39,13 @@ struct CaskFile {
}
}
public static func from(url: URL) async -> CaskFile? {
public static func from(_ container: Container, url: URL) async -> CaskFile? {
var string: String?
if url.scheme == "file" {
string = try? String(contentsOf: url)
} else {
string = await CaskFile.loadFromApi(url)
string = await CaskFile.loadFromApi(container, url)
}
guard let string else {

View File

@@ -9,7 +9,7 @@
import Foundation
protocol BrewCommand {
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws
func getCommandTitle() -> String
}
@@ -78,10 +78,14 @@ extension BrewCommand {
return nil
}
internal func run(_ command: String, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
internal func run(
shell: ShellProtocol,
_ command: String,
_ onProgress: @escaping (BrewCommandProgress) -> Void
) async throws {
var loggedMessages: [String] = []
let (process, _) = try! await Shell.attach(
let (process, _) = try! await shell.attach(
command,
didReceiveOutput: { text, _ in
if !text.isEmpty {
@@ -104,15 +108,18 @@ extension BrewCommand {
}
}
internal func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
internal func checkPhpTap(
shell: ShellProtocol,
_ onProgress: @escaping (BrewCommandProgress) -> Void
) async throws {
if !BrewDiagnostics.shared.installedTaps.contains("shivammathur/php") {
let command = "brew tap shivammathur/php"
try await run(command, onProgress)
try await run(shell: shell, command, onProgress)
}
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
if !BrewDiagnostics.shared.installedTaps.contains("shivammathur/extensions") {
let command = "brew tap shivammathur/extensions"
try await run(command, onProgress)
try await run(shell: shell, command, onProgress)
}
}
}

View File

@@ -9,6 +9,13 @@
import Foundation
class InstallPhpExtensionCommand: BrewCommand {
// MARK: - Container
var container: Container
// MARK: - Variables
let installing: [BrewPhpExtension]
func getExtensionNames() -> String {
@@ -19,11 +26,15 @@ class InstallPhpExtensionCommand: BrewCommand {
return "phpman.steps.installing".localized(getExtensionNames())
}
public init(install extensions: [BrewPhpExtension]) {
// MARK: - Methods
public init(_ container: Container,
install extensions: [BrewPhpExtension]) {
self.container = container
self.installing = extensions
}
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
let progressTitle = "phpman.steps.wait".localized
onProgress(.create(
@@ -33,16 +44,16 @@ class InstallPhpExtensionCommand: BrewCommand {
))
// Make sure the tap is installed
try await self.checkPhpTap(onProgress)
try await self.checkPhpTap(shell: shell, onProgress)
// Make sure that the extension(s) are installed
try await self.installPackages(onProgress)
try await self.installPackages(shell, onProgress)
// Finally, complete all operations
await self.completedOperations(onProgress)
}
private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
private func installPackages(_ shell: ShellProtocol, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
// If no installations are needed, early exit
if self.installing.isEmpty {
return
@@ -52,10 +63,10 @@ class InstallPhpExtensionCommand: BrewCommand {
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
export HOMEBREW_DOWNLOAD_CONCURRENCY=auto; \
\(Paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force
\(container.paths.brew) install \(self.installing.map { $0.formulaName }.joined(separator: " ")) --force
"""
try await run(command, onProgress)
try await run(shell: shell, command, onProgress)
}
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
@@ -64,11 +75,11 @@ class InstallPhpExtensionCommand: BrewCommand {
// Restart PHP-FPM
if let installed = self.installing.first {
await Actions.restartPhpFpm(version: installed.phpVersion)
await Actions(container).restartPhpFpm(version: installed.phpVersion)
}
// Check which version of PHP are now installed
await PhpEnvironments.detectPhpVersions()
await container.phpEnvs.reloadPhpVersions()
// Keep track of the currently installed version
await MainMenu.shared.refreshActiveInstallation()

View File

@@ -9,9 +9,20 @@
import Foundation
class RemovePhpExtensionCommand: BrewCommand {
// MARK: - Container
var container: Container
// MARK: - Variables
public let phpExtension: BrewPhpExtension
public init(remove formula: BrewPhpExtension) {
// MARK: - Methods
public init(_ container: Container,
remove formula: BrewPhpExtension) {
self.container = container
self.phpExtension = formula
}
@@ -19,7 +30,7 @@ class RemovePhpExtensionCommand: BrewCommand {
return "phpman.steps.removing".localized(phpExtension.name)
}
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
onProgress(.create(
value: 0.2,
title: getCommandTitle(),
@@ -27,7 +38,7 @@ class RemovePhpExtensionCommand: BrewCommand {
))
// Keep track of the file that contains the information about the extension
let existing = PhpEnvironments.shared
let existing = container.phpEnvs
.cachedPhpInstallations[phpExtension.phpVersion]?
.extensions.first(where: { ext in
ext.name == phpExtension.name
@@ -37,12 +48,12 @@ class RemovePhpExtensionCommand: BrewCommand {
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
export HOMEBREW_DOWNLOAD_CONCURRENCY=auto; \
\(Paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies
\(container.paths.brew) remove \(phpExtension.formulaName) --force --ignore-dependencies
"""
var loggedMessages: [String] = []
let (process, _) = try! await Shell.attach(
let (process, _) = try! await shell.attach(
command,
didReceiveOutput: { text, _ in
if !text.isEmpty {
@@ -60,9 +71,9 @@ class RemovePhpExtensionCommand: BrewCommand {
await performExtensionCleanup(for: ext)
}
await PhpEnvironments.detectPhpVersions()
_ = await container.phpEnvs.detectPhpVersions()
await Actions.restartPhpFpm(version: phpExtension.phpVersion)
await Actions(container).restartPhpFpm(version: phpExtension.phpVersion)
await MainMenu.shared.refreshActiveInstallation()
@@ -77,7 +88,7 @@ class RemovePhpExtensionCommand: BrewCommand {
// The extension's default configuration file can be removed
Log.info("The extension was found in a default extension .ini location. Purging that .ini file.")
do {
try FileSystem.remove(ext.file)
try container.filesystem.remove(ext.file)
} catch {
Log.err("The file `\(ext.file)` could not be removed.")
}

View File

@@ -9,11 +9,20 @@
import Foundation
class ModifyPhpVersionCommand: BrewCommand {
// MARK: - Container
var container: Container
// MARK: - Variables
let title: String
let installing: [BrewPhpFormula]
let upgrading: [BrewPhpFormula]
let phpGuard: PhpGuard
// MARK: - Methods
func getCommandTitle() -> String {
return title
}
@@ -32,17 +41,19 @@ class ModifyPhpVersionCommand: BrewCommand {
re-installed and linked again.
*/
public init(
_ container: Container,
title: String,
upgrading: [BrewPhpFormula],
installing: [BrewPhpFormula]
) {
self.container = container
self.title = title
self.installing = installing
self.upgrading = upgrading
self.phpGuard = PhpGuard()
}
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
let progressTitle = "phpman.steps.wait".localized
onProgress(.create(
@@ -58,7 +69,7 @@ class ModifyPhpVersionCommand: BrewCommand {
})
// Make sure the tap is installed
try await self.checkPhpTap(onProgress)
try await self.checkPhpTap(shell: shell, onProgress)
if unavailable == nil {
// Try to run all upgrade and installation operations
@@ -67,11 +78,11 @@ class ModifyPhpVersionCommand: BrewCommand {
} else {
// Simply upgrade `php` to the latest version
try await self.upgradeMainPhpFormula(unavailable!, onProgress)
await PhpEnvironments.shared.determinePhpAlias()
await container.phpEnvs.determinePhpAlias()
}
// Re-check the installed versions
await PhpEnvironments.detectPhpVersions()
_ = await container.phpEnvs.detectPhpVersions()
// After performing operations, attempt to run repairs if needed
try await self.repairBrokenPackages(onProgress)
@@ -94,12 +105,12 @@ class ModifyPhpVersionCommand: BrewCommand {
let command = """
export HOMEBREW_DOWNLOAD_CONCURRENCY=auto; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
\(Paths.brew) upgrade php;
\(Paths.brew) install php@\(short);
\(container.paths.brew) upgrade php;
\(container.paths.brew) install php@\(short);
"""
// Run the upgrade command
try await run(command, onProgress)
try await run(shell: container.shell, command, onProgress)
}
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
@@ -112,10 +123,10 @@ class ModifyPhpVersionCommand: BrewCommand {
export HOMEBREW_DOWNLOAD_CONCURRENCY=auto; \
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
\(Paths.brew) upgrade \(self.upgrading.map { $0.name }.joined(separator: " "))
\(container.paths.brew) upgrade \(self.upgrading.map { $0.name }.joined(separator: " "))
"""
try await run(command, onProgress)
try await run(shell: container.shell, command, onProgress)
}
private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
@@ -127,16 +138,16 @@ class ModifyPhpVersionCommand: BrewCommand {
let command = """
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
\(Paths.brew) install \(self.installing.map { $0.name }.joined(separator: " ")) --force
\(container.paths.brew) install \(self.installing.map { $0.name }.joined(separator: " ")) --force
"""
try await run(command, onProgress)
try await run(shell: container.shell, command, onProgress)
}
private func repairBrokenPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
// Determine which PHP installations are considered unhealthy
// Build a list of formulae to reinstall
let requiringRepair = PhpEnvironments.shared
let requiringRepair = container.phpEnvs
.cachedPhpInstallations.values
.filter({ !$0.isHealthy })
.map { installation in
@@ -159,10 +170,10 @@ class ModifyPhpVersionCommand: BrewCommand {
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=true; \
\(Paths.brew) reinstall \(requiringRepair.joined(separator: " ")) --force
\(container.paths.brew) reinstall \(requiringRepair.joined(separator: " ")) --force
"""
try await run(command, onProgress)
try await run(shell: container.shell, command, onProgress)
}
private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async {
@@ -170,10 +181,10 @@ class ModifyPhpVersionCommand: BrewCommand {
onProgress(.create(value: 0.95, title: self.title, description: "phpman.steps.reloading".localized))
// Ensure all symlinks are correctly linked
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
await BrewDiagnostics.shared.checkForOutdatedPhpInstallationSymlinks()
// Check which version of PHP are now installed
await PhpEnvironments.detectPhpVersions()
_ = await container.phpEnvs.detectPhpVersions()
// Keep track of the currently installed version
await MainMenu.shared.refreshActiveInstallation()

View File

@@ -9,11 +9,24 @@
import Foundation
class RemovePhpVersionCommand: BrewCommand {
// MARK: - Container
var container: Container
// MARK: - Variables
let formula: String
let version: String
let phpGuard: PhpGuard
init(formula: String) {
// MARK: - Methods
init(
_ container: Container,
formula: String
) {
self.container = container
self.version = formula
.replacingOccurrences(of: "php@", with: "")
.replacingOccurrences(of: "shivammathur/php/", with: "")
@@ -25,7 +38,7 @@ class RemovePhpVersionCommand: BrewCommand {
return "phpman.steps.removing".localized("PHP \(version)...")
}
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
onProgress(.create(
value: 0.2,
title: getCommandTitle(),
@@ -36,18 +49,18 @@ class RemovePhpVersionCommand: BrewCommand {
export HOMEBREW_DOWNLOAD_CONCURRENCY=auto; \
export HOMEBREW_NO_INSTALL_UPGRADE=true; \
export HOMEBREW_NO_INSTALL_CLEANUP=true; \
\(Paths.brew) remove \(formula) --force --ignore-dependencies
\(container.paths.brew) remove \(formula) --force --ignore-dependencies
"""
do {
try await BrewPermissionFixer().fixPermissions()
try await BrewPermissionFixer(container).fixPermissions()
} catch {
return
}
var loggedMessages: [String] = []
let (process, _) = try! await Shell.attach(
let (process, _) = try! await shell.attach(
command,
didReceiveOutput: { text, _ in
if !text.isEmpty {
@@ -61,7 +74,7 @@ class RemovePhpVersionCommand: BrewCommand {
if process.terminationStatus <= 0 {
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
await PhpEnvironments.detectPhpVersions()
_ = await container.phpEnvs.detectPhpVersions()
await MainMenu.shared.refreshActiveInstallation()

View File

@@ -19,7 +19,7 @@ class FakeCommand: BrewCommand {
self.version = version
}
func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
onProgress(.create(value: 0.2, title: "Hello", description: "Doing the work"))
await delay(seconds: 2)
onProgress(.create(value: 0.5, title: "Hello", description: "Doing some more work"))

View File

@@ -9,7 +9,6 @@
import Foundation
class NginxConfigurationFile: CreatedFromFile {
/// Contents of the Nginx file in question, as a string.
var contents: String!
@@ -20,8 +19,11 @@ class NginxConfigurationFile: CreatedFromFile {
var tld: String
/** Resolves an nginx configuration file (.conf) */
static func from(filePath: String) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
static func from(
_ container: Container,
filePath: String,
) -> Self? {
let path = filePath.replacingOccurrences(of: "~", with: container.paths.homePath)
do {
let fileContents = try String(contentsOfFile: path)

View File

@@ -14,7 +14,7 @@ class ValetUpgrader {
let path = "~/.composer/composer.json".replacingTildeWithHomeDirectory
do {
if FileSystem.fileExists(path) {
if App.shared.container.filesystem.fileExists(path) {
return try JSONDecoder().decode(
ComposerJson.self,
from: String(
@@ -62,7 +62,7 @@ class ValetUpgrader {
}
@MainActor private static func upgradeValet() {
ComposerWindow().updateGlobalDependencies(
ComposerWindow(App.shared.container).updateGlobalDependencies(
notify: true,
completion: { success in
if success {

View File

@@ -43,6 +43,7 @@ class FakeValetInteractor: ValetInteractor {
if let scanner = ValetScanner.active as? FakeDomainScanner {
scanner.proxies.append(
FakeValetProxy(
container,
domain: domain,
target: proxy,
secure: secure,
@@ -75,7 +76,7 @@ class FakeValetInteractor: ValetInteractor {
override func isolate(site: ValetSite, version: String) async throws {
await delay(seconds: delayTime)
site.isolatedPhpVersion = PhpEnvironments.shared.cachedPhpInstallations[version]
site.isolatedPhpVersion = App.shared.container.phpEnvs.cachedPhpInstallations[version]
site.evaluateCompatibility()
}

View File

@@ -14,33 +14,44 @@ struct ValetInteractionError: Error {
}
class ValetInteractor {
static var shared = ValetInteractor()
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Shared Instance
static var shared = ValetInteractor(App.shared.container)
public static func useFake() {
ValetInteractor.shared = FakeValetInteractor()
ValetInteractor.shared = FakeValetInteractor(App.shared.container)
}
// MARK: - Managing Domains
public func link(path: String, domain: String) async throws {
await Shell.quiet("cd '\(path)' && \(Paths.valet) link '\(domain)' && valet links")
await container.shell.quiet("cd '\(path)' && \(container.paths.valet) link '\(domain)' && valet links")
}
public func unlink(site: ValetSite) async throws {
await Shell.quiet("valet unlink '\(site.name)'")
await container.shell.quiet("valet unlink '\(site.name)'")
}
public func proxy(domain: String, proxy: String, secure: Bool) async throws {
let command = secure
? "\(Paths.valet) proxy \(domain) \(proxy) --secure"
: "\(Paths.valet) proxy \(domain) \(proxy)"
? "\(container.paths.valet) proxy \(domain) \(proxy) --secure"
: "\(container.paths.valet) proxy \(domain) \(proxy)"
await Shell.quiet(command)
await Actions.restartNginx()
await container.shell.quiet(command)
await Actions(container).restartNginx()
}
public func remove(proxy: ValetProxy) async throws {
await Shell.quiet("valet unproxy '\(proxy.domain)'")
await container.shell.quiet("valet unproxy '\(proxy.domain)'")
}
// MARK: - Modifying Domains
@@ -54,15 +65,15 @@ class ValetInteractor {
// Use modernized version of command using domain name
// This will allow us to secure multiple domains that use the same path
var command = "sudo \(Paths.valet) \(action) '\(site.name)' && exit;"
var command = "sudo \(container.paths.valet) \(action) '\(site.name)' && exit;"
// For Valet 2, use the old syntax; this has a known issue so Valet 3+ is preferred
if !Valet.enabled(feature: .isolatedSites) {
command = "cd '\(site.absolutePath)' && sudo \(Paths.valet) \(action) && exit;"
command = "cd '\(site.absolutePath)' && sudo \(container.paths.valet) \(action) && exit;"
}
// Run the command
await Shell.quiet(command)
await container.shell.quiet(command)
// Check if the secured status has actually changed
site.determineSecured()
@@ -78,16 +89,16 @@ class ValetInteractor {
// Build the list of commands we will need to run
let commands: [String] = [
// Unproxy the given domain
"\(Paths.valet) unproxy \(proxy.domain)",
"\(container.paths.valet) unproxy \(proxy.domain)",
// Re-create the proxy (with the inverse secured status)
originalSecureStatus
? "\(Paths.valet) proxy \(proxy.domain) \(proxy.target)"
: "\(Paths.valet) proxy \(proxy.domain) \(proxy.target) --secure"
? "\(container.paths.valet) proxy \(proxy.domain) \(proxy.target)"
: "\(container.paths.valet) proxy \(proxy.domain) \(proxy.target) --secure"
]
// Run the commands
for command in commands {
await Shell.quiet(command)
await container.shell.quiet(command)
}
// Check if the secured status has actually changed
@@ -99,14 +110,14 @@ class ValetInteractor {
}
// Restart nginx to load the new configuration
await Actions.restartNginx()
await Actions(container).restartNginx()
}
public func isolate(site: ValetSite, version: String) async throws {
let command = "sudo \(Paths.valet) isolate php@\(version) --site '\(site.name)'"
let command = "sudo \(container.paths.valet) isolate php@\(version) --site '\(site.name)'"
// Run the command
await Shell.quiet(command)
await container.shell.quiet(command)
// Check if the secured status has actually changed
site.determineIsolated()
@@ -119,10 +130,10 @@ class ValetInteractor {
}
public func unisolate(site: ValetSite) async throws {
let command = "sudo \(Paths.valet) unisolate --site '\(site.name)'"
let command = "sudo \(container.paths.valet) unisolate --site '\(site.name)'"
// Run the command
await Shell.quiet(command)
await container.shell.quiet(command)
// Check if the secured status has actually changed
site.determineIsolated()

View File

@@ -12,8 +12,12 @@ protocol ValetListable {
func getListableName() -> String
func getListableTLD() -> String
func getListableSecured() -> Bool
func getListableCertificateExpiryDate() -> Date?
func getListableAbsolutePath() -> String
func getListablePhpVersion() -> String
@@ -26,4 +30,6 @@ protocol ValetListable {
func getListableFavorited() -> Bool
func toggleSecure() async throws
}

View File

@@ -9,6 +9,21 @@
import Foundation
class FakeValetProxy: ValetProxy {
convenience init(
fakeDomain: String,
target: String,
secure: Bool,
tld: String
) {
self.init(
App.shared.container,
domain: fakeDomain,
target: tld,
secure: secure,
tld: tld
)
}
override func determineSecured() {
return
}

View File

@@ -14,27 +14,39 @@ class ValetProxy: ValetListable {
var target: String
var secured: Bool = false
var certificateExpiryDate: Date?
var isCertificateExpired: Bool {
guard let certificateExpiryDate = certificateExpiryDate else {
return false
}
return certificateExpiryDate < Date()
}
var favorited: Bool = false
var favoriteSignature: String {
"proxy:domain:\(domain).\(tld)|target:\(target)"
}
init(domain: String, target: String, secure: Bool, tld: String) {
var container: Container
init(_ container: Container, domain: String, target: String, secure: Bool, tld: String) {
self.container = container
self.domain = domain
self.tld = tld
self.target = target
self.secured = false
}
convenience init(_ configuration: NginxConfigurationFile) {
convenience init(_ container: Container, _ configuration: NginxConfigurationFile) {
self.init(
container,
domain: configuration.domain,
target: configuration.proxy!,
secure: false,
tld: configuration.tld
)
self.favorited = Favorites.shared.contains(domain: self.domain)
self.favorited = container.favorites.contains(domain: self.domain)
self.determineSecured()
}
@@ -44,10 +56,18 @@ class ValetProxy: ValetListable {
return self.domain
}
func getListableTLD() -> String {
return self.tld
}
func getListableSecured() -> Bool {
return self.secured
}
func getListableCertificateExpiryDate() -> Date? {
return self.certificateExpiryDate
}
func getListableAbsolutePath() -> String {
return self.domain
}
@@ -75,12 +95,23 @@ class ValetProxy: ValetListable {
// MARK: - Interactions
func determineSecured() {
self.secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.domain).\(self.tld).key")
let certificatePath = "~/.config/valet/Certificates/\(self.domain).\(self.tld).crt"
let (exists, expiryDate) = CertificateValidator(container)
.validateCertificate(at: certificatePath)
if exists, let expiryDate, expiryDate < Date() {
Log.warn("Certificate for \(self.domain).\(self.tld) expired at: \(expiryDate). It should be renewed.")
}
// Persist the information for the list
self.secured = exists
self.certificateExpiryDate = expiryDate
}
func toggleFavorite() {
self.favorited.toggle()
Favorites.shared.toggle(domain: self.favoriteSignature)
container.favorites.toggle(domain: self.favoriteSignature)
}
func toggleSecure() async throws {

View File

@@ -33,7 +33,7 @@ class FakeDomainScanner: DomainScanner {
]
var proxies: [ValetProxy] = [
FakeValetProxy(domain: "mailgun", target: "http://127.0.0.1:9999", secure: true, tld: "test")
FakeValetProxy(fakeDomain: "mailgun", target: "http://127.0.0.1:9999", secure: true, tld: "test")
]
// MARK: - Sites

View File

@@ -10,12 +10,20 @@ import Foundation
class ValetDomainScanner: DomainScanner {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Sites
func resolveSiteCount(paths: [String]) -> Int {
return paths.map { path in
do {
let entries = try FileSystem
let entries = try container.filesystem
.getShallowContentsOfDirectory(path)
return entries
@@ -35,7 +43,7 @@ class ValetDomainScanner: DomainScanner {
paths.forEach { path in
do {
let entries = try FileSystem
let entries = try container.filesystem
.getShallowContentsOfDirectory(path)
return entries.forEach {
@@ -59,7 +67,7 @@ class ValetDomainScanner: DomainScanner {
// Get the TLD from the global Valet object
let tld = Valet.shared.config.tld
if !FileSystem.anyExists(path) {
if !container.filesystem.anyExists(path) {
Log.warn("Could not parse the site: \(path), skipping!")
}
@@ -69,10 +77,10 @@ class ValetDomainScanner: DomainScanner {
return nil
}
if FileSystem.isSymlink(path) {
return ValetSite(aliasPath: path, tld: tld)
} else if FileSystem.isDirectory(path) {
return ValetSite(absolutePath: path, tld: tld)
if container.filesystem.isSymlink(path) {
return ValetSite(container, aliasPath: path, tld: tld)
} else if container.filesystem.isDirectory(path) {
return ValetSite(container, absolutePath: path, tld: tld)
}
return nil
@@ -85,7 +93,7 @@ class ValetDomainScanner: DomainScanner {
private func isSite(_ entry: String, forPath path: String) -> Bool {
let siteDir = path + "/" + entry
return (FileSystem.isDirectory(siteDir) || FileSystem.isSymlink(siteDir))
return (container.filesystem.isDirectory(siteDir) || container.filesystem.isSymlink(siteDir))
}
// MARK: - Proxies
@@ -98,13 +106,13 @@ class ValetDomainScanner: DomainScanner {
return !$0.starts(with: ".")
}
.compactMap {
return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)")
return NginxConfigurationFile.from(container, filePath: "\(directoryPath)/\($0)")
}
.filter {
return $0.proxy != nil
}
.map {
return ValetProxy($0)
return ValetProxy(container, $0)
}
}
}

View File

@@ -10,7 +10,7 @@ import Foundation
class ValetScanner {
static var active: DomainScanner = ValetDomainScanner()
static var active: DomainScanner = ValetDomainScanner(App.shared.container)
public static func useFake() {
ValetScanner.active = FakeDomainScanner()

View File

@@ -0,0 +1,127 @@
//
// CertificateValidator.swift
// PHP Monitor
//
// Created by Assistant on 29/10/2025.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import Security
/**
A utility class for validating SSL certificates, including checking expiration dates.
*/
class CertificateValidator {
/// The dependency container for file system access
private let container: Container
init(_ container: Container) {
self.container = container
}
/**
Checks if a certificate file exists and returns its expiration date.
- Parameter certificatePath: Path to the certificate file (supports ~ for home directory)
- Returns: A tuple containing (exists: Bool, expirationDate: Date?)
*/
func validateCertificate(at certificatePath: String) -> (exists: Bool, expirationDate: Date?) {
let exists = container.filesystem.fileExists(certificatePath)
guard exists else {
return (exists: false, expirationDate: nil)
}
let expirationDate = getCertificateExpirationDate(at: certificatePath)
return (exists: true, expirationDate: expirationDate)
}
/**
Loads certificate data from a file path using the filesystem abstraction.
- Parameter path: The file path to the certificate
- Returns: Certificate data as CFData, or nil if loading fails
*/
private func loadCertificateData(from path: String) -> CFData? {
do {
let certificateString = try container.filesystem.getStringFromFile(path)
// Remove PEM headers and footers, and whitespace
let cleanedCertificate = certificateString
.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
.replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let certificateData = Data(base64Encoded: cleanedCertificate) else {
return nil
}
return certificateData as CFData
} catch {
Log.err("Failed to read certificate file at \(path): \(error)")
return nil
}
}
/**
Gets detailed information about a certificate.
- Parameter certificatePath: Path to the certificate file
- Returns: A dictionary containing certificate details, or nil if the certificate couldn't be read
*/
func getCertificateInfo(at certificatePath: String) -> [String: Any]? {
guard let certificateData = loadCertificateData(from: certificatePath),
let certificate = SecCertificateCreateWithData(nil, certificateData) else {
return nil
}
guard let certDict = SecCertificateCopyValues(certificate, nil, nil) as? [String: Any] else {
return nil
}
var info: [String: Any] = [:]
// Extract common name
if let subjectDict = certDict[kSecOIDX509V1SubjectName as String] as? [String: Any],
let subjectArray = subjectDict[kSecPropertyKeyValue as String] as? [[String: Any]] {
for item in subjectArray {
if let label = item[kSecPropertyKeyLabel as String] as? String,
label == "Common Name",
let value = item[kSecPropertyKeyValue as String] as? String {
info["commonName"] = value
break
}
}
}
// Extract expiration date
if let validityDict = certDict[kSecOIDX509V1ValidityNotAfter as String] as? [String: Any],
let validityValue = validityDict[kSecPropertyKeyValue as String] as? NSNumber {
let expirationDate = Date(timeIntervalSinceReferenceDate: validityValue.doubleValue)
info["expirationDate"] = expirationDate
}
// Extract issue date
if let validityDict = certDict[kSecOIDX509V1ValidityNotBefore as String] as? [String: Any],
let validityValue = validityDict[kSecPropertyKeyValue as String] as? NSNumber {
let issueDate = Date(timeIntervalSinceReferenceDate: validityValue.doubleValue)
info["issueDate"] = issueDate
}
return info
}
/**
Gets the expiration date of a certificate.
- Parameter certificatePath: Path to the certificate file
- Returns: The expiration date, or nil if the certificate couldn't be read
*/
func getCertificateExpirationDate(at certificatePath: String) -> Date? {
guard let info = getCertificateInfo(at: certificatePath) else {
return nil
}
return info["expirationDate"] as? Date
}
}

View File

@@ -20,6 +20,7 @@ class FakeValetSite: ValetSite {
isolated: String? = nil
) {
self.init(
App.shared.container,
name: name,
tld: tld,
absolutePath: path,
@@ -39,10 +40,10 @@ class FakeValetSite: ValetSite {
}
if let isolated = isolated {
self.isolatedPhpVersion = PhpInstallation(isolated)
self.isolatedPhpVersion = PhpInstallation(container, isolated)
}
if PhpEnvironments.shared.currentInstall != nil {
if container.phpEnvs.currentInstall != nil {
self.evaluateCompatibility()
}
}

View File

@@ -10,6 +10,9 @@ import Foundation
class ValetSite: ValetListable {
/// Dependency container.
var container: Container
/// Name of the site. Does not include the TLD.
var name: String
@@ -20,7 +23,7 @@ class ValetSite: ValetListable {
/// replacing the user's home folder with ~.
lazy var absolutePathRelative: String = {
return self.absolutePath
.replacingOccurrences(of: Paths.homePath, with: "~")
.replacingOccurrences(of: container.paths.homePath, with: "~")
}()
/// The TLD used to locate this site.
@@ -35,6 +38,17 @@ class ValetSite: ValetListable {
/// Whether the site has been secured.
var secured: Bool!
/// When the certificate expires.
var certificateExpiryDate: Date?
/// A simple bool to check if the certificate has expired.
var isCertificateExpired: Bool {
guard let certificateExpiryDate = certificateExpiryDate else {
return false
}
return certificateExpiryDate < Date()
}
/// What driver is currently in use. If not detected, defaults to nil.
var driver: String?
@@ -57,7 +71,7 @@ class ValetSite: ValetListable {
/// Which version of PHP is actually used to serve this site.
var servingPhpVersion: String {
return self.isolatedPhpVersion?.versionNumber.short
?? PhpEnvironments.phpInstall?.version.short
?? container.phpEnvs.phpInstall?.version.short
?? "???"
}
@@ -67,12 +81,14 @@ class ValetSite: ValetListable {
}
init(
_ container: Container,
name: String,
tld: String,
absolutePath: String,
aliasPath: String? = nil,
makeDeterminations: Bool = true
) {
self.container = container
self.name = name
self.tld = tld
self.absolutePath = absolutePath
@@ -80,7 +96,7 @@ class ValetSite: ValetListable {
self.secured = false
if makeDeterminations {
self.favorited = Favorites.shared.contains(domain: favoriteSignature)
self.favorited = container.favorites.contains(domain: favoriteSignature)
determineSecured()
determineIsolated()
determineComposerPhpVersion()
@@ -88,28 +104,28 @@ class ValetSite: ValetListable {
}
}
convenience init(absolutePath: String, tld: String) {
convenience init(_ container: Container, absolutePath: String, tld: String) {
let name = URL(fileURLWithPath: absolutePath).lastPathComponent
self.init(name: name, tld: tld, absolutePath: absolutePath)
self.init(container, name: name, tld: tld, absolutePath: absolutePath)
}
convenience init(aliasPath: String, tld: String) {
convenience init(_ container: Container, aliasPath: String, tld: String) {
let name = URL(fileURLWithPath: aliasPath).lastPathComponent
let absolutePath = try! FileSystem.getDestinationOfSymlink(aliasPath)
self.init(name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
let absolutePath = try! container.filesystem.getDestinationOfSymlink(aliasPath)
self.init(container, name: name, tld: tld, absolutePath: absolutePath, aliasPath: aliasPath)
}
/**
Determine whether a site is isolated.
*/
public func determineIsolated() {
if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") {
if !PhpEnvironments.shared.cachedPhpInstallations.keys.contains(version) {
if let version = ValetSite.isolatedVersion(container, "~/.config/valet/Nginx/\(self.name).\(self.tld)") {
if !container.phpEnvs.cachedPhpInstallations.keys.contains(version) {
Log.err("The PHP version \(version) is isolated for the site \(self.name) "
+ "but that PHP version is unavailable.")
return
}
self.isolatedPhpVersion = PhpEnvironments.shared.cachedPhpInstallations[version]
self.isolatedPhpVersion = container.phpEnvs.cachedPhpInstallations[version]
} else {
self.isolatedPhpVersion = nil
}
@@ -117,10 +133,21 @@ class ValetSite: ValetListable {
/**
Checks if a certificate file can be found in the `valet/Certificates` directory.
- Note: The file is not validated, only its presence is checked.
Also tracks the expiry date of the certificate if it exists.
*/
public func determineSecured() {
secured = FileSystem.fileExists("~/.config/valet/Certificates/\(self.name).\(self.tld).key")
let certificatePath = "~/.config/valet/Certificates/\(self.name).\(self.tld).crt"
let (exists, expiryDate) = CertificateValidator(container)
.validateCertificate(at: certificatePath)
if exists, let expiryDate, expiryDate < Date() {
Log.warn("Certificate for \(self.name).\(self.tld) expired at: \(expiryDate). It should be renewed.")
}
// Persist the information for the list
self.secured = exists
self.certificateExpiryDate = expiryDate
}
/**
@@ -182,7 +209,7 @@ class ValetSite: ValetListable {
let path = "\(absolutePath)/composer.json"
do {
if FileSystem.fileExists(path) {
if container.filesystem.fileExists(path) {
let decoded = try JSONDecoder().decode(
ComposerJson.self,
from: String(
@@ -213,7 +240,7 @@ class ValetSite: ValetListable {
for (suffix, source) in files {
do {
let path = "\(absolutePath)/\(suffix)"
if FileSystem.fileExists(path) {
if container.filesystem.fileExists(path) {
return try self.handleValetFile(path, source)
}
} catch {
@@ -250,7 +277,7 @@ class ValetSite: ValetListable {
return
}
guard let linked = PhpEnvironments.phpInstall else {
guard let linked = container.phpEnvs.phpInstall else {
self.isCompatibleWithPreferredPhpVersion = false
return
}
@@ -271,10 +298,13 @@ class ValetSite: ValetListable {
// MARK: - File Parsing
public static func isolatedVersion(_ filePath: String) -> String? {
if FileSystem.fileExists(filePath) {
public static func isolatedVersion(
_ container: Container,
_ filePath: String
) -> String? {
if container.filesystem.fileExists(filePath) {
return NginxConfigurationFile
.from(filePath: filePath)?
.from(container, filePath: filePath)?
.isolatedVersion ?? nil
}
@@ -287,10 +317,18 @@ class ValetSite: ValetListable {
return self.name
}
func getListableTLD() -> String {
return self.tld
}
func getListableSecured() -> Bool {
return self.secured
}
func getListableCertificateExpiryDate() -> Date? {
return self.certificateExpiryDate
}
func getListableAbsolutePath() -> String {
return self.absolutePath
}
@@ -323,7 +361,7 @@ class ValetSite: ValetListable {
func toggleFavorite() {
self.favorited.toggle()
Favorites.shared.toggle(domain: self.favoriteSignature)
container.favorites.toggle(domain: self.favoriteSignature)
}
func isolate(version: String) async throws {

View File

@@ -21,6 +21,9 @@ class Valet {
static let shared = Valet()
/// The dependency container.
var container: Container
/// The version of Valet that was detected.
var version: VersionNumber?
@@ -44,7 +47,8 @@ class Valet {
/// When initialising the Valet singleton, assume no sites or proxies loaded.
/// We will load the version later.
init() {
init(container: Container = App.shared.container) {
self.container = container
self.version = nil
self.sites = []
self.proxies = []
@@ -65,8 +69,8 @@ class Valet {
}
lazy var installed: Bool = {
return FileSystem.fileExists(Paths.binPath.appending("/valet"))
&& FileSystem.anyExists("~/.config/valet")
return container.filesystem.fileExists(container.paths.binPath.appending("/valet"))
&& container.filesystem.anyExists("~/.config/valet")
}()
/**
@@ -83,13 +87,26 @@ class Valet {
return self.shared.sites + self.shared.proxies
}
/**
Retrieve a list of all domains, including sites & proxies
that have expired certificates.
*/
public static func getExpiredDomainListable() -> [ValetListable] {
return self.getDomainListable().filter { item in
if let expiry = item.getListableCertificateExpiryDate() {
return expiry < Date()
}
return false
}
}
/**
Updates the internal version number of Laravel Valet.
If this version number cannot be determined, it fails,
and the app cannot start.
*/
public func updateVersionNumber() async {
let output = await Shell.pipe("valet --version").out
let output = await container.shell.pipe("valet --version").out
// Failure condition #1: does not contain Laravel Valet
if !output.contains("Laravel Valet") {
@@ -119,7 +136,9 @@ class Valet {
do {
config = try JSONDecoder().decode(
Valet.Configuration.self,
from: FileSystem.getStringFromFile("~/.config/valet/config.json").data(using: .utf8)!
from: container.filesystem
.getStringFromFile("~/.config/valet/config.json")
.data(using: .utf8)!
)
} catch {
Log.err(error)
@@ -183,7 +202,7 @@ class Valet {
return
}
if PhpEnvironments.phpInstall == nil {
if container.phpEnvs.phpInstall == nil {
Log.info("Cannot validate Valet version if no PHP version is linked.")
return
}
@@ -206,7 +225,7 @@ class Valet {
Determine if any platform issues are detected when running `valet --version`.
*/
public func hasPlatformIssues() async -> Bool {
return await Shell.pipe("valet --version")
return await container.shell.pipe("valet --version")
.out.contains("Composer detected issues in your platform")
}
@@ -240,20 +259,21 @@ class Valet {
that means that Valet won't work properly.
*/
func phpFpmConfigurationValid() async -> Bool {
guard let version = PhpEnvironments.shared.currentInstall?.version else {
guard let version = container.phpEnvs.currentInstall?.version else {
Log.info("Cannot check PHP-FPM status: no version of PHP is active")
return true
}
if version.short == "5.6" {
// 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"
return await Shell.pipe("cat \(fileName)").out
let fileName = "\(container.paths.etcPath)/php/5.6/php-fpm.conf"
return await container.shell.pipe("cat \(fileName)").out
.contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return FileSystem.fileExists("\(Paths.etcPath)/php/\(version.short)/php-fpm.d/valet-fpm.conf")
return container.filesystem
.fileExists("\(container.paths.etcPath)/php/\(version.short)/php-fpm.d/valet-fpm.conf")
}
/**

View File

@@ -10,12 +10,11 @@ import Cocoa
import NVAlert
extension MainMenu {
// MARK: - Actions
@MainActor @objc func linkPhpBinary() {
Task {
await Actions.linkPhp()
await actions.linkPhp()
}
}
@@ -46,7 +45,7 @@ extension MainMenu {
}
asyncExecution {
try Actions.fixHomebrewPermissions()
try self.actions.fixHomebrewPermissions()
} success: {
NVAlert()
.withInformation(
@@ -63,27 +62,27 @@ extension MainMenu {
@objc func restartPhpFpm() {
Task { // Simple restart service
await Actions.restartPhpFpm()
await actions.restartPhpFpm()
}
}
@objc func restartNginx() {
Task { // Simple restart service
await Actions.restartNginx()
await actions.restartNginx()
}
}
@objc func restartDnsMasq() {
Task { // Simple restart service
await Actions.restartDnsMasq()
await actions.restartDnsMasq()
}
}
@MainActor @objc func restartValetServices() {
Task { // Restart services and show notification
await Actions.restartDnsMasq()
await Actions.restartPhpFpm()
await Actions.restartNginx()
await actions.restartDnsMasq()
await actions.restartPhpFpm()
await actions.restartNginx()
LocalNotification.send(
title: "notification.services_restarted".localized,
@@ -95,7 +94,7 @@ extension MainMenu {
@MainActor @objc func stopValetServices() {
Task { // Stop services and show notification
await Actions.stopValetServices()
await actions.stopValetServices()
LocalNotification.send(
title: "notification.services_stopped".localized,
@@ -106,7 +105,7 @@ extension MainMenu {
}
@objc func disableAllXdebugModes() {
guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else {
guard let file = container.phpEnvs.getConfigFile(forKey: "xdebug.mode") else {
Log.info("xdebug.mode could not be found in any .ini file, aborting.")
return
}
@@ -125,12 +124,12 @@ extension MainMenu {
@objc func toggleXdebugMode(sender: XdebugMenuItem) {
Log.info("Switching Xdebug to mode: \(sender.mode)")
guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else {
guard let file = container.phpEnvs.getConfigFile(forKey: "xdebug.mode") else {
return Log.info("xdebug.mode could not be found in any .ini file, aborting.")
}
do {
var modes = Xdebug.activeModes
var modes = Xdebug(container).activeModes
if let index = modes.firstIndex(of: sender.mode) {
modes.remove(at: index)
@@ -158,7 +157,7 @@ extension MainMenu {
await sender.phpExtension?.toggle()
if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) {
await Actions.restartPhpFpm()
await actions.restartPhpFpm()
}
}
}
@@ -213,43 +212,43 @@ extension MainMenu {
@objc func openPhpInfo() {
asyncWithBusyUI {
Task { // Create temporary file and open the URL
let url = await Actions.createTempPhpInfoFile()
let url = await self.actions.createTempPhpInfoFile()
NSWorkspace.shared.open(url)
}
}
}
@MainActor @objc func updateGlobalComposerDependencies() {
ComposerWindow().updateGlobalDependencies(
ComposerWindow(container).updateGlobalDependencies(
notify: true,
completion: { _ in }
)
}
@objc func openActiveConfigFolder() {
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
// TODO: Can't open the config if no PHP version is active
return
}
if install.hasErrorState {
Actions.openGenericPhpConfigFolder()
actions.openGenericPhpConfigFolder()
return
}
Actions.openPhpConfigFolder(version: install.version.short)
actions.openPhpConfigFolder(version: install.version.short)
}
@objc func openPhpMonitorConfigurationFile() {
Actions.openPhpMonitorConfigFile()
actions.openPhpMonitorConfigFile()
}
@objc func openGlobalComposerFolder() {
Actions.openGlobalComposerFolder()
actions.openGlobalComposerFolder()
}
@objc func openValetConfigFolder() {
Actions.openValetConfigFolder()
actions.openValetConfigFolder()
}
@objc func switchToPhpVersion(sender: PhpMenuItem) {
@@ -260,7 +259,7 @@ extension MainMenu {
if silently {
MainMenu.shared.shouldSwitchSilently = true
}
if PhpEnvironments.shared.availablePhpVersions.contains(version) {
if container.phpEnvs.availablePhpVersions.contains(version) {
Task { MainMenu.shared.switchToPhpVersion(version) }
} else {
Task {
@@ -279,37 +278,37 @@ extension MainMenu {
MainMenu.shared.shouldSwitchSilently = true
}
if !PhpEnvironments.shared.availablePhpVersions.contains(version) {
if !container.phpEnvs.availablePhpVersions.contains(version) {
Log.warn("This PHP version is currently unavailable, not switching!")
return
}
PhpEnvironments.shared.isBusy = true
PhpEnvironments.shared.delegate = self
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
container.phpEnvs.isBusy = true
container.phpEnvs.delegate = self
container.phpEnvs.delegate?.switcherDidStartSwitching(to: version)
refreshIcon()
rebuild()
await PhpEnvironments.switcher.performSwitch(to: version)
PhpEnvironments.shared.currentInstall = ActivePhpInstallation()
container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher()
PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version)
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
}
@objc func switchToPhpVersion(_ version: String) {
PhpEnvironments.shared.isBusy = true
PhpEnvironments.shared.delegate = self
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
container.phpEnvs.isBusy = true
container.phpEnvs.delegate = self
container.phpEnvs.delegate?.switcherDidStartSwitching(to: version)
Task(priority: .userInitiated) { [unowned self] in
refreshIcon()
rebuild()
await PhpEnvironments.switcher.performSwitch(to: version)
PhpEnvironments.shared.currentInstall = ActivePhpInstallation()
container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher()
PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version)
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
}
}
@@ -324,18 +323,18 @@ extension MainMenu {
*/
func switchToPhp(_ version: String) async {
Task { @MainActor [self] in
PhpEnvironments.shared.isBusy = true
PhpEnvironments.shared.delegate = self
PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version)
container.phpEnvs.isBusy = true
container.phpEnvs.delegate = self
container.phpEnvs.delegate?.switcherDidStartSwitching(to: version)
}
refreshIcon()
rebuild()
await PhpEnvironments.switcher.performSwitch(to: version)
PhpEnvironments.shared.currentInstall = ActivePhpInstallation()
container.phpEnvs.currentInstall = ActivePhpInstallation(container)
App.shared.handlePhpConfigWatcher()
PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version)
container.phpEnvs.delegate?.switcherDidCompleteSwitch(to: version)
}
}

View File

@@ -46,7 +46,7 @@ extension MainMenu {
]
) {
if behaviours.contains(.reloadsPhpInstallation) || behaviours.contains(.setsBusyUI) {
PhpEnvironments.shared.isBusy = true
App.shared.container.phpEnvs.isBusy = true
}
Task(priority: .userInitiated) { [unowned self] in
@@ -59,7 +59,7 @@ extension MainMenu {
Task { @MainActor [self, error] in
if behaviours.contains(.reloadsPhpInstallation) {
PhpEnvironments.shared.currentInstall = ActivePhpInstallation()
container.phpEnvs.currentInstall = ActivePhpInstallation(container)
}
if behaviours.contains(.updatesMenuBarContents) {
@@ -72,7 +72,7 @@ extension MainMenu {
}
if behaviours.contains(.setsBusyUI) {
PhpEnvironments.shared.isBusy = false
App.shared.container.phpEnvs.isBusy = false
}
if error != nil {

View File

@@ -13,9 +13,9 @@ import NVAlert
extension MainMenu {
@MainActor @objc func fixMyValet() {
let previousVersion = PhpEnvironments.phpInstall?.version.short
let previousVersion = container.phpEnvs.phpInstall?.version.short
if !PhpEnvironments.shared.availablePhpVersions.contains(PhpEnvironments.brewPhpAlias) {
if !App.shared.container.phpEnvs.availablePhpVersions.contains(PhpEnvironments.brewPhpAlias) {
presentAlertForMissingFormula()
return
}
@@ -33,7 +33,7 @@ extension MainMenu {
}
Task { @MainActor in
await Actions.fixMyValet()
await Actions(container).fixMyValet()
if previousVersion == PhpEnvironments.brewPhpAlias || previousVersion == nil {
self.presentAlertForSameVersion()

View File

@@ -31,16 +31,16 @@ extension MainMenu {
*/
private func onEnvironmentPass() async {
// Determine what the `php` formula is aliased to
await PhpEnvironments.shared.determinePhpAlias()
await container.phpEnvs.determinePhpAlias()
// Make sure that broken symlinks are removed ASAP
await BrewDiagnostics.checkForOutdatedPhpInstallationSymlinks()
await BrewDiagnostics.shared.checkForOutdatedPhpInstallationSymlinks()
// Initialize preferences
_ = Preferences.shared
Preferences.shared = Preferences(container)
// Put some useful diagnostics information in log
BrewDiagnostics.logBootInformation()
BrewDiagnostics.shared.logBootInformation()
// Attempt to find out more info about Valet
if Valet.shared.version != nil {
@@ -54,23 +54,20 @@ extension MainMenu {
await Brew.shared.determineVersion()
// Actually detect the PHP versions
await PhpEnvironments.detectPhpVersions()
await container.phpEnvs.reloadPhpVersions()
// Verify third party taps
// The missing tap(s) will be actionable later
await BrewDiagnostics.verifyThirdPartyTaps()
await BrewDiagnostics.shared.verifyThirdPartyTaps()
// Check for an alias conflict
await BrewDiagnostics.checkForCaskConflict()
// Attempt to find out if PHP-FPM is broken
PhpEnvironments.prepare()
await BrewDiagnostics.shared.checkForCaskConflict()
// Set up the filesystem watcher for the Homebrew binaries
App.shared.prepareHomebrewWatchers()
// Check for other problems
WarningManager.shared.evaluateWarnings()
container.warningManager.evaluateWarnings()
// Set up the config watchers on launch (updated automatically when switching)
App.shared.handlePhpConfigWatcher()
@@ -92,7 +89,7 @@ extension MainMenu {
await Valet.shared.startPreloadingSites()
// After preloading sites, check for PHP-FPM pool conflicts
await BrewDiagnostics.checkForValetMisconfiguration()
await BrewDiagnostics.shared.checkForValetMisconfiguration()
// Check if PHP-FPM is broken (should be fixed automatically if phpmon >= 6.0)
await Valet.shared.notifyAboutBrokenPhpFpm()
@@ -108,7 +105,7 @@ extension MainMenu {
Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.")
// We are ready!
PhpEnvironments.shared.isBusy = false
container.phpEnvs.isBusy = false
// Finally!
Log.info("PHP Monitor is ready to serve!")
@@ -184,10 +181,10 @@ extension MainMenu {
private func detectApplications() async {
Log.info("Detecting applications...")
App.shared.detectedApplications = await Application.detectPresetApplications()
App.shared.detectedApplications = await Application.detectPresetApplications(container)
let customApps = Preferences.custom.scanApps?.map { appName in
return Application(appName, .user_supplied)
return Application(container, appName, .user_supplied)
} ?? []
var detectedCustomApps: [Application] = []

View File

@@ -18,7 +18,7 @@ extension MainMenu {
nonisolated func switcherDidCompleteSwitch(to version: String) {
// Mark as no longer busy
Task { @MainActor in
PhpEnvironments.shared.isBusy = false
container.phpEnvs.isBusy = false
}
Task { // Things to do after reloading domain list data
@@ -31,14 +31,14 @@ extension MainMenu {
refreshIcon()
rebuild()
if Valet.installed && !PhpEnvironments.shared.validate(version) {
if Valet.installed && !container.phpEnvs.validate(version) {
self.suggestFixMyValet(failed: version)
return
}
// Run composer updates
if Preferences.isEnabled(.autoComposerGlobalUpdateAfterSwitch) {
ComposerWindow().updateGlobalDependencies(
ComposerWindow(App.shared.container).updateGlobalDependencies(
notify: false,
completion: { _ in
self.notifyAboutVersionChange(to: version)
@@ -99,7 +99,7 @@ extension MainMenu {
.withPrimary(text: "alert.global_composer_platform_issues.buttons.update".localized, action: { alert in
alert.close(with: .OK)
Log.info("The user has chosen to update global dependencies.")
ComposerWindow().updateGlobalDependencies(
ComposerWindow(App.shared.container).updateGlobalDependencies(
notify: true,
completion: { success in
Log.info("Dependencies updated successfully: \(success)")
@@ -135,7 +135,7 @@ extension MainMenu {
preference: .notifyAboutVersionChange
)
guard PhpEnvironments.phpInstall != nil else {
guard container.phpEnvs.phpInstall != nil else {
Log.err("Cannot notify about version change if PHP is unlinked")
return
}

View File

@@ -10,6 +10,13 @@ import NVAlert
@MainActor
class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate {
var container: Container {
return App.shared.container
}
var actions: Actions {
return Actions(container)
}
static let shared = MainMenu()
@@ -78,8 +85,8 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
/** Reloads which PHP versions is currently active. */
@objc func refreshActiveInstallation() {
if !PhpEnvironments.shared.isBusy {
PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load()
if !container.phpEnvs.isBusy {
container.phpEnvs.currentInstall = ActivePhpInstallation.load(container)
refreshIcon()
rebuild()
} else {
@@ -124,7 +131,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
NVAlert().withInformation(
title: "startup.unsupported_versions_explanation.title".localized,
subtitle: "startup.unsupported_versions_explanation.subtitle".localized(
PhpEnvironments.shared.incompatiblePhpVersions
container.phpEnvs.incompatiblePhpVersions
.map({ version in
return "• PHP \(version)"
})
@@ -156,9 +163,8 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
/** Refreshes the icon with the PHP version. */
@objc func refreshIcon() {
Task { @MainActor [self] in
if PhpEnvironments.shared.isBusy {
if container.phpEnvs.isBusy {
Log.perf("Refreshing icon: currently busy")
setStatusBar(image: NSImage.statusBarIcon)
} else {
@@ -170,7 +176,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
// The dynamic icon has been requested
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
setStatusBarImage(version: "???")
return
}
@@ -184,6 +190,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
// MARK: - Menu Item Functionality
@objc func openAbout() {
if NSEvent.modifierFlags.contains(.option) && NSEvent.modifierFlags.contains(.command) {
fatalError("Debug crash triggered via About menu with OPT+CMD.")
}
NSApplication.shared.activate(ignoringOtherApps: true)
NSApplication.shared.orderFrontStandardAboutPanel(self)
}

View File

@@ -13,7 +13,7 @@ import Cocoa
extension StatusMenu {
@MainActor func addPhpVersionMenuItems() {
if PhpEnvironments.phpInstall == nil {
if container.phpEnvs.phpInstall == nil {
addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280))
addItems([
NSMenuItem.separator(),
@@ -23,29 +23,29 @@ extension StatusMenu {
return
}
if PhpEnvironments.phpInstall!.hasErrorState {
if container.phpEnvs.phpInstall!.hasErrorState {
let brokenMenuItems = ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"]
return addItems(brokenMenuItems.map { NSMenuItem(title: $0.localized) })
}
addItem(HeaderView.asMenuItem(
text: "\("mi_php_version".localized) \(PhpEnvironments.phpInstall!.version.long)",
text: "\("mi_php_version".localized) \(container.phpEnvs.phpInstall!.version.long)",
minimumWidth: 280 // this ensures the menu is at least wide enough not to cause clipping
))
}
@MainActor func addPhpActionMenuItems() {
if PhpEnvironments.shared.isBusy {
if App.shared.container.phpEnvs.isBusy {
addItem(NSMenuItem(title: "mi_busy".localized))
return
}
if PhpEnvironments.shared.availablePhpVersions.isEmpty
&& PhpEnvironments.shared.incompatiblePhpVersions.isEmpty {
if App.shared.container.phpEnvs.availablePhpVersions.isEmpty
&& App.shared.container.phpEnvs.incompatiblePhpVersions.isEmpty {
return
}
if PhpEnvironments.shared.currentInstall == nil {
if App.shared.container.phpEnvs.currentInstall == nil {
return
}
@@ -55,7 +55,7 @@ extension StatusMenu {
}
@MainActor func addServicesManagerMenuItem() {
if PhpEnvironments.shared.isBusy {
if App.shared.container.phpEnvs.isBusy {
return
}
@@ -67,10 +67,10 @@ extension StatusMenu {
@MainActor func addSwitchToPhpMenuItems() {
var shortcutKey = 1
for index in (0..<PhpEnvironments.shared.availablePhpVersions.count) {
for index in (0..<App.shared.container.phpEnvs.availablePhpVersions.count) {
// Get the short and long version
let shortVersion = PhpEnvironments.shared.availablePhpVersions[index]
let longVersion = PhpEnvironments.shared.cachedPhpInstallations[shortVersion]!.versionNumber
let shortVersion = App.shared.container.phpEnvs.availablePhpVersions[index]
let longVersion = App.shared.container.phpEnvs.cachedPhpInstallations[shortVersion]!.versionNumber
let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool
let versionString = long ? longVersion.text : shortVersion
@@ -78,7 +78,7 @@ extension StatusMenu {
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let brew = (shortVersion == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(shortVersion)"
let isActive = (shortVersion == PhpEnvironments.phpInstall?.version.short)
let isActive = (shortVersion == container.phpEnvs.phpInstall?.version.short)
let menuItem = PhpMenuItem(
title: "\("mi_php_switch".localized) \(versionString) (\(brew))",
@@ -92,11 +92,11 @@ extension StatusMenu {
addItem(menuItem)
}
if !PhpEnvironments.shared.incompatiblePhpVersions.isEmpty {
if !App.shared.container.phpEnvs.incompatiblePhpVersions.isEmpty {
addItem(NSMenuItem.separator())
addItem(NSMenuItem(
title: "⚠️ " + "mi_php_unsupported".localized(
"\(PhpEnvironments.shared.incompatiblePhpVersions.count)"
"\(App.shared.container.phpEnvs.incompatiblePhpVersions.count)"
),
action: #selector(MainMenu.showIncompatiblePhpVersionsAlert)
))
@@ -104,7 +104,6 @@ extension StatusMenu {
}
@MainActor func addPreferencesMenuItems() {
addItems([
NSMenuItem.separator(),
NSMenuItem(title: "mi_preferences".localized,
@@ -187,7 +186,7 @@ extension StatusMenu {
),
NSMenuItem(
title: "mi_update_global_composer".localized,
action: PhpEnvironments.shared.isBusy
action: App.shared.container.phpEnvs.isBusy
? nil
: #selector(MainMenu.updateGlobalComposerDependencies),
keyEquivalent: "g",
@@ -200,7 +199,7 @@ extension StatusMenu {
// MARK: - Stats
@MainActor func addStatsMenuItem() {
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
Log.info("Not showing stats menu item if no PHP version is linked.")
return
}
@@ -217,7 +216,7 @@ extension StatusMenu {
// MARK: - Extensions
@MainActor func addExtensionsMenuItems() {
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
Log.info("Not showing extensions menu items if no PHP version is linked.")
return
}
@@ -276,7 +275,9 @@ extension StatusMenu {
// MARK: - Xdebug
@MainActor func addXdebugMenuItem() {
if !Xdebug.enabled {
let xdebug = Xdebug(container)
if !xdebug.enabled {
addItem(NSMenuItem.separator())
return
}
@@ -284,7 +285,7 @@ extension StatusMenu {
addItems([
NSMenuItem(title: "mi_xdebug_mode".localized, submenu: [
HeaderView.asMenuItem(text: "mi_xdebug_available_modes".localized)
] + Xdebug.asMenuItems() + [
] + xdebug.asMenuItems() + [
HeaderView.asMenuItem(text: "mi_xdebug_actions".localized),
NSMenuItem(title: "mi_xdebug_disable_all".localized,
action: #selector(MainMenu.disableAllXdebugModes))
@@ -297,13 +298,13 @@ extension StatusMenu {
@MainActor func addPhpDoctorMenuItem() {
if !Preferences.isEnabled(.showPhpDoctorSuggestions) ||
!WarningManager.shared.hasWarnings() {
!App.shared.container.warningManager.hasWarnings() {
return
}
addItems([
HeaderView.asMenuItem(text: "mi_php_doctor".localized),
NSMenuItem(title: "mi_recommendations_count".localized(WarningManager.shared.warnings.count)),
NSMenuItem(title: "mi_recommendations_count".localized(App.shared.container.warningManager.warnings.count)),
NSMenuItem(title: "mi_view_recommendations".localized, action: #selector(MainMenu.openWarnings)),
NSMenuItem.separator()
])

View File

@@ -8,17 +8,21 @@
import Cocoa
class StatusMenu: NSMenu {
var container: Container {
return App.shared.container
}
// swiftlint:disable cyclomatic_complexity
@MainActor func addMenuItems() {
addPhpVersionMenuItems()
addItem(NSMenuItem.separator())
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayGlobalVersionSwitcher) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayGlobalVersionSwitcher) {
addPhpActionMenuItems()
addItem(NSMenuItem.separator())
}
if PhpEnvironments.phpInstall != nil && Valet.installed && Preferences.isEnabled(.displayServicesManager) {
if container.phpEnvs.phpInstall != nil && Valet.installed && Preferences.isEnabled(.displayServicesManager) {
addServicesManagerMenuItem()
addItem(NSMenuItem.separator())
}
@@ -28,23 +32,23 @@ class StatusMenu: NSMenu {
addItem(NSMenuItem.separator())
}
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayPhpConfigFinder) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayPhpConfigFinder) {
addConfigurationMenuItems()
addItem(NSMenuItem.separator())
}
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayComposerToolkit) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayComposerToolkit) {
addComposerMenuItems()
addItem(NSMenuItem.separator())
}
if !PhpEnvironments.shared.isBusy {
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayLimitsWidget) {
if !App.shared.container.phpEnvs.isBusy {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayLimitsWidget) {
addStatsMenuItem()
addItem(NSMenuItem.separator())
}
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayExtensions) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayExtensions) {
addExtensionsMenuItems()
NSMenuItem.separator()
@@ -53,11 +57,11 @@ class StatusMenu: NSMenu {
addPhpDoctorMenuItem()
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayPresets) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayPresets) {
addPresetsMenuItem()
}
if PhpEnvironments.phpInstall != nil && Preferences.isEnabled(.displayMisc) {
if container.phpEnvs.phpInstall != nil && Preferences.isEnabled(.displayMisc) {
addFirstAidAndServicesMenuItems()
}
}

View File

@@ -10,11 +10,14 @@ import Foundation
import NVAlert
class PhpGuard {
var currentVersion: String?
var container: Container {
return App.shared.container
}
init() {
guard let linked = PhpEnvironments.phpInstall else {
guard let linked = container.phpEnvs.phpInstall else {
Log.warn("PHP Guard is unable to determine the current PHP version!")
return
}

View File

@@ -45,14 +45,14 @@ struct CustomPrefs: Decodable {
extension Preferences {
func loadCustomPreferences() async {
// Ensure the configuration directory is created if missing
await Shell.quiet("mkdir -p ~/.config/phpmon")
await container.shell.quiet("mkdir -p ~/.config/phpmon")
// Move the legacy file
await moveOutdatedConfigurationFile()
// Attempt to load the file if it exists
let url = URL(fileURLWithPath: "\(Paths.homePath)/.config/phpmon/config.json")
if FileSystem.fileExists(url.path) {
let url = URL(fileURLWithPath: "\(container.paths.homePath)/.config/phpmon/config.json")
if container.filesystem.fileExists(url.path) {
Log.info("A custom ~/.config/phpmon/config.json file was found. Attempting to parse...")
loadCustomPreferencesFile(url)
@@ -62,9 +62,9 @@ extension Preferences {
}
func moveOutdatedConfigurationFile() async {
if FileSystem.fileExists("~/.phpmon.conf.json") && !FileSystem.fileExists("~/.config/phpmon/config.json") {
if container.filesystem.fileExists("~/.phpmon.conf.json") && !container.filesystem.fileExists("~/.config/phpmon/config.json") {
Log.info("An outdated configuration file was found. Moving it...")
await Shell.quiet("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
await container.shell.quiet("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
Log.info("The configuration file was copied successfully!")
}
}
@@ -88,7 +88,7 @@ extension Preferences {
if customPreferences.hasEnvironmentVariables() {
Log.info("Configuring the additional exports...")
if let shell = Shell as? RealShell {
if let shell = App.shared.container.shell as? RealShell {
shell.exports = customPreferences.exportAsString
}
}

View File

@@ -9,16 +9,17 @@
import Foundation
class Preferences {
// MARK: - Singleton
static var shared: Preferences!
static var shared = Preferences()
var container: Container
var customPreferences: CustomPrefs
var cachedPreferences: [PreferenceName: Any?]
public init() {
public init(_ container: Container) {
self.container = container
Preferences.handleFirstTimeLaunch()
cachedPreferences = Self.cache()
customPreferences = CustomPrefs(

View File

@@ -17,7 +17,7 @@ class GeneralPreferencesVC: GenericPreferenceVC {
let vc = NSStoryboard(name: "Main", bundle: nil)
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
_ = vc
return vc
.addView(when: true, vc.getLanguageOptionsPV())
.addView(when: true, vc.getShowPhpDoctorSuggestionsPV())
.addView(when: true, vc.getAutoRestartServicesPV())
@@ -25,12 +25,7 @@ class GeneralPreferencesVC: GenericPreferenceVC {
.addView(when: true, vc.getShortcutPV())
.addView(when: true, vc.getIntegrationsPV())
.addView(when: true, vc.getAutomaticUpdateCheckPV())
if #available(macOS 13, *) {
vc.views.append(CheckboxPreferenceView.makeLoginItemView())
}
return vc
.addView(when: true, CheckboxPreferenceView.makeLoginItemView())
}
}

View File

@@ -106,7 +106,7 @@ class Stats {
*/
public static func evaluateSponsorMessageShouldBeDisplayed() {
if Shell is TestableShell {
if App.shared.container.shell is TestableShell {
return Log.info("A fake shell is in use, skipping sponsor alert.")
}

View File

@@ -10,6 +10,10 @@ import Foundation
import NVAlert
struct Preset: Codable, Equatable {
var container: Container {
return App.shared.container
}
let name: String
let version: String?
let extensions: [String: Bool]
@@ -79,7 +83,7 @@ struct Preset: Codable, Equatable {
if self.version != nil {
if await !switchToPhpVersionIfValid() {
PresetHelper.rollbackPreset = nil
await Actions.restartPhpFpm()
await Actions(container).restartPhpFpm()
return
}
}
@@ -89,7 +93,7 @@ struct Preset: Codable, Equatable {
applyConfigurationValue(key: conf.key, value: conf.value ?? "")
}
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
Log.info("Cannot toggle extensions if no PHP version is linked.")
return
}
@@ -107,7 +111,7 @@ struct Preset: Codable, Equatable {
PresetHelper.loadRollbackPresetFromFile()
// Restart PHP FPM process (also reloads menu, which will show the preset rollback)
await Actions.restartPhpFpm()
await Actions(container).restartPhpFpm()
Task { @MainActor in
// Show the correct notification
@@ -130,12 +134,12 @@ struct Preset: Codable, Equatable {
// MARK: - Apply Functionality
private func switchToPhpVersionIfValid() async -> Bool {
if PhpEnvironments.shared.currentInstall?.version.short == self.version! {
if container.phpEnvs.currentInstall?.version.short == self.version! {
Log.info("The version we are supposed to switch to is already active.")
return true
}
if PhpEnvironments.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil {
if container.phpEnvs.availablePhpVersions.first(where: { $0 == self.version }) != nil {
await MainMenu.shared.switchToPhp(self.version!)
return true
} else {
@@ -145,7 +149,7 @@ struct Preset: Codable, Equatable {
subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
description: "alert.php_switch_unavailable.info".localized(
version!,
PhpEnvironments.shared.availablePhpVersions.joined(separator: ", ")
container.phpEnvs.availablePhpVersions.joined(separator: ", ")
)
).withPrimary(
text: "alert.php_switch_unavailable.ok".localized
@@ -156,7 +160,7 @@ struct Preset: Codable, Equatable {
}
private func applyConfigurationValue(key: String, value: String) {
guard let file = PhpEnvironments.shared.getConfigFile(forKey: key) else {
guard let file = container.phpEnvs.getConfigFile(forKey: key) else {
return
}
@@ -217,7 +221,7 @@ struct Preset: Codable, Equatable {
return nil
}
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
return nil
}
@@ -234,7 +238,7 @@ struct Preset: Codable, Equatable {
private func diffExtensions() -> [String: Bool] {
var items: [String: Bool] = [:]
guard let install = PhpEnvironments.phpInstall else {
guard let install = container.phpEnvs.phpInstall else {
fatalError("If no PHP version is linked, diffing extensions is not possible.")
}
@@ -256,7 +260,7 @@ struct Preset: Codable, Equatable {
var items: [String: String?] = [:]
for (key, _) in self.configuration {
guard let file = PhpEnvironments.shared.getConfigFile(forKey: key) else {
guard let file = container.phpEnvs.getConfigFile(forKey: key) else {
break
}
@@ -272,11 +276,11 @@ struct Preset: Codable, Equatable {
private func persistRevert() async {
let data = try! JSONEncoder().encode(self.revertSnapshot)
await Shell.quiet("mkdir -p ~/.config/phpmon")
await container.shell.quiet("mkdir -p ~/.config/phpmon")
try! String(data: data, encoding: .utf8)!
.write(
toFile: "\(Paths.homePath)/.config/phpmon/preset_revert.json",
toFile: "\(container.paths.homePath)/.config/phpmon/preset_revert.json",
atomically: true,
encoding: .utf8
)

View File

@@ -16,7 +16,7 @@ class PresetHelper {
public static func loadRollbackPresetFromFile() {
guard let revert = try? String(
contentsOfFile: "\(Paths.homePath)/.config/phpmon/preset_revert.json",
contentsOfFile: "\(App.shared.container.paths.homePath)/.config/phpmon/preset_revert.json",
encoding: .utf8
) else {
PresetHelper.rollbackPreset = nil

View File

@@ -6,14 +6,28 @@
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
///
/// This class is still WIP and pending for a future release of PHP Monitor.
///
class InstallHomebrew {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Methods
public func run() async throws {
let script = """
NONINTERACTIVE=1 /bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
"""
_ = try await Shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
_ = try await container.shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
print(string)
}, withTimeout: 60 * 10)
}

View File

@@ -7,11 +7,22 @@
//
class ZshRunCommand {
// MARK: - Container
var container: Container
init(_ container: Container) {
self.container = container
}
// MARK: - Methods
/**
Adds a given line to .zshrc, which may be needed to adjust the PATH.
*/
private func add(_ text: String) async -> Bool {
let outcome = await Shell.pipe("""
let outcome = await container.shell.pipe("""
touch ~/.zshrc && \
grep -qxF '\(text)' ~/.zshrc \
|| echo '\n\n\(text)\n' >> ~/.zshrc

View File

@@ -0,0 +1,72 @@
//
// SecurePopoverView.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/10/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import SwiftUI
struct SecurePopoverView: View {
var container: Container {
return App.shared.container
}
@State var name: String
@State var tld: String
@State var expires: Date?
var callback: () -> Void = {}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
if expires == nil {
Text("cert_popover.insecure_domain".localized("\(name).\(tld)"))
.fontWeight(.bold)
DisclaimerView(
iconName: "info.circle.fill",
message: "cert_popover.insecure_domain_text".localized,
color: Color.statusColorRed
)
} else {
Text("cert_popover.secure_domain".localized("\(name).\(tld)"))
.fontWeight(.bold)
.fixedSize(horizontal: false, vertical: true)
if let expires {
Text("cert_popover.secure_domain_traffic".localized)
.font(.subheadline)
.fixedSize(horizontal: false, vertical: true)
if expires < Date() {
DisclaimerView(
iconName: "exclamationmark.triangle.fill",
message: "cert_popover.secure_domain_expired".localized(expires.formatted()),
color: Color.statusColorOrange
)
Button("cert_popover.cta_renewal".localized) {
callback()
}.padding(.top, 10)
} else {
DisclaimerView(
iconName: "checkmark.circle.fill",
message: "cert_popover.secure_domain_expiring_later".localized(expires.formatted()),
color: Color.statusColorGreen
)
}
}
}
}.frame(width: 400, alignment: .center)
.padding(20)
.background(
Color(NSColor.windowBackgroundColor)
.padding(-80)
)
}
}
#Preview("Example") {
SecurePopoverView(
name: "hello",
tld: "test",
expires: nil
)
}

Some files were not shown because too many files have changed in this diff Show More