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:
@@ -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):
|
||||
|
||||
|
||||
@@ -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" */;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2600"
|
||||
LastUpgradeVersion = "2610"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
12
README.md
12
README.md
@@ -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, that’s 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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
23
phpmon/Common/Extensions/NSImageExtension.swift
Normal file
23
phpmon/Common/Extensions/NSImageExtension.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
//
|
||||
|
||||
protocol ApiProtocol {
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
we’ll 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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).")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,5 @@
|
||||
import Foundation
|
||||
|
||||
protocol CreatedFromFile {
|
||||
|
||||
static func from(filePath: String) -> Self?
|
||||
|
||||
static func from(_ container: Container, filePath: String) -> Self?
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
return (Process(), output)
|
||||
}
|
||||
|
||||
func reload() {
|
||||
// does nothing
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeShellOutput: Codable {
|
||||
|
||||
62
phpmon/Container/Container+Fake.swift
Normal file
62
phpmon/Container/Container+Fake.swift
Normal 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
|
||||
}
|
||||
}
|
||||
15
phpmon/Container/Container+Real.swift
Normal file
15
phpmon/Container/Container+Real.swift
Normal 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
|
||||
}
|
||||
}
|
||||
46
phpmon/Container/Container.swift
Normal file
46
phpmon/Container/Container.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
150
phpmon/Domain/App/CrashReporter.swift
Normal file
150
phpmon/Domain/App/CrashReporter.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`!")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
])
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
72
phpmon/Domain/SwiftUI/Domains/SecurePopoverView.swift
Normal file
72
phpmon/Domain/SwiftUI/Domains/SecurePopoverView.swift
Normal 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
Reference in New Issue
Block a user