DESCracker/ios/DESCracker/Network/SlaveClient.swift
Tom Butcher ac345b2859 Refactor iOS application structure for DES Cracker
- Replaced Objective-C AppDelegate with Swift implementation for better performance and modern syntax.
- Removed legacy files (AppDelegate.h, AppDelegate.m, main.m, PSCBruteEngine.h, PSCBruteEngine.m, PSCCpuEngine.h, PSCCpuEngine.m, PSCMetalEngine.h, PSCMetalEngine.m, PSCProtocol.h, PSCProtocol.m, PSCSlaveClient.h, PSCSlaveClient.m) to streamline the codebase.
- Introduced new Compute engines (CpuEngine and MetalEngine) for enhanced brute force capabilities.
- Updated Info.plist to support multiple scenes and added scene delegate for improved lifecycle management.
- Modified results.json to reflect updated elapsed time for brute force operations.
2026-09-19 19:20:55 +01:00

493 lines
18 KiB
Swift

import Foundation
protocol SlaveClientDelegate: AnyObject {
func slaveClient(_ client: SlaveClient, didChangeStatus status: String, connected: Bool)
func slaveClient(_ client: SlaveClient, didLog line: String)
func slaveClient(_ client: SlaveClient, didUpdateWorkers workers: [WorkerSnapshot])
func slaveClient(_ client: SlaveClient, didHitJob jobId: String, index: String, keyHex: String)
}
final class SlaveClient: NSObject, URLSessionWebSocketDelegate {
weak var delegate: SlaveClientDelegate?
var cpuWorkers: Int
var isConnected: Bool { task?.state == .running }
var metalAvailable: Bool { metal.isAvailable }
var metalName: String { metal.gpuName }
var metalMemory: UInt64 { metal.memoryBytes }
var cpuCores: Int { cpu.coreCount }
private var devices: [ComputeDevice] = []
private var session: URLSession!
private var task: URLSessionWebSocketTask?
private var pingTimer: Timer?
private var jobs: [String: DeviceJob] = [:]
private var completedBlocks: [String: Int] = [:]
private var progressGates: [String: ProgressGate] = [:]
private var rateStats: [String: RateStats] = [:]
private let metal = MetalEngine()
private let cpu = CpuEngine()
private let syncQueue = DispatchQueue(label: "com.descracker.slave")
private var connectURL = ""
private var loggedMetalCompile = false
override init() {
cpuWorkers = max(1, ProcessInfo.processInfo.processorCount)
super.init()
let config = URLSessionConfiguration.default
config.waitsForConnectivity = false
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
func prepareEngines() throws {
try cpu.prepare()
}
func prepareEngines(completion: @escaping (Bool, Error?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
var captured: Error?
var ok = true
do {
try self.prepareEngines()
} catch {
captured = error
ok = false
}
DispatchQueue.main.async {
completion(ok, captured)
}
}
}
func setDevices(_ devices: [ComputeDevice]) {
syncQueue.async {
self.devices = devices
for device in self.devices where self.completedBlocks[device.key] == nil {
self.completedBlocks[device.key] = 0
}
self.emitWorkers()
}
}
func connect(to host: String, port: Int) {
disconnect()
let urlString = "ws://\(host):\(port)"
connectURL = urlString
notifyStatus("Connecting to \(urlString)…", connected: false)
guard let url = URL(string: urlString) else {
log("Invalid URL \(urlString)")
notifyStatus("Disconnected", connected: false)
return
}
task = session.webSocketTask(with: url)
task?.resume()
}
func disconnect() {
syncQueue.sync {
clearPing()
killAll()
}
let current = task
task = nil
current?.cancel(with: .normalClosure, reason: nil)
syncQueue.async { self.emitWorkers() }
}
func urlSession(
_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?
) {
guard webSocketTask === task else { return }
syncQueue.async {
let deviceJSON = self.devices.map { $0.jsonObject() }
let hostname = ProcessInfo.processInfo.hostName
self.send([
"type": "hello",
"hostname": hostname,
"platform": "ios arm64",
"devices": deviceJSON,
])
let exposed = self.devices.isEmpty
? "no workers"
: self.devices.map(\.name).joined(separator: ", ")
self.notifyStatus("Connected to \(self.connectURL)", connected: true)
self.log("Hello sent (protocol \(Wire.protocolVersion)) — exposing \(exposed)")
self.emitWorkers()
DispatchQueue.main.async {
self.pingTimer?.invalidate()
self.pingTimer = Timer.scheduledTimer(withTimeInterval: Wire.pingInterval, repeats: true) { [weak self] _ in
self?.send(["type": "ping"])
}
}
self.listen()
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if task !== self.task && self.task != nil { return }
syncQueue.async {
self.clearPing()
self.killAll()
if self.task === task {
self.task = nil
}
if let error, (error as NSError).code != NSURLErrorCancelled {
self.log("WebSocket error: \(error.localizedDescription)")
}
self.notifyStatus("Disconnected", connected: false)
self.emitWorkers()
}
}
private func listen() {
guard let task else { return }
task.receive { [weak self] result in
guard let self, task === self.task else { return }
switch result {
case .failure:
return
case .success(let message):
if case .string(let text) = message, let msg = Wire.decodeMessage(text) {
self.onServer(msg)
}
self.listen()
}
}
}
private func onServer(_ msg: [String: Any]) {
let type = msg["type"] as? String
switch type {
case "hello_ack":
log("Master ack v\(msg["protocolVersion"] ?? "?")")
case "assign":
guard let block = AssignBlock.fromJSON(msg) else {
log("Ignored malformed assign")
return
}
runAssign(block)
case "cancel":
syncQueue.async {
self.killAll()
self.log("Cancel received")
self.notifyStatus("Idle (cancelled)", connected: true)
self.emitWorkers()
}
case "shutdown":
DispatchQueue.main.async { self.disconnect() }
default:
break
}
}
private func engine(for device: ComputeDevice) -> BruteEngine {
device.type == "metal" ? metal : cpu
}
private func device(forKey key: String) -> ComputeDevice? {
if let match = devices.first(where: { $0.key == key }) { return match }
return devices.first(where: { $0.type == "metal" }) ?? devices.first
}
private func runAssign(_ block: AssignBlock) {
syncQueue.async { [weak self] in
guard let self else { return }
let matched = self.devices.first(where: { $0.key == block.deviceKey })
guard let device = matched ?? self.device(forKey: block.deviceKey) else {
self.send(["type": "error", "jobId": block.jobId, "message": "no compute device selected"])
return
}
if matched == nil {
self.log("No device \(block.deviceKey); using \(device.name)")
}
self.jobs[device.key]?.kill?()
self.clearProgressGate(device.key)
self.log(String(
format: "Assigned %@ len=%u pad=0x%02x start=%llu count=%llu",
device.name,
block.keyLen,
block.padByte,
block.start,
block.count
))
if device.type == "metal" && !self.loggedMetalCompile {
self.loggedMetalCompile = true
self.log("Compiling Metal kernel for this GPU (first job may pause)…")
}
if device.type == "cpu" && block.cpuWorkers == nil {
block.cpuWorkers = self.cpuWorkers
}
let started = Date()
let hitCount = Locked(0)
let engine = self.engine(for: device)
let deviceKey = device.key
let jobId = block.jobId
let job = DeviceJob(jobId: jobId, count: block.count)
job.kill = { engine.cancel() }
self.jobs[deviceKey] = job
self.beginRate(deviceKey)
self.updateStatus()
self.emitWorkers()
engine.runAssign(
block,
onProgress: { done, count, rate in
self.syncQueue.async {
if let live = self.jobs[deviceKey], live.jobId == jobId {
live.done = done
live.count = count
live.rate = rate
}
self.sendProgress(jobId: jobId, deviceKey: deviceKey, done: done, count: count, rate: rate)
self.updateStatus()
self.emitWorkers()
}
},
onHit: { index, key, plain in
self.syncQueue.async {
hitCount.value += 1
let indexS = Wire.formatDec64(index)
let keyHex = Wire.formatHex64(key)
let plainHex = Wire.formatHex64(plain)
self.send([
"type": "hit",
"jobId": jobId,
"index": indexS,
"keyHex": keyHex,
"plainHex": plainHex,
])
DispatchQueue.main.async {
self.delegate?.slaveClient(self, didHitJob: jobId, index: indexS, keyHex: keyHex)
}
}
},
completion: { _, elapsed, cancelled, error in
self.syncQueue.async {
let still = self.jobs[deviceKey]
let same = still?.jobId == jobId
if cancelled {
if same, let still {
self.endRate(deviceKey, keys: still.done)
self.jobs.removeValue(forKey: deviceKey)
self.clearProgressGate(deviceKey)
}
self.updateStatus()
self.emitWorkers()
return
}
if let error {
if same, let still {
self.endRate(deviceKey, keys: still.done)
self.jobs.removeValue(forKey: deviceKey)
self.clearProgressGate(deviceKey)
}
self.send([
"type": "error",
"jobId": jobId,
"message": error.localizedDescription,
])
self.log(error.localizedDescription)
self.updateStatus()
self.emitWorkers()
return
}
if same {
self.jobs.removeValue(forKey: deviceKey)
self.endRate(deviceKey, keys: block.count)
self.completedBlocks[deviceKey] = (self.completedBlocks[deviceKey] ?? 0) + 1
self.clearProgressGate(deviceKey)
let elapsedOut = elapsed > 0 ? elapsed : Date().timeIntervalSince(started)
self.send([
"type": "block_complete",
"jobId": jobId,
"deviceKey": deviceKey,
"hits": hitCount.value,
"elapsed": elapsedOut,
])
}
self.updateStatus()
self.emitWorkers()
}
}
)
}
}
private func send(_ msg: [String: Any]) {
guard let json = Wire.encodeMessage(msg),
let task, task.state == .running
else { return }
task.send(.string(json)) { [weak self] error in
if let error {
self?.log("Send failed: \(error.localizedDescription)")
}
}
}
private func rate(for deviceKey: String) -> RateStats {
if let stats = rateStats[deviceKey] { return stats }
let stats = RateStats()
rateStats[deviceKey] = stats
return stats
}
private func beginRate(_ deviceKey: String) {
let stats = rate(for: deviceKey)
if stats.busyStarted == nil {
stats.busyStarted = Date()
}
}
private func endRate(_ deviceKey: String, keys: UInt64) {
let stats = rate(for: deviceKey)
if let started = stats.busyStarted {
stats.activeMs += Date().timeIntervalSince(started) * 1000
stats.busyStarted = nil
}
if keys > 0 {
stats.keysDone += keys
}
}
private func killAll() {
for (deviceKey, job) in jobs {
endRate(deviceKey, keys: job.done)
job.kill?()
clearProgressGate(deviceKey)
}
jobs.removeAll()
}
private func clearPing() {
DispatchQueue.main.async {
self.pingTimer?.invalidate()
self.pingTimer = nil
}
}
private func progressGate(_ deviceKey: String) -> ProgressGate {
if let gate = progressGates[deviceKey] { return gate }
let gate = ProgressGate()
progressGates[deviceKey] = gate
return gate
}
private func sendProgress(jobId: String, deviceKey: String, done: UInt64, count: UInt64, rate: Double) {
let gate = progressGate(deviceKey)
gate.payload = [
"type": "progress",
"jobId": jobId,
"deviceKey": deviceKey,
"done": Wire.formatDec64(done),
"count": Wire.formatDec64(count),
"rate": rate,
]
let wait = Wire.progressInterval - (Date().timeIntervalSince1970 - gate.lastSent)
if wait <= 0 {
flushProgress(deviceKey)
return
}
if !gate.timerPending {
gate.timerPending = true
let key = deviceKey
syncQueue.asyncAfter(deadline: .now() + wait) {
self.progressGates[key]?.timerPending = false
self.flushProgress(key)
}
}
}
private func flushProgress(_ deviceKey: String) {
guard let gate = progressGates[deviceKey], let payload = gate.payload else { return }
let live = jobs[deviceKey]
if live == nil || live?.jobId != payload["jobId"] as? String {
gate.payload = nil
return
}
gate.payload = nil
gate.lastSent = Date().timeIntervalSince1970
send(payload)
}
private func clearProgressGate(_ deviceKey: String) {
guard let gate = progressGates[deviceKey] else { return }
gate.timerPending = false
gate.payload = nil
}
private func emitWorkers() {
let rows: [WorkerSnapshot] = devices.map { device in
let live = jobs[device.key]
let done = live?.done ?? 0
let count = live?.count ?? 0
let pct = live != nil && count > 0 ? Double(done) * 100.0 / Double(count) : 0
let row = WorkerSnapshot()
row.deviceKey = device.key
row.deviceName = device.name
row.deviceType = device.type
row.current = live == nil ? "idle" : "\(Wire.formatCount(done)) / \(Wire.formatCount(count))"
row.pct = pct
row.completedBlocks = completedBlocks[device.key] ?? 0
row.rate = live?.rate ?? 0
row.busy = live != nil
return row
}
DispatchQueue.main.async {
self.delegate?.slaveClient(self, didUpdateWorkers: rows)
}
}
private func updateStatus() {
let busy = Array(jobs.values)
let connected = task != nil
if busy.isEmpty {
notifyStatus(connected ? "Idle" : "Disconnected", connected: connected)
return
}
if busy.count == 1, let job = busy.first {
let shortId = job.jobId.count > 8 ? String(job.jobId.prefix(8)) : job.jobId
notifyStatus("Working \(shortId) \(job.done)/\(job.count)", connected: true)
return
}
notifyStatus("\(busy.count) workers assigned", connected: true)
}
private func notifyStatus(_ status: String, connected: Bool) {
DispatchQueue.main.async {
self.delegate?.slaveClient(self, didChangeStatus: status, connected: connected)
}
}
private func log(_ line: String) {
DispatchQueue.main.async {
self.delegate?.slaveClient(self, didLog: line)
}
}
}
private final class DeviceJob {
var jobId: String
var done: UInt64 = 0
var count: UInt64
var rate = 0.0
var kill: (() -> Void)?
init(jobId: String, count: UInt64) {
self.jobId = jobId
self.count = count
}
}
private final class RateStats {
var keysDone: UInt64 = 0
var activeMs: TimeInterval = 0
var busyStarted: Date?
}
private final class ProgressGate {
var lastSent: TimeInterval = 0
var timerPending = false
var payload: [String: Any]?
}