mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 03:50:08 +02:00
♻️ Reworked helper scripts
- Add 'Welcome Tour' to First Aid menu - Updated 'Welcome Tour' - Helpers are now always written to ~/.config/phpmon/bin - Updated helpers (now symlinked) - Updated checks for when to symlink helpers
This commit is contained in:
@ -1677,7 +1677,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 920;
|
||||
CURRENT_PROJECT_VERSION = 950;
|
||||
DEBUG = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@ -1704,7 +1704,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 920;
|
||||
CURRENT_PROJECT_VERSION = 950;
|
||||
DEBUG = NO;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
|
@ -19,9 +19,12 @@ public class Paths {
|
||||
|
||||
private var userName: String
|
||||
|
||||
private var PATH: String
|
||||
|
||||
init() {
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
|
||||
PATH = String(Shell.pipe("echo $PATH")).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
@ -57,6 +60,10 @@ public class Paths {
|
||||
return shared.userName
|
||||
}
|
||||
|
||||
public static var PATH: String {
|
||||
return shared.PATH
|
||||
}
|
||||
|
||||
public static var cellarPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Cellar"
|
||||
}
|
||||
|
@ -33,6 +33,15 @@ class Filesystem {
|
||||
return exists && !isDirectory.boolValue
|
||||
}
|
||||
|
||||
public static func fileIsSymlink(_ path: String) -> Bool {
|
||||
do {
|
||||
let attribs = try FileManager.default.attributesOfItem(atPath: path)
|
||||
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if a directory exists at the provided path.
|
||||
*/
|
||||
|
@ -16,8 +16,18 @@ class PhpHelper {
|
||||
// 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 = "/Users/\(Paths.whoami)/.config/phpmon/bin/pm\(dotless)"
|
||||
|
||||
// Check if the ~/.config/phpmon/bin directory is in the PATH
|
||||
let inPath = Paths.PATH.contains("/Users/\(Paths.whoami)/.config/phpmon/bin")
|
||||
|
||||
// Check if we can create symlinks (`/usr/local/bin` must be writable)
|
||||
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
|
||||
|
||||
do {
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
Shell.run("mkdir -p ~/.config/phpmon/bin")
|
||||
|
||||
if FileManager.default.fileExists(atPath: destination) {
|
||||
let contents = try String(contentsOfFile: destination)
|
||||
if !contents.contains(keyPhrase) {
|
||||
@ -52,10 +62,40 @@ class PhpHelper {
|
||||
|
||||
// Make sure the file is executable
|
||||
Shell.run("chmod +x \(destination)")
|
||||
|
||||
// Create a symlink if the folder is not in the PATH
|
||||
if !inPath {
|
||||
// First, check if we can create symlinks at all
|
||||
if !canWriteSymlinks {
|
||||
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the symlink
|
||||
self.createSymlink(dotless)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
|
||||
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
|
||||
}
|
||||
}
|
||||
|
||||
private static func createSymlink(_ dotless: String) {
|
||||
let source = "/Users/\(Paths.whoami)/.config/phpmon/bin/pm\(dotless)"
|
||||
let destination = "/usr/local/bin/pm\(dotless)"
|
||||
|
||||
if !Filesystem.fileExists(destination) {
|
||||
Log.info("Creating new symlink: \(destination)")
|
||||
Shell.run("ln -s \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
if !Filesystem.fileIsSymlink(destination) {
|
||||
Log.info("Overwriting existing file with new symlink: \(destination)")
|
||||
Shell.run("ln -fs \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("Symlink in \(destination) already exists, OK.")
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,9 @@ extension MainMenu {
|
||||
let installation = PhpEnv.phpInstall
|
||||
installation.notifyAboutBrokenPhpFpm()
|
||||
|
||||
// Check for other problems
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Set up the config watchers on launch (updated automatically when switching)
|
||||
Log.info("Setting up watchers...")
|
||||
App.shared.handlePhpConfigWatcher()
|
||||
@ -85,15 +88,11 @@ extension MainMenu {
|
||||
// Start the background refresh timer
|
||||
startSharedTimer()
|
||||
|
||||
// Check warnings
|
||||
WarningManager.shared.evaluateWarnings()
|
||||
|
||||
// Update the stats
|
||||
Stats.incrementSuccessfulLaunchCount()
|
||||
Stats.evaluateSponsorMessageShouldBeDisplayed()
|
||||
|
||||
// Present first launch screen if needed
|
||||
#warning("You should definitely tweak this view again")
|
||||
if Stats.successfulLaunchCount == 0 && !isRunningSwiftUIPreview {
|
||||
Log.info("Should present the first launch screen!")
|
||||
DispatchQueue.main.async {
|
||||
|
@ -126,6 +126,16 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
||||
ServicesManager.shared.loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Shows the Welcome Tour screen, again.
|
||||
Did this need a comment? No, probably not.
|
||||
*/
|
||||
@objc func showWelcomeTour() {
|
||||
DispatchQueue.main.async {
|
||||
OnboardingWindowController.show()
|
||||
}
|
||||
}
|
||||
|
||||
/** Reloads the menu in the background, using `asyncExecution`. */
|
||||
@objc func reloadPhpMonitorMenuInBackground() {
|
||||
asyncExecution({
|
||||
|
@ -199,6 +199,10 @@ extension StatusMenu {
|
||||
let servicesMenu = NSMenu()
|
||||
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_first_aid".localized))
|
||||
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_view_onboarding".localized,
|
||||
action: #selector(MainMenu.showWelcomeTour), keyEquivalent: ""))
|
||||
|
||||
let fixMyValetMenuItem = NSMenuItem(
|
||||
title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion),
|
||||
action: #selector(MainMenu.fixMyValet), keyEquivalent: ""
|
||||
@ -216,22 +220,15 @@ extension StatusMenu {
|
||||
servicesMenu.addItem(NSMenuItem.separator())
|
||||
servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized))
|
||||
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_dnsmasq".localized,
|
||||
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_php_fpm".localized,
|
||||
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_nginx".localized,
|
||||
action: #selector(MainMenu.restartNginx), keyEquivalent: "n")
|
||||
)
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_restart_valet_services".localized,
|
||||
action: #selector(MainMenu.restartValetServices), keyEquivalent: "s")
|
||||
)
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_dnsmasq".localized,
|
||||
action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d"))
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_php_fpm".localized,
|
||||
action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p"))
|
||||
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_nginx".localized,
|
||||
action: #selector(MainMenu.restartNginx), keyEquivalent: "n"))
|
||||
servicesMenu.addItem(NSMenuItem(title: "mi_restart_valet_services".localized,
|
||||
action: #selector(MainMenu.restartValetServices), keyEquivalent: "s"))
|
||||
servicesMenu.addItem(
|
||||
NSMenuItem(title: "mi_stop_valet_services".localized,
|
||||
action: #selector(MainMenu.stopValetServices), keyEquivalent: "s"),
|
||||
|
@ -25,18 +25,17 @@ struct OnboardingTextItem: View {
|
||||
Text(title.localizedForSwiftUI)
|
||||
.font(.system(size: 14))
|
||||
.lineLimit(3)
|
||||
HStack {
|
||||
Text(description.localizedForSwiftUI)
|
||||
.foregroundColor(Color.secondary)
|
||||
.font(.system(size: 13))
|
||||
.lineLimit(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
Text(description.localizedForSwiftUI)
|
||||
.foregroundColor(Color.secondary)
|
||||
.font(.system(size: 13))
|
||||
.lineLimit(6)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray.opacity(0.3), lineWidth: 1))
|
||||
.overlay(RoundedRectangle(cornerRadius: 5)
|
||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1))
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,9 +56,13 @@ struct OnboardingView: View {
|
||||
.padding(.bottom, 5)
|
||||
Text("onboarding.explore".localized)
|
||||
.padding(.bottom)
|
||||
.padding(.trailing)
|
||||
}
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.padding(.leading)
|
||||
.padding(.trailing)
|
||||
|
||||
VStack {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
OnboardingTextItem(
|
||||
@ -67,6 +70,11 @@ struct OnboardingView: View {
|
||||
title: "onboarding.tour.menu_bar.title",
|
||||
description: "onboarding.tour.menu_bar"
|
||||
)
|
||||
OnboardingTextItem(
|
||||
icon: "checkmark.circle.fill",
|
||||
title: "onboarding.tour.services.title",
|
||||
description: "onboarding.tour.services"
|
||||
)
|
||||
OnboardingTextItem(
|
||||
icon: "list.bullet.circle.fill",
|
||||
title: "onboarding.tour.domains.title",
|
||||
@ -79,6 +87,7 @@ struct OnboardingView: View {
|
||||
)
|
||||
}
|
||||
}.padding()
|
||||
|
||||
VStack(spacing: 20) {
|
||||
HStack {
|
||||
Image(systemName: "questionmark.circle.fill")
|
||||
|
@ -32,11 +32,16 @@ class WarningManager {
|
||||
),
|
||||
Warning(
|
||||
command: {
|
||||
!Paths.PATH.contains("/Users/\(Paths.whoami)/.config/phpmon/bin") &&
|
||||
!FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
|
||||
},
|
||||
name: "`/usr/local/bin` not writable",
|
||||
name: "Helpers cannot be symlinked and not in PATH",
|
||||
title: "warnings.helper_permissions.title",
|
||||
paragraphs: ["warnings.helper_permissions.description", "warnings.helper_permissions.unavailable"],
|
||||
paragraphs: [
|
||||
"warnings.helper_permissions.description",
|
||||
"warnings.helper_permissions.unavailable",
|
||||
"warnings.helper_permissions.symlink"
|
||||
],
|
||||
url: "https://github.com/nicoverbruggen/phpmon/wiki/PHP-Monitor-helper-binaries"
|
||||
)
|
||||
]
|
||||
@ -59,7 +64,7 @@ class WarningManager {
|
||||
|
||||
for check in self.evaluations {
|
||||
if await check.applies() {
|
||||
Log.info("[WARNING] \(check.name)")
|
||||
Log.info("[DOCTOR] \(check.name) (!)")
|
||||
self.warnings.append(check)
|
||||
continue
|
||||
}
|
||||
|
@ -74,6 +74,8 @@
|
||||
"mi_no_presets" = "No presets available.";
|
||||
"mi_set_up_presets" = "Learn more about presets...";
|
||||
|
||||
"mi_view_onboarding" = "Show Welcome Tour...";
|
||||
|
||||
"mi_xdebug_available_modes" = "Available Modes";
|
||||
"mi_xdebug_actions" = "Actions";
|
||||
"mi_xdebug_disable_all" = "Disable All Modes";
|
||||
@ -518,7 +520,8 @@ If you are seeing this message but are confused why this folder has gone missing
|
||||
|
||||
"warnings.helper_permissions.title" = "PHP Monitor’s helpers are currently unavailable.";
|
||||
"warnings.helper_permissions.description" = "PHP Monitor comes with various helper binaries. Using these binaries allows you to easily invoke a specific version of PHP without switching the linked PHP version.";
|
||||
"warnings.helper_permissions.unavailable" = "However, these helpers are currently *unavailable* because PHP Monitor could not create the required symlinks (alternatively, you could add PHP Monitor's helper directory to your `PATH` variable to make this warning go away as well).";
|
||||
"warnings.helper_permissions.unavailable" = "However, these helpers are potentially *unavailable* because PHP Monitor cannot currently create or update the required symlinks.";
|
||||
"warnings.helper_permissions.symlink" = "If you do not wish to make `/usr/local/bin` writable, you can add PHP Monitor's helper directory to your `PATH` variable to make this warning go away. (Click on ”Learn More” to find out how to fix this issue.)";
|
||||
|
||||
"warnings.arm_compatibility.title" = "You are running PHP Monitor using Rosetta on Apple Silicon, which means your PHP environment is also running via Rosetta.";
|
||||
"warnings.arm_compatibility.description" = "You appear to be running an ARM-compatible version of macOS, but you are currently running PHP Monitor using Rosetta. While this will work correctly, it is recommended that you use the native version of Homebrew.";
|
||||
@ -527,13 +530,15 @@ If you are seeing this message but are confused why this folder has gone missing
|
||||
|
||||
"onboarding.title" = "Welcome Tour";
|
||||
"onboarding.welcome" = "Welcome to PHP Monitor!";
|
||||
"onboarding.explore" = "Learn more about some of the features that PHP Monitor has to offer.";
|
||||
"onboarding.explore" = "Learn more about some of the features that PHP Monitor has to offer. You can find a more comprehensive list of features on GitHub.";
|
||||
"onboarding.tour.menu_bar.title" = "Get Started";
|
||||
"onboarding.tour.menu_bar" = "PHP Monitor lives in your menu bar. From here, you can switch the globally linked PHP version, start or stop services, locate config files, and more.";
|
||||
"onboarding.tour.faq_hint" = "I recommend that you check out the [README](https://github.com/nicoverbruggen/phpmon/blob/main/README.md) on GitHub: it contains a comprehensive FAQ with various tips and common questions and answers.";
|
||||
"onboarding.tour.domains.title" = "Domains";
|
||||
"onboarding.tour.domains" = "By opening the Domains window via the Menu Bar item, you can view which domains are linked and parked.";
|
||||
"onboarding.tour.isolation.title" = "Isolation";
|
||||
"onboarding.tour.isolation" = "If you have Valet 3 installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain!";
|
||||
"onboarding.tour.once" = "You will only see the Welcome Tour once. You can re-open the Welcome Tour later via the menu bar icon.";
|
||||
"onboarding.tour.services.title" = "Manage Services";
|
||||
"onboarding.tour.services" = "Once you click on the menu bar item, you can see at a glance based on the checkmarks or crosses if all of the Homebrew services are up and running. You can also click on a service to quickly toggle it. You can also add your own!";
|
||||
"onboarding.tour.domains.title" = "Manage Domains";
|
||||
"onboarding.tour.domains" = "By opening the Domains window via the menu bar item, you can view which domains are linked and parked, as well as active nginx proxies.";
|
||||
"onboarding.tour.isolation.title" = "Isolate Domains";
|
||||
"onboarding.tour.isolation" = "If you have Valet 3 installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain, and that domain only!";
|
||||
"onboarding.tour.once" = "You will only see the Welcome Tour once. You can re-open the Welcome Tour later via the menu bar icon (under First Aid & Services).";
|
||||
"onboarding.tour.close" = "Close Tour";
|
||||
|
Reference in New Issue
Block a user