[英]How to create a minimal daemon process in a swift 2 command line tool?
[英]Swift Command Line Tool utilizing Process() and multithreading crashes after a certain number of execution rounds (~3148)
我在 Swift 中实现了一个密码生成器脚本,它利用Process()
来执行 Mac OS X 命令行任务。 密码本身只是随机字符串,然后由命令行任务加密(bcrypt),如下所示:
/usr/sbin/htpasswd -bnBC 10 '' this_is_the_password | /usr/bin/tr -d ':\n'
还使用多个线程并行生成具有哈希值的密码。
注意:多线程和命令行任务(与我尝试的其他几个本机 Swift 库相比)在执行时间方面都大大提高了性能。
该程序在前约 3148轮中运行良好,并且总是在这个数字附近崩溃(可能与运行的线程数相关)。 例如,如果我配置2000个密码,则代码按预期执行会终止而不会出现任何错误。
在BREAKPOINT_1
处的execute(...)
function 的catch
块中的Process+Pipe.swift
中设置断点会导致:
Thread 1: signal SIGCHLD
po error.localizedDescription
"The operation couldn\\U2019t be completed. (NSPOSIXErrorDomain error 9 - Bad file descriptor)"
当取消注释四个//return self.hash(string, cost: cost)
代码片段以忽略错误时,以下错误最终使执行崩溃(再次在execute(...)
中,但不一定在catch
块中):
Program stops ...
Thread 32: EXC_BAD_ACCESS (code=2, address=0x700003e6bfd4)
... on manual continue ...
Thread 2: EXC_BAD_ACCESS (code=2, address=0x700007e85fd4)
po process
error: Trying to put the stack in unreadable memory at: 0x700003e6bf40.
相关的代码组件是Main.swift
,它初始化并启动(然后停止) PasswordGenerator
,然后循环n次以通过nextPassword()
从PasswordGenerator
获取密码。 PasswordGenerator
本身利用Process
扩展中的execute(...)
来运行生成 hash 的命令行任务。
class Main {
private static func generate(...) {
...
PasswordGenerator.start()
for _ in 0..<n {
let nextPassword = PasswordGenerator.nextPassword()
let readablePassword = nextPassword.readable
let password = nextPassword.hash
...
}
PasswordGenerator.stop()
...
}
}
PasswordGenerator
并行运行多个线程。 nextPassword()
尝试获取密码(如果passwords
数组中有密码),否则等待100秒。
struct PasswordGenerator {
typealias Password = (readable: String, hash: String)
private static let semaphore = DispatchSemaphore(value: 1)
private static var active = false
private static var passwords: [Password] = []
static func nextPassword() -> Password {
self.semaphore.wait()
if let password = self.passwords.popLast() {
self.semaphore.signal()
return password
} else {
self.semaphore.signal()
sleep(100)
return self.nextPassword()
}
}
static func start(
numberOfWorkers: UInt = 32,
passwordLength: UInt = 10,
cost: UInt = 10
) {
self.active = true
for id in 0..<numberOfWorkers {
self.runWorker(id: id, passwordLength: passwordLength, cost: cost)
}
}
static func stop() {
self.semaphore.wait()
self.active = false
self.semaphore.signal()
}
private static func runWorker(
id: UInt,
passwordLength: UInt = 10,
cost: UInt = 10
) {
DispatchQueue.global().async {
var active = true
repeat {
// Update active.
self.semaphore.wait()
active = self.active
print("numberOfPasswords: \(self.passwords.count)")
self.semaphore.signal()
// Generate Password.
// Important: The bycrypt(cost: ...) step must be done outside the Semaphore!
let readable = String.random(length: Int(passwordLength))
let password = Password(readable: readable, hash: Encryption.hash(readable, cost: cost))
// Add Password.
self.semaphore.wait()
self.passwords.append(password)
self.semaphore.signal()
} while active
}
}
}
struct Encryption {
static func hash(_ string: String, cost: UInt = 10) -> String {
// /usr/sbin/htpasswd -bnBC 10 '' this_is_the_password | /usr/bin/tr -d ':\n'
let command = "/usr/sbin/htpasswd"
let arguments: [String] = "-bnBC \(cost) '' \(string)".split(separator: " ").map(String.init)
let result1 = Process.execute(
command: command,//"/usr/sbin/htpasswd",
arguments: arguments//["-bnBC", "\(cost)", "''", string]
)
let errorString1 = String(
data: result1?.error?.fileHandleForReading.readDataToEndOfFile() ?? Data(),
encoding: String.Encoding.utf8
) ?? ""
guard errorString1.isEmpty else {
// return self.hash(string, cost: cost)
fatalError("Error: Command \(command) \(arguments.joined(separator: " ")) failed with error: \(errorString1)")
}
guard let output1 = result1?.output else {
// return self.hash(string, cost: cost)
fatalError("Error: Command \(command) \(arguments.joined(separator: " ")) failed! No output.")
}
let command2 = "/usr/bin/tr"
let arguments2: [String] = "-d ':\n'".split(separator: " ").map(String.init)
let result2 = Process.execute(
command: command2,
arguments: arguments2,
standardInput: output1
)
let errorString2 = String(
data: result2?.error?.fileHandleForReading.readDataToEndOfFile() ?? Data(),
encoding: String.Encoding.utf8
) ?? ""
guard errorString2.isEmpty else {
// return self.hash(string, cost: cost)
fatalError("Error: Command \(command) \(arguments.joined(separator: " ")) failed with error: \(errorString2)")
}
guard let output2 = result2?.output else {
// return self.hash(string, cost: cost)
fatalError("Error: Command \(command) \(arguments.joined(separator: " ")) failed! No output.")
}
guard
let hash = String(
data: output2.fileHandleForReading.readDataToEndOfFile(),
encoding: String.Encoding.utf8
)?.replacingOccurrences(of: "$2y$", with: "$2a$")
else {
fatalError("Hash: String replacement failed!")
}
return hash
}
}
extension Process {
static func execute(
command: String,
arguments: [String] = [],
standardInput: Any? = nil
) -> (output: Pipe?, error: Pipe?)? {
let process = Process()
process.executableURL = URL(fileURLWithPath: command)
process.arguments = arguments
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
if let standardInput = standardInput {
process.standardInput = standardInput
}
do {
try process.run()
} catch {
print(error.localizedDescription)
// BREAKPOINT_1
return nil
}
process.waitUntilExit()
return (output: outputPipe, error: errorPipe)
}
}
execute(...)
代码有问题吗? 看来这是一个 Swift 错误。 我做了一些测试,只需运行大量Process.run()
就可以重现它。 针对 Swift 提出问题:
我在研究此错误时找到了修复方法。 尽管文档声称, Pipe
不会自动关闭其读取文件句柄。
因此,如果您在读取后添加try outputPipe.fileHandleForReading.close()
,这将解决问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.