mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
485729f9a5 | |||
5eb36a9bdf | |||
86113d2067 | |||
70c04d4dc7 | |||
5d69b423c1 | |||
1617b57b1e | |||
33825e7b66 | |||
464b7106b2 | |||
0cf85f9958 | |||
00cf2bc360 | |||
dbb5329908 |
@ -7,6 +7,8 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
|
||||||
|
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
|
||||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
|
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
|
||||||
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
|
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
|
||||||
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
|
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
|
||||||
@ -18,6 +20,7 @@
|
|||||||
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
|
||||||
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
|
C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; };
|
||||||
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
|
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; };
|
||||||
|
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; };
|
||||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
|
||||||
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
|
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
|
||||||
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
|
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
|
||||||
@ -27,6 +30,8 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
|
||||||
|
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
|
||||||
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
@ -41,6 +46,7 @@
|
|||||||
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
|
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
|
||||||
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
|
||||||
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
|
C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = "<group>"; };
|
||||||
|
C474B00524C0E98C00066A22 /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
|
||||||
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
C476FF9722B0DD830098105B /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
||||||
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||||
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
|
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
|
||||||
@ -61,6 +67,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
C405A4CD24B9B9070062FAFA /* IAP */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */,
|
||||||
|
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */,
|
||||||
|
);
|
||||||
|
path = IAP;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
C41C1B2A22B0097F00E7CF16 = {
|
C41C1B2A22B0097F00E7CF16 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -91,6 +106,7 @@
|
|||||||
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
|
C41C1B4022B0098000E7CF16 /* phpmon.entitlements */,
|
||||||
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
|
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */,
|
||||||
C473319E2470923A009A0597 /* Localizable.strings */,
|
C473319E2470923A009A0597 /* Localizable.strings */,
|
||||||
|
C405A4CD24B9B9070062FAFA /* IAP */,
|
||||||
);
|
);
|
||||||
path = phpmon;
|
path = phpmon;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -147,6 +163,7 @@
|
|||||||
C476FF9722B0DD830098105B /* Alert.swift */,
|
C476FF9722B0DD830098105B /* Alert.swift */,
|
||||||
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
|
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
|
||||||
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
|
||||||
|
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -220,7 +237,9 @@
|
|||||||
files = (
|
files = (
|
||||||
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
|
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */,
|
||||||
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
|
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */,
|
||||||
|
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */,
|
||||||
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
|
C473319F2470923A009A0597 /* Localizable.strings in Resources */,
|
||||||
|
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -242,6 +261,7 @@
|
|||||||
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
|
||||||
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */,
|
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */,
|
||||||
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
|
||||||
|
C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */,
|
||||||
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
|
C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */,
|
||||||
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
|
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */,
|
||||||
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
|
C4EE188422D3386B00E126E5 /* Constants.swift in Sources */,
|
||||||
@ -385,7 +405,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 25;
|
||||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = phpmon/Info.plist;
|
INFOPLIST_FILE = phpmon/Info.plist;
|
||||||
@ -393,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.1;
|
MARKETING_VERSION = 2.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -409,7 +429,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 25;
|
||||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
INFOPLIST_FILE = phpmon/Info.plist;
|
INFOPLIST_FILE = phpmon/Info.plist;
|
||||||
@ -417,7 +437,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.1;
|
MARKETING_VERSION = 2.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1150"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||||
|
BuildableName = "PHP Monitor.app"
|
||||||
|
BlueprintName = "PHP Monitor"
|
||||||
|
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||||
|
BuildableName = "PHP Monitor.app"
|
||||||
|
BlueprintName = "PHP Monitor"
|
||||||
|
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||||
|
BuildableName = "PHP Monitor.app"
|
||||||
|
BlueprintName = "PHP Monitor"
|
||||||
|
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
23
README.md
23
README.md
@ -1,18 +1,20 @@
|
|||||||
# PHP Monitor
|
# PHP Monitor
|
||||||
|
|
||||||
|
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
|
||||||
|
|
||||||
PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar.
|
PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar.
|
||||||
|
|
||||||
It also gives you quick access to various useful functionality (like switching PHP versions, restarting services, accessing configuration files, and more).
|
It also gives you quick access to various useful functionality (like switching PHP versions, restarting services, accessing configuration files, and more).
|
||||||
|
|
||||||
<img src="./docs/screenshot.png" width="278px" alt="phpmon screenshot"/>
|
<img src="./docs/screenshot.png" width="362px" alt="phpmon screenshot"/>
|
||||||
|
|
||||||
For me, it comes in handy when running multiple versions of PHP with Homebrew. If you wish to be able to see at a glance which version is currently linked & active with Laravel Valet, PHP Monitor is your new best friend.
|
For me, it comes in handy when running multiple versions of PHP with Homebrew. If you wish to be able to see at a glance which version is currently linked & active with Laravel Valet, PHP Monitor is your new best friend.
|
||||||
|
|
||||||
It's also super convenient to and switch between versions.
|
It's also super convenient to switch between different versions of PHP, or to find your currently active .ini file!
|
||||||
|
|
||||||
## 🖥 System requirements
|
## 🖥 System requirements
|
||||||
|
|
||||||
* macOS 10.15 Catalina
|
* macOS 10.15 Catalina or higher (works on macOS 11 Big Sur)
|
||||||
* PHP 7.4 installed with Homebrew 2.x
|
* PHP 7.4 installed with Homebrew 2.x
|
||||||
* Laravel Valet 2.x
|
* Laravel Valet 2.x
|
||||||
|
|
||||||
@ -20,12 +22,12 @@ _Please note that future versions of PHP will not work automatically, minor chan
|
|||||||
|
|
||||||
## 🚀 How to install
|
## 🚀 How to install
|
||||||
|
|
||||||
You can install via Homebrew, or may download the latest [release](https://github.com/nicoverbruggen/phpmon/releases).
|
You can install via Homebrew, or may download the latest [release][1].
|
||||||
|
|
||||||
To install via Homebrew, run:
|
To install via Homebrew, run:
|
||||||
|
|
||||||
brew tap nicoverbruggen/homebrew-cask
|
brew tap nicoverbruggen/homebrew-cask
|
||||||
brew cask install phpmon
|
brew cask install phpmon
|
||||||
|
|
||||||
## 👨💻 Why I built this
|
## 👨💻 Why I built this
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ This means:
|
|||||||
The utility runs the following commands:
|
The utility runs the following commands:
|
||||||
|
|
||||||
- Unlink all detected PHP versions
|
- Unlink all detected PHP versions
|
||||||
- Switch to PHP 7.4 (this is done in order to ensure that Valet works, even when attempting to use PHP 5.6)
|
- Switch to PHP 7.4 (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
|
||||||
- Stop all php-fpm service instances
|
- Stop all php-fpm service instances
|
||||||
- Link the desired version of PHP
|
- Link the desired version of PHP
|
||||||
- Start the correct php-fpm service for the desired PHP version
|
- Start the correct php-fpm service for the desired PHP version
|
||||||
@ -80,8 +82,11 @@ Follow instructions as specified in the alert in order to resolve any issues.
|
|||||||
|
|
||||||
## 📝 Additional information
|
## 📝 Additional information
|
||||||
|
|
||||||
Please consult the [additional information](docs/ADDITIONAL.md) file that contains more information.
|
Please consult the [additional information][2] file that contains more information.
|
||||||
|
|
||||||
## ⭐️ Is this helpful?
|
## ⭐️ Is this helpful?
|
||||||
|
|
||||||
If this software has been useful to you, star the repository so I know that the software is being used. I did not include any tracking or analytics software, so if you encounter issues, let me know via an issue.
|
If this software has been useful to you, star the repository, so I know that the software is being used. I did not include any tracking or analytics software, so if you encounter issues, let me know via an issue.
|
||||||
|
|
||||||
|
[1]: https://github.com/nicoverbruggen/phpmon/releases
|
||||||
|
[2]: docs/ADDITIONAL.md
|
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
The following versions of PHP Monitor are supported:
|
||||||
|
|
||||||
|
| Version | Supported | Runs on macOS |
|
||||||
|
| ------- | ------------------ | ----- |
|
||||||
|
| 2.1 | ✅ | Catalina (10.15), Big Sur (11.0) |
|
||||||
|
| < 2.1 | ❌ | Catalina (10.15) |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Contact Nico Verbruggen at the email address used for the commits in the repository. Please include "PHP Monitor" in the subject.
|
@ -1,6 +1,6 @@
|
|||||||
# Release Procedure
|
# Release Procedure
|
||||||
|
|
||||||
1. Merge into `master`
|
1. Merge into `main`
|
||||||
2. Create tag
|
2. Create tag
|
||||||
3. Add changes to changelog
|
3. Add changes to changelog
|
||||||
4. Archive
|
4. Archive
|
||||||
@ -10,4 +10,4 @@
|
|||||||
8. Calculate SHA256: `openssl dgst -sha256 phpmon-2.x.zip`
|
8. Calculate SHA256: `openssl dgst -sha256 phpmon-2.x.zip`
|
||||||
9. Upload to GitHub
|
9. Upload to GitHub
|
||||||
10. Update Cask
|
10. Update Cask
|
||||||
11. Check new version can be installed via Cask
|
11. Check new version can be installed via Cask
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 615 KiB After Width: | Height: | Size: 295 KiB |
@ -7,18 +7,37 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@NSApplicationMain
|
@NSApplicationMain
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate {
|
||||||
|
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Shell singleton that keeps track of the history of all
|
||||||
|
(invoked by PHP Monitor) shell commands. It is used to
|
||||||
|
invoke all commands in this application.
|
||||||
|
*/
|
||||||
let sharedShell : Shell
|
let sharedShell : Shell
|
||||||
|
|
||||||
|
/**
|
||||||
|
The App singleton contains information about the state of
|
||||||
|
the application and global variables.
|
||||||
|
*/
|
||||||
let state : App
|
let state : App
|
||||||
|
|
||||||
|
/**
|
||||||
|
The MainMenu singleton is responsible for rendering the
|
||||||
|
menu bar item and its menu, as well as its actions.
|
||||||
|
*/
|
||||||
let menu : MainMenu
|
let menu : MainMenu
|
||||||
|
|
||||||
// MARK: - Initializer
|
// MARK: - Initializer
|
||||||
|
|
||||||
|
/**
|
||||||
|
When the application initializes, create all singletons.
|
||||||
|
*/
|
||||||
override init() {
|
override init() {
|
||||||
self.sharedShell = Shell.user
|
self.sharedShell = Shell.user
|
||||||
self.state = App.shared
|
self.state = App.shared
|
||||||
@ -27,13 +46,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
/**
|
||||||
|
When the application has finished launching, we'll want to set up
|
||||||
|
the user notification center delegate, and kickoff the menu
|
||||||
|
startup procedure.
|
||||||
|
*/
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
NSUserNotificationCenter.default.delegate = self
|
||||||
self.menu.startup()
|
self.menu.startup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ aNotification: Notification) {
|
// MARK: - NSUserNotificationCenterDelegate
|
||||||
self.state.windowController = nil
|
|
||||||
|
/**
|
||||||
|
When a notification is sent, the delegate of the notification center
|
||||||
|
is asked whether the notification should be presented or not. Since
|
||||||
|
the user can now disable notifications per application since macOS
|
||||||
|
Catalina, any and all notifications should be displayed.
|
||||||
|
*/
|
||||||
|
func userNotificationCenter(
|
||||||
|
_ center: NSUserNotificationCenter,
|
||||||
|
shouldPresent notification: NSUserNotification
|
||||||
|
) -> Bool {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,62 +10,102 @@ import Foundation
|
|||||||
|
|
||||||
class Startup {
|
class Startup {
|
||||||
|
|
||||||
public static func checkEnvironment()
|
public var failed : Bool = false
|
||||||
|
public var failureCallback = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||||
|
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||||
|
|
||||||
|
- Parameter success: Callback that is fired if the application can proceed with launch
|
||||||
|
- Parameter failure: Callback that is fired if the application must retry launch
|
||||||
|
*/
|
||||||
|
public func checkEnvironment(success: () -> Void, failure: @escaping () -> Void)
|
||||||
{
|
{
|
||||||
self.presentAlertOnMainThreadIf(
|
self.failureCallback = failure
|
||||||
|
|
||||||
|
self.performEnvironmentCheck(
|
||||||
!Shell.user.pipe("which php").contains("/usr/local/bin/php"),
|
!Shell.user.pipe("which php").contains("/usr/local/bin/php"),
|
||||||
messageText: "PHP is not correctly installed",
|
messageText: "PHP is not correctly installed",
|
||||||
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)"
|
informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)",
|
||||||
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.presentAlertOnMainThreadIf(
|
self.performEnvironmentCheck(
|
||||||
!Shell.user.pipe("ls /usr/local/opt | grep php@7.4").contains("php@7.4"),
|
!Shell.user.pipe("ls /usr/local/opt | grep php@7.4").contains("php@7.4"),
|
||||||
messageText: "PHP 7.4 is not correctly installed",
|
messageText: "PHP 7.4 is not correctly installed",
|
||||||
informativeText: "PHP 7.4 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php@7.4` in order for PHP Monitor to detect this installation."
|
informativeText: "PHP 7.4 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php@7.4` in order for PHP Monitor to detect this installation.",
|
||||||
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.presentAlertOnMainThreadIf(
|
self.performEnvironmentCheck(
|
||||||
!Shell.user.pipe("which valet").contains("/usr/local/bin/valet"),
|
!Shell.user.pipe("which valet").contains("/usr/local/bin/valet"),
|
||||||
messageText: "Laravel Valet is not correctly installed",
|
messageText: "Laravel Valet is not correctly installed",
|
||||||
informativeText: "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue."
|
informativeText: "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue.",
|
||||||
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.presentAlertOnMainThreadIf(
|
self.performEnvironmentCheck(
|
||||||
!Shell.user.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
|
!Shell.user.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"),
|
||||||
messageText: "Brew has not been added to sudoers.d",
|
messageText: "Brew has not been added to sudoers.d",
|
||||||
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue."
|
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.",
|
||||||
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
self.presentAlertOnMainThreadIf(
|
self.performEnvironmentCheck(
|
||||||
!Shell.user.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
|
!Shell.user.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"),
|
||||||
messageText: "Valet has not been added to sudoers.d",
|
messageText: "Valet has not been added to sudoers.d",
|
||||||
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue."
|
informativeText: "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue.",
|
||||||
|
breaking: true
|
||||||
)
|
)
|
||||||
|
|
||||||
let services = Shell.user.pipe("brew services list | grep php")
|
let services = Shell.user.pipe("brew services list | grep php")
|
||||||
self.presentAlertOnMainThreadIf(
|
self.performEnvironmentCheck(
|
||||||
(services.countInstances(of: "started") > 1),
|
(services.countInstances(of: "started") > 1),
|
||||||
messageText: "Multiple PHP services are active",
|
messageText: "Multiple PHP services are active",
|
||||||
informativeText: "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes." +
|
informativeText: "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes." +
|
||||||
"\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar." +
|
"\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar." +
|
||||||
"\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies)." +
|
"\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies)." +
|
||||||
"\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue." +
|
"\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue." +
|
||||||
"\n\nFor more information about this issue, please see the README.md file in the repository on GitHub."
|
"\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.",
|
||||||
|
breaking: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!self.failed) {
|
||||||
|
success()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func presentAlertOnMainThreadIf(
|
/**
|
||||||
|
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
|
||||||
|
*
|
||||||
|
* - Parameter condition: Condition to check for
|
||||||
|
* - Parameter messageText: Short description of what is wrong
|
||||||
|
* - Parameter informativeText: Expanded description of the environment check that failed
|
||||||
|
* - Parameter breaking: If the application should terminate afterwards
|
||||||
|
*/
|
||||||
|
private func performEnvironmentCheck(
|
||||||
_ condition: Bool,
|
_ condition: Bool,
|
||||||
messageText: String,
|
messageText: String,
|
||||||
informativeText: String
|
informativeText: String,
|
||||||
|
breaking: Bool
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (condition) {
|
if (condition) {
|
||||||
|
// Only breaking issues will cause the notification
|
||||||
|
if (breaking) {
|
||||||
|
self.failed = true
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
Alert.present(
|
// Present the information to the user
|
||||||
|
_ = Alert.present(
|
||||||
messageText: messageText,
|
messageText: messageText,
|
||||||
informativeText: informativeText
|
informativeText: informativeText
|
||||||
)
|
)
|
||||||
|
// Only breaking issues will throw the extra retry modal
|
||||||
|
if (breaking) {
|
||||||
|
self.failureCallback()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,16 @@ class Alert {
|
|||||||
public static func present(
|
public static func present(
|
||||||
messageText: String,
|
messageText: String,
|
||||||
informativeText: String,
|
informativeText: String,
|
||||||
buttonTitle: String = "OK"
|
buttonTitle: String = "OK",
|
||||||
) {
|
secondButtonTitle: String = ""
|
||||||
|
) -> Bool {
|
||||||
let alert = NSAlert.init()
|
let alert = NSAlert.init()
|
||||||
alert.messageText = messageText
|
alert.messageText = messageText
|
||||||
alert.informativeText = informativeText
|
alert.informativeText = informativeText
|
||||||
alert.addButton(withTitle: buttonTitle)
|
alert.addButton(withTitle: buttonTitle)
|
||||||
alert.runModal()
|
if (!secondButtonTitle.isEmpty) {
|
||||||
|
alert.addButton(withTitle: secondButtonTitle)
|
||||||
|
}
|
||||||
|
return alert.runModal() == .alertFirstButtonReturn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
phpmon/Classes/Helpers/LocalNotification.swift
Normal file
19
phpmon/Classes/Helpers/LocalNotification.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// LocalNotification.swift
|
||||||
|
// PHP Monitor
|
||||||
|
//
|
||||||
|
// Created by Nico Verbruggen on 16/07/2020.
|
||||||
|
// Copyright © 2020 Nico Verbruggen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class LocalNotification {
|
||||||
|
public static func send(title: String, subtitle: String)
|
||||||
|
{
|
||||||
|
let notification = NSUserNotification()
|
||||||
|
notification.title = title
|
||||||
|
notification.subtitle = subtitle
|
||||||
|
NSUserNotificationCenter.default.deliver(notification)
|
||||||
|
}
|
||||||
|
}
|
@ -12,17 +12,18 @@ class StatusMenu : NSMenu {
|
|||||||
|
|
||||||
public func addPhpVersionMenuItems()
|
public func addPhpVersionMenuItems()
|
||||||
{
|
{
|
||||||
var string = "We are not sure what version of PHP you are running."
|
var string = "mi_unsure".localized
|
||||||
if (App.shared.currentVersion != nil) {
|
if (App.shared.currentVersion != nil) {
|
||||||
if (!App.shared.currentVersion!.error) {
|
if (!App.shared.currentVersion!.error) {
|
||||||
string = "You are running PHP \(App.shared.currentVersion!.long)"
|
// in case the php version loaded without issue
|
||||||
|
string = "\("mi_php_version".localized) \(App.shared.currentVersion!.long)"
|
||||||
self.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: string, action: nil, keyEquivalent: ""))
|
||||||
} else {
|
} else {
|
||||||
// in case of an error show the error message
|
// in case of an error show the error message
|
||||||
self.addItem(NSMenuItem(title: "Oof! It appears your PHP installation is broken...", action: nil, keyEquivalent: ""))
|
["mi_php_broken_1", "mi_php_broken_2",
|
||||||
self.addItem(NSMenuItem(title: "Try running `php -v` in your terminal.", action: nil, keyEquivalent: ""))
|
"mi_php_broken_3", "mi_php_broken_4"].forEach { (message) in
|
||||||
self.addItem(NSMenuItem(title: "You could also try switching to another version.", action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: message.localized, action: nil, keyEquivalent: ""))
|
||||||
self.addItem(NSMenuItem(title: "Running `brew reinstall php` (or for the equivalent version) might help.", action: nil, keyEquivalent: ""))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,30 +35,30 @@ class StatusMenu : NSMenu {
|
|||||||
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
|
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
|
||||||
let version = App.shared.availablePhpVersions[index]
|
let version = App.shared.availablePhpVersions[index]
|
||||||
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
let action = #selector(MainMenu.switchToPhpVersion(sender:))
|
||||||
let menuItem = NSMenuItem(title: "Switch to PHP \(version)", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
|
let menuItem = NSMenuItem(title: "\("mi_php_switch".localized) \(version)", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
|
||||||
menuItem.tag = index
|
menuItem.tag = index
|
||||||
shortcutKey = shortcutKey + 1
|
shortcutKey = shortcutKey + 1
|
||||||
self.addItem(menuItem)
|
self.addItem(menuItem)
|
||||||
}
|
}
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
self.addItem(NSMenuItem(title: "Active Services", action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_active_services".localized, action: nil, keyEquivalent: ""))
|
||||||
self.addItem(NSMenuItem(title: "Restart php-fpm service", action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "f"))
|
self.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized, action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "f"))
|
||||||
self.addItem(NSMenuItem(title: "Restart nginx service", action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
self.addItem(NSMenuItem(title: "mi_restart_nginx".localized, action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
||||||
self.addItem(NSMenuItem(title: "Force load latest PHP version", action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_force_load_latest".localized, action: #selector(MainMenu.forceRestartLatestPhp), keyEquivalent: ""))
|
||||||
}
|
}
|
||||||
if (App.shared.busy) {
|
if (App.shared.busy) {
|
||||||
self.addItem(NSMenuItem(title: "PHP Monitor is busy...", action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_busy".localized, action: nil, keyEquivalent: ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addPhpConfigurationMenuItems()
|
public func addPhpConfigurationMenuItems()
|
||||||
{
|
{
|
||||||
if (App.shared.currentVersion != nil) {
|
if (App.shared.currentVersion != nil) {
|
||||||
self.addItem(NSMenuItem(title: "Configuration", action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_configuration".localized, action: nil, keyEquivalent: ""))
|
||||||
self.addItem(NSMenuItem(title: "Valet configuration (.config/valet)", action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
|
||||||
self.addItem(NSMenuItem(title: "PHP configuration file (php.ini)", action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
|
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
|
||||||
self.addItem(NSMenuItem.separator())
|
self.addItem(NSMenuItem.separator())
|
||||||
self.addItem(NSMenuItem(title: "Enabled Extensions", action: nil, keyEquivalent: ""))
|
self.addItem(NSMenuItem(title: "mi_enabled_extensions".localized, action: nil, keyEquivalent: ""))
|
||||||
self.addXdebugMenuItem()
|
self.addXdebugMenuItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,7 +69,7 @@ class StatusMenu : NSMenu {
|
|||||||
if (xdebugFound) {
|
if (xdebugFound) {
|
||||||
let xdebugOn = App.shared.currentVersion!.xdebugEnabled
|
let xdebugOn = App.shared.currentVersion!.xdebugEnabled
|
||||||
let xdebugToggleMenuItem = NSMenuItem(
|
let xdebugToggleMenuItem = NSMenuItem(
|
||||||
title: "Xdebug",
|
title: "mi_xdebug".localized,
|
||||||
action: #selector(MainMenu.toggleXdebug), keyEquivalent: "x"
|
action: #selector(MainMenu.toggleXdebug), keyEquivalent: "x"
|
||||||
)
|
)
|
||||||
if (xdebugOn) {
|
if (xdebugOn) {
|
||||||
@ -77,7 +78,7 @@ class StatusMenu : NSMenu {
|
|||||||
self.addItem(xdebugToggleMenuItem)
|
self.addItem(xdebugToggleMenuItem)
|
||||||
} else {
|
} else {
|
||||||
let disabledItem = NSMenuItem(
|
let disabledItem = NSMenuItem(
|
||||||
title: "xdebug.so missing",
|
title: "mi_xdebug_missing".localized,
|
||||||
action: nil, keyEquivalent: "x"
|
action: nil, keyEquivalent: "x"
|
||||||
)
|
)
|
||||||
disabledItem.isEnabled = false
|
disabledItem.isEnabled = false
|
||||||
|
47
phpmon/IAP/InternetAccessPolicy.plist
Normal file
47
phpmon/IAP/InternetAccessPolicy.plist
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>ApplicationDescription</key>
|
||||||
|
<string>PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files.</string>
|
||||||
|
<key>DeveloperName</key>
|
||||||
|
<string>Nico Verbruggen</string>
|
||||||
|
<key>Website</key>
|
||||||
|
<string>https://github.com/nicoverbruggen/phpmon</string>
|
||||||
|
<key>Connections</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>IsIncoming</key>
|
||||||
|
<false/>
|
||||||
|
<key>Host</key>
|
||||||
|
<string>registry.npmjs.org</string>
|
||||||
|
<key>NetworkProtocol</key>
|
||||||
|
<string>TCP</string>
|
||||||
|
<key>Port</key>
|
||||||
|
<string>80, 443</string>
|
||||||
|
<key>Relevance</key>
|
||||||
|
<string>Essential</string>
|
||||||
|
<key>Purpose</key>
|
||||||
|
<string>PHP Monitor directly invokes Homebrew which contacts the NPM Registry.</string>
|
||||||
|
<key>DenyConsequences</key>
|
||||||
|
<string>If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail.</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>IsIncoming</key>
|
||||||
|
<false/>
|
||||||
|
<key>Host</key>
|
||||||
|
<string>github.com, api.github.com</string>
|
||||||
|
<key>NetworkProtocol</key>
|
||||||
|
<string>TCP</string>
|
||||||
|
<key>Port</key>
|
||||||
|
<string>443</string>
|
||||||
|
<key>Relevance</key>
|
||||||
|
<string>Essential</string>
|
||||||
|
<key>Purpose</key>
|
||||||
|
<string>PHP Monitor directly invokes Homebrew which contacts GitHub.</string>
|
||||||
|
<key>DenyConsequences</key>
|
||||||
|
<string>If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail.</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
2
phpmon/IAP/InternetAccessPolicy.strings
Normal file
2
phpmon/IAP/InternetAccessPolicy.strings
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Top-level, general application description:
|
||||||
|
"ApplicationDescription" = "PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files.";
|
@ -6,6 +6,38 @@
|
|||||||
Copyright © 2020 Nico Verbruggen. All rights reserved.
|
Copyright © 2020 Nico Verbruggen. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// MENU ITEMS (MI)
|
||||||
|
|
||||||
|
"mi_busy" = "PHP Monitor is busy...";
|
||||||
|
"mi_unsure" = "We are not sure what version of PHP you are running.";
|
||||||
|
"mi_php_version" = "You are running PHP";
|
||||||
|
"mi_php_switch" = "Switch to PHP";
|
||||||
|
"mi_php_broken_1" = "Oof! It appears your PHP installation is broken...";
|
||||||
|
"mi_php_broken_2" = "Try running `php -v` in your terminal.";
|
||||||
|
"mi_php_broken_3" = "You could also try switching to another version.";
|
||||||
|
"mi_php_broken_4" = "Running `brew reinstall php` (or for the equivalent version) might help.";
|
||||||
|
|
||||||
|
"mi_active_services" = "Active Services";
|
||||||
|
"mi_restart_php_fpm" = "Restart php-fpm service";
|
||||||
|
"mi_restart_nginx" = "Restart nginx service";
|
||||||
|
"mi_force_load_latest" = "Force load latest PHP version";
|
||||||
|
|
||||||
|
"mi_configuration" = "Configuration";
|
||||||
|
"mi_valet_config" = "Valet configuration (.config/valet)";
|
||||||
|
"mi_php_config" = "PHP Configuration file (php.ini)";
|
||||||
|
"mi_enabled_extensions" = "Enabled Extensions";
|
||||||
|
|
||||||
|
"mi_xdebug" = "Xdebug";
|
||||||
|
"mi_xdebug_missing" = "xdebug.so missing";
|
||||||
|
|
||||||
|
"mi_quit" = "Quit PHP Monitor";
|
||||||
|
"mi_about" = "About PHP Monitor";
|
||||||
|
|
||||||
|
// NOTIFICATIONS
|
||||||
|
|
||||||
|
"notification.version_changed_title" = "PHP %@ now active";
|
||||||
|
"notification.version_changed_desc" = "PHP Monitor has finished the switch to PHP %@.";
|
||||||
|
|
||||||
// ALERTS
|
// ALERTS
|
||||||
|
|
||||||
// Force Reload Started
|
// Force Reload Started
|
||||||
@ -15,3 +47,9 @@
|
|||||||
// Force Reload Done
|
// Force Reload Done
|
||||||
"alert.force_reload_done.title" = "PHP has been force reloaded";
|
"alert.force_reload_done.title" = "PHP has been force reloaded";
|
||||||
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP.";
|
"alert.force_reload_done.info" = "All appropriate services have been restarted, and the latest version of PHP is now active. You can now try switching to another version of PHP.";
|
||||||
|
|
||||||
|
// PHP Monitor Cannot Start
|
||||||
|
"alert.cannot_start.title" = "PHP Monitor cannot start";
|
||||||
|
"alert.cannot_start.info" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks.";
|
||||||
|
"alert.cannot_start.close" = "Close";
|
||||||
|
"alert.cannot_start.retry" = "Retry";
|
||||||
|
@ -32,9 +32,4 @@ class App {
|
|||||||
*/
|
*/
|
||||||
var timer: Timer?
|
var timer: Timer?
|
||||||
|
|
||||||
/**
|
|
||||||
The window controller that will show the log.
|
|
||||||
*/
|
|
||||||
var windowController: NSWindowController? = nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,13 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
|
|
||||||
/// Immediately executes a command.
|
/**
|
||||||
|
Immediately executes a command.
|
||||||
|
|
||||||
|
- Parameter path: The path of the command or program to invoke.
|
||||||
|
- Parameter arguments: A list of arguments that are passed on.
|
||||||
|
*/
|
||||||
public static func execute(path: String, arguments: [String]) -> String {
|
public static func execute(path: String, arguments: [String]) -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = path
|
task.launchPath = path
|
||||||
|
@ -12,31 +12,71 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
|
|
||||||
static let shared = MainMenu()
|
static let shared = MainMenu()
|
||||||
|
|
||||||
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
/**
|
||||||
|
The status bar item with variable length.
|
||||||
|
*/
|
||||||
|
let statusItem = NSStatusBar.system.statusItem(
|
||||||
|
withLength: NSStatusItem.variableLength
|
||||||
|
)
|
||||||
|
|
||||||
// MARK: - UI related
|
// MARK: - UI related
|
||||||
|
|
||||||
|
/**
|
||||||
|
Kick off the startup of the rendering of the main menu.
|
||||||
|
*/
|
||||||
public func startup() {
|
public func startup() {
|
||||||
// Start with the icon
|
// Start with the icon
|
||||||
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!)
|
||||||
// Perform environment boot checks
|
// Perform environment boot checks
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
Startup.checkEnvironment()
|
Startup().checkEnvironment(success: {
|
||||||
App.shared.availablePhpVersions = Actions.detectPhpVersions()
|
self.onEnvironmentPass()
|
||||||
self.updatePhpVersionInStatusBar()
|
}, failure: {
|
||||||
// Schedule a request to fetch the PHP version every 60 seconds
|
self.onEnvironmentFail()
|
||||||
DispatchQueue.main.async {
|
})
|
||||||
App.shared.timer = Timer.scheduledTimer(
|
}
|
||||||
timeInterval: 60,
|
}
|
||||||
target: self,
|
|
||||||
selector: #selector(self.updatePhpVersionInStatusBar),
|
/**
|
||||||
userInfo: nil,
|
When the environment is all clear and the app can run, let's go.
|
||||||
repeats: true
|
*/
|
||||||
)
|
private func onEnvironmentPass() {
|
||||||
|
App.shared.availablePhpVersions = Actions.detectPhpVersions()
|
||||||
|
self.updatePhpVersionInStatusBar()
|
||||||
|
// Schedule a request to fetch the PHP version every 60 seconds
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
App.shared.timer = Timer.scheduledTimer(
|
||||||
|
timeInterval: 60,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(self.updatePhpVersionInStatusBar),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
When the environment is not OK, present an alert to inform the user.
|
||||||
|
*/
|
||||||
|
private func onEnvironmentFail() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let close = Alert.present(
|
||||||
|
messageText: "alert.cannot_start.title".localized,
|
||||||
|
informativeText: "alert.cannot_start.info".localized,
|
||||||
|
buttonTitle: "alert.cannot_start.close".localized,
|
||||||
|
secondButtonTitle: "alert.cannot_start.retry".localized
|
||||||
|
)
|
||||||
|
if (!close) {
|
||||||
|
self.startup()
|
||||||
|
} else {
|
||||||
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Update the menu's contents, based on what's going on.
|
||||||
|
*/
|
||||||
public func update() {
|
public func update() {
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
||||||
// Create a new menu
|
// Create a new menu
|
||||||
@ -55,8 +95,8 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
// Add about & quit menu items
|
// Add about & quit menu items
|
||||||
menu.addItem(NSMenuItem(title: "About PHP Monitor", action: #selector(self.openAbout), keyEquivalent: ""))
|
menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(self.openAbout), keyEquivalent: ""))
|
||||||
menu.addItem(NSMenuItem(title: "Quit PHP Monitor", action: #selector(self.terminateApp), keyEquivalent: "q"))
|
menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(self.terminateApp), keyEquivalent: "q"))
|
||||||
|
|
||||||
// Make sure every item can be interacted with
|
// Make sure every item can be interacted with
|
||||||
menu.items.forEach({ (item) in
|
menu.items.forEach({ (item) in
|
||||||
@ -70,10 +110,19 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sets the status bar image based on a version string.
|
||||||
|
*/
|
||||||
func setStatusBarImage(version: String) {
|
func setStatusBarImage(version: String) {
|
||||||
self.setStatusBar(image: MenuBarImageGenerator.textToImage(text: version))
|
self.setStatusBar(
|
||||||
|
image: MenuBarImageGenerator.textToImage(text: version)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sets the status bar image, based on the provided NSImage.
|
||||||
|
The image will be used as a template image.
|
||||||
|
*/
|
||||||
func setStatusBar(image: NSImage) {
|
func setStatusBar(image: NSImage) {
|
||||||
if let button = statusItem.button {
|
if let button = statusItem.button {
|
||||||
image.isTemplate = true
|
image.isTemplate = true
|
||||||
@ -83,6 +132,14 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
|
|
||||||
// MARK: - Nicer callbacks
|
// MARK: - Nicer callbacks
|
||||||
|
|
||||||
|
/**
|
||||||
|
Executes a specific callback and fires the completion callback,
|
||||||
|
while updating the UI as required. As long as the completion callback
|
||||||
|
does not fire, the app is presumed to be busy and the UI reflects this.
|
||||||
|
|
||||||
|
- Parameter execute: Escaping callback of the work that needs to happen.
|
||||||
|
- Parameter completion: Callback that is fired when the work is done.
|
||||||
|
*/
|
||||||
private func waitAndExecute(_ execute: @escaping () -> Void, _ completion: @escaping () -> Void = {})
|
private func waitAndExecute(_ execute: @escaping () -> Void, _ completion: @escaping () -> Void = {})
|
||||||
{
|
{
|
||||||
App.shared.busy = true
|
App.shared.busy = true
|
||||||
@ -99,7 +156,7 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - User Interface
|
||||||
|
|
||||||
@objc func updatePhpVersionInStatusBar() {
|
@objc func updatePhpVersionInStatusBar() {
|
||||||
App.shared.currentVersion = PhpVersion()
|
App.shared.currentVersion = PhpVersion()
|
||||||
@ -121,6 +178,8 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
@objc public func restartPhpFpm() {
|
@objc public func restartPhpFpm() {
|
||||||
self.waitAndExecute({
|
self.waitAndExecute({
|
||||||
Actions.restartPhpFpm()
|
Actions.restartPhpFpm()
|
||||||
@ -140,12 +199,14 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc public func forceRestartLatestPhp() {
|
@objc public func forceRestartLatestPhp() {
|
||||||
Alert.present(
|
// Tell the user the switch is about to occur
|
||||||
|
_ = Alert.present(
|
||||||
messageText: "alert.force_reload.title".localized,
|
messageText: "alert.force_reload.title".localized,
|
||||||
informativeText: "alert.force_reload.info".localized
|
informativeText: "alert.force_reload.info".localized
|
||||||
)
|
)
|
||||||
|
// Start switching
|
||||||
self.waitAndExecute({ Actions.fixMyPhp() }, {
|
self.waitAndExecute({ Actions.fixMyPhp() }, {
|
||||||
Alert.present(
|
_ = Alert.present(
|
||||||
messageText: "alert.force_reload_done.title".localized,
|
messageText: "alert.force_reload_done.title".localized,
|
||||||
informativeText: "alert.force_reload_done.info".localized
|
informativeText: "alert.force_reload_done.info".localized
|
||||||
)
|
)
|
||||||
@ -169,6 +230,7 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
|
|
||||||
@objc public func switchToPhpVersion(sender: AnyObject) {
|
@objc public func switchToPhpVersion(sender: AnyObject) {
|
||||||
self.setBusyImage()
|
self.setBusyImage()
|
||||||
|
// TODO: A wise man once said: using tags is not good. Fix this.
|
||||||
let index = sender.tag!
|
let index = sender.tag!
|
||||||
let version = App.shared.availablePhpVersions[index]
|
let version = App.shared.availablePhpVersions[index]
|
||||||
App.shared.busy = true
|
App.shared.busy = true
|
||||||
@ -188,6 +250,11 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.updatePhpVersionInStatusBar()
|
self.updatePhpVersionInStatusBar()
|
||||||
self.update()
|
self.update()
|
||||||
|
// Send a notification that the switch has been completed
|
||||||
|
LocalNotification.send(
|
||||||
|
title: String(format: "notification.version_changed_title".localized, version),
|
||||||
|
subtitle: String(format: "notification.version_changed_desc".localized, version)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,11 +267,4 @@ class MainMenu: NSObject, NSWindowDelegate {
|
|||||||
@objc public func terminateApp() {
|
@objc public func terminateApp() {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Cleanup when window closes
|
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
|
||||||
App.shared.windowController = nil
|
|
||||||
Shell.user.delegate = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,38 +8,30 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
protocol ShellDelegate: class {
|
|
||||||
func didCompleteCommand(historyItem: ShellHistoryItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShellHistoryItem {
|
|
||||||
var command: String
|
|
||||||
var output: String
|
|
||||||
var date: Date
|
|
||||||
|
|
||||||
init(command: String, output: String) {
|
|
||||||
self.command = command
|
|
||||||
self.output = output
|
|
||||||
self.date = Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Shell {
|
class Shell {
|
||||||
|
|
||||||
// Singleton to access a user shell (with --login)
|
/**
|
||||||
|
Singleton to access a user shell (with --login)
|
||||||
|
*/
|
||||||
static let user = Shell()
|
static let user = Shell()
|
||||||
|
|
||||||
var history : [ShellHistoryItem] = []
|
/**
|
||||||
|
Runs a shell command without using the output.
|
||||||
var delegate : ShellDelegate?
|
Uses the default shell.
|
||||||
|
|
||||||
/// Runs a shell command without using the description.
|
- Parameter command: The command to run
|
||||||
|
*/
|
||||||
public func run(_ command: String) {
|
public func run(_ command: String) {
|
||||||
// Equivalent of piping to /dev/null; don't do anything with the string
|
// Equivalent of piping to /dev/null; don't do anything with the string
|
||||||
_ = self.pipe(command)
|
_ = self.pipe(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs a shell command and returns the output.
|
/**
|
||||||
|
Runs a shell command and returns the output.
|
||||||
|
|
||||||
|
- Parameter command: The command to run
|
||||||
|
- Parameter shell: Path to the shell to invoke
|
||||||
|
*/
|
||||||
public func pipe(_ command: String, shell: String = "/bin/sh") -> String {
|
public func pipe(_ command: String, shell: String = "/bin/sh") -> String {
|
||||||
let task = Process()
|
let task = Process()
|
||||||
task.launchPath = shell
|
task.launchPath = shell
|
||||||
@ -51,17 +43,10 @@ class Shell {
|
|||||||
|
|
||||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
|
||||||
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
|
let output: String = NSString(
|
||||||
|
data: data,
|
||||||
let historyItem = ShellHistoryItem(command: command, output: output)
|
encoding: String.Encoding.utf8.rawValue
|
||||||
|
)! as String
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [unowned self] in
|
|
||||||
self.history.append(historyItem)
|
|
||||||
// Keep the last 100 items
|
|
||||||
self.history = self.history.suffix(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate?.didCompleteCommand(historyItem: historyItem)
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user