1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-06 19:40:08 +02:00

Load extension info for all PHP versions

In order to make this possible, I've added a new `sync()` method to the
Shellable protocol, which now should allow us to run shell commands
synchronously. Back to basics, as this was how *all* commands were
run in legacy versions of PHP Monitor.

The advantage here is that there is now a choice. Previously, you'd
have to use the `system()` helper that I added.

Usage of that helper is now discouraged, as is the synchronous
shell method, but it may be useful for some methods where waiting
for the outcome of the output is required.

(Important: the `system()` helper is still required when determining
what the preferred terminal is during the initialization of the
`Paths` class.)
This commit is contained in:
2023-11-26 21:26:48 +01:00
parent 23cf575026
commit e8306289ce
15 changed files with 155 additions and 71 deletions

View File

@ -1213,6 +1213,8 @@
033D459C2B0D506B00070080 /* PHP Versions */ = {
isa = PBXGroup;
children = (
C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */,
C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */,
);
path = "PHP Versions";
sourceTree = "<group>";
@ -1667,6 +1669,15 @@
path = phpmon;
sourceTree = "<group>";
};
C4513F8D2B13CD08001AD760 /* PHP Extensions */ = {
isa = PBXGroup;
children = (
033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */,
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */,
);
path = "PHP Extensions";
sourceTree = "<group>";
};
C456A0D02AA6175D0080144F /* Config */ = {
isa = PBXGroup;
children = (
@ -1961,12 +1972,9 @@
C4B79EBA29CA38D100A483EE /* Commands */ = {
isa = PBXGroup;
children = (
C4513F8D2B13CD08001AD760 /* PHP Extensions */,
033D459C2B0D506B00070080 /* PHP Versions */,
C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */,
C43BCD4329FBEF40001547BC /* ModifyPhpVersionCommand.swift */,
C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */,
033D45972B0D4EC600070080 /* InstallPhpExtensionCommand.swift */,
033D459D2B0D513900070080 /* RemovePhpExtensionCommand.swift */,
);
path = Commands;
sourceTree = "<group>";

View File

@ -112,7 +112,7 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
PHP extensions that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
</details>

View File

@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks

View File

@ -10,7 +10,6 @@ import Foundation
/**
Run a simple blocking Shell command on the user's own system.
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
*/
public func system(_ command: String) -> String {
let task = Process()

View File

@ -62,13 +62,6 @@ class ActivePhpInstallation {
return
}
// Load extension information
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
iniFiles.append(file)
}
// Get configuration values
limits = Limits(
memory_limit: getByteCount(key: "memory_limit"),
@ -76,15 +69,10 @@ class ActivePhpInstallation {
post_max_size: getByteCount(key: "post_max_size")
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(
path: Paths.php,
arguments: ["-r", "echo php_ini_scanned_files();"],
trimNewlines: false
)
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
let paths = ActiveShell.shared
.sync("\(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

View File

@ -172,7 +172,7 @@ class PhpEnvironments {
let phpAliasInstall = PhpInstallation(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.missingBinary {
if !phpAliasInstall.isMissingBinary {
supportedVersions.insert(phpAlias)
}
}

View File

@ -12,21 +12,36 @@ class PhpInstallation {
var versionNumber: VersionNumber
var missingBinary: Bool = false
var iniFiles: [PhpConfigurationFile] = []
var isMissingBinary: Bool = false
var isHealthy: Bool = true
var extensions: [PhpExtension] {
return self.iniFiles.flatMap({ $0.extensions })
}
/**
In order to determine details about a PHP installation,
well simply run `php-config --version` in the relevant directory.
*/
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
versionNumber = VersionNumber.make(from: version)!
self.versionNumber = VersionNumber.make(from: version)!
determineVersion(phpConfigExecutablePath, phpExecutablePath)
determineHealth(phpExecutablePath)
determineIniFiles(phpExecutablePath)
// Find all enabled extensions
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
}
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
@ -36,13 +51,15 @@ class PhpInstallation {
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! VersionNumber.parse(longVersionString)
versionNumber = try! VersionNumber.parse(longVersionString)
} else {
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
missingBinary = true
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
isMissingBinary = true
}
}
private func determineHealth(_ phpExecutablePath: String) {
if FileSystem.fileExists(phpExecutablePath) {
let testCommand = Command.execute(
path: phpExecutablePath,
@ -59,4 +76,18 @@ class PhpInstallation {
}
}
}
private func determineIniFiles(_ phpExecutablePath: String) {
let paths = ActiveShell.shared
.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) {
iniFiles.append(file)
}
}
}
}

View File

@ -86,14 +86,37 @@ class RealShell: ShellProtocol {
// MARK: - Shellable Protocol
func sync(_ command: String) -> ShellOutput {
let task = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
sleep(3)
}
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
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)
}
return .out(stdOut, stdErr)
}
func pipe(_ command: String) async -> ShellOutput {
let task = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
// Seriously slow down how long it takes for the shell to return output
// (in order to debug or identify async issues)
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
Log.info("[SLOW SHELL] \(command)")
await delay(seconds: 3.0)
@ -104,20 +127,20 @@ class RealShell: ShellProtocol {
task.launch()
task.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 {
var args = task.arguments ?? []
let last = "\"" + (args.popLast() ?? "") + "\""
var log = """
log(task: task, stdOut: stdOut, stdErr: stdErr)
}
return .out(stdOut, stdErr)
}
private func log(task: Process, stdOut: String, stdErr: String) {
var args = task.arguments ?? []
let last = "\"" + (args.popLast() ?? "") + "\""
var log = """
<~~~~~~~~~~~~~~~~~~~~~~~
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
@ -126,22 +149,19 @@ class RealShell: ShellProtocol {
\(stdOut)
"""
if !stdErr.isEmpty {
log.append("""
if !stdErr.isEmpty {
log.append("""
[ERR]:
\(stdErr)
""")
}
}
log.append("""
log.append("""
~~~~~~~~~~~~~~~~~~~~~~~~>
""")
Log.info(log)
}
return .out(stdOut, stdErr)
Log.info(log)
}
func quiet(_ command: String) async {

View File

@ -14,6 +14,16 @@ protocol ShellProtocol {
*/
var PATH: String { get }
/**
Run a command synchronously. Use with caution.
Common usage:
```
let output = Shell.sync("php -v")
```
*/
func sync(_ command: String) -> ShellOutput
/**
Run a command asynchronously.
Returns the most relevant output (prefers error output if it exists).

View File

@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
var expectations: [String: BatchFakeShellOutput] = [:]
func sync(_ command: String) -> ShellOutput {
// This assertion will only fire during test builds
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
guard let expectation = expectations[command] else {
return .err("No Expected Output")
}
return expectation.syncOutput()
}
func quiet(_ command: String) async {
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
}
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
return output
}
/**
Outputs the fake shell output as expected, but does this synchronously.
*/
public func syncOutput(
ignoreDelay: Bool = false
) -> ShellOutput {
let output = ShellOutput.empty()
for item in items {
if !ignoreDelay {
sleep(UInt32(item.delay))
}
if item.stream == .stdErr {
output.err += item.output
} else if item.stream == .stdOut {
output.out += item.output
}
}
return output
}
/**
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
*/

View File

@ -142,7 +142,7 @@ class Startup {
return await Shell.pipe("\(Paths.binPath)/php -v").err
.contains("Library not loaded")
},
name: "`no dyld issue detected",
name: "no `dyld` issue (`Library not loaded`) detected",
titleText: "startup.errors.dyld_library.title".localized,
subtitleText: "startup.errors.dyld_library.subtitle".localized(
Paths.optPath

View File

@ -21,8 +21,15 @@ class ModifyPhpVersionCommand: BrewCommand {
/**
You can pass in which PHP versions need to be upgraded and which ones need to be installed.
The process will be executed in two steps: first upgrades, then installations.
Upgrades come first because... well, otherwise installations may very well break.
Each version that is installed will need to be checked afterwards (if it is OK).
Each version that is installed will need to be checked afterwards. Installing a
newer formula may break other PHP installations, which in turn need to be fixed.
- Important: If any PHP formula is a major upgrade that causes a PHP "version" to be
uninstalled, this is remedied by running `upgradeMainPhpFormula()`. This process
will ensure that the upgrade is applied, but the also that old version is
re-installed and linked again.
*/
public init(
title: String,
@ -44,6 +51,8 @@ class ModifyPhpVersionCommand: BrewCommand {
description: "PHP Monitor is preparing Homebrew..."
))
// Determine if a formula will become unavailable
// This is the case when `php` will be bumped to a new version
let unavailable = upgrading.first(where: { formula in
formula.unavailableAfterUpgrade
})
@ -71,10 +80,7 @@ class ModifyPhpVersionCommand: BrewCommand {
await self.completedOperations(onProgress)
}
private func upgradeMainPhpFormula(
_ unavailable: BrewPhpFormula,
_ onProgress: @escaping (BrewCommandProgress) -> Void
) async throws {
private func upgradeMainPhpFormula(_ unavailable: BrewPhpFormula, _ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
// Determine which version was previously available (that will become unavailable)
guard let short = try? VersionNumber
.parse(unavailable.installedVersion!).short else {
@ -92,18 +98,6 @@ class ModifyPhpVersionCommand: BrewCommand {
try await run(command, onProgress)
}
private func checkPhpTap(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
if !BrewDiagnostics.installedTaps.contains("shivammathur/php") {
let command = "brew tap shivammathur/php"
try await run(command, onProgress)
}
if !BrewDiagnostics.installedTaps.contains("shivammathur/extensions") {
let command = "brew tap shivammathur/extensions"
try await run(command, onProgress)
}
}
private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws {
// If no upgrades are needed, early exit
if self.upgrading.isEmpty {