mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-04-01 17:20:09 +02:00
♻️ Use discardableResult
- Removed ShellProtocol.quiet(), now that pipe() is discardable - Detecting PHP versions is also discardable - The system command is also discardable This is a nice quality of life change overall, and gets rid of a couple of silly `_ =` assignments.
This commit is contained in:
@@ -122,7 +122,7 @@ class Actions {
|
||||
try! container.filesystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
|
||||
|
||||
// Tell php-cgi to run the PHP and output as an .html file
|
||||
await container.shell.quiet("\(paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
await container.shell.pipe("\(paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
|
||||
|
||||
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
|
||||
}
|
||||
|
||||
0
phpmon/Common/Core/AppleScript.swift
Normal file
0
phpmon/Common/Core/AppleScript.swift
Normal file
@@ -18,7 +18,7 @@ func brew(
|
||||
_ command: String,
|
||||
sudo: Bool = false,
|
||||
) async {
|
||||
await container.shell.quiet("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
|
||||
await container.shell.pipe("\(sudo ? "sudo " : "")" + "\(container.paths.brew) \(command)")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,9 +37,9 @@ func sed(
|
||||
// Check if gsed exists; it is able to follow symlinks,
|
||||
// which we want to do to toggle the extension
|
||||
if container.filesystem.fileExists("\(container.paths.binPath)/gsed") {
|
||||
await container.shell.quiet("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
await container.shell.pipe("\(container.paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
} else {
|
||||
await container.shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
await container.shell.pipe("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,20 +71,3 @@ func delay(seconds: Double) async {
|
||||
func url(_ string: String) -> URL {
|
||||
return URL(string: string)!
|
||||
}
|
||||
|
||||
/**
|
||||
Execute a script with administrative privileges.
|
||||
*/
|
||||
func sudo(_ script: String) throws {
|
||||
let source = "do shell script \"\(script)\" with administrator privileges"
|
||||
|
||||
Log.info("Running script via AppleScript as administrator: `\(source)`")
|
||||
|
||||
let appleScript = NSAppleScript(source: source)
|
||||
|
||||
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
|
||||
|
||||
if eventResult == nil {
|
||||
throw AdminPrivilegeError(kind: .applescriptNilError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class RealFileSystem: FileSystemProtocol {
|
||||
// MARK: — FS Attributes
|
||||
|
||||
func makeExecutable(_ path: String) throws {
|
||||
_ = container.shell.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
container.shell.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
||||
@@ -48,7 +48,7 @@ class Application {
|
||||
(This will open the app if it isn't open yet.)
|
||||
*/
|
||||
@objc public func open(arg: String) {
|
||||
Task { await container.shell.quiet("/usr/bin/open -a \"\(name)\" \"\(arg)\"") }
|
||||
Task { await container.shell.pipe("/usr/bin/open -a \"\(name)\" \"\(arg)\"") }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
/**
|
||||
Run a simple blocking Shell command on the user's own system.
|
||||
*/
|
||||
@discardableResult
|
||||
public func system(_ command: String) -> String {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/sh"
|
||||
|
||||
@@ -218,7 +218,7 @@ class PhpEnvironments {
|
||||
}
|
||||
|
||||
public func reloadPhpVersions() async {
|
||||
_ = await self.detectPhpVersions()
|
||||
await self.detectPhpVersions()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +228,7 @@ class PhpEnvironments {
|
||||
|
||||
Returns a `Set<String>` of installations that are considered valid.
|
||||
*/
|
||||
@discardableResult
|
||||
public func detectPhpVersions() async -> Set<String> {
|
||||
let files = await container.shell.pipe("ls \(container.paths.optPath) | grep php@").out
|
||||
|
||||
|
||||
@@ -127,13 +127,13 @@ class PhpHelper {
|
||||
|
||||
if !container.filesystem.fileExists(destination) {
|
||||
Log.info("Creating new symlink: \(destination)")
|
||||
await container.shell.quiet("ln -s \(source) \(destination)")
|
||||
await container.shell.pipe("ln -s \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
if !App.shared.container.filesystem.isSymlink(destination) {
|
||||
Log.info("Overwriting existing file with new symlink: \(destination)")
|
||||
await container.shell.quiet("ln -fs \(source) \(destination)")
|
||||
await container.shell.pipe("ln -fs \(source) \(destination)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import Foundation
|
||||
extension InternalSwitcher {
|
||||
typealias FixApplied = Bool
|
||||
|
||||
@discardableResult
|
||||
public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied {
|
||||
// Early exit if Valet is not installed
|
||||
if !Valet.installed {
|
||||
|
||||
@@ -53,7 +53,7 @@ class InternalSwitcher: PhpSwitcher {
|
||||
for formula in versions {
|
||||
if Valet.installed {
|
||||
Log.info("Ensuring that the Valet configuration is valid...")
|
||||
_ = await self.ensureValetConfigurationIsValidForPhpVersion(formula)
|
||||
await self.ensureValetConfigurationIsValidForPhpVersion(formula)
|
||||
}
|
||||
|
||||
Log.info("Will start PHP \(version)... (primary: \(version == formula))")
|
||||
@@ -112,7 +112,7 @@ class InternalSwitcher: PhpSwitcher {
|
||||
|
||||
if Valet.enabled(feature: .isolatedSites) && primary {
|
||||
let socketVersion = version.replacing(".", with: "")
|
||||
await container.shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
|
||||
await container.shell.pipe("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
|
||||
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
let process = getShellProcess(for: command)
|
||||
|
||||
@@ -220,6 +221,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let process = getShellProcess(for: command)
|
||||
|
||||
@@ -285,10 +287,7 @@ class RealShell: ShellProtocol, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = await self.pipe(command)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
|
||||
@@ -22,6 +22,7 @@ protocol ShellProtocol {
|
||||
let output = Shell.sync("php -v")
|
||||
```
|
||||
*/
|
||||
@discardableResult
|
||||
func sync(_ command: String) -> ShellOutput
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,7 @@ protocol ShellProtocol {
|
||||
let output = await Shell.pipe("php -v")
|
||||
```
|
||||
*/
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput
|
||||
|
||||
/**
|
||||
@@ -43,14 +45,9 @@ protocol ShellProtocol {
|
||||
- Parameter timeout: Timeout in seconds. If the command exceeds this, it is terminated.
|
||||
- Returns: The shell output. If the command times out, returns empty output.
|
||||
*/
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput
|
||||
|
||||
/**
|
||||
Run a command asynchronously, without returning the output of the command.
|
||||
Returns the most relevant output (prefers error output if it exists).
|
||||
*/
|
||||
func quiet(_ command: String) async
|
||||
|
||||
/**
|
||||
Runs a command asynchronously, and fires closure with `stdout` or `stderr` data as it comes in.
|
||||
|
||||
@@ -61,6 +58,7 @@ protocol ShellProtocol {
|
||||
Unlike `sync`, `pipe` and `quiet`, you can capture both `stdout` and `stderr` with this mechanism.
|
||||
The end result is still the most relevant output (where error output is preferred if it exists).
|
||||
*/
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
|
||||
@@ -19,6 +19,7 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||
|
||||
@discardableResult
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
// This assertion will only fire during test builds
|
||||
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||
@@ -30,19 +31,18 @@ public class TestableShell: ShellProtocol {
|
||||
return expectation.syncOutput()
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
return await pipe(command, timeout: 60)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func pipe(_ command: String, timeout: TimeInterval) async -> ShellOutput {
|
||||
let (_, output) = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: timeout)
|
||||
return output
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func attach(
|
||||
_ command: String,
|
||||
didReceiveOutput: @escaping (String, ShellStream) -> Void,
|
||||
|
||||
@@ -71,7 +71,7 @@ class RemovePhpExtensionCommand: BrewCommand {
|
||||
await performExtensionCleanup(for: ext)
|
||||
}
|
||||
|
||||
_ = await container.phpEnvs.detectPhpVersions()
|
||||
await container.phpEnvs.detectPhpVersions()
|
||||
|
||||
await Actions(container).restartPhpFpm(version: phpExtension.phpVersion)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class ModifyPhpVersionCommand: BrewCommand {
|
||||
}
|
||||
|
||||
// Re-check the installed versions
|
||||
_ = await container.phpEnvs.detectPhpVersions()
|
||||
await container.phpEnvs.detectPhpVersions()
|
||||
|
||||
// After performing operations, attempt to run repairs if needed
|
||||
try await self.repairBrokenPackages(onProgress)
|
||||
@@ -186,7 +186,7 @@ class ModifyPhpVersionCommand: BrewCommand {
|
||||
await BrewDiagnostics.shared.checkForOutdatedPhpInstallationSymlinks()
|
||||
|
||||
// Check which version of PHP are now installed
|
||||
_ = await container.phpEnvs.detectPhpVersions()
|
||||
await container.phpEnvs.detectPhpVersions()
|
||||
|
||||
// Keep track of the currently installed version
|
||||
await MainMenu.shared.refreshActiveInstallation()
|
||||
|
||||
@@ -74,7 +74,7 @@ class RemovePhpVersionCommand: BrewCommand {
|
||||
if process.terminationStatus == 0 {
|
||||
onProgress(.create(value: 0.95, title: getCommandTitle(), description: "phpman.steps.reloading".localized))
|
||||
|
||||
_ = await container.phpEnvs.detectPhpVersions()
|
||||
await container.phpEnvs.detectPhpVersions()
|
||||
|
||||
await MainMenu.shared.refreshActiveInstallation()
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ class ValetInteractor {
|
||||
// MARK: - Managing Domains
|
||||
|
||||
public func link(path: String, domain: String) async throws {
|
||||
await container.shell.quiet("cd '\(path)' && \(container.paths.valet) link '\(domain)' && valet links")
|
||||
await container.shell.pipe("cd '\(path)' && \(container.paths.valet) link '\(domain)' && valet links")
|
||||
}
|
||||
|
||||
public func unlink(site: ValetSite) async throws {
|
||||
await container.shell.quiet("valet unlink '\(site.name)'")
|
||||
await container.shell.pipe("valet unlink '\(site.name)'")
|
||||
}
|
||||
|
||||
public func proxy(domain: String, proxy: String, secure: Bool) async throws {
|
||||
@@ -46,12 +46,12 @@ class ValetInteractor {
|
||||
? "\(container.paths.valet) proxy \(domain) \(proxy) --secure"
|
||||
: "\(container.paths.valet) proxy \(domain) \(proxy)"
|
||||
|
||||
await container.shell.quiet(command)
|
||||
await container.shell.pipe(command)
|
||||
await Actions(container).restartNginx()
|
||||
}
|
||||
|
||||
public func remove(proxy: ValetProxy) async throws {
|
||||
await container.shell.quiet("valet unproxy '\(proxy.domain)'")
|
||||
await container.shell.pipe("valet unproxy '\(proxy.domain)'")
|
||||
}
|
||||
|
||||
// MARK: - Modifying Domains
|
||||
@@ -73,7 +73,7 @@ class ValetInteractor {
|
||||
}
|
||||
|
||||
// Run the command
|
||||
await container.shell.quiet(command)
|
||||
await container.shell.pipe(command)
|
||||
|
||||
// Check if the secured status has actually changed
|
||||
site.determineSecured()
|
||||
@@ -98,7 +98,7 @@ class ValetInteractor {
|
||||
|
||||
// Run the commands
|
||||
for command in commands {
|
||||
await container.shell.quiet(command)
|
||||
await container.shell.pipe(command)
|
||||
}
|
||||
|
||||
// Check if the secured status has actually changed
|
||||
@@ -117,7 +117,7 @@ class ValetInteractor {
|
||||
let command = "sudo \(container.paths.valet) isolate php@\(version) --site '\(site.name)'"
|
||||
|
||||
// Run the command
|
||||
await container.shell.quiet(command)
|
||||
await container.shell.pipe(command)
|
||||
|
||||
// Check if the secured status has actually changed
|
||||
site.determineIsolated()
|
||||
@@ -133,7 +133,7 @@ class ValetInteractor {
|
||||
let command = "sudo \(container.paths.valet) unisolate --site '\(site.name)'"
|
||||
|
||||
// Run the command
|
||||
await container.shell.quiet(command)
|
||||
await container.shell.pipe(command)
|
||||
|
||||
// Check if the secured status has actually changed
|
||||
site.determineIsolated()
|
||||
|
||||
@@ -37,7 +37,7 @@ struct CustomPrefs: Decodable {
|
||||
extension Preferences {
|
||||
func loadCustomPreferences() async {
|
||||
// Ensure the configuration directory is created if missing
|
||||
await container.shell.quiet("mkdir -p ~/.config/phpmon")
|
||||
await container.shell.pipe("mkdir -p ~/.config/phpmon")
|
||||
|
||||
// Move the legacy file
|
||||
await moveOutdatedConfigurationFile()
|
||||
@@ -57,7 +57,7 @@ extension Preferences {
|
||||
if container.filesystem.fileExists("~/.phpmon.conf.json")
|
||||
&& !container.filesystem.fileExists("~/.config/phpmon/config.json") {
|
||||
Log.info("An outdated configuration file was found. Moving it...")
|
||||
await container.shell.quiet("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
|
||||
await container.shell.pipe("cp ~/.phpmon.conf.json ~/.config/phpmon/config.json")
|
||||
Log.info("The configuration file was copied successfully!")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class AppearancePreferencesVC: GenericPreferenceVC {
|
||||
let vc = NSStoryboard(name: "Main", bundle: nil)
|
||||
.instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC
|
||||
|
||||
_ = vc.addView(when: true, vc.getDynamicIconPV())
|
||||
vc.addView(when: true, vc.getDynamicIconPV())
|
||||
.addView(when: true, vc.getIconOptionsPV())
|
||||
.addView(when: true, vc.getIconDensityPV())
|
||||
.addView(when: true, vc.getMenuIconsPV())
|
||||
|
||||
@@ -28,6 +28,7 @@ class GenericPreferenceVC: NSViewController {
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addView(when condition: Bool, _ view: NSView) -> GenericPreferenceVC {
|
||||
if condition {
|
||||
self.views.append(view)
|
||||
|
||||
@@ -276,7 +276,7 @@ struct Preset: Codable, Equatable {
|
||||
private func persistRevert() async {
|
||||
let data = try! JSONEncoder().encode(self.revertSnapshot)
|
||||
|
||||
await container.shell.quiet("mkdir -p ~/.config/phpmon")
|
||||
await container.shell.pipe("mkdir -p ~/.config/phpmon")
|
||||
|
||||
try! String(data: data, encoding: .utf8)!
|
||||
.write(
|
||||
|
||||
@@ -27,7 +27,7 @@ class InstallHomebrew {
|
||||
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
"""
|
||||
|
||||
_ = try await container.shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
|
||||
try await container.shell.attach(script, didReceiveOutput: { (string: String, _: ShellStream) in
|
||||
print(string)
|
||||
}, withTimeout: 60 * 10)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class ZshRunCommand {
|
||||
/**
|
||||
Adds a given line to .zshrc, which may be needed to adjust the PATH.
|
||||
*/
|
||||
@discardableResult
|
||||
private func add(_ text: String) async -> Bool {
|
||||
// Escape single quotes to prevent shell injection
|
||||
let escaped = text.replacingOccurrences(of: "'", with: "'\\''")
|
||||
@@ -44,13 +45,13 @@ class ZshRunCommand {
|
||||
Adds Homebrew binaries to the PATH.
|
||||
*/
|
||||
public func addHomebrewPath() async {
|
||||
_ = await add("export PATH=$HOME/bin:/opt/homebrew/bin:$PATH")
|
||||
await add("export PATH=$HOME/bin:/opt/homebrew/bin:$PATH")
|
||||
}
|
||||
|
||||
/**
|
||||
Adds PHP Monitor binaries to the PATH.
|
||||
*/
|
||||
public func addPhpMonitorPath() async {
|
||||
_ = await add("export PATH=$HOME/bin:~/.config/phpmon/bin:$PATH")
|
||||
await add("export PATH=$HOME/bin:~/.config/phpmon/bin:$PATH")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ extension DomainListVC {
|
||||
}
|
||||
|
||||
@objc func openInFinder() {
|
||||
Task { return await App.shared.container.shell.quiet("open '\(selectedSite!.absolutePath)'") }
|
||||
Task { return await App.shared.container.shell.pipe("open '\(selectedSite!.absolutePath)'") }
|
||||
}
|
||||
|
||||
@objc func openInTerminal() {
|
||||
Task { await App.shared.container.shell.quiet("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") }
|
||||
Task { await App.shared.container.shell.pipe("open -b com.apple.terminal '\(selectedSite!.absolutePath)'") }
|
||||
}
|
||||
|
||||
@objc func openWithApp(sender: ApplicationMenuItem) {
|
||||
@@ -58,7 +58,7 @@ extension DomainListVC {
|
||||
let rowToReload = tableView.selectedRow
|
||||
|
||||
waitAndExecute {
|
||||
await App.shared.container.shell.quiet(command)
|
||||
await App.shared.container.shell.pipe(command)
|
||||
} completion: { [self] in
|
||||
beforeCellReload()
|
||||
tableView.reloadData(forRowIndexes: [rowToReload], columnIndexes: [0, 1, 2, 3, 4])
|
||||
|
||||
@@ -87,7 +87,7 @@ extension WarningManager {
|
||||
] },
|
||||
url: "https://github.com/shivammathur/homebrew-php",
|
||||
fix: {
|
||||
await self.container.shell.quiet("brew tap shivammathur/php")
|
||||
await self.container.shell.pipe("brew tap shivammathur/php")
|
||||
await BrewDiagnostics.shared.loadInstalledTaps()
|
||||
await self.checkEnvironment()
|
||||
}
|
||||
@@ -103,7 +103,7 @@ extension WarningManager {
|
||||
] },
|
||||
url: "https://github.com/shivammathur/homebrew-extensions",
|
||||
fix: {
|
||||
await self.container.shell.quiet("brew tap shivammathur/extensions")
|
||||
await self.container.shell.pipe("brew tap shivammathur/extensions")
|
||||
await BrewDiagnostics.shared.loadInstalledTaps()
|
||||
await self.checkEnvironment()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct RealFileSystemTest {
|
||||
#expect(filesystem.directoryExists("\(temporaryDirectory)/brew/etc/lib"))
|
||||
#expect(filesystem.directoryExists("\(temporaryDirectory)/brew/etc/lib/c"))
|
||||
|
||||
_ = system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c")
|
||||
system("ln -s \(temporaryDirectory)/brew/etc/lib/c \(temporaryDirectory)/c")
|
||||
#expect(filesystem.directoryExists("\(temporaryDirectory)/c"))
|
||||
#expect(filesystem.isSymlink("\(temporaryDirectory)/c"))
|
||||
#expect(
|
||||
|
||||
@@ -89,9 +89,9 @@ struct RealShellTest {
|
||||
let start = ContinuousClock.now
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
|
||||
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
|
||||
group.addTask { await container.shell.quiet("php -r \"usleep(700000);\"") }
|
||||
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
|
||||
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
|
||||
group.addTask { await container.shell.pipe("php -r \"usleep(700000);\"") }
|
||||
}
|
||||
|
||||
let duration = start.duration(to: .now)
|
||||
|
||||
Reference in New Issue
Block a user