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:
parent
bb690f3a1f
commit
ac345b2859
@ -1,5 +1,5 @@
|
||||
{
|
||||
"elapsed_sec": 46.262,
|
||||
"elapsed_sec": 261.712,
|
||||
"pad_matches": [],
|
||||
"confirmed": []
|
||||
}
|
||||
|
||||
@ -7,43 +7,39 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
A40000000000000000000001 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* main.m */; };
|
||||
A40000000000000000000002 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* AppDelegate.m */; };
|
||||
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* PSCSlaveViewController.m */; };
|
||||
A40000000000000000000004 /* PSCProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* PSCProtocol.m */; };
|
||||
A40000000000000000000005 /* PSCSlaveClient.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* PSCSlaveClient.m */; };
|
||||
A40000000000000000000006 /* PSCMetalEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000014 /* PSCMetalEngine.m */; };
|
||||
A40000000000000000000007 /* PSCCpuEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000016 /* PSCCpuEngine.m */; };
|
||||
A40000000000000000000008 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000017 /* des.c */; };
|
||||
A40000000000000000000009 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000019 /* des_bruteforce.metal */; };
|
||||
A40000000000000000000010 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* Assets.xcassets */; };
|
||||
A40000000000000000000011 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000020 /* Metal.framework */; };
|
||||
A40000000000000000000012 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A20000000000000000000021 /* QuartzCore.framework */; };
|
||||
A40000000000000000000001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000001 /* AppDelegate.swift */; };
|
||||
A40000000000000000000002 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000002 /* SceneDelegate.swift */; };
|
||||
A40000000000000000000003 /* SlaveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000003 /* SlaveViewController.swift */; };
|
||||
A40000000000000000000004 /* Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000004 /* Protocol.swift */; };
|
||||
A40000000000000000000005 /* SlaveClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000005 /* SlaveClient.swift */; };
|
||||
A40000000000000000000006 /* BruteEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000006 /* BruteEngine.swift */; };
|
||||
A40000000000000000000007 /* MetalEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000007 /* MetalEngine.swift */; };
|
||||
A40000000000000000000008 /* CpuEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000008 /* CpuEngine.swift */; };
|
||||
A40000000000000000000009 /* des.c in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000009 /* des.c */; };
|
||||
A40000000000000000000010 /* des_bruteforce.metal in Sources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000011 /* des_bruteforce.metal */; };
|
||||
A40000000000000000000011 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A20000000000000000000013 /* Assets.xcassets */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
A20000000000000000000002 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000003 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
A20000000000000000000004 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A20000000000000000000005 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A20000000000000000000006 /* PSCSlaveViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveViewController.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000007 /* PSCSlaveViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveViewController.m; sourceTree = "<group>"; };
|
||||
A20000000000000000000008 /* PSCProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCProtocol.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000009 /* PSCProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCProtocol.m; sourceTree = "<group>"; };
|
||||
A20000000000000000000010 /* PSCSlaveClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCSlaveClient.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000011 /* PSCSlaveClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCSlaveClient.m; sourceTree = "<group>"; };
|
||||
A20000000000000000000012 /* PSCBruteEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCBruteEngine.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000013 /* PSCMetalEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCMetalEngine.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000014 /* PSCMetalEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCMetalEngine.m; sourceTree = "<group>"; };
|
||||
A20000000000000000000015 /* PSCCpuEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PSCCpuEngine.h; sourceTree = "<group>"; };
|
||||
A20000000000000000000016 /* PSCCpuEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PSCCpuEngine.m; 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; };
|
||||
A20000000000000000000001 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000002 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000003 /* SlaveViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveViewController.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000004 /* Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Protocol.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000005 /* SlaveClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlaveClient.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000006 /* BruteEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BruteEngine.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000007 /* MetalEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalEngine.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000008 /* CpuEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CpuEngine.swift; sourceTree = "<group>"; };
|
||||
A20000000000000000000009 /* des.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = des.c; path = ../native/common/des.c; sourceTree = SOURCE_ROOT; };
|
||||
A20000000000000000000010 /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = des.h; path = ../native/common/des.h; sourceTree = SOURCE_ROOT; };
|
||||
A20000000000000000000011 /* des_bruteforce.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; name = des_bruteforce.metal; path = ../native/metal/des_bruteforce.metal; sourceTree = SOURCE_ROOT; };
|
||||
A20000000000000000000012 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A20000000000000000000013 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A20000000000000000000014 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
|
||||
A20000000000000000000015 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||
A20000000000000000000016 /* DESCracker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DESCracker-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -51,22 +47,31 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A40000000000000000000011 /* Metal.framework in Frameworks */,
|
||||
A40000000000000000000012 /* QuartzCore.framework in Frameworks */,
|
||||
A40000000000000000000012 /* Metal.framework in Frameworks */,
|
||||
A40000000000000000000013 /* QuartzCore.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
A30000000000000000000000 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A30000000000000000000001 /* DESCracker */,
|
||||
A30000000000000000000007 /* Frameworks */,
|
||||
A30000000000000000000006 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A30000000000000000000001 /* DESCracker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000001 /* main.m */,
|
||||
A20000000000000000000002 /* AppDelegate.h */,
|
||||
A20000000000000000000003 /* AppDelegate.m */,
|
||||
A20000000000000000000004 /* Info.plist */,
|
||||
A20000000000000000000005 /* Assets.xcassets */,
|
||||
A20000000000000000000001 /* AppDelegate.swift */,
|
||||
A20000000000000000000002 /* SceneDelegate.swift */,
|
||||
A20000000000000000000016 /* DESCracker-Bridging-Header.h */,
|
||||
A20000000000000000000012 /* Info.plist */,
|
||||
A20000000000000000000013 /* Assets.xcassets */,
|
||||
A30000000000000000000002 /* UI */,
|
||||
A30000000000000000000003 /* Network */,
|
||||
A30000000000000000000004 /* Compute */,
|
||||
@ -78,8 +83,7 @@
|
||||
A30000000000000000000002 /* UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000006 /* PSCSlaveViewController.h */,
|
||||
A20000000000000000000007 /* PSCSlaveViewController.m */,
|
||||
A20000000000000000000003 /* SlaveViewController.swift */,
|
||||
);
|
||||
path = UI;
|
||||
sourceTree = "<group>";
|
||||
@ -87,10 +91,8 @@
|
||||
A30000000000000000000003 /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000008 /* PSCProtocol.h */,
|
||||
A20000000000000000000009 /* PSCProtocol.m */,
|
||||
A20000000000000000000010 /* PSCSlaveClient.h */,
|
||||
A20000000000000000000011 /* PSCSlaveClient.m */,
|
||||
A20000000000000000000004 /* Protocol.swift */,
|
||||
A20000000000000000000005 /* SlaveClient.swift */,
|
||||
);
|
||||
path = Network;
|
||||
sourceTree = "<group>";
|
||||
@ -98,11 +100,9 @@
|
||||
A30000000000000000000004 /* Compute */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000012 /* PSCBruteEngine.h */,
|
||||
A20000000000000000000013 /* PSCMetalEngine.h */,
|
||||
A20000000000000000000014 /* PSCMetalEngine.m */,
|
||||
A20000000000000000000015 /* PSCCpuEngine.h */,
|
||||
A20000000000000000000016 /* PSCCpuEngine.m */,
|
||||
A20000000000000000000006 /* BruteEngine.swift */,
|
||||
A20000000000000000000007 /* MetalEngine.swift */,
|
||||
A20000000000000000000008 /* CpuEngine.swift */,
|
||||
);
|
||||
path = Compute;
|
||||
sourceTree = "<group>";
|
||||
@ -110,9 +110,9 @@
|
||||
A30000000000000000000005 /* Native */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000017 /* des.c */,
|
||||
A20000000000000000000018 /* des.h */,
|
||||
A20000000000000000000019 /* des_bruteforce.metal */,
|
||||
A20000000000000000000009 /* des.c */,
|
||||
A20000000000000000000010 /* des.h */,
|
||||
A20000000000000000000011 /* des_bruteforce.metal */,
|
||||
);
|
||||
name = Native;
|
||||
sourceTree = "<group>";
|
||||
@ -128,21 +128,12 @@
|
||||
A30000000000000000000007 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A20000000000000000000020 /* Metal.framework */,
|
||||
A20000000000000000000021 /* QuartzCore.framework */,
|
||||
A20000000000000000000014 /* Metal.framework */,
|
||||
A20000000000000000000015 /* QuartzCore.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A30000000000000000000000 /* Root */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A30000000000000000000001 /* DESCracker */,
|
||||
A30000000000000000000007 /* Frameworks */,
|
||||
A30000000000000000000006 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -174,6 +165,7 @@
|
||||
TargetAttributes = {
|
||||
A10000000000000000000002 = {
|
||||
CreatedOnToolsVersion = 16.0;
|
||||
LastSwiftMigration = 1600;
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -185,7 +177,7 @@
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = A30000000000000000000000 /* Root */;
|
||||
mainGroup = A30000000000000000000000;
|
||||
productRefGroup = A30000000000000000000006 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -200,7 +192,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A40000000000000000000010 /* Assets.xcassets in Resources */,
|
||||
A40000000000000000000011 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -211,15 +203,16 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A40000000000000000000001 /* main.m in Sources */,
|
||||
A40000000000000000000002 /* AppDelegate.m in Sources */,
|
||||
A40000000000000000000003 /* PSCSlaveViewController.m in Sources */,
|
||||
A40000000000000000000004 /* PSCProtocol.m in Sources */,
|
||||
A40000000000000000000005 /* PSCSlaveClient.m in Sources */,
|
||||
A40000000000000000000006 /* PSCMetalEngine.m in Sources */,
|
||||
A40000000000000000000007 /* PSCCpuEngine.m in Sources */,
|
||||
A40000000000000000000008 /* des.c in Sources */,
|
||||
A40000000000000000000009 /* des_bruteforce.metal in Sources */,
|
||||
A40000000000000000000001 /* AppDelegate.swift in Sources */,
|
||||
A40000000000000000000002 /* SceneDelegate.swift in Sources */,
|
||||
A40000000000000000000003 /* SlaveViewController.swift in Sources */,
|
||||
A40000000000000000000004 /* Protocol.swift in Sources */,
|
||||
A40000000000000000000005 /* SlaveClient.swift in Sources */,
|
||||
A40000000000000000000006 /* BruteEngine.swift in Sources */,
|
||||
A40000000000000000000007 /* MetalEngine.swift in Sources */,
|
||||
A40000000000000000000008 /* CpuEngine.swift in Sources */,
|
||||
A40000000000000000000009 /* des.c in Sources */,
|
||||
A40000000000000000000010 /* des_bruteforce.metal in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -270,10 +263,14 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
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;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -315,9 +312,13 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_COMPILER_FLAGS = "-fno-unroll-loops";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
@ -327,16 +328,14 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = B832M34SD9;
|
||||
ENABLE_PREVIEWS = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/DESCracker",
|
||||
"$(SRCROOT)/DESCracker/UI",
|
||||
"$(SRCROOT)/DESCracker/Network",
|
||||
"$(SRCROOT)/DESCracker/Compute",
|
||||
"$(SRCROOT)/../native/common",
|
||||
);
|
||||
INFOPLIST_FILE = DESCracker/Info.plist;
|
||||
@ -353,13 +352,10 @@
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = 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";
|
||||
USER_HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/DESCracker/UI",
|
||||
"$(SRCROOT)/DESCracker/Network",
|
||||
"$(SRCROOT)/DESCracker/Compute",
|
||||
"$(SRCROOT)/../native/common",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -368,16 +364,14 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = B832M34SD9;
|
||||
ENABLE_PREVIEWS = NO;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(SRCROOT)/DESCracker",
|
||||
"$(SRCROOT)/DESCracker/UI",
|
||||
"$(SRCROOT)/DESCracker/Network",
|
||||
"$(SRCROOT)/DESCracker/Compute",
|
||||
"$(SRCROOT)/../native/common",
|
||||
);
|
||||
INFOPLIST_FILE = DESCracker/Info.plist;
|
||||
@ -394,13 +388,9 @@
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "DESCracker/DESCracker-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
USER_HEADER_SEARCH_PATHS = (
|
||||
"$(SRCROOT)/DESCracker/UI",
|
||||
"$(SRCROOT)/DESCracker/Network",
|
||||
"$(SRCROOT)/DESCracker/Compute",
|
||||
"$(SRCROOT)/../native/common",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
7
ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
ios/DESCracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
Binary file not shown.
@ -1,5 +0,0 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
@property (nonatomic, strong) UIWindow *window;
|
||||
@end
|
||||
@ -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
|
||||
21
ios/DESCracker/AppDelegate.swift
Normal file
21
ios/DESCracker/AppDelegate.swift
Normal 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
|
||||
}
|
||||
}
|
||||
22
ios/DESCracker/Compute/BruteEngine.swift
Normal file
22
ios/DESCracker/Compute/BruteEngine.swift
Normal 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()
|
||||
}
|
||||
153
ios/DESCracker/Compute/CpuEngine.swift
Normal file
153
ios/DESCracker/Compute/CpuEngine.swift
Normal 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
|
||||
}
|
||||
}
|
||||
238
ios/DESCracker/Compute/MetalEngine.swift
Normal file
238
ios/DESCracker/Compute/MetalEngine.swift
Normal 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: ¶ms) { 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
1
ios/DESCracker/DESCracker-Bridging-Header.h
Normal file
1
ios/DESCracker/DESCracker-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
||||
#include "des.h"
|
||||
@ -31,6 +31,23 @@
|
||||
</dict>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<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>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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];
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
144
ios/DESCracker/Network/Protocol.swift
Normal file
144
ios/DESCracker/Network/Protocol.swift
Normal 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
|
||||
}
|
||||
492
ios/DESCracker/Network/SlaveClient.swift
Normal file
492
ios/DESCracker/Network/SlaveClient.swift
Normal 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]?
|
||||
}
|
||||
18
ios/DESCracker/SceneDelegate.swift
Normal file
18
ios/DESCracker/SceneDelegate.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "PSCSlaveClient.h"
|
||||
|
||||
@interface PSCSlaveViewController : UIViewController
|
||||
@end
|
||||
@ -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
|
||||
680
ios/DESCracker/UI/SlaveViewController.swift
Normal file
680
ios/DESCracker/UI/SlaveViewController.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
@ -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]));
|
||||
}
|
||||
}
|
||||
@ -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 SBOX[8][64] = {
|
||||
{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},
|
||||
{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},
|
||||
{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},
|
||||
{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}
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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;
|
||||
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;
|
||||
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]) {
|
||||
ulong cd = perm(key, PC1_TBL, 56, 64);
|
||||
ulong cd = perm_n<56>(key, PC1_TBL, 64);
|
||||
uint c = uint(cd >> 28);
|
||||
uint d = uint(cd & 0x0FFFFFFFUL);
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint r = 0; r < 16; r++) {
|
||||
c = rotl28(c, SHIFTS[r]);
|
||||
d = rotl28(d, SHIFTS[r]);
|
||||
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) {
|
||||
ulong er = perm(ulong(r), E_TBL, 48, 32) ^ subkey;
|
||||
ulong er = perm_n<48>(ulong(r), E_TBL, 32) ^ subkey;
|
||||
uint s = 0;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint i = 0; i < 8; i++) {
|
||||
uint chunk = uint((er >> (42 - 6 * i)) & 0x3F);
|
||||
uint row = ((chunk & 0x20) >> 4) | (chunk & 1);
|
||||
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) {
|
||||
ulong ip = perm(block, IP_TBL, 64, 64);
|
||||
ulong ip = perm_n<64>(block, IP_TBL, 64);
|
||||
uint l = uint(ip >> 32);
|
||||
uint r = uint(ip & 0xFFFFFFFFUL);
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint i = 0; i < 16; i++) {
|
||||
uint rnd = decrypt ? (15 - i) : i;
|
||||
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;
|
||||
}
|
||||
ulong pre = (ulong(r) << 32) | ulong(l);
|
||||
return perm(pre, FP_TBL, 64, 64);
|
||||
return perm_n<64>(pre, FP_TBL, 64);
|
||||
}
|
||||
|
||||
struct Params {
|
||||
@ -121,16 +126,19 @@ struct Hit {
|
||||
|
||||
inline ulong make_key(ulong index, constant Params &p, const device uchar *charset) {
|
||||
uchar bytes[8];
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint i = 0; i < 8; i++) {
|
||||
bytes[i] = uchar(p.pad_byte);
|
||||
}
|
||||
ulong n = index;
|
||||
uint clen = p.charset_len;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (int pos = int(p.key_len) - 1; pos >= 0; pos--) {
|
||||
bytes[pos] = charset[n % clen];
|
||||
n /= clen;
|
||||
}
|
||||
ulong key = 0;
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint i = 0; i < 8; 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) {
|
||||
#pragma clang loop unroll(disable)
|
||||
for (uint i = 0; i < p.fill_count; i++) {
|
||||
if (pt == p.fills[i]) {
|
||||
return true;
|
||||
|
||||
@ -207,11 +207,8 @@ export class MasterServer {
|
||||
});
|
||||
}
|
||||
|
||||
private workerByJob(info: SlaveInfo, jobId: string, deviceKey?: string): SlaveDeviceState | undefined {
|
||||
return (
|
||||
info.workers.find((w) => w.jobId === jobId) ??
|
||||
(deviceKey ? info.workers.find((w) => w.device.key === deviceKey) : undefined)
|
||||
);
|
||||
private workerByJob(info: SlaveInfo, jobId: string): SlaveDeviceState | undefined {
|
||||
return info.workers.find((w) => w.jobId === jobId);
|
||||
}
|
||||
|
||||
private onClient(id: string, msg: ClientMessage): void {
|
||||
@ -244,7 +241,7 @@ export class MasterServer {
|
||||
this.emitSlaves();
|
||||
break;
|
||||
case "progress": {
|
||||
const worker = this.workerByJob(info, msg.jobId, msg.deviceKey);
|
||||
const worker = info.workers.find((w) => w.jobId === msg.jobId);
|
||||
if (!worker) {
|
||||
break;
|
||||
}
|
||||
@ -259,7 +256,7 @@ export class MasterServer {
|
||||
this.events.onHit?.(info, msg.jobId, msg.index, msg.keyHex, msg.plainHex);
|
||||
break;
|
||||
case "block_complete": {
|
||||
const worker = this.workerByJob(info, msg.jobId, msg.deviceKey);
|
||||
const worker = info.workers.find((w) => w.jobId === msg.jobId);
|
||||
if (!worker) {
|
||||
break;
|
||||
}
|
||||
|
||||
@ -140,6 +140,10 @@ class BlockAllocator {
|
||||
this.retries.push({ start: block.start, count: block.count });
|
||||
}
|
||||
|
||||
has(jobId: string): boolean {
|
||||
return this.inflight.has(jobId);
|
||||
}
|
||||
|
||||
get finished(): boolean {
|
||||
return this.completed === this.space && this.inflight.size === 0 && this.retries.length === 0;
|
||||
}
|
||||
@ -214,6 +218,7 @@ export class MasterSearch {
|
||||
private overallPrior = 0n;
|
||||
private overallTotal = 1n;
|
||||
private stageCompleted = 0n;
|
||||
private stagePeak = 0n;
|
||||
private stageTotal = 1n;
|
||||
private stage: StageJob | null = null;
|
||||
private dispatch: (() => void) | null = null;
|
||||
@ -348,12 +353,7 @@ export class MasterSearch {
|
||||
prev.onProgress?.(slave, worker, jobId, done, count, rate);
|
||||
const tracked = this.roster.get(remoteWorkerId(slave.id, worker.device.key));
|
||||
if (tracked) {
|
||||
tracked.jobId = jobId;
|
||||
tracked.done = done;
|
||||
tracked.count = count;
|
||||
tracked.rate = rate;
|
||||
tracked.busy = true;
|
||||
beginWork(tracked);
|
||||
this.applyProgress(tracked, jobId, done, count, rate);
|
||||
}
|
||||
this.emitProgress();
|
||||
};
|
||||
@ -363,6 +363,8 @@ export class MasterSearch {
|
||||
};
|
||||
this.server.events.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));
|
||||
if (tracked) {
|
||||
endWork(tracked, tracked.count);
|
||||
@ -373,7 +375,6 @@ export class MasterSearch {
|
||||
tracked.count = 0n;
|
||||
tracked.completedBlocks += 1;
|
||||
}
|
||||
this.stageAlloc?.complete(jobId);
|
||||
this.emitProgress();
|
||||
if (tracked) {
|
||||
this.feedWorker?.(tracked);
|
||||
@ -384,6 +385,7 @@ export class MasterSearch {
|
||||
prev.onDropped?.(slave, jobId);
|
||||
if (jobId) {
|
||||
this.stageAlloc?.fail(jobId);
|
||||
this.syncStageCompleted();
|
||||
for (const tracked of this.roster.values()) {
|
||||
if (tracked.jobId === jobId) {
|
||||
endWork(tracked, tracked.done);
|
||||
@ -420,6 +422,7 @@ export class MasterSearch {
|
||||
this.stage = job;
|
||||
this.stageTotal = job.space;
|
||||
this.stageCompleted = 0n;
|
||||
this.stagePeak = 0n;
|
||||
const alloc = new BlockAllocator(job.space);
|
||||
this.stageAlloc = alloc;
|
||||
this.events.onLog?.(
|
||||
@ -536,6 +539,7 @@ export class MasterSearch {
|
||||
this.feedWorker = null;
|
||||
this.stageAlloc = null;
|
||||
this.stageCompleted = job.space;
|
||||
this.stagePeak = job.space;
|
||||
this.emitProgress();
|
||||
}
|
||||
|
||||
@ -574,10 +578,7 @@ export class MasterSearch {
|
||||
onProgress: (done, count, rate) => {
|
||||
const live = this.roster.get(id);
|
||||
if (live) {
|
||||
live.done = done;
|
||||
live.count = count;
|
||||
live.rate = rate;
|
||||
live.busy = true;
|
||||
this.applyProgress(live, block.jobId, done, count, rate);
|
||||
}
|
||||
this.emitProgress(device.name);
|
||||
},
|
||||
@ -601,10 +602,12 @@ export class MasterSearch {
|
||||
endWork(live, block.count);
|
||||
}
|
||||
}
|
||||
this.syncStageCompleted();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
this.events.onLog?.(`${device.name}: ${err instanceof Error ? err.message : String(err)}`);
|
||||
alloc.fail(block.jobId);
|
||||
this.syncStageCompleted();
|
||||
keepDevice = false;
|
||||
})
|
||||
.finally(() => {
|
||||
@ -623,7 +626,7 @@ export class MasterSearch {
|
||||
if (keepDevice) {
|
||||
localIdle.add(device.key);
|
||||
}
|
||||
this.stageCompleted = alloc.completed;
|
||||
this.syncStageCompleted();
|
||||
this.emitProgress();
|
||||
if (keepDevice && live && !this.cancel) {
|
||||
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 {
|
||||
this.syncStageCompleted();
|
||||
let inflight = 0n;
|
||||
let rateSum = 0;
|
||||
const rows: WorkerRow[] = [];
|
||||
for (const w of this.roster.values()) {
|
||||
if (w.busy) {
|
||||
inflight += w.done;
|
||||
const liveJob = w.busy && w.jobId != null && (!this.stageAlloc || this.stageAlloc.has(w.jobId));
|
||||
if (liveJob) {
|
||||
inflight += this.liveDone(w);
|
||||
rateSum += w.rate;
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
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 elapsed = Math.max((Date.now() - this.started) / 1000, 1e-6);
|
||||
const rate = rateSum > 0 ? rateSum : Number(overallDone) / elapsed;
|
||||
|
||||
@ -313,7 +313,7 @@ QProgressBar#workerProgress {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
text-align: center;
|
||||
color: #5eead4;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -712,7 +712,7 @@ export function createMainWindow(): QMainWindow {
|
||||
setCell(widgets, index, 3, row.current);
|
||||
const value = Math.min(1000, Math.max(0, Math.round(row.pct * 10)));
|
||||
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, 6, formatNumber(row.completedKeys));
|
||||
setCell(widgets, index, 7, row.rate);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user