Swift 使用 Process() 和多线程的命令行工具在一定数量的执行轮次后崩溃 (~3148)
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 轮中很好,总是 在这个数字附近崩溃(可能与线程数 运行ning)。
例如,如果我配置 2000 密码,代码将按预期终止,没有任何错误。
常见错误消息
在 BREAKPOINT_1
处 execute(...)
函数的 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
本身利用 execute(...)
从 Process
扩展到 运行 生成散列的命令行任务。
Main.swift
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.swift
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
}
}
}
Encryption.swift
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: "y$", with: "a$")
else {
fatalError("Hash: String replacement failed!")
}
return hash
}
}
进程+Pipe.swift
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)
}
}
问题
- 为什么程序会崩溃?
- 为什么像 2000 这样的大密码也不会崩溃?
- 多线程实现是否正确?
execute(...)
代码有问题吗?
这似乎是一个 Swift 错误。我做了一些测试,只需 运行 很多 Process.run()
就可以重现它。针对 Swift 提出问题:
我在研究这个错误时找到了修复方法。似乎,尽管文档声称,Pipe
不会自动关闭其读取文件句柄。
因此,如果您在阅读后添加 try outputPipe.fileHandleForReading.close()
,这将解决问题。
我在 Swift 中实现了一个密码生成器脚本,它利用 Process()
来执行 Mac OS X 命令行任务。密码本身只是随机字符串,然后由命令行任务加密 (bcrypt),如下所示:
/usr/sbin/htpasswd -bnBC 10 '' this_is_the_password | /usr/bin/tr -d ':\n'
还使用多个线程并行生成密码及其哈希值。
注意:多线程和命令行任务(与其他几个相比本机 Swift 我尝试过的库)在执行时间方面显着提高了性能。
问题
程序 运行 在第一 ~3148 轮中很好,总是 在这个数字附近崩溃(可能与线程数 运行ning)。 例如,如果我配置 2000 密码,代码将按预期终止,没有任何错误。
常见错误消息
在 BREAKPOINT_1
处 execute(...)
函数的 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
本身利用 execute(...)
从 Process
扩展到 运行 生成散列的命令行任务。
Main.swift
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.swift
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
}
}
}
Encryption.swift
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: "y$", with: "a$")
else {
fatalError("Hash: String replacement failed!")
}
return hash
}
}
进程+Pipe.swift
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)
}
}
问题
- 为什么程序会崩溃?
- 为什么像 2000 这样的大密码也不会崩溃?
- 多线程实现是否正确?
execute(...)
代码有问题吗?
这似乎是一个 Swift 错误。我做了一些测试,只需 运行 很多 Process.run()
就可以重现它。针对 Swift 提出问题:
我在研究这个错误时找到了修复方法。似乎,尽管文档声称,Pipe
不会自动关闭其读取文件句柄。
因此,如果您在阅读后添加 try outputPipe.fileHandleForReading.close()
,这将解决问题。