1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-25 21:50:08 +01:00

♻️ Reworked command history

This commit is contained in:
2026-02-24 15:10:27 +01:00
parent 6f5501573a
commit f85d51290b
12 changed files with 166 additions and 192 deletions

View File

@@ -189,6 +189,8 @@
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 */; };
03D0F9752F4DE6FE00613D1E /* TestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DEC28F764A00026AC4E /* TestableCommand.swift */; };
03D0F9762F4DE6FE00613D1E /* TestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4928DB966A007ACC74 /* TestableShell.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 */; };
@@ -487,8 +489,8 @@
C46EBC4528DB95F0007ACC74 /* ShellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4328DB95F0007ACC74 /* ShellProtocol.swift */; };
C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4628DB9644007ACC74 /* RealShell.swift */; };
C46EBC4828DB9644007ACC74 /* RealShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4628DB9644007ACC74 /* RealShell.swift */; };
C46EBC4A28DB966A007ACC74 /* TestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4928DB966A007ACC74 /* TestableShell.swift */; };
C46EBC4B28DB966A007ACC74 /* TestableShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4928DB966A007ACC74 /* TestableShell.swift */; };
C46EBC4C28DB95F0007ACC74 /* TrackedShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */; };
C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; };
C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; };
C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; };
@@ -547,9 +549,13 @@
C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; };
C471E7F728F9BACB0021E251 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C471E7F928F9BA600021E251 /* TrackedShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */; };
C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; };
C471E7FA28F9BA8F0021E251 /* TrackedShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */; };
C471E7FA28F9BACB0021E251 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; };
C471E7FB28F9BAA30021E251 /* TrackedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DED28F764A00026AC4E /* TrackedCommand.swift */; };
C471E7FB28F9BACE0021E251 /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C471E7FC28F9BAA30021E251 /* TrackedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DED28F764A00026AC4E /* TrackedCommand.swift */; };
C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; };
C471E7FD28F9BACE0021E251 /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; };
C471E7FE28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; };
@@ -968,8 +974,9 @@
C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E4404527C56F4700D225E1 /* ValetSite.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 */; };
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DEC28F764A00026AC4E /* TestableCommand.swift */; };
C4E49DF028F764A00026AC4E /* TrackedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DED28F764A00026AC4E /* TrackedCommand.swift */; };
C4E49DF128F764A00026AC4E /* TrackedCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DED28F764A00026AC4E /* TrackedCommand.swift */; };
C4E684092AF26B830023ED25 /* BrewTapFormulae.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */; };
C4E6840A2AF26B830023ED25 /* BrewTapFormulae.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */; };
C4E6840B2AF26B830023ED25 /* BrewTapFormulae.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */; };
@@ -1267,6 +1274,7 @@
C46EBC4328DB95F0007ACC74 /* ShellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellProtocol.swift; sourceTree = "<group>"; };
C46EBC4628DB9644007ACC74 /* RealShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealShell.swift; sourceTree = "<group>"; };
C46EBC4928DB966A007ACC74 /* TestableShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableShell.swift; sourceTree = "<group>"; };
C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedShell.swift; sourceTree = "<group>"; };
C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; };
C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFile.swift; sourceTree = "<group>"; };
C46FA98A2822F08F00D78807 /* PhpConfigurationFileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFileTest.swift; sourceTree = "<group>"; };
@@ -1363,6 +1371,7 @@
C4E4404527C56F4700D225E1 /* ValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetSite.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>"; };
C4E49DED28F764A00026AC4E /* TrackedCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackedCommand.swift; sourceTree = "<group>"; };
C4E684082AF26B830023ED25 /* BrewTapFormulae.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewTapFormulae.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
@@ -1573,6 +1582,16 @@
path = Container;
sourceTree = "<group>";
};
03D0F9772F4DE7E800613D1E /* Monitoring */ = {
isa = PBXGroup;
children = (
031A80DB2F4CF1690016F7DD /* CommandTracker.swift */,
C46EBC4A28DB966A007ACC74 /* TrackedShell.swift */,
C4E49DED28F764A00026AC4E /* TrackedCommand.swift */,
);
path = Monitoring;
sourceTree = "<group>";
};
03D53E902E8AE089001B1671 /* Testables */ = {
isa = PBXGroup;
children = (
@@ -2285,6 +2304,7 @@
C4F787A628EF811000790735 /* Shell */,
C4C8900128F0E27900CE5E97 /* Filesystem */,
C4E49DE528F763E20026AC4E /* Command */,
03D0F9772F4DE7E800613D1E /* Monitoring */,
C40C7F2127721F7300DDDCDC /* Core */,
54B20EDF263AA22C00D3250E /* PHP */,
C44CCD4327AFE93300CE40E5 /* Errors */,
@@ -2452,7 +2472,6 @@
C4B5853D2770FE3900DA4FBE /* RealCommand.swift */,
C4E49DEC28F764A00026AC4E /* TestableCommand.swift */,
C4E49DE928F7643D0026AC4E /* CommandProtocol.swift */,
031A80DB2F4CF1690016F7DD /* CommandTracker.swift */,
);
path = Command;
sourceTree = "<group>";
@@ -2921,7 +2940,7 @@
C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */,
C45B914E295608E300F4EC78 /* ValetServicesManager.swift in Sources */,
C4D5576429C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */,
C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */,
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */,
C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
037F44222EDB92EC002EBF75 /* HomebrewWatchManager.swift in Sources */,
C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */,
@@ -2969,7 +2988,7 @@
C40175B82903108900763A68 /* ValetInteractor.swift in Sources */,
C4ACE9E129F84EDD00110766 /* PhpGuard.swift in Sources */,
C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */,
C46EBC4A28DB966A007ACC74 /* TestableShell.swift in Sources */,
C46EBC4B28DB966A007ACC74 /* TestableShell.swift in Sources */,
C44C198D276E3A1C0072762D /* TerminalProgressWindowController.swift in Sources */,
0379C49F2ED71D050035D7EA /* Startup+Launch.swift in Sources */,
54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */,
@@ -2986,6 +3005,7 @@
C40934A2298EEB2C00D25014 /* CaskFile.swift in Sources */,
C495F5AF28A42E080087F70A /* EnvironmentCheck.swift in Sources */,
C46EBC4428DB95F0007ACC74 /* ShellProtocol.swift in Sources */,
C46EBC4C28DB95F0007ACC74 /* TrackedShell.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */,
54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */,
@@ -3104,6 +3124,7 @@
039C29182E8AA314007F5FAB /* TestableWebApi.swift in Sources */,
C47015022C46D6910069AAE7 /* NVAlertExtension.swift in Sources */,
C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */,
C4E49DF028F764A00026AC4E /* TrackedCommand.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3295,6 +3316,7 @@
031E2B6B2B1525A7007C29E1 /* BrewPhpExtension.swift in Sources */,
C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */,
C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */,
C471E7FA28F9BA8F0021E251 /* TrackedShell.swift in Sources */,
C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */,
037F441D2EDB9195002EBF75 /* ConfigWatchManager.swift in Sources */,
C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */,
@@ -3310,6 +3332,7 @@
C42106682AFA9FF400DF3732 /* PhpVersionManagerView+Actions.swift in Sources */,
C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */,
C471E7DE28F9BAA30021E251 /* CommandProtocol.swift in Sources */,
C471E7FB28F9BAA30021E251 /* TrackedCommand.swift in Sources */,
C471E82928F9BB330021E251 /* Valet.swift in Sources */,
C471E80728F9BAD40021E251 /* PhpConfigurationFile.swift in Sources */,
0329A9A32E92A69000A62A12 /* WarningManager+Evaluations.swift in Sources */,
@@ -3554,10 +3577,12 @@
C471E7ED28F9BAC30021E251 /* Process.swift in Sources */,
C471E81128F9BAE80021E251 /* NSMenuItemExtension.swift in Sources */,
C471E7CC28F9BA5B0021E251 /* TestableShell.swift in Sources */,
C471E7F928F9BA600021E251 /* TrackedShell.swift in Sources */,
C4E6840C2AF26B830023ED25 /* BrewTapFormulae.swift in Sources */,
C471E80C28F9BAE80021E251 /* NSWindowExtension.swift in Sources */,
C471E7CA28F9BA480021E251 /* TestableFileSystem.swift in Sources */,
C471E7DD28F9BAA30021E251 /* CommandProtocol.swift in Sources */,
C471E7FC28F9BAA30021E251 /* TrackedCommand.swift in Sources */,
C471E7D128F9BA630021E251 /* RealFileSystem.swift in Sources */,
033D459B2B0D4EC600070080 /* InstallPhpExtensionCommand.swift in Sources */,
C471E82B28F9BB340021E251 /* Valet.swift in Sources */,
@@ -3674,6 +3699,7 @@
C4D5576529C77CC5001A44CD /* PhpVersionManagerWindowController.swift in Sources */,
C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */,
C4E49DEB28F7643D0026AC4E /* CommandProtocol.swift in Sources */,
C4E49DF128F764A00026AC4E /* TrackedCommand.swift in Sources */,
C4F2E4382752F08D0020E974 /* BrewDiagnostics.swift in Sources */,
C485707428BF454E00539B36 /* ServicesView.swift in Sources */,
03B947DE2F43692500B6F899 /* TestURL.swift in Sources */,
@@ -3731,7 +3757,7 @@
C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */,
03B675EC2EBA30D800EE04A9 /* NSImageExtension.swift in Sources */,
C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */,
C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */,
03D0F9752F4DE6FE00613D1E /* TestableCommand.swift in Sources */,
C4611E612AEAD3110010BE24 /* ByteLimitView.swift in Sources */,
C40175B92903108900763A68 /* ValetInteractor.swift in Sources */,
03ACC6452ECCAB190070D4CD /* RealWebApiTest.swift in Sources */,
@@ -3851,7 +3877,7 @@
C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */,
C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */,
C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */,
C46EBC4B28DB966A007ACC74 /* TestableShell.swift in Sources */,
03D0F9762F4DE6FE00613D1E /* TestableShell.swift in Sources */,
C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */,
C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */,
C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */,

View File

@@ -9,21 +9,6 @@
import Foundation
protocol CommandProtocol {
/**
Immediately executes a command, without tracking.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
- Parameter withStandardError: Outputs standard error output to the same string output as well.
*/
func executeRaw(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String
/**
Immediately executes a command.
@@ -55,20 +40,6 @@ protocol CommandProtocol {
}
extension CommandProtocol {
func execute(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String {
executeRaw(
path: path,
arguments: arguments,
trimNewlines: trimNewlines,
withStandardError: withStandardError
)
}
func execute(
path: String,
arguments: [String],
@@ -83,24 +54,3 @@ extension CommandProtocol {
}
}
protocol TrackedCommandProtocol: CommandProtocol, CommandTrackingProvider {}
extension TrackedCommandProtocol {
func execute(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String {
let commandDescription = "\(path) \(arguments.joined(separator: " "))"
return trackedCommand(description: commandDescription) {
executeRaw(
path: path,
arguments: arguments,
trimNewlines: trimNewlines,
withStandardError: withStandardError
)
}
}
}

View File

@@ -7,14 +7,10 @@
import Cocoa
public class RealCommand: TrackedCommandProtocol {
let commandTracker: CommandTracker
public class RealCommand: CommandProtocol {
init() {}
init(commandTracker: CommandTracker) {
self.commandTracker = commandTracker
}
public func executeRaw(
public func execute(
path: String,
arguments: [String],
trimNewlines: Bool,

View File

@@ -15,7 +15,7 @@ class TestableCommand: CommandProtocol {
var commands: [String: String]
public func executeRaw(
public func execute(
path: String,
arguments: [String],
trimNewlines: Bool,

View File

@@ -13,9 +13,9 @@ class CommandTracker: ObservableObject {
nonisolated init() {}
private let maxStoredCommands = 200
@Published private(set) var commands: [TrackedCommand] = []
@Published private(set) var commands: [LoggedCommand] = []
var activeCommands: [TrackedCommand] {
var activeCommands: [LoggedCommand] {
commands.filter { !$0.isCompleted }
}
@@ -25,7 +25,7 @@ class CommandTracker: ObservableObject {
@discardableResult
func track(_ command: String, id: UUID = UUID()) -> UUID {
let tracked = TrackedCommand(id: id, command: command, startedAt: Date())
let tracked = LoggedCommand(id: id, command: command, startedAt: Date())
commands.append(tracked)
if commands.count > maxStoredCommands {
commands.removeFirst(commands.count - maxStoredCommands)
@@ -54,9 +54,9 @@ class CommandTracker: ObservableObject {
}
}
// MARK: - Tracked Command
// MARK: - Logged Command
struct TrackedCommand: Identifiable {
struct LoggedCommand: Identifiable {
let id: UUID
let command: String
let startedAt: Date
@@ -72,7 +72,7 @@ struct TrackedCommand: Identifiable {
if duration < 0.001 {
let micros = max(1, Int(duration * 1_000_000))
return "Completed in \(micros) μs"
return "Completed in \(micros) us"
}
let ms = max(1, Int(duration * 1000))
@@ -83,30 +83,3 @@ struct TrackedCommand: Identifiable {
return "Running for \(ms) ms"
}
}
// MARK: - Command Tracking
protocol CommandTrackingProvider {
var commandTracker: CommandTracker { get }
}
extension CommandTrackingProvider {
func trackedCommand<T>(description: String, _ work: () -> T) -> T {
let trackingId = commandTracker.trackFromAnyThread(description)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return work()
}
func trackedCommandAsync<T>(
description: String,
_ work: () async throws -> T
) async rethrows -> T {
let trackingId = commandTracker.trackFromAnyThread(description)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return try await work()
}
}

View File

@@ -0,0 +1,38 @@
//
// TrackedCommand.swift
// PHP Monitor
//
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
final class TrackedCommand: CommandProtocol {
private let command: CommandProtocol
private let commandTracker: CommandTracker
init(command: CommandProtocol, commandTracker: CommandTracker) {
self.command = command
self.commandTracker = commandTracker
}
func execute(
path: String,
arguments: [String],
trimNewlines: Bool,
withStandardError: Bool
) -> String {
let commandDescription = "\(path) \(arguments.joined(separator: " "))"
let trackingId = commandTracker.trackFromAnyThread(commandDescription)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return command.execute(
path: path,
arguments: arguments,
trimNewlines: trimNewlines,
withStandardError: withStandardError
)
}
}

View File

@@ -0,0 +1,65 @@
//
// TrackedShell.swift
// PHP Monitor
//
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
final class TrackedShell: ShellProtocol {
private let shell: ShellProtocol
private let commandTracker: CommandTracker
init(shell: ShellProtocol, commandTracker: CommandTracker) {
self.shell = shell
self.commandTracker = commandTracker
}
var PATH: String {
shell.PATH
}
func sync(_ command: String) -> ShellOutput {
let trackingId = commandTracker.trackFromAnyThread(command)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return shell.sync(command)
}
@discardableResult
func pipe(_ command: String) async -> ShellOutput {
let trackingId = commandTracker.trackFromAnyThread(command)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return await shell.pipe(command)
}
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let trackingId = commandTracker.trackFromAnyThread(command)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return await shell.pipe(command, timeout: timeout)
}
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) {
let trackingId = commandTracker.trackFromAnyThread(command)
defer {
commandTracker.completeFromAnyThread(trackingId)
}
return try await shell.attach(command, didReceiveOutput: didReceiveOutput, withTimeout: timeout)
}
func reloadEnvPath() {
shell.reloadEnvPath()
}
}

View File

@@ -9,15 +9,12 @@
import Foundation
@preconcurrency import Dispatch
class RealShell: TrackedShellProtocol, @unchecked Sendable {
init(binPath: String, commandTracker: CommandTracker) {
class RealShell: ShellProtocol, @unchecked Sendable {
init(binPath: String) {
self.binPath = binPath
self.commandTracker = commandTracker
self._PATH = RealShell.getPath()
self._exports = [:]
}
let commandTracker: CommandTracker
private(set) var binPath: String
/**
@@ -157,7 +154,8 @@ class RealShell: TrackedShellProtocol, @unchecked Sendable {
// MARK: - Shellable Protocol
func syncRaw(_ command: String) -> ShellOutput {
@discardableResult
func sync(_ command: String) -> ShellOutput {
let process = getShellProcess(for: command)
let outputPipe = Pipe()
@@ -188,7 +186,7 @@ class RealShell: TrackedShellProtocol, @unchecked Sendable {
}
@discardableResult
func pipeRaw(_ command: String) async -> ShellOutput {
func pipe(_ command: String) async -> ShellOutput {
let process = getShellProcess(for: command)
let outputPipe = Pipe()
@@ -224,7 +222,7 @@ class RealShell: TrackedShellProtocol, @unchecked Sendable {
}
@discardableResult
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput {
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let process = getShellProcess(for: command)
let outputPipe = Pipe()
@@ -290,7 +288,7 @@ class RealShell: TrackedShellProtocol, @unchecked Sendable {
}
@discardableResult
func attachRaw(
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval = 5.0
@@ -382,6 +380,7 @@ class RealShell: TrackedShellProtocol, @unchecked Sendable {
})
}
func reloadEnvPath() {
// Instead of replacing the entire shell instance, we simply re-fetch the PATH
self.PATH = RealShell.getPath()

View File

@@ -15,7 +15,7 @@ protocol ShellProtocol {
var PATH: String { get }
/**
Run a command synchronously without tracking. Use with caution!
Run a command synchronously. Use with caution!
Common usage:
```
@@ -24,9 +24,6 @@ protocol ShellProtocol {
@return The shell output. If the command times out, returns empty output.
*/
@discardableResult
func syncRaw(_ command: String) -> ShellOutput
/**
Run a command synchronously. Use with caution!
@@ -50,9 +47,6 @@ protocol ShellProtocol {
@return The shell output. If the command times out, returns empty output.
*/
@discardableResult
func pipeRaw(_ command: String) async -> ShellOutput
@discardableResult
func pipe(_ command: String) async -> ShellOutput
@@ -65,9 +59,6 @@ protocol ShellProtocol {
@return The shell output. If the command times out, returns empty output.
*/
@discardableResult
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
@@ -82,13 +73,6 @@ protocol ShellProtocol {
@return A tuple, containing the `Process` and `ShellOutput` objects.
*/
@discardableResult
func attachRaw(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput)
@discardableResult
func attach(
_ command: String,
@@ -102,42 +86,6 @@ protocol ShellProtocol {
func reloadEnvPath()
}
protocol TrackedShellProtocol: ShellProtocol, CommandTrackingProvider {}
extension TrackedShellProtocol {
@discardableResult
func sync(_ command: String) -> ShellOutput {
trackedCommand(description: command) {
syncRaw(command)
}
}
@discardableResult
func pipe(_ command: String) async -> ShellOutput {
await trackedCommandAsync(description: command) {
await pipeRaw(command)
}
}
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
await trackedCommandAsync(description: command) {
await pipeRaw(command, timeout: timeout)
}
}
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) {
try await trackedCommandAsync(description: command) {
try await attachRaw(command, didReceiveOutput: didReceiveOutput, withTimeout: timeout)
}
}
}
enum ShellStream: Codable {
case stdOut, stdErr, stdIn
}

View File

@@ -20,7 +20,7 @@ public class TestableShell: ShellProtocol {
var expectations: [String: BatchFakeShellOutput] = [:]
@discardableResult
func syncRaw(_ command: String) -> ShellOutput {
func sync(_ command: String) -> ShellOutput {
// This assertion will only fire during test builds
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
@@ -32,18 +32,18 @@ public class TestableShell: ShellProtocol {
}
@discardableResult
func pipeRaw(_ command: String) async -> ShellOutput {
await pipeRaw(command, timeout: 60)
func pipe(_ command: String) async -> ShellOutput {
await pipe(command, timeout: 60)
}
@discardableResult
func pipeRaw(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let (_, output) = try! await self.attachRaw(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
let (_, output) = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
return output
}
@discardableResult
func attachRaw(
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
@@ -73,29 +73,6 @@ public class TestableShell: ShellProtocol {
// does nothing
}
@discardableResult
func sync(_ command: String) -> ShellOutput {
syncRaw(command)
}
@discardableResult
func pipe(_ command: String) async -> ShellOutput {
await pipeRaw(command)
}
@discardableResult
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
await pipeRaw(command, timeout: timeout)
}
@discardableResult
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) {
try await attachRaw(command, didReceiveOutput: didReceiveOutput, withTimeout: timeout)
}
}
struct FakeShellOutput: Codable {

View File

@@ -69,8 +69,10 @@ class Container: @unchecked Sendable {
self.filesystem = RealFileSystem(container: self)
self.paths = Paths(container: self)
self.commandTracker = CommandTracker()
self.shell = RealShell(binPath: paths.binPath, commandTracker: commandTracker)
self.command = RealCommand(commandTracker: commandTracker)
let realShell = RealShell(binPath: paths.binPath)
self.shell = TrackedShell(shell: realShell, commandTracker: commandTracker)
let realCommand = RealCommand()
self.command = TrackedCommand(command: realCommand, commandTracker: commandTracker)
self.webApi = RealWebApi(container: self)
if coreOnly {

View File

@@ -34,7 +34,7 @@ struct CommandHistoryView: View {
Spacer()
}
} else {
ForEach(commandTracker.commands) { command in
ForEach(commandTracker.commands) { command in
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {