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.
This commit is contained in:
Tom Butcher 2026-09-19 19:20:55 +01:00
parent bb690f3a1f
commit ac345b2859
33 changed files with 1978 additions and 2080 deletions

View File

@ -1,5 +1,5 @@
{ {
"elapsed_sec": 46.262, "elapsed_sec": 261.712,
"pad_matches": [], "pad_matches": [],
"confirmed": [] "confirmed": []
} }

View File

@ -7,43 +7,39 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
A40000000000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* main.m */; }; A40000000000000000000001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* AppDelegate.swift */; };
A40000000000000000000002 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* AppDelegate.m */; }; A40000000000000000000002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000002 /* SceneDelegate.swift */; };
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* PSCSlaveViewController.m */; }; A40000000000000000000003 /* SlaveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* SlaveViewController.swift */; };
A40000000000000000000004 /* PSCProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* PSCProtocol.m */; }; A40000000000000000000004 /* Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000004 /* Protocol.swift */; };
A40000000000000000000005 /* PSCSlaveClient.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* PSCSlaveClient.m */; }; A40000000000000000000005 /* SlaveClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* SlaveClient.swift */; };
A40000000000000000000006 /* PSCMetalEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* PSCMetalEngine.m */; }; A40000000000000000000006 /* BruteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000006 /* BruteEngine.swift */; };
A40000000000000000000007 /* PSCCpuEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000016 /* PSCCpuEngine.m */; }; A40000000000000000000007 /* MetalEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* MetalEngine.swift */; };
A40000000000000000000008 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000017 /* des.c */; }; A40000000000000000000008 /* CpuEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000008 /* CpuEngine.swift */; };
A40000000000000000000009 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000019 /* des_bruteforce.metal */; }; A40000000000000000000009 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* des.c */; };
A40000000000000000000010 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* Assets.xcassets */; }; A40000000000000000000010 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* des_bruteforce.metal */; };
A40000000000000000000011 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000020 /* Metal.framework */; }; A40000000000000000000011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000013 /* Assets.xcassets */; };
A40000000000000000000012 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000021 /* QuartzCore.framework */; }; A40000000000000000000012 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* Metal.framework */; };
A40000000000000000000013 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000015 /* QuartzCore.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
A10000000000000000000003 /* DES Cracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DES Cracker.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A10000000000000000000003 /* DES Cracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DES Cracker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
A20000000000000000000001 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; A20000000000000000000001 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A20000000000000000000002 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; A20000000000000000000002 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
A20000000000000000000003 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; }; A20000000000000000000003 /* SlaveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveViewController.swift; sourceTree = "<group>"; };
A20000000000000000000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; A20000000000000000000004 /* Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protocol.swift; sourceTree = "<group>"; };
A20000000000000000000005 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; A20000000000000000000005 /* SlaveClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveClient.swift; sourceTree = "<group>"; };
A20000000000000000000006 /* PSCSlaveViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveViewController.h; sourceTree = "<group>"; }; A20000000000000000000006 /* BruteEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BruteEngine.swift; sourceTree = "<group>"; };
A20000000000000000000007 /* PSCSlaveViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveViewController.m; sourceTree = "<group>"; }; A20000000000000000000007 /* MetalEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalEngine.swift; sourceTree = "<group>"; };
A20000000000000000000008 /* PSCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCProtocol.h; sourceTree = "<group>"; }; A20000000000000000000008 /* CpuEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuEngine.swift; sourceTree = "<group>"; };
A20000000000000000000009 /* PSCProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCProtocol.m; sourceTree = "<group>"; }; A20000000000000000000009 /* des.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = des.c; path = ../native/common/des.c; sourceTree = SOURCE_ROOT; };
A20000000000000000000010 /* PSCSlaveClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveClient.h; sourceTree = "<group>"; }; A20000000000000000000010 /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = des.h; path = ../native/common/des.h; sourceTree = SOURCE_ROOT; };
A20000000000000000000011 /* PSCSlaveClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveClient.m; sourceTree = "<group>"; }; A20000000000000000000011 /* des_bruteforce.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; name = des_bruteforce.metal; path = ../native/metal/des_bruteforce.metal; sourceTree = SOURCE_ROOT; };
A20000000000000000000012 /* PSCBruteEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCBruteEngine.h; sourceTree = "<group>"; }; A20000000000000000000012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A20000000000000000000013 /* PSCMetalEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCMetalEngine.h; sourceTree = "<group>"; }; A20000000000000000000013 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A20000000000000000000014 /* PSCMetalEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCMetalEngine.m; sourceTree = "<group>"; }; A20000000000000000000014 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
A20000000000000000000015 /* PSCCpuEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCCpuEngine.h; sourceTree = "<group>"; }; A20000000000000000000015 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
A20000000000000000000016 /* PSCCpuEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCCpuEngine.m; sourceTree = "<group>"; }; A20000000000000000000016 /* DESCracker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DESCracker-Bridging-Header.h"; sourceTree = "<group>"; };
A20000000000000000000017 /* des.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = des.c; path = ../native/common/des.c; sourceTree = SOURCE_ROOT; };
A20000000000000000000018 /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = des.h; path = ../native/common/des.h; sourceTree = SOURCE_ROOT; };
A20000000000000000000019 /* des_bruteforce.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; name = des_bruteforce.metal; path = ../native/metal/des_bruteforce.metal; sourceTree = SOURCE_ROOT; };
A20000000000000000000020 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
A20000000000000000000021 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -51,22 +47,31 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A40000000000000000000011 /* Metal.framework in Frameworks */, A40000000000000000000012 /* Metal.framework in Frameworks */,
A40000000000000000000012 /* QuartzCore.framework in Frameworks */, A40000000000000000000013 /* QuartzCore.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
A30000000000000000000000 = {
isa = PBXGroup;
children = (
A30000000000000000000001 /* DESCracker */,
A30000000000000000000007 /* Frameworks */,
A30000000000000000000006 /* Products */,
);
sourceTree = "<group>";
};
A30000000000000000000001 /* DESCracker */ = { A30000000000000000000001 /* DESCracker */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000001 /* main.m */, A20000000000000000000001 /* AppDelegate.swift */,
A20000000000000000000002 /* AppDelegate.h */, A20000000000000000000002 /* SceneDelegate.swift */,
A20000000000000000000003 /* AppDelegate.m */, A20000000000000000000016 /* DESCracker-Bridging-Header.h */,
A20000000000000000000004 /* Info.plist */, A20000000000000000000012 /* Info.plist */,
A20000000000000000000005 /* Assets.xcassets */, A20000000000000000000013 /* Assets.xcassets */,
A30000000000000000000002 /* UI */, A30000000000000000000002 /* UI */,
A30000000000000000000003 /* Network */, A30000000000000000000003 /* Network */,
A30000000000000000000004 /* Compute */, A30000000000000000000004 /* Compute */,
@ -78,8 +83,7 @@
A30000000000000000000002 /* UI */ = { A30000000000000000000002 /* UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000006 /* PSCSlaveViewController.h */, A20000000000000000000003 /* SlaveViewController.swift */,
A20000000000000000000007 /* PSCSlaveViewController.m */,
); );
path = UI; path = UI;
sourceTree = "<group>"; sourceTree = "<group>";
@ -87,10 +91,8 @@
A30000000000000000000003 /* Network */ = { A30000000000000000000003 /* Network */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000008 /* PSCProtocol.h */, A20000000000000000000004 /* Protocol.swift */,
A20000000000000000000009 /* PSCProtocol.m */, A20000000000000000000005 /* SlaveClient.swift */,
A20000000000000000000010 /* PSCSlaveClient.h */,
A20000000000000000000011 /* PSCSlaveClient.m */,
); );
path = Network; path = Network;
sourceTree = "<group>"; sourceTree = "<group>";
@ -98,11 +100,9 @@
A30000000000000000000004 /* Compute */ = { A30000000000000000000004 /* Compute */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000012 /* PSCBruteEngine.h */, A20000000000000000000006 /* BruteEngine.swift */,
A20000000000000000000013 /* PSCMetalEngine.h */, A20000000000000000000007 /* MetalEngine.swift */,
A20000000000000000000014 /* PSCMetalEngine.m */, A20000000000000000000008 /* CpuEngine.swift */,
A20000000000000000000015 /* PSCCpuEngine.h */,
A20000000000000000000016 /* PSCCpuEngine.m */,
); );
path = Compute; path = Compute;
sourceTree = "<group>"; sourceTree = "<group>";
@ -110,9 +110,9 @@
A30000000000000000000005 /* Native */ = { A30000000000000000000005 /* Native */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000017 /* des.c */, A20000000000000000000009 /* des.c */,
A20000000000000000000018 /* des.h */, A20000000000000000000010 /* des.h */,
A20000000000000000000019 /* des_bruteforce.metal */, A20000000000000000000011 /* des_bruteforce.metal */,
); );
name = Native; name = Native;
sourceTree = "<group>"; sourceTree = "<group>";
@ -128,21 +128,12 @@
A30000000000000000000007 /* Frameworks */ = { A30000000000000000000007 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A20000000000000000000020 /* Metal.framework */, A20000000000000000000014 /* Metal.framework */,
A20000000000000000000021 /* QuartzCore.framework */, A20000000000000000000015 /* QuartzCore.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A30000000000000000000000 /* Root */ = {
isa = PBXGroup;
children = (
A30000000000000000000001 /* DESCracker */,
A30000000000000000000007 /* Frameworks */,
A30000000000000000000006 /* Products */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -174,6 +165,7 @@
TargetAttributes = { TargetAttributes = {
A10000000000000000000002 = { A10000000000000000000002 = {
CreatedOnToolsVersion = 16.0; CreatedOnToolsVersion = 16.0;
LastSwiftMigration = 1600;
}; };
}; };
}; };
@ -185,7 +177,7 @@
en, en,
Base, Base,
); );
mainGroup = A30000000000000000000000 /* Root */; mainGroup = A30000000000000000000000;
productRefGroup = A30000000000000000000006 /* Products */; productRefGroup = A30000000000000000000006 /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@ -200,7 +192,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A40000000000000000000010 /* Assets.xcassets in Resources */, A40000000000000000000011 /* Assets.xcassets in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -211,15 +203,16 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A40000000000000000000001 /* main.m in Sources */, A40000000000000000000001 /* AppDelegate.swift in Sources */,
A40000000000000000000002 /* AppDelegate.m in Sources */, A40000000000000000000002 /* SceneDelegate.swift in Sources */,
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */, A40000000000000000000003 /* SlaveViewController.swift in Sources */,
A40000000000000000000004 /* PSCProtocol.m in Sources */, A40000000000000000000004 /* Protocol.swift in Sources */,
A40000000000000000000005 /* PSCSlaveClient.m in Sources */, A40000000000000000000005 /* SlaveClient.swift in Sources */,
A40000000000000000000006 /* PSCMetalEngine.m in Sources */, A40000000000000000000006 /* BruteEngine.swift in Sources */,
A40000000000000000000007 /* PSCCpuEngine.m in Sources */, A40000000000000000000007 /* MetalEngine.swift in Sources */,
A40000000000000000000008 /* des.c in Sources */, A40000000000000000000008 /* CpuEngine.swift in Sources */,
A40000000000000000000009 /* des_bruteforce.metal in Sources */, A40000000000000000000009 /* des.c in Sources */,
A40000000000000000000010 /* des_bruteforce.metal in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -270,10 +263,14 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_COMPILER_FLAGS = "-fno-unroll-loops";
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
}; };
@ -315,9 +312,13 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_COMPILER_FLAGS = "-fno-unroll-loops";
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;
@ -327,16 +328,14 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B832M34SD9;
ENABLE_PREVIEWS = NO; ENABLE_PREVIEWS = NO;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/DESCracker",
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common", "$(SRCROOT)/../native/common",
); );
INFOPLIST_FILE = DESCracker/Info.plist; INFOPLIST_FILE = DESCracker/Info.plist;
@ -353,13 +352,10 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OBJC_BRIDGING_HEADER = "DESCracker/DESCracker-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = (
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
}; };
name = Debug; name = Debug;
}; };
@ -368,16 +364,14 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = B832M34SD9;
ENABLE_PREVIEWS = NO; ENABLE_PREVIEWS = NO;
GENERATE_INFOPLIST_FILE = NO; GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = ( HEADER_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"$(SRCROOT)/DESCracker",
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common", "$(SRCROOT)/../native/common",
); );
INFOPLIST_FILE = DESCracker/Info.plist; INFOPLIST_FILE = DESCracker/Info.plist;
@ -394,13 +388,9 @@
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OBJC_BRIDGING_HEADER = "DESCracker/DESCracker-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
USER_HEADER_SEARCH_PATHS = (
"$(SRCROOT)/DESCracker/UI",
"$(SRCROOT)/DESCracker/Network",
"$(SRCROOT)/DESCracker/Compute",
"$(SRCROOT)/../native/common",
);
}; };
name = Release; name = Release;
}; };

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -1,5 +0,0 @@
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *window;
@end

View File

@ -1,14 +0,0 @@
#import "AppDelegate.h"
#import "PSCSlaveViewController.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
self.window.backgroundColor = [UIColor colorWithRed:0x10 / 255.0 green:0x15 / 255.0 blue:0x1c / 255.0 alpha:1];
self.window.rootViewController = [[PSCSlaveViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
@end

View File

@ -0,0 +1,21 @@
import UIKit
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
true
}
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
config.delegateClass = SceneDelegate.self
return config
}
}

View File

@ -0,0 +1,22 @@
import Foundation
enum EngineError: LocalizedError {
case message(String)
var errorDescription: String? {
switch self {
case .message(let text):
return text
}
}
}
protocol BruteEngine: AnyObject {
func runAssign(
_ block: AssignBlock,
onProgress: ((UInt64, UInt64, Double) -> Void)?,
onHit: ((UInt64, UInt64, UInt64) -> Void)?,
completion: ((Int, TimeInterval, Bool, Error?) -> Void)?
)
func cancel()
}

View File

@ -0,0 +1,153 @@
import Foundation
import QuartzCore
final class CpuEngine: BruteEngine {
let coreCount: Int
private let workQueue = DispatchQueue(label: "com.descracker.cpu")
private var cancelled = false
private var generation: UInt64 = 0
init() {
let n = ProcessInfo.processInfo.processorCount
coreCount = max(1, n)
}
func prepare() throws {
if des_selftest() == 0 {
throw EngineError.message("CPU DES self-test failed")
}
}
func cancel() {
cancelled = true
generation &+= 1
}
func runAssign(
_ block: AssignBlock,
onProgress: ((UInt64, UInt64, Double) -> Void)?,
onHit: ((UInt64, UInt64, UInt64) -> Void)?,
completion: ((Int, TimeInterval, Bool, Error?) -> Void)?
) {
workQueue.async { [weak self] in
guard let self else { return }
self.cancelled = false
self.generation &+= 1
let generation = self.generation
let t0 = CACurrentMediaTime()
let keyLen = block.keyLen
let charset = Array(block.charset.utf8)
let padByte = block.padByte
let target = block.target
let start = block.start
let count = block.count
var workers = block.cpuWorkers.map { max(1, $0) } ?? self.coreCount
if workers == 0 { workers = 1 }
var fills = [UInt64](repeating: 0, count: 8)
var nFills: UInt32 = 0
for item in block.fills.prefix(8) {
fills[Int(nFills)] = item
nFills += 1
}
let done = Locked(UInt64(0))
let hits = Locked(0)
let cancelledFlag = Locked(false)
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .userInitiated)
let chunk = count / UInt64(workers)
let rem = count % UInt64(workers)
var cursor = start
for w in 0..<workers {
let n = chunk + (w < rem ? 1 : 0)
if n == 0 { continue }
let wstart = cursor
let wcount = n
cursor += n
group.enter()
pool.async {
defer { group.leave() }
var local: UInt64 = 0
charset.withUnsafeBufferPointer { csPtr in
fills.withUnsafeBufferPointer { fillPtr in
for i in 0..<wcount {
if local & 0x3FFF == 0 && (self.cancelled || self.generation != generation) {
cancelledFlag.value = true
break
}
let index = wstart + i
let key = make_key(
index,
keyLen,
UInt32(charset.count),
padByte,
csPtr.baseAddress
)
var sk = [UInt64](repeating: 0, count: 16)
sk.withUnsafeMutableBufferPointer { des_key_schedule(key, $0.baseAddress) }
let pt = sk.withUnsafeBufferPointer { des_crypt(target, $0.baseAddress, 1) }
if is_fill(pt, fillPtr.baseAddress, nFills) != 0 {
hits.value += 1
onHit?(index, key, pt)
}
local += 1
if local & 0x3FFF == 0 {
let soFar = done.add(0x4000)
var elapsed = CACurrentMediaTime() - t0
if elapsed < 1e-6 { elapsed = 1e-6 }
onProgress?(min(soFar, count), count, Double(soFar) / elapsed)
local = 0
}
}
}
}
if local > 0 {
_ = done.add(local)
}
}
}
group.wait()
let cancelled = cancelledFlag.value || self.cancelled || self.generation != generation
if !cancelled {
var elapsed = CACurrentMediaTime() - t0
if elapsed < 1e-6 { elapsed = 1e-6 }
onProgress?(count, count, Double(count) / elapsed)
}
completion?(hits.value, CACurrentMediaTime() - t0, cancelled, nil)
}
}
}
final class Locked<Value> {
private let lock = NSLock()
private var storage: Value
init(_ value: Value) {
storage = value
}
var value: Value {
get {
lock.lock()
defer { lock.unlock() }
return storage
}
set {
lock.lock()
storage = newValue
lock.unlock()
}
}
func add(_ amount: UInt64) -> UInt64 where Value == UInt64 {
lock.lock()
storage += amount
let result = storage
lock.unlock()
return result
}
}

View File

@ -0,0 +1,238 @@
import Foundation
import Metal
import QuartzCore
private let maxHits = 256
private let threadsPerGroup = 256
private let defaultBatch: UInt32 = 32768
private struct Hit {
var index: UInt64
var key: UInt64
var plain: UInt64
}
private struct Params {
var start: UInt64
var keyLen: UInt32
var charsetLen: UInt32
var padByte: UInt32
var fillCount: UInt32
var batchCount: UInt32
var pad: UInt32
var target: UInt64
var fills: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64)
}
final class MetalEngine: BruteEngine {
let gpuName: String
let memoryBytes: UInt64
var isAvailable: Bool { device != nil }
private let device: MTLDevice?
private var queue: MTLCommandQueue?
private var library: MTLLibrary?
private var brutePipeline: MTLComputePipelineState?
private let workQueue = DispatchQueue(label: "com.descracker.metal")
private var cancelled = false
private var activeJobId: String?
init() {
let device = MTLCreateSystemDefaultDevice()
self.device = device
if let device {
gpuName = device.name
memoryBytes = device.recommendedMaxWorkingSetSize
} else {
gpuName = "Metal GPU"
memoryBytes = 0
}
}
func cancel() {
cancelled = true
activeJobId = nil
}
func prepare() throws {
guard let device else {
throw EngineError.message("No Metal GPU")
}
try workQueue.sync {
try self.prepareOnWorkQueue(device: device)
}
}
private func prepareOnWorkQueue(device: MTLDevice) throws {
if queue != nil { return }
guard let lib = device.makeDefaultLibrary() else {
throw EngineError.message("Failed to load Metal library")
}
guard lib.makeFunction(name: "des_brute") != nil else {
throw EngineError.message("Metal kernel missing (des_brute)")
}
guard let queue = device.makeCommandQueue() else {
throw EngineError.message("Failed to create Metal command queue")
}
library = lib
self.queue = queue
}
private func ensureBrutePipeline() throws {
if brutePipeline != nil { return }
guard let library, let device else {
throw EngineError.message("Metal engine is not ready")
}
guard let brute = library.makeFunction(name: "des_brute") else {
throw EngineError.message("Metal kernel missing (des_brute)")
}
brutePipeline = try device.makeComputePipelineState(function: brute)
}
private func currentBatchSize() -> UInt32 {
switch ProcessInfo.processInfo.thermalState {
case .critical: return 4096
case .serious: return 8192
default: return defaultBatch
}
}
func runAssign(
_ block: AssignBlock,
onProgress: ((UInt64, UInt64, Double) -> Void)?,
onHit: ((UInt64, UInt64, UInt64) -> Void)?,
completion: ((Int, TimeInterval, Bool, Error?) -> Void)?
) {
workQueue.async { [weak self] in
guard let self else { return }
self.cancelled = false
self.activeJobId = block.jobId
let jobId = block.jobId
let t0 = CACurrentMediaTime()
var hits = 0
var runError: Error?
guard let device = self.device else {
completion?(0, 0, false, EngineError.message("No Metal GPU"))
return
}
do {
try self.prepareOnWorkQueue(device: device)
try self.ensureBrutePipeline()
} catch {
completion?(0, 0, false, error)
return
}
guard let queue = self.queue, let pipeline = self.brutePipeline else {
completion?(0, 0, false, EngineError.message("Metal engine is not ready"))
return
}
let charset = Array(block.charset.utf8)
var fills = [UInt64](repeating: 0, count: 8)
var nFills = 0
for item in block.fills.prefix(8) {
fills[nFills] = item
nFills += 1
}
var charsetStorage = charset
if charsetStorage.isEmpty {
charsetStorage = [0]
}
guard let paramsBuf = device.makeBuffer(length: 104, options: .storageModeShared),
let charsetBuf = charsetStorage.withUnsafeBytes({ raw in
device.makeBuffer(
bytes: raw.baseAddress!,
length: max(charset.count, 1),
options: .storageModeShared
)
}),
let hitsBuf = device.makeBuffer(length: maxHits * MemoryLayout<Hit>.stride, options: .storageModeShared),
let countBuf = device.makeBuffer(length: 4, options: .storageModeShared)
else {
completion?(0, 0, false, EngineError.message("Failed to allocate Metal buffers"))
return
}
var done: UInt64 = 0
let count = block.count
var cancelled = false
while done < count {
autoreleasepool {
if self.cancelled || self.activeJobId != jobId {
cancelled = true
return
}
let batch = self.currentBatchSize()
let remaining = count - done
let n = UInt64(min(UInt64(batch), remaining))
countBuf.contents().assumingMemoryBound(to: UInt32.self).pointee = 0
var params = Params(
start: block.start + done,
keyLen: block.keyLen,
charsetLen: UInt32(charset.count),
padByte: block.padByte,
fillCount: UInt32(nFills),
batchCount: UInt32(n),
pad: 0,
target: block.target,
fills: (
fills[0], fills[1], fills[2], fills[3],
fills[4], fills[5], fills[6], fills[7]
)
)
withUnsafeBytes(of: &params) { src in
paramsBuf.contents().copyMemory(from: src.baseAddress!, byteCount: 104)
}
guard let cmd = queue.makeCommandBuffer(),
let enc = cmd.makeComputeCommandEncoder()
else {
runError = EngineError.message("Failed to encode Metal command")
cancelled = true
return
}
enc.setComputePipelineState(pipeline)
enc.setBuffer(paramsBuf, offset: 0, index: 0)
enc.setBuffer(charsetBuf, offset: 0, index: 1)
enc.setBuffer(hitsBuf, offset: 0, index: 2)
enc.setBuffer(countBuf, offset: 0, index: 3)
var tpg = min(threadsPerGroup, pipeline.maxTotalThreadsPerThreadgroup)
if tpg == 0 { tpg = 1 }
let groups = (Int(n) + tpg - 1) / tpg
enc.dispatchThreadgroups(
MTLSize(width: groups, height: 1, depth: 1),
threadsPerThreadgroup: MTLSize(width: tpg, height: 1, depth: 1)
)
enc.endEncoding()
cmd.commit()
cmd.waitUntilCompleted()
if let error = cmd.error {
runError = error
cancelled = true
return
}
var hitn = Int(countBuf.contents().assumingMemoryBound(to: UInt32.self).pointee)
if hitn > maxHits { hitn = maxHits }
let outHits = hitsBuf.contents().assumingMemoryBound(to: Hit.self)
for i in 0..<hitn {
hits += 1
onHit?(outHits[i].index, outHits[i].key, outHits[i].plain)
}
done += n
var elapsed = CACurrentMediaTime() - t0
if elapsed < 1e-6 { elapsed = 1e-6 }
onProgress?(done, count, Double(done) / elapsed)
}
if runError != nil || cancelled { break }
}
if self.activeJobId == jobId {
self.activeJobId = nil
}
completion?(hits, CACurrentMediaTime() - t0, cancelled && runError == nil, runError)
}
}
}

View File

@ -1,20 +0,0 @@
#import <Foundation/Foundation.h>
@class PSCAssignBlock;
NS_ASSUME_NONNULL_BEGIN
typedef void (^PSCBruteProgressBlock)(uint64_t done, uint64_t count, double rate);
typedef void (^PSCBruteHitBlock)(uint64_t index, uint64_t key, uint64_t plain);
typedef void (^PSCBruteFinishBlock)(NSInteger hits, NSTimeInterval elapsed, BOOL cancelled, NSError *_Nullable error);
@protocol PSCBruteEngine <NSObject>
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(nullable PSCBruteProgressBlock)onProgress
onHit:(nullable PSCBruteHitBlock)onHit
completion:(nullable PSCBruteFinishBlock)completion;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,7 +0,0 @@
#import <Foundation/Foundation.h>
#import "PSCBruteEngine.h"
@interface PSCCpuEngine : NSObject <PSCBruteEngine>
@property (nonatomic, assign, readonly) NSUInteger coreCount;
- (BOOL)prepare:(NSError **)error;
@end

View File

@ -1,155 +0,0 @@
#import "PSCCpuEngine.h"
#import "PSCProtocol.h"
#import "des.h"
#import <stdatomic.h>
#import <string.h>
#import <QuartzCore/QuartzCore.h>
@interface PSCCpuEngine () {
atomic_bool _cancelled;
atomic_ullong _generation;
}
@property (nonatomic, strong) dispatch_queue_t workQueue;
@end
@implementation PSCCpuEngine
- (instancetype)init {
self = [super init];
if (self) {
NSUInteger n = NSProcessInfo.processInfo.processorCount;
_coreCount = n > 0 ? n : 1;
_workQueue = dispatch_queue_create("com.descracker.cpu", DISPATCH_QUEUE_SERIAL);
atomic_init(&_cancelled, false);
atomic_init(&_generation, 0);
}
return self;
}
- (BOOL)prepare:(NSError **)error {
if (!des_selftest()) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:6 userInfo:@{
NSLocalizedDescriptionKey : @"CPU DES self-test failed"
}];
}
return NO;
}
return YES;
}
- (void)cancel {
atomic_store(&_cancelled, true);
atomic_fetch_add(&_generation, 1);
}
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(PSCBruteProgressBlock)onProgress
onHit:(PSCBruteHitBlock)onHit
completion:(PSCBruteFinishBlock)completion {
dispatch_async(self.workQueue, ^{
atomic_store(&self->_cancelled, false);
uint64_t generation = atomic_fetch_add(&self->_generation, 1) + 1;
NSTimeInterval t0 = CACurrentMediaTime();
uint32_t key_len = block.keyLen;
NSData *charsetData = [block.charset dataUsingEncoding:NSUTF8StringEncoding] ?: [NSData data];
const uint8_t *cs = charsetData.bytes;
uint32_t clen = (uint32_t)charsetData.length;
uint32_t pad_byte = block.padByte;
uint64_t target = block.target;
uint64_t start = block.start;
uint64_t count = block.count;
NSUInteger workers = block.cpuWorkers ? (NSUInteger)MAX(1, block.cpuWorkers.integerValue) : self.coreCount;
if (workers == 0) {
workers = 1;
}
NSMutableData *fillData = [NSMutableData dataWithLength:sizeof(uint64_t) * 8];
uint64_t *fills = fillData.mutableBytes;
uint32_t nfills = 0;
for (NSNumber *item in block.fills) {
if (nfills >= 8) {
break;
}
fills[nfills++] = item.unsignedLongLongValue;
}
__block atomic_ullong done;
atomic_init(&done, 0);
__block NSInteger hits = 0;
__block atomic_bool cancelledFlag;
atomic_init(&cancelledFlag, false);
NSLock *hitLock = [[NSLock alloc] init];
dispatch_queue_t pool = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
dispatch_group_t group = dispatch_group_create();
uint64_t chunk = count / workers;
uint64_t rem = count % workers;
uint64_t cursor = start;
for (NSUInteger w = 0; w < workers; w++) {
uint64_t n = chunk + (w < rem ? 1 : 0);
if (n == 0) {
continue;
}
uint64_t wstart = cursor;
uint64_t wcount = n;
cursor += n;
dispatch_group_async(group, pool, ^{
uint64_t local = 0;
for (uint64_t i = 0; i < wcount; i++) {
if ((local & 0x3FFF) == 0 &&
(atomic_load(&self->_cancelled) || atomic_load(&self->_generation) != generation)) {
atomic_store(&cancelledFlag, true);
break;
}
uint64_t index = wstart + i;
uint64_t key = make_key(index, key_len, clen, pad_byte, cs);
uint64_t sk[16];
des_key_schedule(key, sk);
uint64_t pt = des_crypt(target, sk, 1);
if (is_fill(pt, fills, nfills)) {
[hitLock lock];
hits += 1;
[hitLock unlock];
if (onHit) {
onHit(index, key, pt);
}
}
local++;
if ((local & 0x3FFF) == 0) {
uint64_t soFar = atomic_fetch_add(&done, 0x4000) + 0x4000;
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
if (onProgress) {
onProgress(soFar > count ? count : soFar, count, soFar / elapsed);
}
local = 0;
}
}
if (local) {
atomic_fetch_add(&done, local);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
BOOL cancelled = atomic_load(&cancelledFlag) || atomic_load(&self->_cancelled) ||
atomic_load(&self->_generation) != generation;
if (!cancelled && onProgress) {
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
onProgress(count, count, count / elapsed);
}
if (completion) {
completion(hits, CACurrentMediaTime() - t0, cancelled, nil);
}
});
}
@end

View File

@ -1,9 +0,0 @@
#import <Foundation/Foundation.h>
#import "PSCBruteEngine.h"
@interface PSCMetalEngine : NSObject <PSCBruteEngine>
@property (nonatomic, readonly) BOOL available;
@property (nonatomic, copy, readonly) NSString *gpuName;
@property (nonatomic, assign, readonly) uint64_t memoryBytes;
- (BOOL)prepare:(NSError **)error;
@end

View File

@ -1,266 +0,0 @@
#import "PSCMetalEngine.h"
#import "PSCProtocol.h"
#import <Metal/Metal.h>
#import <stdatomic.h>
#import <string.h>
#import <QuartzCore/QuartzCore.h>
enum { kPSCMaxHits = 256, kPSCThreadsPerGroup = 256, kPSCParamsSize = 104, kPSCDefaultBatch = 1048576 };
typedef struct {
uint64_t index;
uint64_t key;
uint64_t plain;
} PSCHit;
@interface PSCMetalEngine () {
atomic_bool _cancelled;
}
@property (nonatomic, strong) id<MTLDevice> device;
@property (nonatomic, strong) id<MTLCommandQueue> queue;
@property (nonatomic, strong) id<MTLComputePipelineState> brutePipeline;
@property (nonatomic, strong) dispatch_queue_t workQueue;
@property (nonatomic, copy) NSString *activeJobId;
@end
@implementation PSCMetalEngine
- (instancetype)init {
self = [super init];
if (self) {
_device = MTLCreateSystemDefaultDevice();
_workQueue = dispatch_queue_create("com.descracker.metal", DISPATCH_QUEUE_SERIAL);
atomic_init(&_cancelled, false);
if (_device) {
_gpuName = [_device.name copy] ?: @"Metal GPU";
if ([_device respondsToSelector:@selector(recommendedMaxWorkingSetSize)]) {
_memoryBytes = (uint64_t)_device.recommendedMaxWorkingSetSize;
}
} else {
_gpuName = @"Metal GPU";
}
}
return self;
}
- (BOOL)available {
return self.device != nil && self.brutePipeline != nil;
}
static void PSCWriteParams(void *dst, uint64_t start, uint32_t key_len, uint32_t charset_len, uint32_t pad_byte,
uint32_t fill_count, uint32_t batch_count, uint64_t target, const uint64_t *fills,
int nfills) {
uint8_t *p = dst;
memcpy(p, &start, 8);
p += 8;
memcpy(p, &key_len, 4);
p += 4;
memcpy(p, &charset_len, 4);
p += 4;
memcpy(p, &pad_byte, 4);
p += 4;
memcpy(p, &fill_count, 4);
p += 4;
memcpy(p, &batch_count, 4);
p += 4;
uint32_t z = 0;
memcpy(p, &z, 4);
p += 4;
memcpy(p, &target, 8);
p += 8;
for (int i = 0; i < 8; i++) {
uint64_t f = i < nfills ? fills[i] : 0;
memcpy(p, &f, 8);
p += 8;
}
}
- (uint32_t)currentBatchSize {
NSProcessInfoThermalState thermal = NSProcessInfo.processInfo.thermalState;
if (thermal >= NSProcessInfoThermalStateCritical) {
return 262144;
}
if (thermal >= NSProcessInfoThermalStateSerious) {
return 524288;
}
return kPSCDefaultBatch;
}
- (BOOL)prepare:(NSError **)error {
if (!self.device) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:1 userInfo:@{
NSLocalizedDescriptionKey : @"No Metal GPU"
}];
}
return NO;
}
NSError *err = nil;
id<MTLLibrary> lib = [self.device newDefaultLibrary];
if (!lib) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:2 userInfo:@{
NSLocalizedDescriptionKey : @"Failed to load Metal library"
}];
}
return NO;
}
id<MTLFunction> known = [lib newFunctionWithName:@"des_known_answer"];
id<MTLFunction> brute = [lib newFunctionWithName:@"des_brute"];
if (!known || !brute) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:3 userInfo:@{
NSLocalizedDescriptionKey : @"Metal kernels missing (des_known_answer / des_brute)"
}];
}
return NO;
}
id<MTLComputePipelineState> knownPipe = [self.device newComputePipelineStateWithFunction:known error:&err];
if (!knownPipe) {
if (error) {
*error = err;
}
return NO;
}
self.brutePipeline = [self.device newComputePipelineStateWithFunction:brute error:&err];
if (!self.brutePipeline) {
if (error) {
*error = err;
}
return NO;
}
self.queue = [self.device newCommandQueue];
id<MTLBuffer> out = [self.device newBufferWithLength:16 options:MTLResourceStorageModeShared];
id<MTLCommandBuffer> cmd = [self.queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cmd computeCommandEncoder];
[enc setComputePipelineState:knownPipe];
[enc setBuffer:out offset:0 atIndex:0];
[enc dispatchThreads:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)];
[enc endEncoding];
[cmd commit];
[cmd waitUntilCompleted];
uint64_t *words = out.contents;
uint64_t expected = 0x85E813540F0AB405ULL;
uint64_t plain = 0x0123456789ABCDEFULL;
if (words[0] != expected || words[1] != plain) {
if (error) {
*error = [NSError errorWithDomain:@"DESCracker" code:4 userInfo:@{
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Metal DES self-test failed ct=%016llx",
(unsigned long long)words[0]]
}];
}
self.brutePipeline = nil;
return NO;
}
return YES;
}
- (void)cancel {
atomic_store(&_cancelled, true);
self.activeJobId = nil;
}
- (void)runAssign:(PSCAssignBlock *)block
onProgress:(PSCBruteProgressBlock)onProgress
onHit:(PSCBruteHitBlock)onHit
completion:(PSCBruteFinishBlock)completion {
dispatch_async(self.workQueue, ^{
atomic_store(&self->_cancelled, false);
self.activeJobId = block.jobId;
NSString *jobId = [block.jobId copy];
NSTimeInterval t0 = CACurrentMediaTime();
NSInteger hits = 0;
NSError *runError = nil;
if (!self.available) {
if (completion) {
completion(0, 0, NO, [NSError errorWithDomain:@"DESCracker" code:5 userInfo:@{
NSLocalizedDescriptionKey : @"Metal engine is not ready"
}]);
}
return;
}
const char *cs = block.charset.UTF8String ?: "";
uint32_t charset_len = (uint32_t)strlen(cs);
uint64_t fills[8] = {0};
int nfills = 0;
for (NSNumber *item in block.fills) {
if (nfills >= 8) {
break;
}
fills[nfills++] = item.unsignedLongLongValue;
}
id<MTLBuffer> paramsBuf = [self.device newBufferWithLength:kPSCParamsSize options:MTLResourceStorageModeShared];
id<MTLBuffer> charsetBuf = [self.device newBufferWithBytes:cs length:MAX(charset_len, 1)
options:MTLResourceStorageModeShared];
id<MTLBuffer> hitsBuf = [self.device newBufferWithLength:kPSCMaxHits * sizeof(PSCHit)
options:MTLResourceStorageModeShared];
id<MTLBuffer> countBuf = [self.device newBufferWithLength:4 options:MTLResourceStorageModeShared];
uint64_t done = 0;
uint64_t count = block.count;
BOOL cancelled = NO;
while (done < count) {
if (atomic_load(&self->_cancelled) || ![self.activeJobId isEqualToString:jobId]) {
cancelled = YES;
break;
}
uint32_t batch = [self currentBatchSize];
uint64_t remaining = count - done;
uint64_t n = batch < remaining ? batch : remaining;
*(uint32_t *)countBuf.contents = 0;
PSCWriteParams(paramsBuf.contents, block.start + done, block.keyLen, charset_len, block.padByte,
(uint32_t)nfills, (uint32_t)n, block.target, fills, nfills);
id<MTLCommandBuffer> cmd = [self.queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cmd computeCommandEncoder];
[enc setComputePipelineState:self.brutePipeline];
[enc setBuffer:paramsBuf offset:0 atIndex:0];
[enc setBuffer:charsetBuf offset:0 atIndex:1];
[enc setBuffer:hitsBuf offset:0 atIndex:2];
[enc setBuffer:countBuf offset:0 atIndex:3];
NSUInteger groups = (NSUInteger)((n + kPSCThreadsPerGroup - 1) / kPSCThreadsPerGroup);
[enc dispatchThreadgroups:MTLSizeMake(groups, 1, 1)
threadsPerThreadgroup:MTLSizeMake(kPSCThreadsPerGroup, 1, 1)];
[enc endEncoding];
[cmd commit];
[cmd waitUntilCompleted];
if (cmd.error) {
runError = cmd.error;
break;
}
uint32_t hitn = *(uint32_t *)countBuf.contents;
if (hitn > kPSCMaxHits) {
hitn = kPSCMaxHits;
}
PSCHit *outHits = hitsBuf.contents;
for (uint32_t i = 0; i < hitn; i++) {
hits += 1;
if (onHit) {
onHit(outHits[i].index, outHits[i].key, outHits[i].plain);
}
}
done += n;
double elapsed = CACurrentMediaTime() - t0;
if (elapsed < 1e-6) {
elapsed = 1e-6;
}
if (onProgress) {
onProgress(done, count, done / elapsed);
}
}
if ([self.activeJobId isEqualToString:jobId]) {
self.activeJobId = nil;
}
NSTimeInterval elapsed = CACurrentMediaTime() - t0;
if (completion) {
completion(hits, elapsed, cancelled, runError);
}
});
}
@end

View File

@ -0,0 +1 @@
#include "des.h"

View File

@ -31,6 +31,23 @@
</dict> </dict>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>DES Cracker connects to the desktop master on your local network to receive search work.</string> <string>DES Cracker connects to the desktop master on your local network to receive search work.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchScreen</key> <key>UILaunchScreen</key>
<dict> <dict>
<key>UIColorName</key> <key>UIColorName</key>

View File

@ -1,57 +0,0 @@
#import <Foundation/Foundation.h>
#include <stdint.h>
NS_ASSUME_NONNULL_BEGIN
static const NSInteger kPSCProtocolVersion = 2;
static const NSInteger kPSCDefaultPort = 9876;
static const NSTimeInterval kPSCProgressInterval = 0.5;
static const NSTimeInterval kPSCPingInterval = 5.0;
NSString *PSCFormatDec64(uint64_t value);
NSString *PSCFormatHex64(uint64_t value);
NSString *PSCFormatCount(uint64_t value);
NSString *PSCFormatRate(double rate);
uint64_t PSCParseDec64(id value);
uint64_t PSCParseHex64(NSString *hex);
@interface PSCComputeDevice : NSObject
@property (nonatomic, copy) NSString *key;
@property (nonatomic, assign) NSInteger deviceId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *type;
@property (nonatomic, strong, nullable) NSNumber *cores;
@property (nonatomic, strong, nullable) NSNumber *memory;
- (NSDictionary *)JSONObject;
@end
@interface PSCAssignBlock : NSObject
@property (nonatomic, copy) NSString *jobId;
@property (nonatomic, copy) NSString *deviceKey;
@property (nonatomic, assign) uint64_t start;
@property (nonatomic, assign) uint64_t count;
@property (nonatomic, assign) uint32_t keyLen;
@property (nonatomic, copy) NSString *charset;
@property (nonatomic, assign) uint32_t padByte;
@property (nonatomic, assign) uint64_t target;
@property (nonatomic, copy) NSArray<NSNumber *> *fills;
@property (nonatomic, strong, nullable) NSNumber *cpuWorkers;
+ (nullable instancetype)fromJSON:(NSDictionary *)json;
@end
@interface PSCWorkerSnapshot : NSObject
@property (nonatomic, copy) NSString *deviceKey;
@property (nonatomic, copy) NSString *deviceName;
@property (nonatomic, copy) NSString *deviceType;
@property (nonatomic, copy) NSString *current;
@property (nonatomic, assign) double pct;
@property (nonatomic, assign) NSInteger completedBlocks;
@property (nonatomic, assign) double rate;
@property (nonatomic, assign) BOOL busy;
@end
NSDictionary *_Nullable PSCDecodeMessage(NSString *raw);
NSString *_Nullable PSCEncodeMessage(NSDictionary *msg);
NS_ASSUME_NONNULL_END

View File

@ -1,146 +0,0 @@
#import "PSCProtocol.h"
#import <math.h>
#import <stdlib.h>
NSString *PSCFormatDec64(uint64_t value) {
return [NSString stringWithFormat:@"%llu", (unsigned long long)value];
}
NSString *PSCFormatHex64(uint64_t value) {
return [NSString stringWithFormat:@"%016llx", (unsigned long long)value];
}
NSString *PSCFormatCount(uint64_t value) {
double n = (double)value;
if (n >= 1e9) {
return [NSString stringWithFormat:@"%.1fB", n / 1e9];
}
if (n >= 1e6) {
return [NSString stringWithFormat:@"%.1fM", n / 1e6];
}
if (n >= 1e3) {
return [NSString stringWithFormat:@"%.1fk", n / 1e3];
}
return [NSString stringWithFormat:@"%.0f", n];
}
NSString *PSCFormatRate(double rate) {
if (rate <= 0 || !isfinite(rate)) {
return @"—";
}
return [NSString stringWithFormat:@"%@/s", PSCFormatCount((uint64_t)llround(rate))];
}
uint64_t PSCParseDec64(id value) {
if ([value isKindOfClass:[NSString class]]) {
return strtoull([(NSString *)value UTF8String], NULL, 10);
}
if ([value isKindOfClass:[NSNumber class]]) {
return [(NSNumber *)value unsignedLongLongValue];
}
return 0;
}
uint64_t PSCParseHex64(NSString *hex) {
if (hex.length == 0) {
return 0;
}
const char *p = hex.UTF8String;
if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) {
p += 2;
}
return strtoull(p, NULL, 16);
}
@implementation PSCComputeDevice
- (NSDictionary *)JSONObject {
NSMutableDictionary *json = [@{
@"key" : self.key ?: @"",
@"id" : @(self.deviceId),
@"name" : self.name ?: @"",
@"type" : self.type ?: @"cpu",
} mutableCopy];
if (self.cores) {
json[@"cores"] = self.cores;
}
if (self.memory) {
json[@"memory"] = self.memory;
}
return json;
}
@end
@implementation PSCAssignBlock
+ (instancetype)fromJSON:(NSDictionary *)json {
if (![json isKindOfClass:[NSDictionary class]]) {
return nil;
}
PSCAssignBlock *block = [[PSCAssignBlock alloc] init];
block.jobId = [json[@"jobId"] isKindOfClass:[NSString class]] ? json[@"jobId"] : @"";
block.deviceKey = [json[@"deviceKey"] isKindOfClass:[NSString class]] ? json[@"deviceKey"] : @"";
block.start = PSCParseDec64(json[@"start"]);
block.count = PSCParseDec64(json[@"count"]);
block.keyLen = (uint32_t)PSCParseDec64(json[@"keyLen"]);
block.charset = [json[@"charset"] isKindOfClass:[NSString class]] ? json[@"charset"] : @"";
block.padByte = (uint32_t)(PSCParseDec64(json[@"padByte"]) & 0xFF);
id target = json[@"target"];
block.target = [target isKindOfClass:[NSString class]] ? PSCParseHex64(target) : PSCParseDec64(target);
NSMutableArray<NSNumber *> *fills = [NSMutableArray array];
id fillsVal = json[@"fills"];
if ([fillsVal isKindOfClass:[NSArray class]]) {
for (id item in (NSArray *)fillsVal) {
if (fills.count >= 8) {
break;
}
uint64_t f = [item isKindOfClass:[NSString class]] ? PSCParseHex64(item) : PSCParseDec64(item);
[fills addObject:@(f)];
}
}
block.fills = fills;
id workers = json[@"cpuWorkers"];
if ([workers isKindOfClass:[NSNumber class]]) {
block.cpuWorkers = workers;
}
if (block.jobId.length == 0 || block.count == 0 || block.keyLen == 0 || block.charset.length == 0) {
return nil;
}
return block;
}
@end
@implementation PSCWorkerSnapshot
@end
NSDictionary *PSCDecodeMessage(NSString *raw) {
if (raw.length == 0) {
return nil;
}
NSData *data = [raw dataUsingEncoding:NSUTF8StringEncoding];
if (!data) {
return nil;
}
id parsed = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (![parsed isKindOfClass:[NSDictionary class]]) {
return nil;
}
NSDictionary *msg = parsed;
if (![msg[@"type"] isKindOfClass:[NSString class]]) {
return nil;
}
return msg;
}
NSString *PSCEncodeMessage(NSDictionary *msg) {
if (!msg) {
return nil;
}
NSData *data = [NSJSONSerialization dataWithJSONObject:msg options:0 error:nil];
if (!data) {
return nil;
}
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

View File

@ -1,25 +0,0 @@
#import <Foundation/Foundation.h>
#import "PSCProtocol.h"
@class PSCSlaveClient;
@protocol PSCSlaveClientDelegate <NSObject>
- (void)slaveClient:(PSCSlaveClient *)client didChangeStatus:(NSString *)status connected:(BOOL)connected;
- (void)slaveClient:(PSCSlaveClient *)client didLog:(NSString *)line;
- (void)slaveClient:(PSCSlaveClient *)client didUpdateWorkers:(NSArray<PSCWorkerSnapshot *> *)workers;
- (void)slaveClient:(PSCSlaveClient *)client didHitJob:(NSString *)jobId index:(NSString *)index keyHex:(NSString *)keyHex;
@end
@interface PSCSlaveClient : NSObject
@property (nonatomic, weak) id<PSCSlaveClientDelegate> delegate;
@property (nonatomic, readonly, getter=isConnected) BOOL connected;
@property (nonatomic, assign) NSUInteger cpuWorkers;
@property (nonatomic, readonly) BOOL metalAvailable;
@property (nonatomic, copy, readonly) NSString *metalName;
@property (nonatomic, assign, readonly) uint64_t metalMemory;
@property (nonatomic, assign, readonly) NSUInteger cpuCores;
- (BOOL)prepareEngines:(NSError **)error;
- (void)setDevices:(NSArray<PSCComputeDevice *> *)devices;
- (void)connectToHost:(NSString *)host port:(NSInteger)port;
- (void)disconnect;
@end

View File

@ -1,604 +0,0 @@
#import "PSCSlaveClient.h"
#import "PSCCpuEngine.h"
#import "PSCMetalEngine.h"
@interface PSCDeviceJob : NSObject
@property (nonatomic, copy) NSString *jobId;
@property (nonatomic, assign) uint64_t done;
@property (nonatomic, assign) uint64_t count;
@property (nonatomic, assign) double rate;
@property (nonatomic, copy) void (^kill)(void);
@end
@implementation PSCDeviceJob
@end
@interface PSCRateStats : NSObject
@property (nonatomic, assign) uint64_t keysDone;
@property (nonatomic, assign) NSTimeInterval activeMs;
@property (nonatomic, strong) NSDate *busyStarted;
@end
@implementation PSCRateStats
@end
@interface PSCProgressGate : NSObject
@property (nonatomic, assign) NSTimeInterval lastSent;
@property (nonatomic, assign) BOOL timerPending;
@property (nonatomic, copy) NSDictionary *payload;
@end
@implementation PSCProgressGate
@end
@interface PSCSlaveClient () <NSURLSessionWebSocketDelegate>
@property (nonatomic, copy) NSArray<PSCComputeDevice *> *devices;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionWebSocketTask *task;
@property (nonatomic, strong) NSTimer *pingTimer;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCDeviceJob *> *jobs;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *completedBlocks;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCProgressGate *> *progressGates;
@property (nonatomic, strong) NSMutableDictionary<NSString *, PSCRateStats *> *rateStats;
@property (nonatomic, strong) PSCMetalEngine *metal;
@property (nonatomic, strong) PSCCpuEngine *cpu;
@property (nonatomic, strong) dispatch_queue_t syncQueue;
@property (nonatomic, copy) NSString *connectURL;
@end
@implementation PSCSlaveClient
- (instancetype)init {
self = [super init];
if (self) {
_devices = @[];
_jobs = [NSMutableDictionary dictionary];
_completedBlocks = [NSMutableDictionary dictionary];
_progressGates = [NSMutableDictionary dictionary];
_rateStats = [NSMutableDictionary dictionary];
_cpuWorkers = MAX(1, NSProcessInfo.processInfo.processorCount);
_metal = [[PSCMetalEngine alloc] init];
_cpu = [[PSCCpuEngine alloc] init];
_syncQueue = dispatch_queue_create("com.descracker.slave", DISPATCH_QUEUE_SERIAL);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.waitsForConnectivity = NO;
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return self;
}
- (BOOL)isConnected {
return self.task != nil && self.task.state == NSURLSessionTaskStateRunning;
}
- (BOOL)metalAvailable {
return self.metal.available;
}
- (NSString *)metalName {
return self.metal.gpuName;
}
- (uint64_t)metalMemory {
return self.metal.memoryBytes;
}
- (NSUInteger)cpuCores {
return self.cpu.coreCount;
}
- (BOOL)prepareEngines:(NSError **)error {
NSError *metalErr = nil;
BOOL metalOK = [self.metal prepare:&metalErr];
NSError *cpuErr = nil;
BOOL cpuOK = [self.cpu prepare:&cpuErr];
if (!metalOK && error) {
*error = metalErr;
}
if (!cpuOK) {
if (error && !metalErr) {
*error = cpuErr;
}
return NO;
}
return metalOK || cpuOK;
}
- (void)setDevices:(NSArray<PSCComputeDevice *> *)devices {
dispatch_async(self.syncQueue, ^{
self.devices = [devices copy] ?: @[];
for (PSCComputeDevice *device in self.devices) {
if (!self.completedBlocks[device.key]) {
self.completedBlocks[device.key] = @0;
}
}
[self emitWorkers];
});
}
- (void)connectToHost:(NSString *)host port:(NSInteger)port {
[self disconnect];
NSString *urlString = [NSString stringWithFormat:@"ws://%@:%ld", host, (long)port];
self.connectURL = urlString;
[self notifyStatus:[NSString stringWithFormat:@"Connecting to %@…", urlString] connected:NO];
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
[self log:[NSString stringWithFormat:@"Invalid URL %@", urlString]];
[self notifyStatus:@"Disconnected" connected:NO];
return;
}
self.task = [self.session webSocketTaskWithURL:url];
[self.task resume];
}
- (void)disconnect {
dispatch_sync(self.syncQueue, ^{
[self clearPing];
[self killAll];
});
NSURLSessionWebSocketTask *task = self.task;
self.task = nil;
if (task) {
[task cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure reason:nil];
}
dispatch_async(self.syncQueue, ^{
[self emitWorkers];
});
}
- (void)URLSession:(NSURLSession *)session
webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
didOpenWithProtocol:(NSString *)protocol {
(void)session;
(void)protocol;
if (webSocketTask != self.task) {
return;
}
dispatch_async(self.syncQueue, ^{
NSMutableArray *deviceJSON = [NSMutableArray array];
for (PSCComputeDevice *device in self.devices) {
[deviceJSON addObject:[device JSONObject]];
}
NSString *hostname = NSProcessInfo.processInfo.hostName ?: @"iOS";
[self send:@{
@"type" : @"hello",
@"hostname" : hostname,
@"platform" : @"ios arm64",
@"devices" : deviceJSON,
}];
NSString *exposed = self.devices.count == 0 ? @"no workers" : [[self.devices valueForKey:@"name"] componentsJoinedByString:@", "];
[self notifyStatus:[NSString stringWithFormat:@"Connected to %@", self.connectURL] connected:YES];
[self log:[NSString stringWithFormat:@"Hello sent (protocol %ld) — exposing %@", (long)kPSCProtocolVersion, exposed]];
[self emitWorkers];
dispatch_async(dispatch_get_main_queue(), ^{
[self.pingTimer invalidate];
__weak typeof(self) weakSelf = self;
self.pingTimer = [NSTimer scheduledTimerWithTimeInterval:kPSCPingInterval
repeats:YES
block:^(__unused NSTimer *timer) {
[weakSelf send:@{@"type" : @"ping"}];
}];
});
[self listen];
});
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
(void)session;
if (task != self.task && self.task != nil) {
return;
}
dispatch_async(self.syncQueue, ^{
[self clearPing];
[self killAll];
if (self.task == task) {
self.task = nil;
}
if (error && error.code != NSURLErrorCancelled) {
[self log:[NSString stringWithFormat:@"WebSocket error: %@", error.localizedDescription]];
}
[self notifyStatus:@"Disconnected" connected:NO];
[self emitWorkers];
});
}
- (void)listen {
NSURLSessionWebSocketTask *task = self.task;
if (!task) {
return;
}
__weak typeof(self) weakSelf = self;
[task receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) {
PSCSlaveClient *strong = weakSelf;
if (!strong || task != strong.task) {
return;
}
if (error) {
return;
}
if (message.type == NSURLSessionWebSocketMessageTypeString && message.string) {
NSDictionary *msg = PSCDecodeMessage(message.string);
if (msg) {
[strong onServer:msg];
}
}
[strong listen];
}];
}
- (void)onServer:(NSDictionary *)msg {
NSString *type = msg[@"type"];
if ([type isEqualToString:@"hello_ack"]) {
[self log:[NSString stringWithFormat:@"Master ack v%@", msg[@"protocolVersion"] ?: @"?"]];
return;
}
if ([type isEqualToString:@"assign"]) {
PSCAssignBlock *block = [PSCAssignBlock fromJSON:msg];
if (!block) {
[self log:@"Ignored malformed assign"];
return;
}
[self runAssign:block];
return;
}
if ([type isEqualToString:@"cancel"]) {
dispatch_async(self.syncQueue, ^{
[self killAll];
[self log:@"Cancel received"];
[self notifyStatus:@"Idle (cancelled)" connected:YES];
[self emitWorkers];
});
return;
}
if ([type isEqualToString:@"shutdown"]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self disconnect];
});
}
}
- (id<PSCBruteEngine>)engineForDevice:(PSCComputeDevice *)device {
if ([device.type isEqualToString:@"metal"]) {
return self.metal;
}
return self.cpu;
}
- (PSCComputeDevice *)deviceForKey:(NSString *)key {
for (PSCComputeDevice *device in self.devices) {
if ([device.key isEqualToString:key]) {
return device;
}
}
for (PSCComputeDevice *device in self.devices) {
if ([device.type isEqualToString:@"metal"]) {
return device;
}
}
return self.devices.firstObject;
}
- (void)runAssign:(PSCAssignBlock *)block {
dispatch_async(self.syncQueue, ^{
PSCComputeDevice *matched = nil;
for (PSCComputeDevice *device in self.devices) {
if ([device.key isEqualToString:block.deviceKey]) {
matched = device;
break;
}
}
PSCComputeDevice *device = matched ?: [self deviceForKey:block.deviceKey];
if (!device) {
[self send:@{@"type" : @"error", @"jobId" : block.jobId, @"message" : @"no compute device selected"}];
return;
}
if (!matched) {
[self log:[NSString stringWithFormat:@"No device %@; using %@", block.deviceKey, device.name]];
}
PSCDeviceJob *existing = self.jobs[device.key];
if (existing.kill) {
existing.kill();
}
[self clearProgressGate:device.key];
[self log:[NSString stringWithFormat:@"Assigned %@ len=%u pad=0x%02x start=%llu count=%llu", device.name,
block.keyLen, block.padByte, (unsigned long long)block.start,
(unsigned long long)block.count]];
NSDate *started = [NSDate date];
__block NSInteger hitCount = 0;
id<PSCBruteEngine> engine = [self engineForDevice:device];
NSString *deviceKey = device.key;
NSString *jobId = block.jobId;
PSCDeviceJob *job = [[PSCDeviceJob alloc] init];
job.jobId = jobId;
job.done = 0;
job.count = block.count;
job.rate = 0;
__weak typeof(self) weakSelf = self;
job.kill = ^{
[engine cancel];
};
self.jobs[deviceKey] = job;
[self beginRate:deviceKey];
[self updateStatus];
[self emitWorkers];
PSCBruteProgressBlock onProgress = ^(uint64_t done, uint64_t count, double rate) {
dispatch_async(weakSelf.syncQueue, ^{
PSCDeviceJob *live = weakSelf.jobs[deviceKey];
if (live && [live.jobId isEqualToString:jobId]) {
live.done = done;
live.count = count;
live.rate = rate;
}
[weakSelf sendProgressJob:jobId deviceKey:deviceKey done:done count:count rate:rate];
[weakSelf updateStatus];
[weakSelf emitWorkers];
});
};
PSCBruteHitBlock onHit = ^(uint64_t index, uint64_t key, uint64_t plain) {
dispatch_async(weakSelf.syncQueue, ^{
hitCount += 1;
NSString *indexS = PSCFormatDec64(index);
NSString *keyHex = PSCFormatHex64(key);
NSString *plainHex = PSCFormatHex64(plain);
[weakSelf send:@{
@"type" : @"hit",
@"jobId" : jobId,
@"index" : indexS,
@"keyHex" : keyHex,
@"plainHex" : plainHex,
}];
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.delegate slaveClient:weakSelf didHitJob:jobId index:indexS keyHex:keyHex];
});
});
};
[engine runAssign:block
onProgress:onProgress
onHit:onHit
completion:^(NSInteger hits, NSTimeInterval elapsed, BOOL cancelled, NSError *error) {
(void)hits;
dispatch_async(weakSelf.syncQueue, ^{
PSCDeviceJob *still = weakSelf.jobs[deviceKey];
BOOL same = still && [still.jobId isEqualToString:jobId];
if (cancelled) {
if (same) {
[weakSelf endRate:deviceKey keys:still.done];
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf clearProgressGate:deviceKey];
}
[weakSelf updateStatus];
[weakSelf emitWorkers];
return;
}
if (error) {
if (same) {
[weakSelf endRate:deviceKey keys:still.done];
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf clearProgressGate:deviceKey];
}
[weakSelf send:@{
@"type" : @"error",
@"jobId" : jobId,
@"message" : error.localizedDescription ?: @"brute failed",
}];
[weakSelf log:error.localizedDescription];
[weakSelf updateStatus];
[weakSelf emitWorkers];
return;
}
if (same) {
[weakSelf.jobs removeObjectForKey:deviceKey];
[weakSelf endRate:deviceKey keys:block.count];
NSInteger completed = weakSelf.completedBlocks[deviceKey].integerValue + 1;
weakSelf.completedBlocks[deviceKey] = @(completed);
[weakSelf clearProgressGate:deviceKey];
[weakSelf send:@{
@"type" : @"block_complete",
@"jobId" : jobId,
@"deviceKey" : deviceKey,
@"hits" : @(hitCount),
@"elapsed" : @(elapsed > 0 ? elapsed : ([NSDate date].timeIntervalSince1970 - started.timeIntervalSince1970)),
}];
}
[weakSelf updateStatus];
[weakSelf emitWorkers];
});
}];
});
}
- (void)send:(NSDictionary *)msg {
NSString *json = PSCEncodeMessage(msg);
NSURLSessionWebSocketTask *task = self.task;
if (!json || !task || task.state != NSURLSessionTaskStateRunning) {
return;
}
NSURLSessionWebSocketMessage *message = [[NSURLSessionWebSocketMessage alloc] initWithString:json];
[task sendMessage:message completionHandler:^(NSError *error) {
if (error) {
[self log:[NSString stringWithFormat:@"Send failed: %@", error.localizedDescription]];
}
}];
}
- (PSCRateStats *)rateFor:(NSString *)deviceKey {
PSCRateStats *stats = self.rateStats[deviceKey];
if (!stats) {
stats = [[PSCRateStats alloc] init];
self.rateStats[deviceKey] = stats;
}
return stats;
}
- (void)beginRate:(NSString *)deviceKey {
PSCRateStats *stats = [self rateFor:deviceKey];
if (!stats.busyStarted) {
stats.busyStarted = [NSDate date];
}
}
- (void)endRate:(NSString *)deviceKey keys:(uint64_t)keys {
PSCRateStats *stats = [self rateFor:deviceKey];
if (stats.busyStarted) {
stats.activeMs += [[NSDate date] timeIntervalSinceDate:stats.busyStarted] * 1000.0;
stats.busyStarted = nil;
}
if (keys > 0) {
stats.keysDone += keys;
}
}
- (double)averageRate:(NSString *)deviceKey liveDone:(uint64_t)liveDone {
PSCRateStats *stats = [self rateFor:deviceKey];
NSTimeInterval liveMs = stats.busyStarted ? [[NSDate date] timeIntervalSinceDate:stats.busyStarted] * 1000.0 : 0;
double sec = (stats.activeMs + liveMs) / 1000.0;
uint64_t keys = stats.keysDone + liveDone;
if (sec <= 0 || keys == 0) {
return 0;
}
return keys / sec;
}
- (void)killAll {
NSArray<NSString *> *keys = self.jobs.allKeys;
for (NSString *deviceKey in keys) {
PSCDeviceJob *job = self.jobs[deviceKey];
[self endRate:deviceKey keys:job.done];
if (job.kill) {
job.kill();
}
[self clearProgressGate:deviceKey];
}
[self.jobs removeAllObjects];
}
- (void)clearPing {
dispatch_async(dispatch_get_main_queue(), ^{
[self.pingTimer invalidate];
self.pingTimer = nil;
});
}
- (PSCProgressGate *)progressGate:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate) {
gate = [[PSCProgressGate alloc] init];
self.progressGates[deviceKey] = gate;
}
return gate;
}
- (void)sendProgressJob:(NSString *)jobId
deviceKey:(NSString *)deviceKey
done:(uint64_t)done
count:(uint64_t)count
rate:(double)rate {
PSCProgressGate *gate = [self progressGate:deviceKey];
gate.payload = @{
@"type" : @"progress",
@"jobId" : jobId,
@"deviceKey" : deviceKey,
@"done" : PSCFormatDec64(done),
@"count" : PSCFormatDec64(count),
@"rate" : @(rate),
};
NSTimeInterval wait = kPSCProgressInterval - ([NSDate date].timeIntervalSince1970 - gate.lastSent);
if (wait <= 0) {
[self flushProgress:deviceKey];
return;
}
if (!gate.timerPending) {
gate.timerPending = YES;
NSString *key = [deviceKey copy];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(wait * NSEC_PER_SEC)), self.syncQueue, ^{
PSCProgressGate *live = self.progressGates[key];
live.timerPending = NO;
[self flushProgress:key];
});
}
}
- (void)flushProgress:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate.payload) {
return;
}
NSDictionary *payload = gate.payload;
PSCDeviceJob *live = self.jobs[deviceKey];
if (!live || ![live.jobId isEqualToString:payload[@"jobId"]]) {
gate.payload = nil;
return;
}
gate.payload = nil;
gate.lastSent = [NSDate date].timeIntervalSince1970;
[self send:payload];
}
- (void)clearProgressGate:(NSString *)deviceKey {
PSCProgressGate *gate = self.progressGates[deviceKey];
if (!gate) {
return;
}
gate.timerPending = NO;
gate.payload = nil;
}
- (void)emitWorkers {
NSMutableArray<PSCWorkerSnapshot *> *rows = [NSMutableArray array];
for (PSCComputeDevice *device in self.devices) {
PSCDeviceJob *live = self.jobs[device.key];
uint64_t done = live.done;
uint64_t count = live.count;
double pct = live && count > 0 ? (double)done * 100.0 / (double)count : 0;
PSCWorkerSnapshot *row = [[PSCWorkerSnapshot alloc] init];
row.deviceKey = device.key;
row.deviceName = device.name;
row.deviceType = device.type;
row.current = live ? [NSString stringWithFormat:@"%@ / %@", PSCFormatCount(done), PSCFormatCount(count)] : @"idle";
row.pct = pct;
row.completedBlocks = self.completedBlocks[device.key].integerValue;
row.rate = live.rate;
row.busy = live != nil;
[rows addObject:row];
}
NSArray *snapshot = [rows copy];
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didUpdateWorkers:snapshot];
});
}
- (void)updateStatus {
NSArray<PSCDeviceJob *> *busy = self.jobs.allValues;
BOOL connected = self.task != nil;
if (busy.count == 0) {
[self notifyStatus:connected ? @"Idle" : @"Disconnected" connected:connected];
return;
}
if (busy.count == 1) {
PSCDeviceJob *job = busy.firstObject;
NSString *shortId = job.jobId.length > 8 ? [job.jobId substringToIndex:8] : job.jobId;
[self notifyStatus:[NSString stringWithFormat:@"Working %@ %llu/%llu", shortId, (unsigned long long)job.done,
(unsigned long long)job.count]
connected:YES];
return;
}
[self notifyStatus:[NSString stringWithFormat:@"%lu workers assigned", (unsigned long)busy.count] connected:YES];
}
- (void)notifyStatus:(NSString *)status connected:(BOOL)connected {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didChangeStatus:status connected:connected];
});
}
- (void)log:(NSString *)line {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate slaveClient:self didLog:line];
});
}
@end

View File

@ -0,0 +1,144 @@
import Foundation
enum Wire {
static let protocolVersion = 2
static let defaultPort = 9876
static let progressInterval: TimeInterval = 0.5
static let pingInterval: TimeInterval = 5.0
static func formatDec64(_ value: UInt64) -> String {
String(value)
}
static func formatHex64(_ value: UInt64) -> String {
String(format: "%016llx", value)
}
static func formatCount(_ value: UInt64) -> String {
let n = Double(value)
if n >= 1e9 { return String(format: "%.1fB", n / 1e9) }
if n >= 1e6 { return String(format: "%.1fM", n / 1e6) }
if n >= 1e3 { return String(format: "%.1fk", n / 1e3) }
return String(format: "%.0f", n)
}
static func formatRate(_ rate: Double) -> String {
guard rate > 0, rate.isFinite else { return "—" }
return "\(formatCount(UInt64(rate.rounded())))/s"
}
static func parseDec64(_ value: Any?) -> UInt64 {
if let string = value as? String {
return strtoull(string, nil, 10)
}
if let number = value as? NSNumber {
return number.uint64Value
}
return 0
}
static func parseHex64(_ hex: String) -> UInt64 {
guard !hex.isEmpty else { return 0 }
var parsed = hex
if parsed.hasPrefix("0x") || parsed.hasPrefix("0X") {
parsed = String(parsed.dropFirst(2))
}
return strtoull(parsed, nil, 16)
}
static func decodeMessage(_ raw: String) -> [String: Any]? {
guard let data = raw.data(using: .utf8),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
parsed["type"] is String
else {
return nil
}
return parsed
}
static func encodeMessage(_ msg: [String: Any]) -> String? {
guard JSONSerialization.isValidJSONObject(msg),
let data = try? JSONSerialization.data(withJSONObject: msg)
else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
final class ComputeDevice {
var key = ""
var deviceId = 0
var name = ""
var type = "cpu"
var cores: NSNumber?
var memory: NSNumber?
func jsonObject() -> [String: Any] {
var json: [String: Any] = [
"key": key,
"id": deviceId,
"name": name,
"type": type,
]
if let cores { json["cores"] = cores }
if let memory { json["memory"] = memory }
return json
}
}
final class AssignBlock {
var jobId = ""
var deviceKey = ""
var start: UInt64 = 0
var count: UInt64 = 0
var keyLen: UInt32 = 0
var charset = ""
var padByte: UInt32 = 0
var target: UInt64 = 0
var fills: [UInt64] = []
var cpuWorkers: Int?
static func fromJSON(_ json: [String: Any]) -> AssignBlock? {
let block = AssignBlock()
block.jobId = json["jobId"] as? String ?? ""
block.deviceKey = json["deviceKey"] as? String ?? ""
block.start = Wire.parseDec64(json["start"])
block.count = Wire.parseDec64(json["count"])
block.keyLen = UInt32(Wire.parseDec64(json["keyLen"]))
block.charset = json["charset"] as? String ?? ""
block.padByte = UInt32(Wire.parseDec64(json["padByte"]) & 0xFF)
if let target = json["target"] as? String {
block.target = Wire.parseHex64(target)
} else {
block.target = Wire.parseDec64(json["target"])
}
if let fillsVal = json["fills"] as? [Any] {
for item in fillsVal.prefix(8) {
if let hex = item as? String {
block.fills.append(Wire.parseHex64(hex))
} else {
block.fills.append(Wire.parseDec64(item))
}
}
}
if json["cpuWorkers"] != nil {
block.cpuWorkers = Int(Wire.parseDec64(json["cpuWorkers"]))
}
if block.jobId.isEmpty || block.count == 0 || block.keyLen == 0 || block.charset.isEmpty {
return nil
}
return block
}
}
final class WorkerSnapshot {
var deviceKey = ""
var deviceName = ""
var deviceType = ""
var current = "idle"
var pct = 0.0
var completedBlocks = 0
var rate = 0.0
var busy = false
}

View File

@ -0,0 +1,492 @@
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]?
}

View File

@ -0,0 +1,18 @@
import UIKit
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.backgroundColor = Palette.bg
window.rootViewController = SlaveViewController()
window.makeKeyAndVisible()
self.window = window
}
}

View File

@ -1,5 +0,0 @@
#import <UIKit/UIKit.h>
#import "PSCSlaveClient.h"
@interface PSCSlaveViewController : UIViewController
@end

View File

@ -1,619 +0,0 @@
#import "PSCSlaveViewController.h"
static NSString *const kPSCHostKey = @"psc.masterHost";
static NSString *const kPSCPortKey = @"psc.masterPort";
static NSString *const kPSCMetalKey = @"psc.metalOn";
static NSString *const kPSCCpuKey = @"psc.cpuOn";
static NSString *const kPSCWorkersKey = @"psc.cpuWorkers";
static UIColor *PSCColorBG(void) {
return [UIColor colorWithRed:0x10 / 255.0 green:0x15 / 255.0 blue:0x1c / 255.0 alpha:1];
}
static UIColor *PSCColorCard(void) {
return [UIColor colorWithRed:0x17 / 255.0 green:0x1e / 255.0 blue:0x28 / 255.0 alpha:1];
}
static UIColor *PSCColorBorder(void) {
return [UIColor colorWithRed:0x2b / 255.0 green:0x35 / 255.0 blue:0x44 / 255.0 alpha:1];
}
static UIColor *PSCColorText(void) {
return [UIColor colorWithRed:0xe8 / 255.0 green:0xee / 255.0 blue:0xf4 / 255.0 alpha:1];
}
static UIColor *PSCColorMuted(void) {
return [UIColor colorWithRed:0x8b / 255.0 green:0x9a / 255.0 blue:0xab / 255.0 alpha:1];
}
static UIColor *PSCColorTeal(void) {
return [UIColor colorWithRed:0x5e / 255.0 green:0xea / 255.0 blue:0xd4 / 255.0 alpha:1];
}
static UIColor *PSCColorField(void) {
return [UIColor colorWithRed:0x0c / 255.0 green:0x11 / 255.0 blue:0x18 / 255.0 alpha:1];
}
@interface PSCWorkCard : UIView
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *metaLabel;
@property (nonatomic, strong) UIProgressView *progress;
@property (nonatomic, copy) NSString *deviceKey;
@end
@implementation PSCWorkCard
@end
@interface PSCSlaveViewController () <PSCSlaveClientDelegate, UITextFieldDelegate>
@property (nonatomic, strong) PSCSlaveClient *client;
@property (nonatomic, strong) UIScrollView *scroll;
@property (nonatomic, strong) UIStackView *stack;
@property (nonatomic, strong) UILabel *statusPill;
@property (nonatomic, strong) UITextField *hostField;
@property (nonatomic, strong) UITextField *portField;
@property (nonatomic, strong) UIButton *connectBtn;
@property (nonatomic, strong) UIButton *disconnectBtn;
@property (nonatomic, strong) UISwitch *metalSwitch;
@property (nonatomic, strong) UISwitch *cpuSwitch;
@property (nonatomic, strong) UILabel *metalNameLabel;
@property (nonatomic, strong) UILabel *cpuNameLabel;
@property (nonatomic, strong) UILabel *workersValue;
@property (nonatomic, strong) UIStepper *workersStepper;
@property (nonatomic, strong) UIStackView *workStack;
@property (nonatomic, strong) UITextView *logView;
@property (nonatomic, strong) NSMutableString *logText;
@property (nonatomic, assign) BOOL metalReady;
@end
@implementation PSCSlaveViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = PSCColorBG();
self.logText = [NSMutableString string];
self.client = [[PSCSlaveClient alloc] init];
self.client.delegate = self;
[self buildUI];
[self loadDefaults];
[self applyConnectionEnabled:YES];
NSError *err = nil;
BOOL ok = [self.client prepareEngines:&err];
self.metalReady = self.client.metalAvailable;
self.metalNameLabel.text = self.metalReady ? self.client.metalName : @"Metal GPU unavailable";
self.metalSwitch.enabled = self.metalReady;
if (!self.metalReady) {
self.metalSwitch.on = NO;
}
unsigned cores = (unsigned)self.client.cpuCores;
self.cpuNameLabel.text = [NSString stringWithFormat:@"CPU (%u cores)", cores];
self.workersStepper.minimumValue = 1;
self.workersStepper.maximumValue = MAX(1, cores);
if (self.workersStepper.value < 1 || self.workersStepper.value > cores) {
self.workersStepper.value = cores;
}
self.workersValue.text = [NSString stringWithFormat:@"%.0f", self.workersStepper.value];
self.client.cpuWorkers = (NSUInteger)self.workersStepper.value;
if (ok) {
[self appendLog:self.metalReady ? @"Metal and CPU self-tests passed." : @"CPU self-test passed. Metal unavailable."];
} else {
[self appendLog:[NSString stringWithFormat:@"Engine setup: %@", err.localizedDescription ?: @"failed"]];
}
[self rebuildDevices];
[self rebuildWorkCards:@[]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)appDidBackground {
if (self.client.isConnected) {
[self.client disconnect];
[self applyConnectionEnabled:YES];
[self appendLog:@"Disconnected (app backgrounded)."];
}
}
- (UILabel *)caption:(NSString *)text {
UILabel *label = [[UILabel alloc] init];
label.text = text.uppercaseString;
label.textColor = PSCColorMuted();
label.font = [UIFont systemFontOfSize:11 weight:UIFontWeightBold];
return label;
}
- (UIView *)cardWithTitle:(NSString *)title arranged:(NSArray<UIView *> *)views {
UIStackView *inner = [[UIStackView alloc] init];
inner.axis = UILayoutConstraintAxisVertical;
inner.spacing = 10;
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = title;
titleLabel.textColor = PSCColorText();
titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
[inner addArrangedSubview:titleLabel];
for (UIView *view in views) {
[inner addArrangedSubview:view];
}
UIView *card = [[UIView alloc] init];
card.backgroundColor = PSCColorCard();
card.layer.cornerRadius = 12;
card.layer.borderWidth = 1;
card.layer.borderColor = PSCColorBorder().CGColor;
inner.translatesAutoresizingMaskIntoConstraints = NO;
[card addSubview:inner];
[NSLayoutConstraint activateConstraints:@[
[inner.topAnchor constraintEqualToAnchor:card.topAnchor constant:14],
[inner.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:14],
[inner.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-14],
[inner.bottomAnchor constraintEqualToAnchor:card.bottomAnchor constant:-14],
]];
return card;
}
- (UITextField *)fieldWithPlaceholder:(NSString *)placeholder keyboard:(UIKeyboardType)keyboard {
UITextField *field = [[UITextField alloc] init];
field.placeholder = placeholder;
field.textColor = PSCColorText();
field.keyboardType = keyboard;
field.autocapitalizationType = UITextAutocapitalizationTypeNone;
field.autocorrectionType = UITextAutocorrectionTypeNo;
field.spellCheckingType = UITextSpellCheckingTypeNo;
field.delegate = self;
field.returnKeyType = UIReturnKeyDone;
field.font = [UIFont monospacedSystemFontOfSize:15 weight:UIFontWeightRegular];
field.backgroundColor = PSCColorField();
field.layer.cornerRadius = 8;
field.layer.borderWidth = 1;
field.layer.borderColor = PSCColorBorder().CGColor;
field.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];
field.leftViewMode = UITextFieldViewModeAlways;
field.rightView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 1)];
field.rightViewMode = UITextFieldViewModeAlways;
field.attributedPlaceholder = [[NSAttributedString alloc]
initWithString:placeholder
attributes:@{NSForegroundColorAttributeName : PSCColorMuted()}];
[field.heightAnchor constraintEqualToConstant:40].active = YES;
return field;
}
- (UIButton *)buttonTitle:(NSString *)title primary:(BOOL)primary action:(SEL)action {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:title forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold];
btn.layer.cornerRadius = 8;
[btn.heightAnchor constraintEqualToConstant:40].active = YES;
if (primary) {
btn.backgroundColor = PSCColorTeal();
[btn setTitleColor:PSCColorBG() forState:UIControlStateNormal];
} else {
btn.backgroundColor = PSCColorField();
btn.layer.borderWidth = 1;
btn.layer.borderColor = PSCColorBorder().CGColor;
[btn setTitleColor:PSCColorText() forState:UIControlStateNormal];
}
[btn addTarget:self action:action forControlEvents:UIControlEventTouchUpInside];
return btn;
}
- (UIView *)toggleRowSwitch:(UISwitch *)toggle name:(UILabel *)name {
UIStackView *row = [[UIStackView alloc] init];
row.axis = UILayoutConstraintAxisHorizontal;
row.alignment = UIStackViewAlignmentCenter;
row.spacing = 10;
name.textColor = PSCColorText();
name.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium];
name.numberOfLines = 2;
toggle.onTintColor = PSCColorTeal();
[row addArrangedSubview:name];
[row addArrangedSubview:toggle];
[name setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
[toggle setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
return row;
}
- (void)buildUI {
self.scroll = [[UIScrollView alloc] init];
self.scroll.translatesAutoresizingMaskIntoConstraints = NO;
self.scroll.alwaysBounceVertical = YES;
self.scroll.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
[self.view addSubview:self.scroll];
self.stack = [[UIStackView alloc] init];
self.stack.axis = UILayoutConstraintAxisVertical;
self.stack.spacing = 16;
self.stack.translatesAutoresizingMaskIntoConstraints = NO;
[self.scroll addSubview:self.stack];
UILayoutGuide *safe = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[self.scroll.topAnchor constraintEqualToAnchor:safe.topAnchor],
[self.scroll.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scroll.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scroll.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.stack.topAnchor constraintEqualToAnchor:self.scroll.contentLayoutGuide.topAnchor constant:16],
[self.stack.leadingAnchor constraintEqualToAnchor:self.scroll.frameLayoutGuide.leadingAnchor constant:16],
[self.stack.trailingAnchor constraintEqualToAnchor:self.scroll.frameLayoutGuide.trailingAnchor constant:-16],
[self.stack.bottomAnchor constraintEqualToAnchor:self.scroll.contentLayoutGuide.bottomAnchor constant:-24],
]];
UIStackView *header = [[UIStackView alloc] init];
header.axis = UILayoutConstraintAxisHorizontal;
header.alignment = UIStackViewAlignmentCenter;
header.spacing = 12;
UIStackView *brand = [[UIStackView alloc] init];
brand.axis = UILayoutConstraintAxisVertical;
brand.spacing = 2;
UILabel *title = [[UILabel alloc] init];
title.text = @"DES Cracker";
title.textColor = PSCColorTeal();
title.font = [UIFont systemFontOfSize:22 weight:UIFontWeightBold];
UILabel *sub = [[UILabel alloc] init];
sub.text = @"LAN WORKER";
sub.textColor = PSCColorMuted();
sub.font = [UIFont systemFontOfSize:10 weight:UIFontWeightBold];
[brand addArrangedSubview:title];
[brand addArrangedSubview:sub];
self.statusPill = [[UILabel alloc] init];
self.statusPill.text = @"Offline";
self.statusPill.textAlignment = NSTextAlignmentCenter;
self.statusPill.textColor = PSCColorMuted();
self.statusPill.font = [UIFont systemFontOfSize:12 weight:UIFontWeightSemibold];
self.statusPill.backgroundColor = PSCColorField();
self.statusPill.layer.cornerRadius = 12;
self.statusPill.layer.masksToBounds = YES;
self.statusPill.layer.borderWidth = 1;
self.statusPill.layer.borderColor = PSCColorBorder().CGColor;
[self.statusPill.widthAnchor constraintGreaterThanOrEqualToConstant:96].active = YES;
[self.statusPill.heightAnchor constraintEqualToConstant:28].active = YES;
[header addArrangedSubview:brand];
[header addArrangedSubview:self.statusPill];
[brand setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];
self.hostField = [self fieldWithPlaceholder:@"192.168.1.10" keyboard:UIKeyboardTypeNumbersAndPunctuation];
self.portField = [self fieldWithPlaceholder:@"9876" keyboard:UIKeyboardTypeNumberPad];
[self.portField.widthAnchor constraintEqualToConstant:88].active = YES;
UIStackView *hostRow = [[UIStackView alloc] init];
hostRow.axis = UILayoutConstraintAxisHorizontal;
hostRow.spacing = 8;
UIStackView *hostCol = [[UIStackView alloc] init];
hostCol.axis = UILayoutConstraintAxisVertical;
hostCol.spacing = 4;
[hostCol addArrangedSubview:[self caption:@"Master host"]];
[hostCol addArrangedSubview:self.hostField];
UIStackView *portCol = [[UIStackView alloc] init];
portCol.axis = UILayoutConstraintAxisVertical;
portCol.spacing = 4;
[portCol addArrangedSubview:[self caption:@"Port"]];
[portCol addArrangedSubview:self.portField];
[hostRow addArrangedSubview:hostCol];
[hostRow addArrangedSubview:portCol];
self.connectBtn = [self buttonTitle:@"Connect" primary:YES action:@selector(connectTapped)];
self.disconnectBtn = [self buttonTitle:@"Disconnect" primary:NO action:@selector(disconnectTapped)];
UIStackView *btnRow = [[UIStackView alloc] init];
btnRow.axis = UILayoutConstraintAxisHorizontal;
btnRow.spacing = 8;
btnRow.distribution = UIStackViewDistributionFillEqually;
[btnRow addArrangedSubview:self.connectBtn];
[btnRow addArrangedSubview:self.disconnectBtn];
UIView *connectCard = [self cardWithTitle:@"Connection" arranged:@[ hostRow, btnRow ]];
self.metalSwitch = [[UISwitch alloc] init];
self.metalSwitch.on = YES;
[self.metalSwitch addTarget:self action:@selector(devicesChanged) forControlEvents:UIControlEventValueChanged];
self.metalNameLabel = [[UILabel alloc] init];
self.metalNameLabel.text = @"Metal GPU";
UIView *metalRow = [self toggleRowSwitch:self.metalSwitch name:self.metalNameLabel];
self.cpuSwitch = [[UISwitch alloc] init];
self.cpuSwitch.on = YES;
[self.cpuSwitch addTarget:self action:@selector(devicesChanged) forControlEvents:UIControlEventValueChanged];
self.cpuNameLabel = [[UILabel alloc] init];
self.cpuNameLabel.text = @"CPU";
UIView *cpuRow = [self toggleRowSwitch:self.cpuSwitch name:self.cpuNameLabel];
self.workersStepper = [[UIStepper alloc] init];
self.workersStepper.minimumValue = 1;
self.workersStepper.maximumValue = 16;
self.workersStepper.stepValue = 1;
self.workersStepper.value = 4;
[self.workersStepper addTarget:self action:@selector(workersChanged) forControlEvents:UIControlEventValueChanged];
self.workersValue = [[UILabel alloc] init];
self.workersValue.textColor = PSCColorText();
self.workersValue.font = [UIFont monospacedDigitSystemFontOfSize:14 weight:UIFontWeightMedium];
UIStackView *workerRow = [[UIStackView alloc] init];
workerRow.axis = UILayoutConstraintAxisHorizontal;
workerRow.alignment = UIStackViewAlignmentCenter;
workerRow.spacing = 8;
UILabel *workersCaption = [[UILabel alloc] init];
workersCaption.text = @"CPU workers";
workersCaption.textColor = PSCColorMuted();
workersCaption.font = [UIFont systemFontOfSize:13 weight:UIFontWeightMedium];
[workerRow addArrangedSubview:workersCaption];
[workerRow addArrangedSubview:[[UIView alloc] init]];
[workerRow addArrangedSubview:self.workersValue];
[workerRow addArrangedSubview:self.workersStepper];
UILabel *hint = [[UILabel alloc] init];
hint.text = @"Block size is set on the master. This device only exposes workers.";
hint.textColor = PSCColorMuted();
hint.font = [UIFont systemFontOfSize:12];
hint.numberOfLines = 0;
UIView *deviceCard = [self cardWithTitle:@"Devices" arranged:@[ metalRow, cpuRow, workerRow, hint ]];
self.workStack = [[UIStackView alloc] init];
self.workStack.axis = UILayoutConstraintAxisVertical;
self.workStack.spacing = 10;
UIView *workCard = [self cardWithTitle:@"Assigned work" arranged:@[ self.workStack ]];
self.logView = [[UITextView alloc] init];
self.logView.editable = NO;
self.logView.backgroundColor = PSCColorField();
self.logView.textColor = PSCColorText();
self.logView.font = [UIFont monospacedSystemFontOfSize:11 weight:UIFontWeightRegular];
self.logView.layer.cornerRadius = 8;
self.logView.layer.borderWidth = 1;
self.logView.layer.borderColor = PSCColorBorder().CGColor;
self.logView.textContainerInset = UIEdgeInsetsMake(8, 6, 8, 6);
[self.logView.heightAnchor constraintEqualToConstant:180].active = YES;
UIView *logCard = [self cardWithTitle:@"Log" arranged:@[ self.logView ]];
[self.stack addArrangedSubview:header];
[self.stack addArrangedSubview:connectCard];
[self.stack addArrangedSubview:deviceCard];
[self.stack addArrangedSubview:workCard];
[self.stack addArrangedSubview:logCard];
}
- (void)loadDefaults {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
NSString *host = [defaults stringForKey:kPSCHostKey];
self.hostField.text = host.length ? host : @"";
NSInteger port = [defaults integerForKey:kPSCPortKey];
self.portField.text = port > 0 ? [NSString stringWithFormat:@"%ld", (long)port] : @"9876";
if ([defaults objectForKey:kPSCMetalKey] != nil) {
self.metalSwitch.on = [defaults boolForKey:kPSCMetalKey];
}
if ([defaults objectForKey:kPSCCpuKey] != nil) {
self.cpuSwitch.on = [defaults boolForKey:kPSCCpuKey];
}
NSInteger workers = [defaults integerForKey:kPSCWorkersKey];
if (workers > 0) {
self.workersStepper.value = workers;
}
}
- (void)saveDefaults {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
[defaults setObject:self.hostField.text ?: @"" forKey:kPSCHostKey];
[defaults setInteger:self.portField.text.integerValue forKey:kPSCPortKey];
[defaults setBool:self.metalSwitch.on forKey:kPSCMetalKey];
[defaults setBool:self.cpuSwitch.on forKey:kPSCCpuKey];
[defaults setInteger:(NSInteger)self.workersStepper.value forKey:kPSCWorkersKey];
}
- (void)applyConnectionEnabled:(BOOL)canConnect {
self.connectBtn.enabled = canConnect;
self.connectBtn.alpha = canConnect ? 1 : 0.45;
self.disconnectBtn.enabled = !canConnect;
self.disconnectBtn.alpha = canConnect ? 0.45 : 1;
self.hostField.enabled = canConnect;
self.portField.enabled = canConnect;
}
- (NSArray<PSCComputeDevice *> *)selectedDevices {
NSMutableArray<PSCComputeDevice *> *devices = [NSMutableArray array];
if (self.metalSwitch.on && self.metalReady) {
PSCComputeDevice *gpu = [[PSCComputeDevice alloc] init];
gpu.key = @"metal:0";
gpu.deviceId = 0;
gpu.name = self.client.metalName ?: @"Metal GPU";
gpu.type = @"metal";
if (self.client.metalMemory > 0) {
gpu.memory = @(self.client.metalMemory);
}
[devices addObject:gpu];
}
if (self.cpuSwitch.on) {
PSCComputeDevice *cpu = [[PSCComputeDevice alloc] init];
cpu.key = @"cpu:0";
cpu.deviceId = 0;
cpu.name = [NSString stringWithFormat:@"CPU (%lu cores)", (unsigned long)self.client.cpuCores];
cpu.type = @"cpu";
cpu.cores = @(self.client.cpuCores);
[devices addObject:cpu];
}
return devices;
}
- (void)rebuildDevices {
self.client.cpuWorkers = (NSUInteger)self.workersStepper.value;
[self.client setDevices:[self selectedDevices]];
}
- (void)rebuildWorkCards:(NSArray<PSCWorkerSnapshot *> *)workers {
for (UIView *view in self.workStack.arrangedSubviews) {
[self.workStack removeArrangedSubview:view];
[view removeFromSuperview];
}
if (workers.count == 0) {
NSArray *selected = [self selectedDevices];
if (selected.count == 0) {
UILabel *empty = [[UILabel alloc] init];
empty.text = @"Enable Metal and/or CPU to expose workers.";
empty.textColor = PSCColorMuted();
empty.font = [UIFont systemFontOfSize:13];
empty.numberOfLines = 0;
[self.workStack addArrangedSubview:empty];
return;
}
NSMutableArray *idle = [NSMutableArray array];
for (PSCComputeDevice *device in selected) {
PSCWorkerSnapshot *row = [[PSCWorkerSnapshot alloc] init];
row.deviceKey = device.key;
row.deviceName = device.name;
row.deviceType = device.type;
row.current = @"idle";
[idle addObject:row];
}
workers = idle;
}
for (PSCWorkerSnapshot *row in workers) {
PSCWorkCard *card = [[PSCWorkCard alloc] init];
card.deviceKey = row.deviceKey;
UILabel *name = [[UILabel alloc] init];
name.text = row.deviceName;
name.textColor = PSCColorText();
name.font = [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
UILabel *meta = [[UILabel alloc] init];
meta.textColor = PSCColorMuted();
meta.font = [UIFont monospacedDigitSystemFontOfSize:12 weight:UIFontWeightRegular];
meta.numberOfLines = 2;
UIProgressView *progress = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
progress.progressTintColor = PSCColorTeal();
progress.trackTintColor = PSCColorBorder();
card.nameLabel = name;
card.metaLabel = meta;
card.progress = progress;
UIStackView *col = [[UIStackView alloc] initWithArrangedSubviews:@[ name, progress, meta ]];
col.axis = UILayoutConstraintAxisVertical;
col.spacing = 6;
col.translatesAutoresizingMaskIntoConstraints = NO;
[card addSubview:col];
[NSLayoutConstraint activateConstraints:@[
[col.topAnchor constraintEqualToAnchor:card.topAnchor],
[col.leadingAnchor constraintEqualToAnchor:card.leadingAnchor],
[col.trailingAnchor constraintEqualToAnchor:card.trailingAnchor],
[col.bottomAnchor constraintEqualToAnchor:card.bottomAnchor],
]];
[self.workStack addArrangedSubview:card];
[self applySnapshot:row toCard:card];
}
}
- (void)applySnapshot:(PSCWorkerSnapshot *)row toCard:(PSCWorkCard *)card {
card.nameLabel.text = row.deviceName;
card.progress.progress = (float)(row.pct / 100.0);
NSString *rate = row.busy ? PSCFormatRate(row.rate) : @"—";
card.metaLabel.text = [NSString stringWithFormat:@"%@ %@ %ld blocks", row.current, rate, (long)row.completedBlocks];
}
- (void)connectTapped {
[self.view endEditing:YES];
NSString *host = [self.hostField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSInteger port = self.portField.text.integerValue;
if (host.length == 0) {
[self appendLog:@"Enter the master’s IP address."];
return;
}
if (port <= 0 || port > 65535) {
port = kPSCDefaultPort;
self.portField.text = [NSString stringWithFormat:@"%ld", (long)port];
}
if ([self selectedDevices].count == 0) {
[self appendLog:@"Enable at least one device before connecting."];
return;
}
[self saveDefaults];
[self rebuildDevices];
[self applyConnectionEnabled:NO];
[self.client connectToHost:host port:port];
}
- (void)disconnectTapped {
[self.client disconnect];
[self applyConnectionEnabled:YES];
}
- (void)devicesChanged {
[self saveDefaults];
[self rebuildDevices];
}
- (void)workersChanged {
self.workersValue.text = [NSString stringWithFormat:@"%.0f", self.workersStepper.value];
[self saveDefaults];
[self rebuildDevices];
}
- (void)appendLog:(NSString *)line {
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"HH:mm:ss";
NSString *stamp = [fmt stringFromDate:[NSDate date]];
[self.logText appendFormat:@"[%@] %@\n", stamp, line];
self.logView.text = self.logText;
if (self.logView.text.length > 0) {
NSRange bottom = NSMakeRange(self.logView.text.length - 1, 1);
[self.logView scrollRangeToVisible:bottom];
}
}
- (void)setStatus:(NSString *)status connected:(BOOL)connected {
self.statusPill.text = [NSString stringWithFormat:@" %@ ", status];
if (connected) {
self.statusPill.textColor = PSCColorBG();
self.statusPill.backgroundColor = PSCColorTeal();
self.statusPill.layer.borderColor = PSCColorTeal().CGColor;
} else {
self.statusPill.textColor = PSCColorMuted();
self.statusPill.backgroundColor = PSCColorField();
self.statusPill.layer.borderColor = PSCColorBorder().CGColor;
}
if ([status isEqualToString:@"Disconnected"] || [status isEqualToString:@"Offline"]) {
[self applyConnectionEnabled:YES];
}
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark - PSCSlaveClientDelegate
- (void)slaveClient:(PSCSlaveClient *)client didChangeStatus:(NSString *)status connected:(BOOL)connected {
(void)client;
[self setStatus:status connected:connected];
[UIApplication sharedApplication].idleTimerDisabled = connected;
}
- (void)slaveClient:(PSCSlaveClient *)client didLog:(NSString *)line {
(void)client;
[self appendLog:line];
}
- (void)slaveClient:(PSCSlaveClient *)client didUpdateWorkers:(NSArray<PSCWorkerSnapshot *> *)workers {
(void)client;
if (self.workStack.arrangedSubviews.count != workers.count) {
[self rebuildWorkCards:workers];
return;
}
for (NSUInteger i = 0; i < workers.count; i++) {
UIView *view = self.workStack.arrangedSubviews[i];
if (![view isKindOfClass:[PSCWorkCard class]]) {
[self rebuildWorkCards:workers];
return;
}
PSCWorkCard *card = (PSCWorkCard *)view;
PSCWorkerSnapshot *row = workers[i];
if (![card.deviceKey isEqualToString:row.deviceKey]) {
[self rebuildWorkCards:workers];
return;
}
[self applySnapshot:row toCard:card];
}
}
- (void)slaveClient:(PSCSlaveClient *)client didHitJob:(NSString *)jobId index:(NSString *)index keyHex:(NSString *)keyHex {
(void)client;
(void)jobId;
[self appendLog:[NSString stringWithFormat:@"HIT index=%@ key=%@", index, keyHex]];
}
@end

View File

@ -0,0 +1,680 @@
import UIKit
enum Palette {
static let bg = UIColor(red: 0x10 / 255, green: 0x15 / 255, blue: 0x1c / 255, alpha: 1)
static let card = UIColor(red: 0x17 / 255, green: 0x1e / 255, blue: 0x28 / 255, alpha: 1)
static let border = UIColor(red: 0x2b / 255, green: 0x35 / 255, blue: 0x44 / 255, alpha: 1)
static let text = UIColor(red: 0xe8 / 255, green: 0xee / 255, blue: 0xf4 / 255, alpha: 1)
static let muted = UIColor(red: 0x8b / 255, green: 0x9a / 255, blue: 0xab / 255, alpha: 1)
static let teal = UIColor(red: 0x5e / 255, green: 0xea / 255, blue: 0xd4 / 255, alpha: 1)
static let field = UIColor(red: 0x0c / 255, green: 0x11 / 255, blue: 0x18 / 255, alpha: 1)
}
private final class WorkCard: UIView {
var nameLabel: UILabel!
var metaLabel: UILabel!
var progress: UIProgressView!
var deviceKey = ""
}
final class SlaveViewController: UIViewController, SlaveClientDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate {
private let hostKey = "psc.masterHost"
private let portKey = "psc.masterPort"
private let metalKey = "psc.metalOn"
private let cpuKey = "psc.cpuOn"
private let workersKey = "psc.cpuWorkers"
private let client = SlaveClient()
private var scroll: UIScrollView!
private var stack: UIStackView!
private var statusPill: UILabel!
private var hostField: UITextField!
private var portField: UITextField!
private var connectBtn: UIButton!
private var disconnectBtn: UIButton!
private var metalSwitch: UISwitch!
private var cpuSwitch: UISwitch!
private var metalNameLabel: UILabel!
private var cpuNameLabel: UILabel!
private var workersValue: UILabel!
private var workersStepper: UIStepper!
private var workStack: UIStackView!
private var logView: UITextView!
private var logText = ""
private var metalReady = false
private var enginesPrepared = false
private let logStamp: DateFormatter = {
let fmt = DateFormatter()
fmt.dateFormat = "HH:mm:ss"
return fmt
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Palette.bg
client.delegate = self
buildUI()
loadDefaults()
applyConnectionEnabled(true)
let cores = max(1, client.cpuCores)
cpuNameLabel.text = "CPU (\(cores) cores)"
workersStepper.minimumValue = 1
workersStepper.maximumValue = Double(cores)
if workersStepper.value < 1 || workersStepper.value > Double(cores) {
workersStepper.value = Double(cores)
}
workersValue.text = String(format: "%.0f", workersStepper.value)
client.cpuWorkers = Int(workersStepper.value)
metalNameLabel.text = client.metalName.isEmpty ? "Metal GPU" : client.metalName
metalSwitch.isEnabled = false
rebuildWorkCards([])
appendLog("Preparing compute engines…")
client.prepareEngines { [weak self] ok, err in
self?.enginesDidPrepare(ok, error: err)
}
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
tap.cancelsTouchesInView = false
tap.delegate = self
view.addGestureRecognizer(tap)
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardFrameWillChange),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func enginesDidPrepare(_ ok: Bool, error: Error?) {
enginesPrepared = ok
metalReady = client.metalAvailable
metalNameLabel.text = metalReady ? client.metalName : "Metal GPU unavailable"
metalSwitch.isEnabled = metalReady
if !metalReady { metalSwitch.isOn = false }
if ok {
appendLog(metalReady ? "CPU self-test passed. Metal GPU available." : "CPU self-test passed. Metal unavailable.")
applyConnectionEnabled(true)
} else {
appendLog("Engine setup: \(error?.localizedDescription ?? "failed")")
applyConnectionEnabled(false)
connectBtn.isEnabled = false
connectBtn.alpha = 0.45
}
rebuildDevices()
rebuildWorkCards([])
}
@objc private func appDidBackground() {
if client.isConnected {
client.disconnect()
applyConnectionEnabled(true)
appendLog("Disconnected (app backgrounded).")
}
}
private func caption(_ text: String) -> UILabel {
let label = UILabel()
label.text = text.uppercased()
label.textColor = Palette.muted
label.font = .systemFont(ofSize: 11, weight: .bold)
return label
}
private func card(title: String, arranged views: [UIView]) -> UIView {
let inner = UIStackView()
inner.axis = .vertical
inner.spacing = 10
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textColor = Palette.text
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
inner.addArrangedSubview(titleLabel)
views.forEach { inner.addArrangedSubview($0) }
let card = UIView()
card.backgroundColor = Palette.card
card.layer.cornerRadius = 12
card.layer.borderWidth = 1
card.layer.borderColor = Palette.border.cgColor
inner.translatesAutoresizingMaskIntoConstraints = false
card.addSubview(inner)
NSLayoutConstraint.activate([
inner.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
inner.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
inner.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
inner.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14),
])
return card
}
private func field(placeholder: String, keyboard: UIKeyboardType) -> UITextField {
let field = UITextField()
field.placeholder = placeholder
field.textColor = Palette.text
field.keyboardType = keyboard
field.keyboardAppearance = .dark
field.autocapitalizationType = .none
field.autocorrectionType = .no
field.spellCheckingType = .no
field.smartDashesType = .no
field.smartQuotesType = .no
field.smartInsertDeleteType = .no
if #available(iOS 17.0, *) {
field.inlinePredictionType = .no
}
field.delegate = self
field.returnKeyType = .done
field.enablesReturnKeyAutomatically = false
field.font = .monospacedSystemFont(ofSize: 15, weight: .regular)
field.backgroundColor = Palette.field
field.layer.cornerRadius = 8
field.layer.borderWidth = 1
field.layer.borderColor = Palette.border.cgColor
field.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1))
field.leftViewMode = .always
field.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 1))
field.rightViewMode = .always
field.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [.foregroundColor: Palette.muted]
)
field.heightAnchor.constraint(equalToConstant: 40).isActive = true
let bar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
bar.barStyle = .black
bar.isTranslucent = true
let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let done = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(dismissKeyboard))
done.tintColor = Palette.teal
bar.items = [flex, done]
bar.sizeToFit()
field.inputAccessoryView = bar
return field
}
private func button(title: String, primary: Bool, action: Selector) -> UIButton {
let btn = UIButton(type: .system)
btn.setTitle(title, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .semibold)
btn.layer.cornerRadius = 8
btn.heightAnchor.constraint(equalToConstant: 40).isActive = true
if primary {
btn.backgroundColor = Palette.teal
btn.setTitleColor(Palette.bg, for: .normal)
} else {
btn.backgroundColor = Palette.field
btn.layer.borderWidth = 1
btn.layer.borderColor = Palette.border.cgColor
btn.setTitleColor(Palette.text, for: .normal)
}
btn.addTarget(self, action: action, for: .touchUpInside)
return btn
}
private func toggleRow(switch toggle: UISwitch, name: UILabel) -> UIView {
let row = UIStackView()
row.axis = .horizontal
row.alignment = .center
row.spacing = 10
name.textColor = Palette.text
name.font = .systemFont(ofSize: 14, weight: .medium)
name.numberOfLines = 2
toggle.onTintColor = Palette.teal
row.addArrangedSubview(name)
row.addArrangedSubview(toggle)
name.setContentHuggingPriority(.defaultLow, for: .horizontal)
toggle.setContentHuggingPriority(.required, for: .horizontal)
return row
}
private func buildUI() {
scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
scroll.alwaysBounceVertical = true
scroll.keyboardDismissMode = .interactive
scroll.delaysContentTouches = false
view.addSubview(scroll)
stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
scroll.addSubview(stack)
let safe = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
scroll.topAnchor.constraint(equalTo: safe.topAnchor),
scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scroll.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stack.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor, constant: 16),
stack.leadingAnchor.constraint(equalTo: scroll.frameLayoutGuide.leadingAnchor, constant: 16),
stack.trailingAnchor.constraint(equalTo: scroll.frameLayoutGuide.trailingAnchor, constant: -16),
stack.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor, constant: -24),
])
let header = UIStackView()
header.axis = .horizontal
header.alignment = .center
header.spacing = 12
let brand = UIStackView()
brand.axis = .vertical
brand.spacing = 2
let title = UILabel()
title.text = "DES Cracker"
title.textColor = Palette.teal
title.font = .systemFont(ofSize: 22, weight: .bold)
let sub = UILabel()
sub.text = "LAN WORKER"
sub.textColor = Palette.muted
sub.font = .systemFont(ofSize: 10, weight: .bold)
brand.addArrangedSubview(title)
brand.addArrangedSubview(sub)
statusPill = UILabel()
statusPill.text = "Offline"
statusPill.textAlignment = .center
statusPill.textColor = Palette.muted
statusPill.font = .systemFont(ofSize: 12, weight: .semibold)
statusPill.backgroundColor = Palette.field
statusPill.layer.cornerRadius = 12
statusPill.layer.masksToBounds = true
statusPill.layer.borderWidth = 1
statusPill.layer.borderColor = Palette.border.cgColor
statusPill.widthAnchor.constraint(greaterThanOrEqualToConstant: 96).isActive = true
statusPill.heightAnchor.constraint(equalToConstant: 28).isActive = true
header.addArrangedSubview(brand)
header.addArrangedSubview(statusPill)
brand.setContentHuggingPriority(.defaultLow, for: .horizontal)
hostField = field(placeholder: "192.168.1.10", keyboard: .numbersAndPunctuation)
portField = field(placeholder: "9876", keyboard: .numberPad)
portField.widthAnchor.constraint(equalToConstant: 88).isActive = true
let hostRow = UIStackView()
hostRow.axis = .horizontal
hostRow.spacing = 8
let hostCol = UIStackView()
hostCol.axis = .vertical
hostCol.spacing = 4
hostCol.addArrangedSubview(caption("Master host"))
hostCol.addArrangedSubview(hostField)
let portCol = UIStackView()
portCol.axis = .vertical
portCol.spacing = 4
portCol.addArrangedSubview(caption("Port"))
portCol.addArrangedSubview(portField)
hostRow.addArrangedSubview(hostCol)
hostRow.addArrangedSubview(portCol)
connectBtn = button(title: "Connect", primary: true, action: #selector(connectTapped))
disconnectBtn = button(title: "Disconnect", primary: false, action: #selector(disconnectTapped))
let btnRow = UIStackView()
btnRow.axis = .horizontal
btnRow.spacing = 8
btnRow.distribution = .fillEqually
btnRow.addArrangedSubview(connectBtn)
btnRow.addArrangedSubview(disconnectBtn)
let connectCard = card(title: "Connection", arranged: [hostRow, btnRow])
metalSwitch = UISwitch()
metalSwitch.isOn = true
metalSwitch.addTarget(self, action: #selector(devicesChanged), for: .valueChanged)
metalNameLabel = UILabel()
metalNameLabel.text = "Metal GPU"
let metalRow = toggleRow(switch: metalSwitch, name: metalNameLabel)
cpuSwitch = UISwitch()
cpuSwitch.isOn = true
cpuSwitch.addTarget(self, action: #selector(devicesChanged), for: .valueChanged)
cpuNameLabel = UILabel()
cpuNameLabel.text = "CPU"
let cpuRow = toggleRow(switch: cpuSwitch, name: cpuNameLabel)
workersStepper = UIStepper()
workersStepper.minimumValue = 1
workersStepper.maximumValue = 16
workersStepper.stepValue = 1
workersStepper.value = 4
workersStepper.addTarget(self, action: #selector(workersChanged), for: .valueChanged)
workersValue = UILabel()
workersValue.textColor = Palette.text
workersValue.font = .monospacedDigitSystemFont(ofSize: 14, weight: .medium)
let workerRow = UIStackView()
workerRow.axis = .horizontal
workerRow.alignment = .center
workerRow.spacing = 8
let workersCaption = UILabel()
workersCaption.text = "CPU workers"
workersCaption.textColor = Palette.muted
workersCaption.font = .systemFont(ofSize: 13, weight: .medium)
workerRow.addArrangedSubview(workersCaption)
workerRow.addArrangedSubview(UIView())
workerRow.addArrangedSubview(workersValue)
workerRow.addArrangedSubview(workersStepper)
let hint = UILabel()
hint.text = "Block size is set on the master. This device only exposes workers."
hint.textColor = Palette.muted
hint.font = .systemFont(ofSize: 12)
hint.numberOfLines = 0
let deviceCard = card(title: "Devices", arranged: [metalRow, cpuRow, workerRow, hint])
workStack = UIStackView()
workStack.axis = .vertical
workStack.spacing = 10
let workCard = card(title: "Assigned work", arranged: [workStack])
logView = UITextView(usingTextLayoutManager: false)
logView.isEditable = false
logView.isSelectable = true
logView.isScrollEnabled = true
logView.backgroundColor = Palette.field
logView.textColor = Palette.text
logView.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
logView.layer.cornerRadius = 8
logView.layer.borderWidth = 1
logView.layer.borderColor = Palette.border.cgColor
logView.textContainerInset = UIEdgeInsets(top: 8, left: 6, bottom: 8, right: 6)
logView.translatesAutoresizingMaskIntoConstraints = false
let logWrap = UIView()
logWrap.clipsToBounds = true
logWrap.addSubview(logView)
NSLayoutConstraint.activate([
logWrap.heightAnchor.constraint(equalToConstant: 180),
logView.topAnchor.constraint(equalTo: logWrap.topAnchor),
logView.leadingAnchor.constraint(equalTo: logWrap.leadingAnchor),
logView.trailingAnchor.constraint(equalTo: logWrap.trailingAnchor),
logView.bottomAnchor.constraint(equalTo: logWrap.bottomAnchor),
])
let logCard = card(title: "Log", arranged: [logWrap])
stack.addArrangedSubview(header)
stack.addArrangedSubview(connectCard)
stack.addArrangedSubview(deviceCard)
stack.addArrangedSubview(workCard)
stack.addArrangedSubview(logCard)
}
private func loadDefaults() {
let defaults = UserDefaults.standard
let host = defaults.string(forKey: hostKey) ?? ""
hostField.text = host
let port = defaults.integer(forKey: portKey)
portField.text = port > 0 ? String(port) : "9876"
if defaults.object(forKey: metalKey) != nil {
metalSwitch.isOn = defaults.bool(forKey: metalKey)
}
if defaults.object(forKey: cpuKey) != nil {
cpuSwitch.isOn = defaults.bool(forKey: cpuKey)
}
let workers = defaults.integer(forKey: workersKey)
if workers > 0 {
workersStepper.value = Double(workers)
}
}
private func saveDefaults() {
let defaults = UserDefaults.standard
defaults.set(hostField.text ?? "", forKey: hostKey)
defaults.set(Int(portField.text ?? "") ?? 0, forKey: portKey)
defaults.set(metalSwitch.isOn, forKey: metalKey)
defaults.set(cpuSwitch.isOn, forKey: cpuKey)
defaults.set(Int(workersStepper.value), forKey: workersKey)
}
private func applyConnectionEnabled(_ canConnect: Bool) {
let connectOK = canConnect && enginesPrepared
connectBtn.isEnabled = connectOK
connectBtn.alpha = connectOK ? 1 : 0.45
disconnectBtn.isEnabled = !canConnect && enginesPrepared
disconnectBtn.alpha = (!canConnect && enginesPrepared) ? 1 : 0.45
hostField.isEnabled = canConnect
portField.isEnabled = canConnect
}
private func selectedDevices() -> [ComputeDevice] {
var devices: [ComputeDevice] = []
if metalSwitch.isOn && metalReady {
let gpu = ComputeDevice()
gpu.key = "metal:0"
gpu.deviceId = 0
gpu.name = client.metalName.isEmpty ? "Metal GPU" : client.metalName
gpu.type = "metal"
if client.metalMemory > 0 {
gpu.memory = NSNumber(value: client.metalMemory)
}
devices.append(gpu)
}
if cpuSwitch.isOn {
let cpu = ComputeDevice()
cpu.key = "cpu:0"
cpu.deviceId = 0
cpu.name = "CPU (\(client.cpuCores) cores)"
cpu.type = "cpu"
cpu.cores = NSNumber(value: client.cpuCores)
devices.append(cpu)
}
return devices
}
private func rebuildDevices() {
client.cpuWorkers = Int(workersStepper.value)
client.setDevices(selectedDevices())
}
private func rebuildWorkCards(_ workersIn: [WorkerSnapshot]) {
workStack.arrangedSubviews.forEach {
workStack.removeArrangedSubview($0)
$0.removeFromSuperview()
}
var workers = workersIn
if workers.isEmpty {
let selected = selectedDevices()
if selected.isEmpty {
let empty = UILabel()
empty.text = "Enable Metal and/or CPU to expose workers."
empty.textColor = Palette.muted
empty.font = .systemFont(ofSize: 13)
empty.numberOfLines = 0
workStack.addArrangedSubview(empty)
return
}
workers = selected.map { device in
let row = WorkerSnapshot()
row.deviceKey = device.key
row.deviceName = device.name
row.deviceType = device.type
row.current = "idle"
return row
}
}
for row in workers {
let card = WorkCard()
card.deviceKey = row.deviceKey
let name = UILabel()
name.text = row.deviceName
name.textColor = Palette.text
name.font = .systemFont(ofSize: 14, weight: .semibold)
let meta = UILabel()
meta.textColor = Palette.muted
meta.font = .monospacedDigitSystemFont(ofSize: 12, weight: .regular)
meta.numberOfLines = 2
let progress = UIProgressView(progressViewStyle: .default)
progress.progressTintColor = Palette.teal
progress.trackTintColor = Palette.border
card.nameLabel = name
card.metaLabel = meta
card.progress = progress
let col = UIStackView(arrangedSubviews: [name, progress, meta])
col.axis = .vertical
col.spacing = 6
col.translatesAutoresizingMaskIntoConstraints = false
card.addSubview(col)
NSLayoutConstraint.activate([
col.topAnchor.constraint(equalTo: card.topAnchor),
col.leadingAnchor.constraint(equalTo: card.leadingAnchor),
col.trailingAnchor.constraint(equalTo: card.trailingAnchor),
col.bottomAnchor.constraint(equalTo: card.bottomAnchor),
])
workStack.addArrangedSubview(card)
apply(row, to: card)
}
}
private func apply(_ row: WorkerSnapshot, to card: WorkCard) {
card.nameLabel.text = row.deviceName
card.progress.progress = Float(row.pct / 100)
let rate = row.busy ? Wire.formatRate(row.rate) : "—"
card.metaLabel.text = "\(row.current) \(rate) \(row.completedBlocks) blocks"
}
@objc private func connectTapped() {
view.endEditing(true)
if !enginesPrepared {
appendLog("Compute engines are still starting.")
return
}
let host = hostField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
var port = Int(portField.text ?? "") ?? 0
if host.isEmpty {
appendLog("Enter the master’s IP address.")
return
}
if port <= 0 || port > 65535 {
port = Wire.defaultPort
portField.text = String(port)
}
if selectedDevices().isEmpty {
appendLog("Enable at least one device before connecting.")
return
}
saveDefaults()
rebuildDevices()
applyConnectionEnabled(false)
client.connect(to: host, port: port)
}
@objc private func disconnectTapped() {
client.disconnect()
applyConnectionEnabled(true)
}
@objc private func devicesChanged() {
saveDefaults()
rebuildDevices()
}
@objc private func workersChanged() {
workersValue.text = String(format: "%.0f", workersStepper.value)
saveDefaults()
rebuildDevices()
}
private func appendLog(_ line: String) {
let stamp = logStamp.string(from: Date())
logText += "[\(stamp)] \(line)\n"
if logText.count > 16000 {
let start = logText.index(logText.endIndex, offsetBy: -12000)
if let keep = logText[start...].firstIndex(of: "\n") {
logText = String(logText[logText.index(after: keep)...])
}
}
logView.text = logText
DispatchQueue.main.async { [weak logView] in
guard let logView, !logView.text.isEmpty else { return }
let loc = max(0, logView.text.count - 1)
logView.scrollRangeToVisible(NSRange(location: loc, length: 1))
}
}
private func setStatus(_ status: String, connected: Bool) {
statusPill.text = " \(status) "
if connected {
statusPill.textColor = Palette.bg
statusPill.backgroundColor = Palette.teal
statusPill.layer.borderColor = Palette.teal.cgColor
} else {
statusPill.textColor = Palette.muted
statusPill.backgroundColor = Palette.field
statusPill.layer.borderColor = Palette.border.cgColor
}
if status == "Disconnected" || status == "Offline" {
applyConnectionEnabled(true)
}
}
@objc private func dismissKeyboard() {
view.endEditing(true)
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
view.window?.makeKeyAndVisible()
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
var view = touch.view
while let current = view {
if current is UIControl || current is UITextField || current is UITextView {
return false
}
view = current.superview
}
return true
}
@objc private func keyboardFrameWillChange(_ note: Notification) {
guard let screenFrame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
let inView = view.convert(screenFrame, from: nil)
let overlap = max(0, view.bounds.maxY - inView.origin.y)
var inset = scroll.contentInset
inset.bottom = overlap
scroll.contentInset = inset
scroll.verticalScrollIndicatorInsets = inset
}
func slaveClient(_ client: SlaveClient, didChangeStatus status: String, connected: Bool) {
setStatus(status, connected: connected)
UIApplication.shared.isIdleTimerDisabled = connected
}
func slaveClient(_ client: SlaveClient, didLog line: String) {
appendLog(line)
}
func slaveClient(_ client: SlaveClient, didUpdateWorkers workers: [WorkerSnapshot]) {
if workStack.arrangedSubviews.count != workers.count {
rebuildWorkCards(workers)
return
}
for (i, row) in workers.enumerated() {
guard let card = workStack.arrangedSubviews[i] as? WorkCard, card.deviceKey == row.deviceKey else {
rebuildWorkCards(workers)
return
}
apply(row, to: card)
}
}
func slaveClient(_ client: SlaveClient, didHitJob jobId: String, index: String, keyHex: String) {
appendLog("HIT index=\(index) key=\(keyHex)")
}
}

View File

@ -1,8 +0,0 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@ -39,20 +39,22 @@ constant uchar PC2_TBL[48] = {
}; };
constant uchar SHIFTS[16] = {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1}; constant uchar SHIFTS[16] = {1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1};
constant uchar SBOX[8][64] = { constant uchar SBOX[512] = {
{14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7, 0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8, 4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0, 15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13}, 14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7, 0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8, 4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0, 15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13,
{15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10, 3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5, 0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15, 13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9}, 15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10, 3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5, 0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15, 13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9,
{10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8, 13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1, 13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7, 1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12}, 10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8, 13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1, 13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7, 1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12,
{7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15, 13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9, 10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4, 3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14}, 7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15, 13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9, 10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4, 3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14,
{2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9, 14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6, 4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14, 11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3}, 2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9, 14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6, 4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14, 11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3,
{12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11, 10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8, 9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6, 4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13}, 12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11, 10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8, 9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6, 4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13,
{4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1, 13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6, 1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2, 6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12}, 4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1, 13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6, 1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2, 6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12,
{13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7, 1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2, 7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8, 2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11} 13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7, 1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2, 7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8, 2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11
}; };
inline ulong perm(ulong src, constant uchar *tbl, uint nout, uint srcbits) { template<uint Nout>
inline ulong perm_n(ulong src, constant uchar *tbl, uint srcbits) {
ulong outv = 0; ulong outv = 0;
for (uint i = 0; i < nout; i++) { #pragma clang loop unroll(disable)
for (uint i = 0; i < Nout; i++) {
ulong bit = (src >> (srcbits - tbl[i])) & 1UL; ulong bit = (src >> (srcbits - tbl[i])) & 1UL;
outv = (outv << 1) | bit; outv = (outv << 1) | bit;
} }
@ -64,33 +66,36 @@ inline uint rotl28(uint v, uint s) {
} }
inline void des_key_schedule(ulong key, thread ulong sk[16]) { inline void des_key_schedule(ulong key, thread ulong sk[16]) {
ulong cd = perm(key, PC1_TBL, 56, 64); ulong cd = perm_n<56>(key, PC1_TBL, 64);
uint c = uint(cd >> 28); uint c = uint(cd >> 28);
uint d = uint(cd & 0x0FFFFFFFUL); uint d = uint(cd & 0x0FFFFFFFUL);
#pragma clang loop unroll(disable)
for (uint r = 0; r < 16; r++) { for (uint r = 0; r < 16; r++) {
c = rotl28(c, SHIFTS[r]); c = rotl28(c, SHIFTS[r]);
d = rotl28(d, SHIFTS[r]); d = rotl28(d, SHIFTS[r]);
ulong cd2 = (ulong(c) << 28) | ulong(d); ulong cd2 = (ulong(c) << 28) | ulong(d);
sk[r] = perm(cd2, PC2_TBL, 48, 56); sk[r] = perm_n<48>(cd2, PC2_TBL, 56);
} }
} }
inline uint feistel(uint r, ulong subkey) { inline uint feistel(uint r, ulong subkey) {
ulong er = perm(ulong(r), E_TBL, 48, 32) ^ subkey; ulong er = perm_n<48>(ulong(r), E_TBL, 32) ^ subkey;
uint s = 0; uint s = 0;
#pragma clang loop unroll(disable)
for (uint i = 0; i < 8; i++) { for (uint i = 0; i < 8; i++) {
uint chunk = uint((er >> (42 - 6 * i)) & 0x3F); uint chunk = uint((er >> (42 - 6 * i)) & 0x3F);
uint row = ((chunk & 0x20) >> 4) | (chunk & 1); uint row = ((chunk & 0x20) >> 4) | (chunk & 1);
uint col = (chunk >> 1) & 0xF; uint col = (chunk >> 1) & 0xF;
s = (s << 4) | uint(SBOX[i][row * 16 + col]); s = (s << 4) | uint(SBOX[i * 64 + row * 16 + col]);
} }
return uint(perm(ulong(s), P_TBL, 32, 32)); return uint(perm_n<32>(ulong(s), P_TBL, 32));
} }
inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) { inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) {
ulong ip = perm(block, IP_TBL, 64, 64); ulong ip = perm_n<64>(block, IP_TBL, 64);
uint l = uint(ip >> 32); uint l = uint(ip >> 32);
uint r = uint(ip & 0xFFFFFFFFUL); uint r = uint(ip & 0xFFFFFFFFUL);
#pragma clang loop unroll(disable)
for (uint i = 0; i < 16; i++) { for (uint i = 0; i < 16; i++) {
uint rnd = decrypt ? (15 - i) : i; uint rnd = decrypt ? (15 - i) : i;
uint n = l ^ feistel(r, sk[rnd]); uint n = l ^ feistel(r, sk[rnd]);
@ -98,7 +103,7 @@ inline ulong des_crypt(ulong block, thread ulong sk[16], bool decrypt) {
r = n; r = n;
} }
ulong pre = (ulong(r) << 32) | ulong(l); ulong pre = (ulong(r) << 32) | ulong(l);
return perm(pre, FP_TBL, 64, 64); return perm_n<64>(pre, FP_TBL, 64);
} }
struct Params { struct Params {
@ -121,16 +126,19 @@ struct Hit {
inline ulong make_key(ulong index, constant Params &p, const device uchar *charset) { inline ulong make_key(ulong index, constant Params &p, const device uchar *charset) {
uchar bytes[8]; uchar bytes[8];
#pragma clang loop unroll(disable)
for (uint i = 0; i < 8; i++) { for (uint i = 0; i < 8; i++) {
bytes[i] = uchar(p.pad_byte); bytes[i] = uchar(p.pad_byte);
} }
ulong n = index; ulong n = index;
uint clen = p.charset_len; uint clen = p.charset_len;
#pragma clang loop unroll(disable)
for (int pos = int(p.key_len) - 1; pos >= 0; pos--) { for (int pos = int(p.key_len) - 1; pos >= 0; pos--) {
bytes[pos] = charset[n % clen]; bytes[pos] = charset[n % clen];
n /= clen; n /= clen;
} }
ulong key = 0; ulong key = 0;
#pragma clang loop unroll(disable)
for (uint i = 0; i < 8; i++) { for (uint i = 0; i < 8; i++) {
key = (key << 8) | ulong(bytes[i]); key = (key << 8) | ulong(bytes[i]);
} }
@ -138,6 +146,7 @@ inline ulong make_key(ulong index, constant Params &p, const device uchar *chars
} }
inline bool is_fill(ulong pt, constant Params &p) { inline bool is_fill(ulong pt, constant Params &p) {
#pragma clang loop unroll(disable)
for (uint i = 0; i < p.fill_count; i++) { for (uint i = 0; i < p.fill_count; i++) {
if (pt == p.fills[i]) { if (pt == p.fills[i]) {
return true; return true;

View File

@ -207,11 +207,8 @@ export class MasterServer {
}); });
} }
private workerByJob(info: SlaveInfo, jobId: string, deviceKey?: string): SlaveDeviceState | undefined { private workerByJob(info: SlaveInfo, jobId: string): SlaveDeviceState | undefined {
return ( return info.workers.find((w) => w.jobId === jobId);
info.workers.find((w) => w.jobId === jobId) ??
(deviceKey ? info.workers.find((w) => w.device.key === deviceKey) : undefined)
);
} }
private onClient(id: string, msg: ClientMessage): void { private onClient(id: string, msg: ClientMessage): void {
@ -244,7 +241,7 @@ export class MasterServer {
this.emitSlaves(); this.emitSlaves();
break; break;
case "progress": { case "progress": {
const worker = this.workerByJob(info, msg.jobId, msg.deviceKey); const worker = info.workers.find((w) => w.jobId === msg.jobId);
if (!worker) { if (!worker) {
break; break;
} }
@ -259,7 +256,7 @@ export class MasterServer {
this.events.onHit?.(info, msg.jobId, msg.index, msg.keyHex, msg.plainHex); this.events.onHit?.(info, msg.jobId, msg.index, msg.keyHex, msg.plainHex);
break; break;
case "block_complete": { case "block_complete": {
const worker = this.workerByJob(info, msg.jobId, msg.deviceKey); const worker = info.workers.find((w) => w.jobId === msg.jobId);
if (!worker) { if (!worker) {
break; break;
} }

View File

@ -140,6 +140,10 @@ class BlockAllocator {
this.retries.push({ start: block.start, count: block.count }); this.retries.push({ start: block.start, count: block.count });
} }
has(jobId: string): boolean {
return this.inflight.has(jobId);
}
get finished(): boolean { get finished(): boolean {
return this.completed === this.space && this.inflight.size === 0 && this.retries.length === 0; return this.completed === this.space && this.inflight.size === 0 && this.retries.length === 0;
} }
@ -214,6 +218,7 @@ export class MasterSearch {
private overallPrior = 0n; private overallPrior = 0n;
private overallTotal = 1n; private overallTotal = 1n;
private stageCompleted = 0n; private stageCompleted = 0n;
private stagePeak = 0n;
private stageTotal = 1n; private stageTotal = 1n;
private stage: StageJob | null = null; private stage: StageJob | null = null;
private dispatch: (() => void) | null = null; private dispatch: (() => void) | null = null;
@ -348,12 +353,7 @@ export class MasterSearch {
prev.onProgress?.(slave, worker, jobId, done, count, rate); prev.onProgress?.(slave, worker, jobId, done, count, rate);
const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key)); const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key));
if (tracked) { if (tracked) {
tracked.jobId = jobId; this.applyProgress(tracked, jobId, done, count, rate);
tracked.done = done;
tracked.count = count;
tracked.rate = rate;
tracked.busy = true;
beginWork(tracked);
} }
this.emitProgress(); this.emitProgress();
}; };
@ -363,6 +363,8 @@ export class MasterSearch {
}; };
this.server.events.onComplete = (slave, worker, jobId) => { this.server.events.onComplete = (slave, worker, jobId) => {
prev.onComplete?.(slave, worker, jobId); prev.onComplete?.(slave, worker, jobId);
this.stageAlloc?.complete(jobId);
this.syncStageCompleted();
const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key)); const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key));
if (tracked) { if (tracked) {
endWork(tracked, tracked.count); endWork(tracked, tracked.count);
@ -373,7 +375,6 @@ export class MasterSearch {
tracked.count = 0n; tracked.count = 0n;
tracked.completedBlocks += 1; tracked.completedBlocks += 1;
} }
this.stageAlloc?.complete(jobId);
this.emitProgress(); this.emitProgress();
if (tracked) { if (tracked) {
this.feedWorker?.(tracked); this.feedWorker?.(tracked);
@ -384,6 +385,7 @@ export class MasterSearch {
prev.onDropped?.(slave, jobId); prev.onDropped?.(slave, jobId);
if (jobId) { if (jobId) {
this.stageAlloc?.fail(jobId); this.stageAlloc?.fail(jobId);
this.syncStageCompleted();
for (const tracked of this.roster.values()) { for (const tracked of this.roster.values()) {
if (tracked.jobId === jobId) { if (tracked.jobId === jobId) {
endWork(tracked, tracked.done); endWork(tracked, tracked.done);
@ -420,6 +422,7 @@ export class MasterSearch {
this.stage = job; this.stage = job;
this.stageTotal = job.space; this.stageTotal = job.space;
this.stageCompleted = 0n; this.stageCompleted = 0n;
this.stagePeak = 0n;
const alloc = new BlockAllocator(job.space); const alloc = new BlockAllocator(job.space);
this.stageAlloc = alloc; this.stageAlloc = alloc;
this.events.onLog?.( this.events.onLog?.(
@ -536,6 +539,7 @@ export class MasterSearch {
this.feedWorker = null; this.feedWorker = null;
this.stageAlloc = null; this.stageAlloc = null;
this.stageCompleted = job.space; this.stageCompleted = job.space;
this.stagePeak = job.space;
this.emitProgress(); this.emitProgress();
} }
@ -574,10 +578,7 @@ export class MasterSearch {
onProgress: (done, count, rate) => { onProgress: (done, count, rate) => {
const live = this.roster.get(id); const live = this.roster.get(id);
if (live) { if (live) {
live.done = done; this.applyProgress(live, block.jobId, done, count, rate);
live.count = count;
live.rate = rate;
live.busy = true;
} }
this.emitProgress(device.name); this.emitProgress(device.name);
}, },
@ -601,10 +602,12 @@ export class MasterSearch {
endWork(live, block.count); endWork(live, block.count);
} }
} }
this.syncStageCompleted();
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
this.events.onLog?.(`${device.name}: ${err instanceof Error ? err.message : String(err)}`); this.events.onLog?.(`${device.name}: ${err instanceof Error ? err.message : String(err)}`);
alloc.fail(block.jobId); alloc.fail(block.jobId);
this.syncStageCompleted();
keepDevice = false; keepDevice = false;
}) })
.finally(() => { .finally(() => {
@ -623,7 +626,7 @@ export class MasterSearch {
if (keepDevice) { if (keepDevice) {
localIdle.add(device.key); localIdle.add(device.key);
} }
this.stageCompleted = alloc.completed; this.syncStageCompleted();
this.emitProgress(); this.emitProgress();
if (keepDevice && live && !this.cancel) { if (keepDevice && live && !this.cancel) {
this.feedWorker?.(live); this.feedWorker?.(live);
@ -655,13 +658,51 @@ export class MasterSearch {
); );
} }
private syncStageCompleted(): void {
if (this.stageAlloc) {
this.stageCompleted = this.stageAlloc.completed;
}
}
private applyProgress(worker: TrackedWorker, jobId: string, done: bigint, count: bigint, rate: number): void {
if (worker.jobId !== jobId) {
return;
}
if (this.stageAlloc && !this.stageAlloc.has(jobId)) {
return;
}
worker.done = done < 0n ? 0n : done;
worker.count = count < 0n ? 0n : count;
worker.rate = rate;
worker.busy = true;
beginWork(worker);
}
private liveDone(worker: TrackedWorker): bigint {
if (!worker.busy || worker.jobId == null) {
return 0n;
}
if (this.stageAlloc && !this.stageAlloc.has(worker.jobId)) {
return 0n;
}
if (worker.done <= 0n) {
return 0n;
}
if (worker.count > 0n && worker.done > worker.count) {
return worker.count;
}
return worker.done;
}
private emitProgress(workerLabel?: string): void { private emitProgress(workerLabel?: string): void {
this.syncStageCompleted();
let inflight = 0n; let inflight = 0n;
let rateSum = 0; let rateSum = 0;
const rows: WorkerRow[] = []; const rows: WorkerRow[] = [];
for (const w of this.roster.values()) { for (const w of this.roster.values()) {
if (w.busy) { const liveJob = w.busy && w.jobId != null && (!this.stageAlloc || this.stageAlloc.has(w.jobId));
inflight += w.done; if (liveJob) {
inflight += this.liveDone(w);
rateSum += w.rate; rateSum += w.rate;
} }
const pct = w.busy && w.count > 0n ? Number((w.done * 1000n) / w.count) / 10 : 0; const pct = w.busy && w.count > 0n ? Number((w.done * 1000n) / w.count) / 10 : 0;
@ -685,7 +726,15 @@ export class MasterSearch {
busy: w.busy, busy: w.busy,
}); });
} }
const stageDone = this.stageCompleted + inflight; let stageDone = this.stageCompleted + inflight;
if (stageDone > this.stageTotal) {
stageDone = this.stageTotal;
}
if (stageDone < this.stagePeak) {
stageDone = this.stagePeak;
} else {
this.stagePeak = stageDone;
}
const overallDone = this.overallPrior + stageDone; const overallDone = this.overallPrior + stageDone;
const elapsed = Math.max((Date.now() - this.started) / 1000, 1e-6); const elapsed = Math.max((Date.now() - this.started) / 1000, 1e-6);
const rate = rateSum > 0 ? rateSum : Number(overallDone) / elapsed; const rate = rateSum > 0 ? rateSum : Number(overallDone) / elapsed;

View File

@ -313,7 +313,7 @@ QProgressBar#workerProgress {
padding: 0; padding: 0;
min-height: 0; min-height: 0;
text-align: center; text-align: center;
color: #5eead4; color: #ffffff;
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
} }

View File

@ -712,7 +712,7 @@ export function createMainWindow(): QMainWindow {
setCell(widgets, index, 3, row.current); setCell(widgets, index, 3, row.current);
const value = Math.min(1000, Math.max(0, Math.round(row.pct * 10))); const value = Math.min(1000, Math.max(0, Math.round(row.pct * 10)));
widgets.bar.setValue(value); widgets.bar.setValue(value);
widgets.bar.setFormat(row.busy ? `${row.pct.toFixed(1)}%%` : "idle"); widgets.bar.setFormat(row.busy ? `${row.pct.toFixed(1)}%` : "idle");
setCell(widgets, index, 5, formatNumber(row.completedBlocks)); setCell(widgets, index, 5, formatNumber(row.completedBlocks));
setCell(widgets, index, 6, formatNumber(row.completedKeys)); setCell(widgets, index, 6, formatNumber(row.completedKeys));
setCell(widgets, index, 7, row.rate); setCell(widgets, index, 7, row.rate);