完全使用 Async/Await 构建的并发 Shell 框架
现在已使用 swift-docc
完整记录!
高效、并发、简单。
SwiftSlash 的开发是为了解决所有现有 shell 框架的缺点。这些框架包括 SwiftCLI、Shell、ShellOut、ShellKit、Work 以及广受欢迎的 SwiftShell。这些框架在其公共 GitHub 存储库中的累积星数 > 1,874。
这些现有框架的致命弱点是它们内部使用了 Foundation 自身的 Process 类,这是一个内存占用量大的类,没有考虑到并发或复杂的使用场景。对于单个、串行化的执行,这些框架可以正常运行(尽管会泄漏内存)。但是,在高负载下,由于 Process 的缺点,所有这些框架都难以维持稳定。
SwiftSlash 的设计从一开始就有意地解决了这些现有框架的缺点,因此,特意不使用 Process 类。 从 3.0
版本开始,SwiftSlash 是第一个完全使用 Swift async/await 并发范例构建的并发 shell 框架。 在 2.2.2
版本之前,SwiftSlash 使用 Grand Central Dispatch 作为内部并发范例。
由于以根本不同的引擎作为 SwiftSlash 的核心,与其他框架相比,客观的改进如下
SwiftSlash
是目前已知的唯一一个在每次执行命令时都不会泄漏内存的 Swift shell 框架。
SwiftSlash
可以完全安全地并发使用。通过允许 shell 命令并发运行,SwiftSlash 可以在预期串行执行时间的一小部分内完成大量非顺序工作负载。
SwiftSlash
初始化和启动外部命令的效率远高于现有框架(包括内存占用和 CPU 影响)。从 stdin
、stdout
和 stderr
流中处理 I/O 时,也可以看到类似的性能改进。这主要是因为 SwiftSlash 的设计不需要为每个进程创建一个事件循环。
SwiftSlash
的结构确保了一个安全的执行环境。相比之下:Process 类存在许多安全漏洞,包括与子进程共享文件句柄以及不正确地更改指定的工作目录。
SwiftSlash
在底层深入实现了 Swift 的 async/await 并发范例(不是在表面层面)。这允许与其他可能在您的应用程序中发生的并发任务进行最佳的资源共享。
SwiftSlash
会在可能的情况下立即 回收进程,从而使其不可能让僵尸/孤儿进程在您的系统上逗留。这意味着您的应用程序不一定需要等待其启动的进程的退出代码。
SwiftSlash
知道您的进程已被系统分配的有限资源(主要是文件描述符)。它不会启动您的应用程序没有资源支持的命令。在这种情况下(在大量并发使用 SwiftSlash 的情况下),需要比可用资源更多的进程将被排队,并在资源释放后启动。
最后,SwiftSlash 非常易于使用,因为它实现了一个严格而简单的公共 API。
Command
实现了一个便捷函数 runSync()
,这是一种无需设置或 I/O 处理即可执行进程的简单方法。
import SwiftSlash
// EXAMPLE: check the systems ZFS version and print the first line of the output
let commandResult:CommandResult = try await Command(bash:"zfs --version").runSync()
//check the exit code
if commandResult.exitCode == 0 {
//print the first line of output
print("Found ZFS version: \( String(data:commandResult.stdout[0], encoding:.utf8) )")
}
ProcessInterface
是一个强大而灵活的类,它是 SwfitSlash 框架的基础 API。无论您的进程需要同步还是异步处理解析或未解析的数据,ProcessInterface
都为定义此类需求提供了一个一致的平台。
/*
EXAMPLE: query the system for any zfs datasets that might be imported.
stdout: parse as lines
stderr: unparsed, raw data will be sent through the stream as it becomes available
*/
//define the command you'd like to run
let zfsDatasetsCommand:Command = Command(bash:"zfs list -t dataset")
//pass the command structure to a new ProcessInterface. in this example, stdout will be parsed into lines with the lf byte, and stderr will be unparsed (raw data will be passed into the stream)
let zfsProcessInterface = ProcessInterface(command:zfsDatasetsCommand, stdout:.active(.lf), stderr:.active(.unparsedRaw))
//launch the process. if you are running many concurrent processes (using most of the available resources), this is where your process will be queued until there are enough resources to support the launched process.
try await zfsProcessInterface.launch()
//handle lines of stdout as they come in
var datasetLines = [Data]()
for await outputLine in await zfsProcessInterface.stdout {
guard let outputLineString = String(data: outputLine, encoding: .utf8) else { continue }
print("dataset found: \(outputLineString)")
datasetLines.append(outputLine)
}
//build the blob of stderr data if any was passed
var stderrBlob = Data()
for await stderrChunk in await zfsProcessInterface.stderr {
print("\(stderrChunk.count) bytes were sent through stderr")
stderrBlob += stderrChunk
}
//data can be written to stdin after a process is launched, like so...
zfsProcessInterface.write(stdin:"hello".data(using:.utf8)!)
//retrieve the exit code of the process.
let exitCode = try await zfsProcessInterface.exitCode()
if (exitCode == 0) {
//do work based on success
} else {
//do work based on error
}
SwiftSlash 在 MIT 许可证下提供,并且不提供任何保证。请参阅 LICENSE。
如有疑问,请通过 Twitter 联系 @tannerdsilva
。