diff --git a/.version b/.version index b61023a..d674d8d 100644 --- a/.version +++ b/.version @@ -1,6 +1,6 @@ { - "latestVersionNum": 16, - "latestVersion": "2.2.0", + "latestVersionNum": 17, + "latestVersion": "2.3.0", "updateType": "hint", - "releaseNotes": "1. 查询key 分页列表时, 总数量使用异步查询,列表快速返回 #36 \n2. 增加lua脚本执行功能。" + "releaseNotes": "1. 使用reids 连接池。 \n2. table通用组件支持拷贝行数据, 右键菜单添加快捷键。 #49 \n3. 文本框禁用情况下支持选中复制 #49。 \n4. 表格拖动排序问题修复。 \n5. 点击一个失败的链接,正常redis服务器也无法登录bug修复 #48。 \n6. redis client 重构。" } diff --git a/redis-pro.xcodeproj/project.pbxproj b/redis-pro.xcodeproj/project.pbxproj index 23cff39..51a34bc 100644 --- a/redis-pro.xcodeproj/project.pbxproj +++ b/redis-pro.xcodeproj/project.pbxproj @@ -132,7 +132,12 @@ 627E1BC4282F462C00163D6B /* RenameStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627E1BC3282F462C00163D6B /* RenameStore.swift */; }; 627E1BC6282FC72B00163D6B /* HashValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627E1BC5282FC72B00163D6B /* HashValueStore.swift */; }; 627E1BC8282FCEE300163D6B /* ScanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627E1BC7282FCEE300163D6B /* ScanStore.swift */; }; - 627E1BCA28328A0600163D6B /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627E1BC928328A0600163D6B /* WindowController.swift */; }; + 6280596828B218D800126E81 /* RediStackClientString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6280596728B218D800126E81 /* RediStackClientString.swift */; }; + 6280596A28B23DBB00126E81 /* RediStackClientStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6280596928B23DBB00126E81 /* RediStackClientStream.swift */; }; + 628F9A9E28967C120003B6C0 /* RediStack in Frameworks */ = {isa = PBXBuildFile; productRef = 628F9A9D28967C120003B6C0 /* RediStack */; }; + 628F9AA02896A3FB0003B6C0 /* RedisCommandExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628F9A9F2896A3FB0003B6C0 /* RedisCommandExt.swift */; }; + 628F9AA2289EA7C50003B6C0 /* SSHTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628F9AA1289EA7C50003B6C0 /* SSHTunnel.swift */; }; + 628F9AA4289EAEE90003B6C0 /* RedisClientSSH.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628F9AA3289EAEE90003B6C0 /* RedisClientSSH.swift */; }; 62C262E02839D4380036A282 /* ListValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C262DF2839D4380036A282 /* ListValueStore.swift */; }; 62C262E22839DD0B0036A282 /* RedisListItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C262E12839DD0B0036A282 /* RedisListItemModel.swift */; }; 62C262E4283A31E40036A282 /* ListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C262E3283A31E40036A282 /* ListEditorView.swift */; }; @@ -156,14 +161,14 @@ 62D741BF288414E00049AB3C /* LuaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D741BE288414E00049AB3C /* LuaView.swift */; }; 62D741C1288415180049AB3C /* LuaStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D741C0288415180049AB3C /* LuaStore.swift */; }; 62D741C3288416470049AB3C /* RedisClientLua.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D741C2288416470049AB3C /* RedisClientLua.swift */; }; + 62E3DCAF28B0C60B00FF865A /* PasteboardHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E3DCAE28B0C60B00FF865A /* PasteboardHelper.swift */; }; + 62E3DCB228B0C93A00FF865A /* TableContextMenuEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E3DCB128B0C93A00FF865A /* TableContextMenuEnum.swift */; }; 62E8F9D22765019D006A5326 /* MIntField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E8F9D12765019D006A5326 /* MIntField.swift */; }; 62E8F9D62765EC97006A5326 /* NSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E8F9D52765EC97006A5326 /* NSecureField.swift */; }; 62E8F9D92765F197006A5326 /* MPasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E8F9D82765F197006A5326 /* MPasswordField.swift */; }; 62E9F13F2849DA9F00F4FABF /* SystemEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9F13E2849DA9F00F4FABF /* SystemEnvironment.swift */; }; CE0290CE2786B75A0058442B /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = CE0290CD2786B75A0058442B /* Puppy */; }; CE0290D6278707280058442B /* redis_proTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0290D5278707280058442B /* redis_proTests.swift */; }; - CE2B15C0275DAF0800A3C8CE /* RediStack in Frameworks */ = {isa = PBXBuildFile; productRef = CE2B15BF275DAF0800A3C8CE /* RediStack */; }; - CE2B15C4275DAF0800A3C8CE /* RedisTypes in Frameworks */ = {isa = PBXBuildFile; productRef = CE2B15C3275DAF0800A3C8CE /* RedisTypes */; }; CE3A187D282525AB00988515 /* KeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A187C282525AB00988515 /* KeyStore.swift */; }; CE3A18812825367000988515 /* ValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A18802825367000988515 /* ValueStore.swift */; }; CE3A1883282536BB00988515 /* StringValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3A1882282536BB00988515 /* StringValueStore.swift */; }; @@ -335,7 +340,11 @@ 627E1BC3282F462C00163D6B /* RenameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameStore.swift; sourceTree = ""; }; 627E1BC5282FC72B00163D6B /* HashValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashValueStore.swift; sourceTree = ""; }; 627E1BC7282FCEE300163D6B /* ScanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanStore.swift; sourceTree = ""; }; - 627E1BC928328A0600163D6B /* WindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = ""; }; + 6280596728B218D800126E81 /* RediStackClientString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RediStackClientString.swift; sourceTree = ""; }; + 6280596928B23DBB00126E81 /* RediStackClientStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RediStackClientStream.swift; sourceTree = ""; }; + 628F9A9F2896A3FB0003B6C0 /* RedisCommandExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedisCommandExt.swift; sourceTree = ""; }; + 628F9AA1289EA7C50003B6C0 /* SSHTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHTunnel.swift; sourceTree = ""; }; + 628F9AA3289EAEE90003B6C0 /* RedisClientSSH.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedisClientSSH.swift; sourceTree = ""; }; 62C262DF2839D4380036A282 /* ListValueStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListValueStore.swift; sourceTree = ""; }; 62C262E12839DD0B0036A282 /* RedisListItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedisListItemModel.swift; sourceTree = ""; }; 62C262E3283A31E40036A282 /* ListEditorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListEditorView.swift; sourceTree = ""; }; @@ -359,6 +368,8 @@ 62D741BE288414E00049AB3C /* LuaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuaView.swift; sourceTree = ""; }; 62D741C0288415180049AB3C /* LuaStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuaStore.swift; sourceTree = ""; }; 62D741C2288416470049AB3C /* RedisClientLua.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedisClientLua.swift; sourceTree = ""; }; + 62E3DCAE28B0C60B00FF865A /* PasteboardHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardHelper.swift; sourceTree = ""; }; + 62E3DCB128B0C93A00FF865A /* TableContextMenuEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableContextMenuEnum.swift; sourceTree = ""; }; 62E8F9D12765019D006A5326 /* MIntField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MIntField.swift; sourceTree = ""; }; 62E8F9D52765EC97006A5326 /* NSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSecureField.swift; sourceTree = ""; }; 62E8F9D82765F197006A5326 /* MPasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPasswordField.swift; sourceTree = ""; }; @@ -382,15 +393,14 @@ buildActionMask = 2147483647; files = ( CEA1EF4D277C56DD00D300E9 /* NIO in Frameworks */, - CE2B15C0275DAF0800A3C8CE /* RediStack in Frameworks */, CEA1EF55277C56DD00D300E9 /* NIOFoundationCompat in Frameworks */, 62D23AAF2818DCA000B2AA3F /* ComposableArchitecture in Frameworks */, + 628F9A9E28967C120003B6C0 /* RediStack in Frameworks */, CE0290CE2786B75A0058442B /* Puppy in Frameworks */, CEA1EF53277C56DD00D300E9 /* NIOEmbedded in Frameworks */, CEA1EF5E277C57BC00D300E9 /* SwiftyJSON in Frameworks */, CEA1EF59277C56DD00D300E9 /* NIOTLS in Frameworks */, CEA1EF4A277C56B100D300E9 /* NIOSSH in Frameworks */, - CE2B15C4275DAF0800A3C8CE /* RedisTypes in Frameworks */, CE83D51827563C28000DF09D /* AppCenterAnalytics in Frameworks */, CEA1EF57277C56DD00D300E9 /* NIOPosix in Frameworks */, CE83D51A27563C28000DF09D /* AppCenterCrashes in Frameworks */, @@ -417,6 +427,7 @@ 4301C84426BBE68400C08E19 /* SSHAuthentication.swift */, 4301C86326BBF62600C08E19 /* SSHRediStackClient.swift */, 4301C86526BBF86E00C08E19 /* SSHForward.swift */, + 628F9AA1289EA7C50003B6C0 /* SSHTunnel.swift */, ); path = SSH; sourceTree = ""; @@ -485,7 +496,6 @@ 4320AB1725B67E2C00A8E214 /* Model */, 4320AB1625B67E2400A8E214 /* Views */, 43822041265763D400DA7F9E /* RedisProCommands.swift */, - 627E1BC928328A0600163D6B /* WindowController.swift */, 4320AAF325B6740900A8E214 /* redis_proApp.swift */, 4320AAF725B6740A00A8E214 /* Assets.xcassets */, 4320AAFC25B6740A00A8E214 /* Info.plist */, @@ -546,15 +556,12 @@ 434047F72611C1D5001519F9 /* Common */ = { isa = PBXGroup; children = ( + 62E3DCB028B0C90300FF865A /* Enums */, 6240E0A92803035F0005E793 /* UserDefaults */, 620AF6B427D485A1002D6895 /* RedisClient */, 4301C84326BBE66F00C08E19 /* SSH */, 43CD286F2670C3BC00E11876 /* Settings */, 62D1BA9428435C0C00F41CAD /* Helpers */, - 62CB3D7F2619B09B0061E8C3 /* RedisKeyTypeEnum.swift */, - 431FD83E26BD160800151934 /* RedisConnectionTypeEnum.swift */, - 431266D4261C5C9000FB6B69 /* ButtonTypeEnum.swift */, - 43CD28AF267C6A9D00E11876 /* MainViewTypeEnum.swift */, 431266D7261C66E100FB6B69 /* MTheme.swift */, 4373C50025C2E75B002B700E /* BizError.swift */, 43481048262D30CE002809ED /* Helps.swift */, @@ -650,6 +657,7 @@ isa = PBXGroup; children = ( 4314A34E2625B46E00053FEE /* RediStackClient.swift */, + 6280596728B218D800126E81 /* RediStackClientString.swift */, 620AF6B727D486F5002D6895 /* RedisClientScan.swift */, 620AF6B527D485EB002D6895 /* RedisClientKeys.swift */, 620AF6B927D48848002D6895 /* RedisClientHash.swift */, @@ -659,7 +667,10 @@ 620AF6C127D48923002D6895 /* RedisClientSystem.swift */, 620AF6C327D48973002D6895 /* RedisClientConfig.swift */, 620AF6C527D48996002D6895 /* RedisClientSlowLog.swift */, + 6280596928B23DBB00126E81 /* RediStackClientStream.swift */, 62D741C2288416470049AB3C /* RedisClientLua.swift */, + 628F9A9F2896A3FB0003B6C0 /* RedisCommandExt.swift */, + 628F9AA3289EAEE90003B6C0 /* RedisClientSSH.swift */, ); path = RedisClient; sourceTree = ""; @@ -744,10 +755,23 @@ 437BC24D26461E2100E2C84D /* NumberHelper.swift */, 43F22EF4269FCE6000A00F97 /* DateHelper.swift */, 43F22F1026A1603F00A00F97 /* StringHelper.swift */, + 62E3DCAE28B0C60B00FF865A /* PasteboardHelper.swift */, ); path = Helpers; sourceTree = ""; }; + 62E3DCB028B0C90300FF865A /* Enums */ = { + isa = PBXGroup; + children = ( + 62CB3D7F2619B09B0061E8C3 /* RedisKeyTypeEnum.swift */, + 431FD83E26BD160800151934 /* RedisConnectionTypeEnum.swift */, + 431266D4261C5C9000FB6B69 /* ButtonTypeEnum.swift */, + 43CD28AF267C6A9D00E11876 /* MainViewTypeEnum.swift */, + 62E3DCB128B0C93A00FF865A /* TableContextMenuEnum.swift */, + ); + path = Enums; + sourceTree = ""; + }; 62E8F9D72765EFC0006A5326 /* Form */ = { isa = PBXGroup; children = ( @@ -804,8 +828,6 @@ CE83D51727563C28000DF09D /* AppCenterAnalytics */, CE83D51927563C28000DF09D /* AppCenterCrashes */, 6237D031275C954A000ACD6A /* Logging */, - CE2B15BF275DAF0800A3C8CE /* RediStack */, - CE2B15C3275DAF0800A3C8CE /* RedisTypes */, CEA1EF49277C56B100D300E9 /* NIOSSH */, CEA1EF4C277C56DD00D300E9 /* NIO */, CEA1EF4E277C56DD00D300E9 /* NIOConcurrencyHelpers */, @@ -818,6 +840,7 @@ CEA1EF5D277C57BC00D300E9 /* SwiftyJSON */, CE0290CD2786B75A0058442B /* Puppy */, 62D23AAE2818DCA000B2AA3F /* ComposableArchitecture */, + 628F9A9D28967C120003B6C0 /* RediStack */, ); productName = "redis-pro"; productReference = 4320AAF025B6740900A8E214 /* redis-pro.app */; @@ -872,12 +895,12 @@ packageReferences = ( CE83D51627563C28000DF09D /* XCRemoteSwiftPackageReference "appcenter-sdk-apple" */, 6237D030275C954A000ACD6A /* XCRemoteSwiftPackageReference "swift-log" */, - CE2B15BE275DAF0800A3C8CE /* XCRemoteSwiftPackageReference "RediStack" */, CEA1EF48277C56B100D300E9 /* XCRemoteSwiftPackageReference "swift-nio-ssh" */, CEA1EF4B277C56DD00D300E9 /* XCRemoteSwiftPackageReference "swift-nio" */, CEA1EF5C277C57BC00D300E9 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, CE0290CC2786B75A0058442B /* XCRemoteSwiftPackageReference "Puppy" */, 62D23AAD2818DCA000B2AA3F /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + 628F9A9C28967C120003B6C0 /* XCRemoteSwiftPackageReference "RediStack" */, ); productRefGroup = 4320AAF125B6740900A8E214 /* Products */; projectDirPath = ""; @@ -935,6 +958,7 @@ CEEC2562282508C0000463F5 /* RedisKeysStore.swift in Sources */, CE916155276B60650026C625 /* NSearchField.swift in Sources */, 436BB6952644E8D6008B4866 /* ZSetEditorView.swift in Sources */, + 6280596A28B23DBB00126E81 /* RediStackClientStream.swift in Sources */, 4314A3472625484100053FEE /* MTextField.swift in Sources */, 436BB6932644D126008B4866 /* SetEditorView.swift in Sources */, 4373C50525C3F0B6002B700E /* UserDefaultsKeysEnum.swift in Sources */, @@ -961,6 +985,7 @@ 4382204026574E3F00DA7F9E /* MSpin.swift in Sources */, 626C4C622820376700B7A542 /* RedisFavoriteDefaultSelectTypeEnum.swift in Sources */, 62D741C1288415180049AB3C /* LuaStore.swift in Sources */, + 6280596828B218D800126E81 /* RediStackClientString.swift in Sources */, 62D741B9284B776D0049AB3C /* RedisConfigStore.swift in Sources */, 62E9F13F2849DA9F00F4FABF /* SystemEnvironment.swift in Sources */, 627E1BC8282FCEE300163D6B /* ScanStore.swift in Sources */, @@ -978,6 +1003,7 @@ 62C262E22839DD0B0036A282 /* RedisListItemModel.swift in Sources */, 62D1BA9828437F5900F41CAD /* Messages.swift in Sources */, 626E10ED27B794E7007ED968 /* AboutView.swift in Sources */, + 62E3DCB228B0C93A00FF865A /* TableContextMenuEnum.swift in Sources */, 43CD28742670E8BE00E11876 /* SettingsView.swift in Sources */, 620AF6BC27D488B1002D6895 /* RedisClientList.swift in Sources */, 62D741B7284B69040049AB3C /* RedisSystemView.swift in Sources */, @@ -1012,7 +1038,6 @@ 430A09D625BEA98300B60DFC /* RedisListView.swift in Sources */, 621EFD8E276CC52E0079D1E3 /* NPasswordField.swift in Sources */, 431266E5261D4E2700FB6B69 /* RedisValueView.swift in Sources */, - 627E1BCA28328A0600163D6B /* WindowController.swift in Sources */, 4301C82F26B0EEB800C08E19 /* Stopwatch.swift in Sources */, 62CB3D752619AEC20061E8C3 /* RedisKeysListView.swift in Sources */, 624F940427FADDD400DF3D3E /* TableCellView.swift in Sources */, @@ -1043,6 +1068,7 @@ 43BB012326390BC20039565E /* ModalView.swift in Sources */, 43CD28712670C3DD00E11876 /* ColorSchemeEnum.swift in Sources */, 434047FA2611C828001519F9 /* RedisFavoriteModel.swift in Sources */, + 628F9AA4289EAEE90003B6C0 /* RedisClientSSH.swift in Sources */, 43F22EED269ED9D500A00F97 /* SlowLogView.swift in Sources */, CE83D51C2758E8D1000DF09D /* NTextField.swift in Sources */, 4373C50925C3F218002B700E /* RedisModel.swift in Sources */, @@ -1050,6 +1076,7 @@ 6237D034275CFF1F000ACD6A /* NIntField.swift in Sources */, 431FD83F26BD160800151934 /* RedisConnectionTypeEnum.swift in Sources */, 4373C50125C2E75B002B700E /* BizError.swift in Sources */, + 628F9AA2289EA7C50003B6C0 /* SSHTunnel.swift in Sources */, 62D741BF288414E00049AB3C /* LuaView.swift in Sources */, 431266EE261D517500FB6B69 /* RedisKeyTypePicker.swift in Sources */, 62E8F9D62765EC97006A5326 /* NSecureField.swift in Sources */, @@ -1069,6 +1096,8 @@ 4301C86426BBF62600C08E19 /* SSHRediStackClient.swift in Sources */, 43822042265763D400DA7F9E /* RedisProCommands.swift in Sources */, 430A09E625C155F800B60DFC /* FormItemText.swift in Sources */, + 62E3DCAF28B0C60B00FF865A /* PasteboardHelper.swift in Sources */, + 628F9AA02896A3FB0003B6C0 /* RedisCommandExt.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1231,7 +1260,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = "\"redis-pro/Preview Content\""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "redis-pro/Info.plist"; @@ -1240,7 +1269,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = "com.cmushroom.redis-pro"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -1256,7 +1285,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 17; DEVELOPMENT_ASSET_PATHS = "\"redis-pro/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -1266,7 +1295,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = "com.cmushroom.redis-pro"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1351,6 +1380,14 @@ minimumVersion = 1.4.2; }; }; + 628F9A9C28967C120003B6C0 /* XCRemoteSwiftPackageReference "RediStack" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Mordil/RediStack"; + requirement = { + branch = master; + kind = branch; + }; + }; 62D23AAD2818DCA000B2AA3F /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture"; @@ -1367,14 +1404,6 @@ minimumVersion = 0.5.0; }; }; - CE2B15BE275DAF0800A3C8CE /* XCRemoteSwiftPackageReference "RediStack" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Mordil/RediStack.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.1; - }; - }; CE83D51627563C28000DF09D /* XCRemoteSwiftPackageReference "appcenter-sdk-apple" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://gitee.com/chengpan168_admin/appcenter-sdk-apple.git"; @@ -1388,7 +1417,7 @@ repositoryURL = "https://github.com/apple/swift-nio-ssh"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.3.3; + minimumVersion = 0.4.1; }; }; CEA1EF4B277C56DD00D300E9 /* XCRemoteSwiftPackageReference "swift-nio" */ = { @@ -1415,6 +1444,11 @@ package = 6237D030275C954A000ACD6A /* XCRemoteSwiftPackageReference "swift-log" */; productName = Logging; }; + 628F9A9D28967C120003B6C0 /* RediStack */ = { + isa = XCSwiftPackageProductDependency; + package = 628F9A9C28967C120003B6C0 /* XCRemoteSwiftPackageReference "RediStack" */; + productName = RediStack; + }; 62D23AAE2818DCA000B2AA3F /* ComposableArchitecture */ = { isa = XCSwiftPackageProductDependency; package = 62D23AAD2818DCA000B2AA3F /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; @@ -1425,16 +1459,6 @@ package = CE0290CC2786B75A0058442B /* XCRemoteSwiftPackageReference "Puppy" */; productName = Puppy; }; - CE2B15BF275DAF0800A3C8CE /* RediStack */ = { - isa = XCSwiftPackageProductDependency; - package = CE2B15BE275DAF0800A3C8CE /* XCRemoteSwiftPackageReference "RediStack" */; - productName = RediStack; - }; - CE2B15C3275DAF0800A3C8CE /* RedisTypes */ = { - isa = XCSwiftPackageProductDependency; - package = CE2B15BE275DAF0800A3C8CE /* XCRemoteSwiftPackageReference "RediStack" */; - productName = RedisTypes; - }; CE83D51727563C28000DF09D /* AppCenterAnalytics */ = { isa = XCSwiftPackageProductDependency; package = CE83D51627563C28000DF09D /* XCRemoteSwiftPackageReference "appcenter-sdk-apple" */; diff --git a/redis-pro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/redis-pro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 995f946..72b0ca3 100644 --- a/redis-pro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/redis-pro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -39,10 +39,19 @@ { "identity" : "redistack", "kind" : "remoteSourceControl", - "location" : "https://github.com/Mordil/RediStack.git", + "location" : "https://github.com/Mordil/RediStack", "state" : { - "revision" : "16037bbb8248eccaf50b8499d3bb9ed945cfd44c", - "version" : "1.2.1" + "branch" : "master", + "revision" : "555062c62e1568ed3125a51103ac42a9e4f7a626" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics", + "state" : { + "revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", + "version" : "1.0.2" } }, { @@ -122,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "154f1d32366449dcccf6375a173adf4ed2a74429", - "version" : "2.38.0" + "revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250", + "version" : "2.41.1" } }, { @@ -131,8 +140,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssh", "state" : { - "revision" : "09778e0388bda898c7592887f2ec84bb81ef21eb", - "version" : "0.3.3" + "revision" : "fe02717fa9f7eb8d82957d6784bc3d1793f9c1e6", + "version" : "0.4.1" + } + }, + { + "identity" : "swift-service-discovery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-discovery", + "state" : { + "revision" : "c83afedb1c95ef0111907cd6e2fd03d7175cc0d0", + "version" : "1.2.0" } }, { diff --git a/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate b/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate index 2f8cfd4..98a5ca9 100644 Binary files a/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate and b/redis-pro.xcodeproj/project.xcworkspace/xcuserdata/chengpan.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 7a8538f..81ca29c 100644 --- a/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -3,4 +3,70 @@ uuid = "1F6FC20B-7A60-4E4D-9CFD-509EE3AA4D81" type = "1" version = "2.0"> + + + + + + + + + + + + + + + + + + diff --git a/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcschemes/xcschememanagement.plist b/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcschemes/xcschememanagement.plist index d2b0cf2..b8fd653 100644 --- a/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/redis-pro.xcodeproj/xcuserdata/chengpan.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,84 +7,105 @@ CustomDump (Playground) 1.xcscheme isShown - + orderHint - 11 + 6 CustomDump (Playground) 2.xcscheme isShown - + + orderHint + 7 + + CustomDump (Playground) 3.xcscheme + + isShown + + orderHint + 25 + + CustomDump (Playground) 4.xcscheme + + isShown + orderHint - 12 + 26 + + CustomDump (Playground) 5.xcscheme + + isShown + + orderHint + 27 CustomDump (Playground).xcscheme isShown - + orderHint - 10 + 5 Playground (Playground) 1.xcscheme isShown - + orderHint - 4 + 9 Playground (Playground) 2.xcscheme isShown - + orderHint - 6 + 10 Playground (Playground) 3.xcscheme isShown - + orderHint 7 Playground (Playground) 4.xcscheme isShown - + orderHint 8 Playground (Playground) 5.xcscheme isShown - + orderHint 9 Playground (Playground).xcscheme isShown - + orderHint - 2 + 8 PromiseKit (Playground) 1.xcscheme isShown - + orderHint 3 PromiseKit (Playground) 2.xcscheme isShown - + orderHint 5 PromiseKit (Playground).xcscheme isShown - + orderHint 1 diff --git a/redis-pro/Common/ButtonTypeEnum.swift b/redis-pro/Common/Enums/ButtonTypeEnum.swift similarity index 100% rename from redis-pro/Common/ButtonTypeEnum.swift rename to redis-pro/Common/Enums/ButtonTypeEnum.swift diff --git a/redis-pro/Common/MainViewTypeEnum.swift b/redis-pro/Common/Enums/MainViewTypeEnum.swift similarity index 100% rename from redis-pro/Common/MainViewTypeEnum.swift rename to redis-pro/Common/Enums/MainViewTypeEnum.swift diff --git a/redis-pro/Common/RedisConnectionTypeEnum.swift b/redis-pro/Common/Enums/RedisConnectionTypeEnum.swift similarity index 100% rename from redis-pro/Common/RedisConnectionTypeEnum.swift rename to redis-pro/Common/Enums/RedisConnectionTypeEnum.swift diff --git a/redis-pro/Common/RedisKeyTypeEnum.swift b/redis-pro/Common/Enums/RedisKeyTypeEnum.swift similarity index 100% rename from redis-pro/Common/RedisKeyTypeEnum.swift rename to redis-pro/Common/Enums/RedisKeyTypeEnum.swift diff --git a/redis-pro/Common/Enums/TableContextMenuEnum.swift b/redis-pro/Common/Enums/TableContextMenuEnum.swift new file mode 100644 index 0000000..7c5ea2f --- /dev/null +++ b/redis-pro/Common/Enums/TableContextMenuEnum.swift @@ -0,0 +1,50 @@ +// +// TableContextMenuEnum.swift +// redis-pro +// +// Created by chengpan on 2022/8/20. +// + +import Foundation +import Cocoa + +enum TableContextMenu: String{ + case DELETE = "Delete" + case EDIT = "Edit" + + // copy + case COPY = "Copy" + case COPY_SCORE = "Copy Score" + case COPY_FIELD = "Copy Field" + case COPY_VALUE = "Copy Value" + + // key list + case RENAME = "Rename" + // client list + case KILL = "Kill" + + var ext: TableContextMenuExt { + switch self { + case .DELETE: + return .init(keyEquivalent: String(Unicode.Scalar(NSBackspaceCharacter)!)) + case .EDIT: + return .init(keyEquivalent: "e") + case .COPY: + return .init(keyEquivalent: "c") + case .COPY_SCORE: + return .init(keyEquivalent: "") + case .COPY_FIELD: + return .init(keyEquivalent: "") + case .COPY_VALUE: + return .init(keyEquivalent: "") + case .RENAME: + return .init(keyEquivalent: "") + case .KILL: + return .init(keyEquivalent: "k") + } + } +} + +struct TableContextMenuExt { + var keyEquivalent: String +} diff --git a/redis-pro/Common/Helpers/PasteboardHelper.swift b/redis-pro/Common/Helpers/PasteboardHelper.swift new file mode 100644 index 0000000..4df3e7e --- /dev/null +++ b/redis-pro/Common/Helpers/PasteboardHelper.swift @@ -0,0 +1,19 @@ +// +// PasteboardHelper.swift +// redis-pro +// +// Created by chengpan on 2022/8/20. +// + +import Foundation +import Cocoa + +class PasteboardHelper { + + static func copy(_ value: String) { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(value, forType: .string) + } + +} diff --git a/redis-pro/Common/RedisClient/RediStackClient.swift b/redis-pro/Common/RedisClient/RediStackClient.swift index cacc575..89cadc4 100644 --- a/redis-pro/Common/RedisClient/RediStackClient.swift +++ b/redis-pro/Common/RedisClient/RediStackClient.swift @@ -21,7 +21,13 @@ class RediStackClient { let logger = Logger(label: "redis-client") var redisModel:RedisModel + + // conn + private let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 3) var connection:RedisConnection? + var connPool:RedisConnectionPool? + + var keepaliveTask: RepeatedTask? // ssh var sshChannel:Channel? @@ -36,7 +42,7 @@ class RediStackClient { var viewStore:ViewStore? - init(redisModel:RedisModel) { + init(_ redisModel:RedisModel) { self.redisModel = redisModel } @@ -71,7 +77,7 @@ class RediStackClient { } func handleError(_ error: Error) { - logger.info("get an error \(error)") + logger.info("system error \(error)") loading(false) Messages.show(error) } @@ -81,6 +87,9 @@ class RediStackClient { */ func initConnection() async -> Bool { begin() + defer { + complete() + } do { let _ = try await getConn() @@ -90,7 +99,6 @@ class RediStackClient { handleError(error) } - complete() return false } @@ -101,300 +109,116 @@ class RediStackClient { } } - // string operator - func set(_ key:String, value:String, ex:Int?) async -> Void { - logger.info("set value, key:\(key), value:\(value), ex:\(ex ?? -1)") - begin() - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - if (ex == nil || ex! == -1) { - conn.set(RedisKey(key), to: value) - .whenComplete({completion in - if case .success(_) = completion { - continuation.resume() - } - - self.complete(completion, continuation: continuation) - }) - } else { - conn.setex(RedisKey(key), to: value, expirationInSeconds: ex!) - .whenComplete({completion in - if case .success(_) = completion { - continuation.resume() - } - - self.complete(completion, continuation: continuation) - }) - } - } - } catch { - handleError(error) - } - - } - - func set(_ key:String, value:String) async -> Void { - logger.info("set value, key:\(key), value:\(value)") - - await set(key, value:value, ex: -1) + // MARK: - Common function + /** + 公共底层请求redis 数据方法, 不处理任何异常, 使用者需要自己行处理异常信息 + */ + func _send(_ command: RedisCommand) async throws -> R { + let conn = try await getConn() - } - - func get(_ key:String) async -> String { - logger.info("get value, key:\(key)") - begin() - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.get(RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("get value key: \(key) complete, r: \(r)") - if r.isNull { - continuation.resume(throwing: BizError(message: "Key `\(key)` is not exist!")) - } else { - continuation.resume(returning: r.string!) - } - } - - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) + return try await withCheckedThrowingContinuation { continuation in + conn.send(command, eventLoop: nil, logger: self.logger) + .whenComplete({completion in + if case .success(let r) = completion { + self.logger.info("string operator, setex complete") + continuation.resume(returning: r) + } + else if case .failure(let error) = completion { + continuation.resume(throwing: error) + } + }) } - return Cons.EMPTY_STRING } - func del(_ key:String) async -> Int { - self.logger.info("delete key \(key)") + func send(_ command: RedisCommand, _ defaultValue: R) async -> R { + self.logger.info("send redis command, command: \(command)") begin() - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.delete(RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("delete redis key \(key) complete, r: \(r)") - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) + defer { + complete() } - return 0 - } - - func expire(_ key:String, seconds:Int) async -> Bool { - logger.info("set key expire key:\(key), seconds:\(seconds)") - begin() do { - - let maxSeconds:Int64 = INT64_MAX / (1000 * 1000 * 1000) - try Assert.isTrue(seconds < maxSeconds, message: "过期时间最大值不能超过 \(maxSeconds) 秒") - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - if seconds < 0 { - conn.send(command: "PERSIST", with: [RESPValue(from: key)]).whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("clear key expire time \(key) complete, r: \(r)") - continuation.resume(returning: true) - } - self.complete(completion, continuation: continuation) - }) - } else { - conn.expire(RedisKey(key), after: TimeAmount.seconds(Int64(seconds))).whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("set key expire time \(key) complete, r: \(r)") - continuation.resume(returning: true) - } - - self.complete(completion, continuation: continuation) - }) - } - - } + return try await _send(command) } catch { handleError(error) } - return false + return defaultValue } - func exist(_ key:String) async -> Bool { - logger.info("get key exist: \(key)") - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.exists(RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("query redis key exist, key: \(key), r:\(r)") - continuation.resume(returning: r > 0) - } - else if case .failure(let error) = completion { - self.logger.error("redis get key exist error \(error)") - continuation.resume(returning: false) - } - }) - - } - } catch { - self.logger.error("redis get key exist error \(error)") - } - return false - } - - func ttl(_ key:String) async -> Int { - logger.info("get ttl key: \(key)") + func send(_ command: RedisCommand) async -> R? { + self.logger.info("send redis command, command: \(command)") begin() + defer { + complete() + } + do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.ttl(RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("query redis key ttl, key: \(key), r:\(r)") - var ttl = -1 - if r == RedisKey.Lifetime.keyDoesNotExist { - ttl = -2 -// continuation.resume(throwing: BizError(message: "Key `\(key)` is not exist!")) -// return - } else if r == RedisKey.Lifetime.unlimited { - // ignore - } else { - ttl = Int(r.timeAmount!.nanoseconds / 1000000000) - } - continuation.resume(returning: ttl) - } - - self.complete(completion, continuation: continuation) - }) - - } + return try await _send(command) } catch { handleError(error) } - return -1 + return nil } - func getTypes(_ keys:[String]) async -> [String:String] { - return await withTaskGroup(of: (String, String).self) { group in - var typeDict = [String:String]() - - // adding tasks to the group and fetching movies - for key in keys { - group.addTask { - let type = await self.type(key) - return (key, type) - } - } - - for await type in group { - typeDict[type.0] = type.1 - } - - return typeDict + func ttlSecond(_ lifetime: RedisKey.Lifetime) -> Int { + switch lifetime { + case .keyDoesNotExist: + return -2 + case .limited(let duration): + return Int(duration.timeAmount.nanoseconds / 1000000000) + default: + return -1 } } - private func type(_ key:String) async -> String { - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "type", with: [RESPValue.init(from: key)]) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r.string!) - } else if case .failure(let error) = completion { - self.logger.error("get key type error: \(error)") - continuation.resume(returning: RedisKeyTypeEnum.NONE.rawValue) - } - }) - - } - } catch { - self.logger.error("get key type error: \(error)") - } - - return RedisKeyTypeEnum.NONE.rawValue - } - - func rename(_ oldKey:String, newKey:String) async -> Bool { - logger.info("rename key, old key:\(oldKey), new key: \(newKey)") - begin() - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "RENAME", with: [RESPValue(from: oldKey), RESPValue(from: newKey)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("rename redis key, old key \(oldKey), new key: \(newKey) complete, r: \(r)") - continuation.resume(returning: r.string == "OK") - } - - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return false + func getConn() async throws -> RedisClient { + return try await getConnPool() + +// if self.connection != nil && self.connection!.isConnected { +// return self.connection! +// } else { +// self.logger.info("get redis connection, but connection is not available...") +// self.close() +// } +// +// if self.redisModel.connectionType == RedisConnectionTypeEnum.SSH.rawValue { +// self.connection = try await initSSHConn() +// } else { +// self.connection = try await initConn(host: self.redisModel.host, port: self.redisModel.port, pass: self.redisModel.password, database: self.redisModel.database) +// } +// +// return self.connection! } - func getConn() async throws -> RedisClient { - if self.connection != nil && self.connection!.isConnected { - return self.connection! + func getConnPool() async throws -> RedisClient { + if self.connPool != nil { + return self.connPool! } else { self.logger.info("get redis connection, but connection is not available...") self.close() } - self.connection = try await initConn() - return self.connection! + if self.redisModel.connectionType == RedisConnectionTypeEnum.SSH.rawValue { + self.connection = try await initSSHConn() + } else { + self.connPool = try initPool(host: self.redisModel.host, port: self.redisModel.port, pass: self.redisModel.password, database: self.redisModel.database) + } + + return self.connPool! } - func initConn() async throws -> RedisConnection { + func initConn(host:String, port:Int, pass:String, database:Int) async throws -> RedisConnection { - if self.redisModel.connectionType == RedisConnectionTypeEnum.SSH.rawValue { - return try await getSSHConn() - } - return try await withUnsafeThrowingContinuation { continuation in - self.logger.info("start get new redis connection...") - + logger.info("init new redis connection, host: \(host), port: \(port), pass: \(pass), database: \(database)") + return try await withCheckedThrowingContinuation { continuation in do { let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 4).next() var configuration: RedisConnection.Configuration - if (self.redisModel.password.isEmpty) { - configuration = try RedisConnection.Configuration(hostname: self.redisModel.host, port: self.redisModel.port, initialDatabase: self.redisModel.database, defaultLogger: logger) + if (pass.isEmpty) { + configuration = try RedisConnection.Configuration(hostname: host, port: port, initialDatabase: database, defaultLogger: logger) } else { - configuration = try RedisConnection.Configuration(hostname: self.redisModel.host, port: self.redisModel.port, password: self.redisModel.password, initialDatabase: self.redisModel.database, defaultLogger: logger) + configuration = try RedisConnection.Configuration(hostname: host, port: port, password: pass, initialDatabase: database, defaultLogger: logger) } let future = RedisConnection.make( @@ -403,52 +227,98 @@ class RediStackClient { ) future.whenSuccess({ redisConnection in - self.logger.info("get new redis connection success, connection id: \(redisConnection.id)") + self.logger.info("init redis connection success, connection id: \(redisConnection.id)") continuation.resume(returning: redisConnection) }) future.whenFailure({ error in - self.logger.info("get new redis connection error: \(error)") + self.logger.info("init redis connection error: \(error)") continuation.resume(throwing: error) }) } catch { + self.logger.info("init redis connection error: \(error)") continuation.resume(throwing: error) } } } - public func initPool() throws -> RedisConnectionPool { - let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 4).next() + public func initPool(host:String, port:Int, pass:String, database:Int) throws -> RedisConnectionPool { + let eventLoop = eventLoopGroup.next() + + let addresses = try [SocketAddress.makeAddressResolvingHost(host, port: port)] + + let password = pass.isEmpty ? nil : pass + + let config: RedisConnectionPool.PoolConnectionConfiguration = .init( + initialDatabase: database + , password: password + , defaultLogger: self.logger, tcpClient: nil) - let addresses = try [SocketAddress.makeAddressResolvingHost(self.redisModel.host, port: self.redisModel.port)] let pool = RedisConnectionPool( configuration: .init( - initialServerConnectionAddresses: addresses, - maximumConnectionCount: .maximumActiveConnections(4), - connectionFactoryConfiguration: .init(connectionInitialDatabase: self.redisModel.database, connectionPassword: self.redisModel.password, connectionDefaultLogger: nil, tcpClient: nil), - minimumConnectionCount: 2, - connectionBackoffFactor: 2, - initialConnectionBackoffDelay: .milliseconds(100), - connectionRetryTimeout: .seconds(60) - ), - boundEventLoop: eventLoop - ) - pool.activate() + initialServerConnectionAddresses: addresses + , connectionCountBehavior: .elastic(maximumConnectionCount: 3, minimumConnectionCount: 2) + , connectionConfiguration: config + , retryStrategy: .exponentialBackoff(initialDelay: .milliseconds(100), timeout: .seconds(5)) + , poolDefaultLogger: self.logger + ) + , boundEventLoop: eventLoop) + pool.activate() + +// _keepalive() self.logger.info("init redis connection pool complete...") return pool } - func close() -> Void { - guard let conn = self.connection else { - logger.info("close redis connection, connection is nil, over...") - return - } + private func _keepalive() { + let eventLoop = eventLoopGroup.next() + self.keepaliveTask = eventLoop.scheduleRepeatedAsyncTask(initialDelay: .seconds(10), delay: .seconds(5)) {_ in + self.logger.info("keep alive connection...") + self.connPool?.leaseConnection() { conn in + return conn.send(.echo("redis-pro heartbeat")) + }.whenComplete({completion in + if case .success(let r) = completion { + self.logger.info("keepalive heartbeat echo: \(r)") + } + else if case .failure(let error) = completion { + self.logger.info("keepalive heartbeat error: \(error)") + } + }) - conn.close().whenComplete({completion in + return eventLoop.makeSucceededVoidFuture() + } + } + + // close + func close() -> Void { + self.connection?.close().whenComplete({completion in self.connection = nil - self.logger.info("redis connection close success") + self.logger.info("redis connection close") }) + self.connPool?.close() + self.connPool = nil + self.logger.info("redis connection pool close") self.closeSSH() + + self.keepaliveTask?.cancel() + } + + deinit { + logger.info("gracefully shutdown event loop group start...") + self.eventLoopGroup.shutdownGracefully({ _ in + self.logger.info("gracefully shutdown event loop group complete...") + }) + } +} + + + +// MARK: RESPValue Conversion +extension RESPValue { + @usableFromInline + func map(to type: T.Type = T.self) throws -> T { + guard let value = T(fromRESP: self) else { throw RedisClientError.failedRESPConversion(to: type) } + return value } } diff --git a/redis-pro/Common/RedisClient/RediStackClientStream.swift b/redis-pro/Common/RedisClient/RediStackClientStream.swift new file mode 100644 index 0000000..00ccc94 --- /dev/null +++ b/redis-pro/Common/RedisClient/RediStackClientStream.swift @@ -0,0 +1,15 @@ +// +// RediStackClientStream.swift +// redis-pro +// +// Created by chengpan on 2022/8/21. +// + +import Foundation +import RediStack + +// MARK: - stream function +// stream +extension RediStackClient { + +} diff --git a/redis-pro/Common/RedisClient/RediStackClientString.swift b/redis-pro/Common/RedisClient/RediStackClientString.swift new file mode 100644 index 0000000..b6cb9a4 --- /dev/null +++ b/redis-pro/Common/RedisClient/RediStackClientString.swift @@ -0,0 +1,121 @@ +// +// RediStackClientKey.swift +// redis-pro +// +// Created by chengpan on 2022/8/21. +// + +import Foundation +import RediStack + +// MARK: - string operator +extension RediStackClient { + + /** + set value expire(seconds) + */ + func set(_ key:String, value:String, ex:Int = -1) async -> Void { + logger.info("set value, key:\(key), value:\(value), ex:\(ex)") + + let command:RedisCommand = ex == -1 ? .set(RedisKey(key), to: value) : .setex(RedisKey(key), to: value, expirationInSeconds: ex) + + await send(command) + } + + func set(_ key:String, value:String) async -> Void { + logger.info("set value, key:\(key), value:\(value)") + + await set(key, value:value, ex: -1) + } + + func get(_ key:String) async -> String { + logger.info("get value, key:\(key)") + + let command:RedisCommand = .get(RedisKey(key)) + let r = await send(command) + return r??.description ?? Cons.EMPTY_STRING + } + + func del(_ key:String) async -> Int { + self.logger.info("delete key \(key)") + + let command:RedisCommand = .del([RedisKey(key)]) + return await send(command, 0) + } + + func expire(_ key:String, seconds:Int = -1) async -> Bool { + logger.info("set key expire key:\(key), seconds:\(seconds)") + + do { + + let maxSeconds:Int64 = INT64_MAX / (1000 * 1000 * 1000) + try Assert.isTrue(seconds < maxSeconds, message: "过期时间最大值不能超过 \(maxSeconds) 秒") + + let command:RedisCommand = seconds < 0 ? + // PERSIST + .init(keyword: "PERSIST", arguments: [.init(from: key)], mapValueToResult: { + return $0.int == 1 + }) : .expire(RedisKey(key), after: .seconds(Int64(seconds))) + return await send(command, false) + + } catch { + handleError(error) + } + return false + } + + func exist(_ key:String) async -> Bool { + logger.info("get key exist: \(key)") + let command:RedisCommand = .exists(RedisKey(key)) + return await send(command) == 1 + } + + func ttl(_ key:String) async -> Int { + logger.info("get ttl key: \(key)") + let command:RedisCommand = .ttl(RedisKey(key)) + return ttlSecond(await send(command, RedisKey.Lifetime.keyDoesNotExist)) + } + + func getTypes(_ keys:[String]) async -> [String:String] { + return await withTaskGroup(of: (String, String).self) { group in + var typeDict = [String:String]() + + // adding tasks to the group and fetching movies + for key in keys { + group.addTask { + let type = await self.type(key) + return (key, type) + } + } + + for await type in group { + typeDict[type.0] = type.1 + } + + return typeDict + } + } + + private func type(_ key:String) async -> String { + do { + let command:RedisCommand = .type(key) + return try await _send(command) + } catch { + self.logger.error("get type error: \(error)") + } + return RedisKeyTypeEnum.NONE.rawValue + } + + + func rename(_ oldKey:String, newKey:String) async -> Bool { + logger.info("rename key, old key:\(oldKey), new key: \(newKey)") + + let command:RedisCommand = .renamenx(oldKey, newKey: newKey) + let r = await send(command, 0) + if r == 0 { + Messages.show("rename key error, new key: \(newKey) already exists.") + } + + return r > 0 + } +} diff --git a/redis-pro/Common/RedisClient/RedisClientConfig.swift b/redis-pro/Common/RedisClient/RedisClientConfig.swift index c20b149..feead9f 100644 --- a/redis-pro/Common/RedisClient/RedisClientConfig.swift +++ b/redis-pro/Common/RedisClient/RedisClientConfig.swift @@ -9,129 +9,33 @@ import Foundation import RediStack -// config +// MARK: -config extension RediStackClient { func getConfigList(_ pattern:String = "*") async -> [RedisConfigItemModel] { logger.info("get redis config list, pattern: \(pattern)...") - begin() - defer { - complete() - } - var _pattern = pattern - if pattern.isEmpty { - _pattern = "*" - } - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "CONFIG", with: [RESPValue(from: "GET"), RESPValue(from: _pattern)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("get redis config list res: \(r)") - - let configs = r.array ?? [] - - var configList = [RedisConfigItemModel]() - - let max:Int = configs.count / 2 - - for index in (0.. = .configList(pattern) + return await send(command, []) } func configRewrite() async -> Bool { logger.info("redis config rewrite ...") - begin() - defer { - complete() - } - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "CONFIG", with: [RESPValue(from: "REWRITE")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("redis config rewrite res: \(r)") - continuation.resume(returning: r.string == "OK") - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return false + let command: RedisCommand = .configRewrite() + return await send(command, false) } func getConfigOne(key:String) async -> String? { logger.info("get redis config ...") - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "CONFIG", with: [RESPValue(from: "GET"), RESPValue(from: key)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("get redis config one res: \(r)") - continuation.resume(returning: r.array?[1].string) - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return nil - + let command: RedisCommand = .getConfig(key) + return await send(command) } func setConfig(key:String, value:String) async -> Bool { logger.info("set redis config, key: \(key), value: \(value)") - begin() - defer { - complete() - } - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "CONFIG", with: [RESPValue(from: "SET"), RESPValue(from: key), RESPValue(from: value)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("set config res: \(r)") - continuation.resume(returning: r.string == "OK") - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return false + let command: RedisCommand = .setConfig(key, value: value) + return await send(command, false) } } diff --git a/redis-pro/Common/RedisClient/RedisClientHash.swift b/redis-pro/Common/RedisClient/RedisClientHash.swift index a844da3..3e0741b 100644 --- a/redis-pro/Common/RedisClient/RedisClientHash.swift +++ b/redis-pro/Common/RedisClient/RedisClientHash.swift @@ -8,6 +8,8 @@ import Foundation import RediStack + +// MARK: - hash function // hash extension RediStackClient { @@ -29,15 +31,15 @@ extension RediStackClient { if isScan { let match = page.keywords.isEmpty ? nil : page.keywords - let pageData:[(String, String?)] = try await hashPageScan(key, page: page) + let pageData:[(String, String?)] = try await _hashPageScan(key, page: page) r = pageData.map({ RedisHashEntryModel(field: $0.0, value: $0.1) }) - let total = try await hashCountScan(key, keywords: match) + let total = try await _hashCountScan(key, keywords: match) page.total = total } else { - let value = try await hashGet(key, field: page.keywords) + let value = try await _hget(key, field: page.keywords) if value != nil { r.append(RedisHashEntryModel(field: page.keywords, value: value)) page.total = 1 @@ -52,72 +54,31 @@ extension RediStackClient { func hset(_ key:String, field:String, value:String) async -> Bool { logger.info("redis hash hset key:\(key), field:\(field), value:\(value)") - begin() - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.hset(field, to: value, in: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("hset success, key:\(key), field:\(field), value:\(value), r:\(r)") - continuation.resume(returning: true) - } - - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return false + let command:RedisCommand = .hset(RedisHashFieldKey(field), to: value, in: RedisKey(key)) + return await send(command, false) } func hdel(_ key:String, field:String) async -> Int { logger.info("redis hash hdel key:\(key), field:\(field)") - begin() - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.hdel(field, from: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("hdel success, key:\(key), field:\(field)") - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return 0 - + let command:RedisCommand = .hdel([RedisHashFieldKey(field)], from: RedisKey(key)) + return await send(command, 0) } - private func hashCountScan(_ key:String, keywords:String?) async throws -> Int { + private func _hashCountScan(_ key:String, keywords:String?) async throws -> Int { if isMatchAll(keywords ?? "") { logger.info("keywords is match all, use hlen...") - return try await hlen(key) + return try await _hlen(key) } var cursor:Int = 0 var count:Int = 0 while true { - let res = try await hscanCount(key, keywords: keywords, cursor: cursor, count: dataCountScanCount) + let res = try await _hscanCount(key, keywords: keywords, cursor: cursor, count: dataCountScanCount) logger.info("loop scan page, current cursor: \(cursor), total count: \(count)") cursor = res.0 - count = count + (res.1 / 2) + count = count + res.1 // 取到结果,或者已满足分页数据 if cursor == 0{ @@ -127,19 +88,19 @@ extension RediStackClient { return count } - private func hashPageScan(_ key:String, page: Page) async throws -> [(String, String?)] { + private func _hashPageScan(_ key:String, page: Page) async throws -> [(String, String?)] { let keywords = page.keywords.isEmpty ? nil : page.keywords var end:Int = page.end var cursor:Int = 0 var keys:[(String, String?)] = [] while true { - let res = try await hscan(key, keywords: keywords, cursor: cursor, count: dataScanCount) + let res = try await _hscan(key, keywords: keywords, cursor: cursor, count: dataScanCount) logger.info("hash loop scan page, current cursor: \(cursor), total count: \(keys.count)") cursor = res.0 keys = keys + res.1 - // 取到结果,或者已满足分页数据 + // 取到结尾,或者已满足分页数据 if cursor == 0 || keys.count >= end { break } @@ -156,138 +117,29 @@ extension RediStackClient { } - private func hlen(_ key:String) async throws -> Int { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.hlen(of: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - self.logger.error("redis hash hlen error \(error)") - continuation.resume(throwing: error) - } - }) - - } + private func _hlen(_ key:String) async throws -> Int { + let command: RedisCommand = .hlen(of: RedisKey(key)) + return try await _send(command) } - private func hscanCount(_ key:String, keywords:String?, cursor:Int, count:Int = 100) async throws -> (Int, Int) { + private func _hscanCount(_ key:String, keywords:String?, cursor:Int, count:Int = 100) async throws -> (Int, Int) { logger.debug("redis hash scan, key: \(key) cursor: \(cursor), keywords: \(String(describing: keywords)), count:\(String(describing: count))") - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - var args: [RESPValue] = [.init(from: key), .init(from: cursor)] - - if let m = keywords { - args.append(.init(from: "MATCH")) - args.append(.init(from: m)) - } - - args.append(.init(from: "COUNT")) - args.append(.init(from: count)) - - conn.send(command: "HSCAN", with: args).whenComplete({completion in - if case .success(let r) = completion { - self.logger.error("redis hash scan r: \(r)") - guard let scanR:[RESPValue] = r.array else { - continuation.resume(returning: (0, 0)) - return - } - - let cursor = Int(scanR[0].string!) ?? 0 - - continuation.resume(returning: (cursor, scanR[1].array?.count ?? 0)) - } - else if case .failure(let error) = completion { - self.logger.error("redis hash scan error: \(error)") - continuation.resume(throwing: error) - } - }) - } + let r = try await _hscan(key, keywords: keywords, cursor: cursor, count: count) + return (r.0, r.1.count) } - private func hscan(_ key:String, keywords:String?, cursor:Int, count:Int = 100) async throws -> (Int, [(String, String?)]) { + private func _hscan(_ key:String, keywords:String?, cursor:Int, count:Int = 100) async throws -> (Int, [(String, String?)]) { logger.debug("redis hash scan, key: \(key) cursor: \(cursor), keywords: \(String(describing: keywords)), count:\(String(describing: count))") - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - var args: [RESPValue] = [.init(from: key), .init(from: cursor)] - - if let m = keywords { - args.append(.init(from: "MATCH")) - args.append(.init(from: m)) - } - - args.append(.init(from: "COUNT")) - args.append(.init(from: count)) - - conn.send(command: "HSCAN", with: args).whenComplete({completion in - if case .success(let r) = completion { - guard let scanR:[RESPValue] = r.array else { - continuation.resume(returning: (0, [])) - return - } - - let cursor = Int(scanR[0].string!) ?? 0 - - continuation.resume(returning: (cursor, self.mapRes(scanR[1].array))) - } - else if case .failure(let error) = completion { - self.logger.error("redis hash scan error: \(error)") - continuation.resume(throwing: error) - } - }) - } - } - - private func mapRes(_ values: [RESPValue]?) -> [(String, String?)]{ - guard let values = values else { return [] } - guard values.count > 0 else { return [] } - - var result: [(String, String?)] = [] - - var index = 0 - repeat { - result.append((values[index].string!, values[index + 1].string)) - index += 2 - } while (index < values.count) - - return result - } - - func hget(_ key:String, field:String) async -> String { - logger.info("get hash field value, key:\(key), field: \(field)") - begin() - - do { - return try await hashGet(key, field: field) ?? "" - } catch { - handleError(error) - } - return Cons.EMPTY_STRING + let command: RedisCommand<(Int, [(String, String?)])> = ._hscan(key, keywords: keywords, cursor: cursor, count: count) + return try await _send(command) } - private func hashGet(_ key:String, field:String) async throws -> String? { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.hget(field, from: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("hget value key: \(key), field: \(field) complete, r: \(r)") - continuation.resume(returning: r.string) - } - - self.complete(completion, continuation: continuation) - }) - - } + private func _hget(_ key:String, field:String) async throws -> String? { + let command: RedisCommand = .hget(key, field: field) + let r = try await _send(command) + return r } } diff --git a/redis-pro/Common/RedisClient/RedisClientKeys.swift b/redis-pro/Common/RedisClient/RedisClientKeys.swift index a69b1ae..8f06b22 100644 --- a/redis-pro/Common/RedisClient/RedisClientKeys.swift +++ b/redis-pro/Common/RedisClient/RedisClientKeys.swift @@ -9,27 +9,15 @@ import Foundation import RediStack import Logging -// key +// MARK: - keys function extension RediStackClient { private func keyScan(cursor:Int, keywords:String?, count:Int? = 1) async throws -> (cursor:Int, keys:[String]) { logger.debug("redis keys scan, cursor: \(cursor), keywords: \(String(describing: keywords)), count:\(String(describing: count))") - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.scan(startingFrom: cursor, matching: keywords, count: count) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - self.logger.error("redis keys scan error \(error)") - continuation.resume(throwing: error) - } - }) - } + let command:RedisCommand<(Int, [RedisKey])> = .scan(startingFrom: cursor, matching: keywords, count: count) + let r = try await _send(command) + return (r.0, r.1.map { $0.rawValue }) } @@ -111,12 +99,9 @@ extension RediStackClient { do { // 带有占位符的情况,使用 if isScan { -// async let totalAsync = keysCountScan(match) let pageData:[String] = try await keysPageScan(page) -// let total = try await totalAsync -// page.total = total return await self.toRedisKeyModels(pageData) } else { let exist = await self.exist(page.keywords) diff --git a/redis-pro/Common/RedisClient/RedisClientList.swift b/redis-pro/Common/RedisClient/RedisClientList.swift index 2781eff..9f845bb 100644 --- a/redis-pro/Common/RedisClient/RedisClientList.swift +++ b/redis-pro/Common/RedisClient/RedisClientList.swift @@ -8,6 +8,7 @@ import Foundation import RediStack +// MARK: - list function // list extension RediStackClient { @@ -21,7 +22,7 @@ extension RediStackClient { do { let start:Int = (page.current - 1) * page.size let r1 = try await llen(key) - let r2 = try await lrange(key, start: start, stop: start + page.size - 1) + let r2 = try await _lrange(key, start: start, stop: start + page.size - 1) let total = r1 page.total = total @@ -38,26 +39,12 @@ extension RediStackClient { return [] } - private func lrange(_ key:String, start:Int, stop:Int) async throws -> [String?] { + private func _lrange(_ key:String, start:Int, stop:Int) async throws -> [String?] { logger.debug("redis list range, key: \(key)") - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.lrange(from: RedisKey(key), firstIndex: start, lastIndex: stop, as: String.self) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis list range error \(error)") - continuation.resume(throwing: error) - } - }) - } + let command: RedisCommand<[RESPValue]> = .lrange(from: RedisKey(key), firstIndex: start, lastIndex: stop) + let r = try await _send(command) + return r.map { $0.string } } func ldel(_ key:String, index:Int, value:String) async -> Int { @@ -85,18 +72,9 @@ extension RediStackClient { private func _lrem(_ key:String, value:String) async throws -> Int { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.lrem(value, from: RedisKey(key), count: 0) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - } + + let command: RedisCommand = .lrem(value, from: RedisKey(key), count: 0) + return try await _send(command) } func lset(_ key:String, index:Int, value:String) async -> Void { @@ -112,106 +90,29 @@ extension RediStackClient { } private func _lset(_ key:String, index:Int, value:String) async throws -> Void { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.lset(index: index, to: value, in: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis list lset error \(error)") - continuation.resume(throwing: error) - } - }) - } + let command: RedisCommand = .lset(index: index, to: value, in: RedisKey(key)) + try await _send(command) } func lpush(_ key:String, value:String) async -> Int { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.lpush(value, into: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - } - } catch { - handleError(error) - } - return 0 + let command: RedisCommand = .lpush(value, into: RedisKey(key)) + return await send(command, 0) } func rpush(_ key:String, value:String) async -> Int { - begin() - defer { - complete() - } - - do { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.rpush(value, into: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - } - } catch { - handleError(error) - } - return 0 + let command: RedisCommand = .rpush(value, into: RedisKey(key)) + return await send(command, 0) } private func _lindex(_ key:String, index:Int) async throws -> String? { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.lindex(index, from: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r.string) - } - - self.complete(completion, continuation: continuation) - }) - } + let command: RedisCommand = .lindex(index, from: RedisKey(key)) + return try await _send(command)?.string } private func llen(_ key:String) async throws -> Int { logger.debug("redis list length, key: \(key)") - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.llen(of: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis list llen error \(error)") - continuation.resume(throwing: error) - } - }) - } + let command: RedisCommand = .llen(of: RedisKey(key)) + return try await _send(command) } } diff --git a/redis-pro/Common/RedisClient/RedisClientLua.swift b/redis-pro/Common/RedisClient/RedisClientLua.swift index 345f64b..c951aee 100644 --- a/redis-pro/Common/RedisClient/RedisClientLua.swift +++ b/redis-pro/Common/RedisClient/RedisClientLua.swift @@ -8,7 +8,7 @@ import Foundation import RediStack -// lua script +// MARK: -lua script extension RediStackClient { func eval(_ lua:String) async -> String { logger.info("lua script eval: \(lua)") @@ -42,19 +42,10 @@ extension RediStackClient { var respValues = argArr.map { RESPValue(from: $0) } respValues.insert(RESPValue(from: script), at: 0) - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "EVAL", with: respValues) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("lua script eval r: \(r)") - continuation.resume(returning: r.description) - } - self.complete(completion, continuation:continuation) - }) - } + let command:RedisCommand = RedisCommand(keyword: "EVAL", arguments: respValues, mapValueToResult: { + $0.description + }) + return await send(command, "eval error") } catch { handleError(error) } @@ -63,94 +54,21 @@ extension RediStackClient { } - func scriptLoad(_ lua:String) async -> String { - logger.info("lua script load: \(lua)") - guard lua.count > 3 else { - return "lua script invalid!" - } - - do { - let lua = StringHelper.trim(StringHelper.removeStartIgnoreCase(lua, start: "eval")) - if !StringHelper.startWith(lua, start: "'") && !StringHelper.startWith(lua, start: "\"") { - throw BizError("lua script syntax error, demo: \"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\" 2 key1 key2 arg1 arg2") - } - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SCRIPT", with: [RESPValue(from: "LOAD"), RESPValue(from: lua)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("lua script eval r: \(r)") - continuation.resume(returning: r.description) - } - self.complete(completion, continuation:continuation) - }) - } - } catch { -// handleError(error) - } - - return "-" - } - - func scriptKill() async -> String { logger.info("lua script kill") - - do { - - let conn = try await initConn() - defer { - conn.close() - complete() - } - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SCRIPT", with: [RESPValue(from: "KILL")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("lua script kill r: \(r)") - Messages.show("Script Kill Complete, \(r.string ?? "")!") - continuation.resume(returning: r.string ?? "") - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - return "-" + + let command:RedisCommand = RedisCommand(keyword: "SCRIPT", arguments: [.init(from: "KILL")], mapValueToResult: { + $0.description + }) + return await send(command, "script kill error") } func scriptFlush() async -> Void { logger.info("lua script flush") - - do { - - let conn = try await getConn() - defer { - complete() - } - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SCRIPT", with: [RESPValue(from: "FLUSH")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("lua script flush r: \(r)") - Messages.show("Script flush Result: \(r.string ?? "")!") - continuation.resume() - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } } } diff --git a/redis-pro/Common/RedisClient/RedisClientSSH.swift b/redis-pro/Common/RedisClient/RedisClientSSH.swift new file mode 100644 index 0000000..b31c11b --- /dev/null +++ b/redis-pro/Common/RedisClient/RedisClientSSH.swift @@ -0,0 +1,30 @@ +// +// RedisClientSSH.swift +// redis-pro +// +// Created by chengpan on 2022/8/6. +// + +import Foundation +import RediStack +import NIO +import NIOSSH +import Logging + +// MARK: -ssh +extension RediStackClient { + + func initSSHConn() async throws -> RedisConnection { + let bindHost = "127.0.0.1" + + let sshTunnel = SSHTunnel(sshHost: self.redisModel.sshHost, sshPort: self.redisModel.sshPort, user: self.redisModel.sshUser, pass: self.redisModel.sshPass, targetHost: self.redisModel.host, targetPort: self.redisModel.port) + let localChannel = try await sshTunnel.openSSHTunnel() + + let localBindPort:Int = localChannel.localAddress?.port ?? 0 + self.logger.info("init forwarding server success, local port: \(localBindPort)") + self.sshLocalChannel = localChannel + + return try await initConn(host: bindHost, port: localBindPort, pass: self.redisModel.password, database: self.redisModel.database) + } + +} diff --git a/redis-pro/Common/RedisClient/RedisClientSet.swift b/redis-pro/Common/RedisClient/RedisClientSet.swift index 4c96dfe..ede7e0d 100644 --- a/redis-pro/Common/RedisClient/RedisClientSet.swift +++ b/redis-pro/Common/RedisClient/RedisClientSet.swift @@ -9,6 +9,7 @@ import Foundation import RediStack +// MARK: - set function // set extension RediStackClient { @@ -30,13 +31,13 @@ extension RediStackClient { if isScan { let match = page.keywords.isEmpty ? nil : page.keywords - let pageData:[String] = try await setPageScan(key, page: page) + let pageData:[String] = try await _setPageScan(key, page: page) r = r + pageData - let total = try await setCountScan(key, keywords: match) + let total = try await _setCountScan(key, keywords: match) page.total = total } else { - let exist = try await sexist(key, ele: page.keywords) + let exist = try await _sexist(key, ele: page.keywords) if exist { r = [page.keywords] page.total = 1 @@ -51,17 +52,17 @@ extension RediStackClient { - private func setCountScan(_ key:String, keywords:String?) async throws -> Int { + private func _setCountScan(_ key:String, keywords:String?) async throws -> Int { if isMatchAll(keywords ?? "") { logger.info("keywords is match all, use scard...") - return try await scard(key) + return try await _scard(key) } var cursor:Int = 0 var count:Int = 0 while true { - let res = try await sscan(key, keywords: keywords, cursor: cursor, count: dataCountScanCount) + let res = try await _sscan(key, keywords: keywords, cursor: cursor, count: dataCountScanCount) logger.info("set loop scan count, current cursor: \(cursor), total count: \(count)") cursor = res.0 count = count + res.1.count @@ -74,14 +75,14 @@ extension RediStackClient { return count } - private func setPageScan(_ key:String, page: Page) async throws -> [String] { + private func _setPageScan(_ key:String, page: Page) async throws -> [String] { let keywords = page.keywords.isEmpty ? nil : page.keywords var end:Int = page.end var cursor:Int = 0 var keys:[String] = [] while true { - let res = try await sscan(key, keywords: keywords, cursor: cursor, count: dataScanCount) + let res = try await _sscan(key, keywords: keywords, cursor: cursor, count: dataScanCount) logger.info("set loop scan page, current cursor: \(cursor), total count: \(keys.count)") cursor = res.0 keys = keys + res.1.map { $0 ?? ""} @@ -102,43 +103,17 @@ extension RediStackClient { } - private func sscan(_ key:String, keywords:String?, cursor:Int, count:Int = 1) async throws -> (Int, [String?]) { + private func _sscan(_ key:String, keywords:String?, cursor:Int, count:Int = 1) async throws -> (Int, [String?]) { logger.debug("redis set scan, key: \(key) cursor: \(cursor), keywords: \(String(describing: keywords)), count:\(String(describing: count))") - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.sscan(RedisKey(key), startingFrom: cursor, matching: keywords, count: count, valueType: String.self) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis set scan key:\(key) error: \(error)") - continuation.resume(throwing: error) - } - }) - } + + let command: RedisCommand<(Int, [RESPValue])> = .sscan(RedisKey(key), startingFrom: cursor, matching: keywords, count: count) + let r = try await _send(command) + return (r.0, r.1.map { $0.string }) } - private func sexist(_ key:String, ele:String?) async throws -> Bool{ - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.sismember(ele, of: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis set ele exist, key:\(key) error: \(error)") - continuation.resume(throwing: error) - } - }) - } + private func _sexist(_ key:String, ele:String?) async throws -> Bool{ + let command: RedisCommand = .sismember(ele, of: RedisKey(key)) + return try await _send(command) } func supdate(_ key:String, from:String, to:String) async -> Int { @@ -149,10 +124,10 @@ extension RediStackClient { logger.info("redis set update, key: \(key), from: \(from), to: \(to)") do { - let r = try await sremInner(key, ele: from) + let r = try await _srem(key, ele: from) try Assert.isTrue(r > 0, message: "set element: `\(from)` is not exist!") - return try await saddInner(key, ele: to) + return try await _sadd(key, ele: to) } catch { handleError(error) } @@ -167,7 +142,7 @@ extension RediStackClient { } do { - return try await sremInner(key, ele: ele) + return try await _srem(key, ele: ele) } catch { handleError(error) } @@ -180,69 +155,27 @@ extension RediStackClient { complete() } do { - return try await saddInner(key, ele: ele) + return try await _sadd(key, ele: ele) } catch { handleError(error) } return 0 } - private func scard(_ key:String) async throws -> Int { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.scard(of: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis scard error, key:\(key), error: \(error)") - continuation.resume(throwing: error) - } - }) - } + private func _scard(_ key:String) async throws -> Int { + let command: RedisCommand = .scard(of: RedisKey(key)) + return try await _send(command) } - private func sremInner(_ key:String, ele:String) async throws -> Int { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.srem(ele, from: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis set srem key:\(key), ele:\(ele), error: \(error)") - continuation.resume(throwing: error) - } - }) - } + private func _srem(_ key:String, ele:String) async throws -> Int { + let command: RedisCommand = .srem(ele, from: RedisKey(key)) + return try await _send(command) } - private func saddInner(_ key:String, ele:String) async throws -> Int { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.sadd(ele, to: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - else if case .failure(let error) = completion { - self.logger.error("redis set add key:\(key), ele:\(ele), error: \(error)") - continuation.resume(throwing: error) - } - }) - } + private func _sadd(_ key:String, ele:String) async throws -> Int { + let command: RedisCommand = .sadd(ele, to: RedisKey(key)) + return try await _send(command) } diff --git a/redis-pro/Common/RedisClient/RedisClientSlowLog.swift b/redis-pro/Common/RedisClient/RedisClientSlowLog.swift index b684df1..cf5b71a 100644 --- a/redis-pro/Common/RedisClient/RedisClientSlowLog.swift +++ b/redis-pro/Common/RedisClient/RedisClientSlowLog.swift @@ -8,91 +8,25 @@ import Foundation import RediStack -// slow log +// MARK: -slow log extension RediStackClient { func slowLogReset() async -> Bool { logger.info("slow log reset ...") + let command: RedisCommand = .slowlogReset() + return await send(command, false) - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SLOWLOG", with: [RESPValue(from: "RESET")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("slow log reset res: \(r)") - continuation.resume(returning: r.string == "OK") - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return false } func slowLogLen() async -> Int { logger.info("get slow log len ...") - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SLOWLOG", with: [RESPValue(from: "LEN")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("slow log reset res: \(r)") - continuation.resume(returning: r.int ?? 0) - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return 0 - + let command: RedisCommand = .slowlogLen() + return await send(command, 0) } func getSlowLog(_ size:Int) async -> [SlowLogModel] { logger.info("get slow log list ...") - - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.send(command: "SLOWLOG", with: [RESPValue(from: "GET"), RESPValue(from: size)]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("get slow log res: \(r)") - - var slowLogs = [SlowLogModel]() - r.array?.forEach({ item in - let itemArray = item.array - - let cmd = itemArray?[3].array!.map({ - $0.string ?? MTheme.NULL_STRING - }).joined(separator: " ") - - slowLogs.append(SlowLogModel(id: itemArray?[0].string, timestamp: itemArray?[1].int, execTime: itemArray?[2].string, cmd: cmd, client: itemArray?[4].string, clientName: itemArray?[5].string)) - }) - - continuation.resume(returning: slowLogs) - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return [] + let command: RedisCommand<[SlowLogModel]> = .slowlogList(size) + return await send(command, []) } } diff --git a/redis-pro/Common/RedisClient/RedisClientSystem.swift b/redis-pro/Common/RedisClient/RedisClientSystem.swift index e24fea3..fd0f691 100644 --- a/redis-pro/Common/RedisClient/RedisClientSystem.swift +++ b/redis-pro/Common/RedisClient/RedisClientSystem.swift @@ -8,262 +8,60 @@ import Foundation import RediStack +// MARK: - system function // system extension RediStackClient { func selectDB(_ database: Int) async -> Bool { - do { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.select(database: database) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("select redis database: \(database), r: \(r)") - continuation.resume(returning: true) - } - - self.complete(completion, continuation: continuation) - }) - } - } catch { - handleError(error) - } - return false + + let command: RedisCommand = .select(database: database) + let _ = await send(command) + return true } func databases() async -> Int { - do { - let conn = try await getConn() - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "CONFIG", with: [RESPValue(from: "GET"), RESPValue(from: "databases")]) - .whenComplete({completion in - if case .success(let r) = completion { - let dbs = r.array - self.logger.info("get config databases: \(String(describing: dbs))") - continuation.resume(returning: NumberHelper.toInt(dbs?[1], defaultValue: 16)) - } - - self.complete(completion, continuation: continuation) - }) - } - } catch { - handleError(error) - } - return 0 + + let command: RedisCommand = .databases() + return await send(command, 0) } func dbsize() async -> Int { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "dbsize") - .whenComplete({completion in - if case .success(let r) = completion { - let dbsize = r.int ?? 0 - self.logger.info("query redis dbsize success: \(dbsize)") - continuation.resume(returning: dbsize) - } - else if case .failure(let error) = completion { - self.logger.error("query redis dbsize error: \(error)") - continuation.resume(returning: 0) - } - }) - - } - } catch { - self.logger.error("query redis dbsize error: \(error)") - } - return 0 + + let command: RedisCommand = .dbsize() + return await send(command, 0) } func flushDB() async -> Bool { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "FLUSHDB") - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("flush db success: \(r)") - continuation.resume(returning: true) - } - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return false + let command: RedisCommand = .flushDB() + return await send(command, false) } func clientKill(_ clientModel:ClientModel) async -> Bool { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "CLIENT", with: [RESPValue(from: "KILL"), RESPValue(from: "\(clientModel.addr)")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("flush db success: \(r)") - continuation.resume(returning: true) - } - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return false + + let command: RedisCommand = .clientKill(clientModel.addr) + return await send(command, false) } func clientList() async -> [ClientModel] { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "CLIENT", with: [RESPValue(from: "LIST")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("query redis server client list success: \(r)") - let resStr = r.string ?? "" - let lines = resStr.components(separatedBy: "\n") - - var resArray = [ClientModel]() - - lines.forEach({ line in - if !line.contains("=") { - return - } - resArray.append(ClientModel(line: line)) - }) - - continuation.resume(returning: resArray) - } - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return [] + + let command: RedisCommand<[ClientModel]> = .clientList() + return await send(command, []) } func info() async -> [RedisInfoModel] { - begin() - defer { - complete() - } - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "info") - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("query redis server info success: \(r.string ?? "")") - let infoStr = r.string ?? "" - let lines = infoStr.components(separatedBy: "\n") - - var redisInfoModels = [RedisInfoModel]() - var item:RedisInfoModel? - - lines.forEach({ line in - if line.starts(with: "#") { - if item != nil { - redisInfoModels.append(item!) - } - - let section = line.replacingOccurrences(of: "#", with: "").trimmingCharacters(in: .whitespacesAndNewlines) - item = RedisInfoModel(section: section) - } - if line.contains(":") { - let infoArr = line.components(separatedBy: ":") - let redisInfoItemModel = RedisInfoItemModel(section: item?.section ?? "", key: infoArr[0].trimmingCharacters(in: .whitespacesAndNewlines), value: infoArr[1].trimmingCharacters(in: .whitespacesAndNewlines)) - item?.infos.append(redisInfoItemModel) - } - }) - continuation.resume(returning: redisInfoModels) - } - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return [] + let command: RedisCommand<[RedisInfoModel]> = .info() + return await send(command, []) } func resetState() async -> Bool { logger.info("reset state...") - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.send(command: "CONFIG", with: [RESPValue(from: "RESETSTAT")]) - .whenComplete({completion in - if case .success(let r) = completion { - self.logger.info("reset state res: \(r)") - continuation.resume(returning: r.string == "OK") - } - self.complete(completion, continuation: continuation) - }) - - } - } catch { - handleError(error) - } - return false + let command: RedisCommand = .resetState() + return await send(command, false) } func ping() async -> Bool { - begin() - - do { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - conn.ping().whenComplete({completion in - if case .success(let pong) = completion { - continuation.resume(returning: "PONG".caseInsensitiveCompare(pong) == .orderedSame) - } - self.complete(completion, continuation:continuation) - }) - } - } catch { - handleError(error) - } - - return false + let command: RedisCommand = .ping() + return await send(command) == "PONG" } } diff --git a/redis-pro/Common/RedisClient/RedisClientZSet.swift b/redis-pro/Common/RedisClient/RedisClientZSet.swift index 6dfd16c..497353e 100644 --- a/redis-pro/Common/RedisClient/RedisClientZSet.swift +++ b/redis-pro/Common/RedisClient/RedisClientZSet.swift @@ -8,6 +8,7 @@ import Foundation import RediStack +// MARK: - zset function // zset extension RediStackClient { @@ -125,22 +126,9 @@ extension RediStackClient { logger.debug("redis set scan, key: \(key) cursor: \(cursor), keywords: \(String(describing: keywords)), count:\(String(describing: count))") - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zscan(RedisKey(key), startingFrom: cursor, matching: keywords, count: count, valueType: String.self) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - self.logger.error("redis set scan key:\(key) error: \(error)") - continuation.resume(throwing: error) - } - }) - - } + let command: RedisCommand<(Int, [(RESPValue, Double)])> = .zscan(RedisKey(key), startingFrom: cursor, matching: keywords, count: count) + let r = try await _send(command) + return (r.0, r.1.map { ($0.0.string ?? Cons.EMPTY_STRING, $0.1) }) } func zupdate(_ key:String, from:String, to:String, score:Double) async -> Bool { @@ -177,39 +165,14 @@ extension RediStackClient { } private func _zadd(_ key:String, score:Double, ele:String) async throws -> Bool { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zadd((element: ele, score: score), to: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - self.logger.error("redis zset zadd key:\(key) error: \(error)") - continuation.resume(throwing: error) - } - }) - - } + let command: RedisCommand = .zadd((ele, score), to: RedisKey(key)) + return try await _send(command) > 0 } private func _zcard(_ key:String) async throws -> Int { - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zcard(of: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - - self.complete(completion, continuation: continuation) - }) - } + let command: RedisCommand = .zcard(of: RedisKey(key)) + return try await _send(command) } func zrem(_ key:String, ele:String) async -> Int { @@ -227,77 +190,35 @@ extension RediStackClient { } private func _zrem(_ key:String, ele:String) async throws -> Int { - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zrem(ele, from: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - continuation.resume(throwing: error) - } - }) - - } - + let command: RedisCommand = .zrem(ele, from: RedisKey(key)) + return try await _send(command) } private func _zscore(_ key:String, ele:String) async throws -> Double? { - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zscore(of: ele, in: RedisKey(key)) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: r) - } - else if case .failure(let error) = completion { - continuation.resume(throwing: error) - } - }) - - } - + let command: RedisCommand = .zscore(of: ele, in: RedisKey(key)) + return try await _send(command) } private func _zrangeByScore(_ key:String, page:Page) async throws -> [(String, String)] { - - let conn = try await getConn() - - return try await withCheckedThrowingContinuation { continuation in - - conn.zrangebyscore(from: RedisKey(key), withMinimumScoreOf: .inclusive(Double.min), limitBy: (offset: page.start, count: page.size), includeScoresInResponse: true) - .whenComplete({completion in - if case .success(let r) = completion { - continuation.resume(returning: self._mapRes(r)) - } - else if case .failure(let error) = completion { - continuation.resume(throwing: error) - } - }) - - } - - } - private func _mapRes(_ values: [RESPValue]?) -> [(String, String)]{ - guard let values = values else { return [] } - guard values.count > 0 else { return [] } - - var result: [(String, String)] = [] - - var index = 0 - repeat { - result.append((values[index].string ?? "", values[index + 1].string ?? "0")) - index += 2 - } while (index < values.count) + let command: RedisCommand<[(RESPValue, Double)]> = .zrangebyscore(from: RedisKey(key), withMinimumScoreOf: .inclusive(Double.min), limitBy: (offset: page.start, count: page.size), returning: .valuesAndScores) + let r:[(RESPValue, Double)] = try await _send(command) + return r.map { ($0.string ?? Cons.EMPTY_STRING, "\($1)") } - return result } + +// private func _mapRes(_ values: [RESPValue]?) -> [(String, String)]{ +// guard let values = values else { return [] } +// guard values.count > 0 else { return [] } +// +// var result: [(String, String)] = [] +// +// var index = 0 +// repeat { +// result.append((values[index].string ?? "", values[index + 1].string ?? "0")) +// index += 2 +// } while (index < values.count) +// +// return result +// } } diff --git a/redis-pro/Common/RedisClient/RedisCommandExt.swift b/redis-pro/Common/RedisClient/RedisCommandExt.swift new file mode 100644 index 0000000..2b5bba9 --- /dev/null +++ b/redis-pro/Common/RedisClient/RedisCommandExt.swift @@ -0,0 +1,276 @@ +// +// RedisCommandExt.swift +// redis-pro +// +// Created by chengpan on 2022/7/31. +// + +import Logging +import Foundation +import RediStack + +private let logger: Logger = Logger(label: "redis-command") + + +// MARK: - Bool +extension RedisCommand where ResultType == Bool { + + public static func flushDB() -> RedisCommand { + return .init(keyword: "FLUSHDB", arguments: [], mapValueToResult: { + $0.string == "OK" + }) + } + + + public static func clientKill(_ addr:String) -> RedisCommand { + return .init(keyword: "CLIENT", arguments: [.init(from: "KILL"), .init(from: addr)], mapValueToResult: { + $0.string == "OK" + }) + } + + public static func resetState() -> RedisCommand { + return .init(keyword: "CONFIG", arguments: [.init(from: "RESETSTAT")], mapValueToResult: { + $0.string == "OK" + }) + } + + public static func configRewrite() -> RedisCommand { + return .init(keyword: "CONFIG", arguments: [.init(from: "REWRITE")], mapValueToResult: { + $0.string == "OK" + }) + } + + public static func setConfig(_ key:String, value:String ) -> RedisCommand { + return .init(keyword: "CONFIG", arguments: [.init(from: "SET"), .init(from: key), .init(from: value)], mapValueToResult: { + $0.string == "OK" + }) + } + + public static func slowlogReset() -> RedisCommand { + return .init(keyword: "SLOWLOG", arguments: [.init(from: "RESET")], mapValueToResult: { + $0.string == "OK" + }) + } +} + + +// MARK: - String optional +extension RedisCommand where ResultType == String? { + @usableFromInline + internal init(keyword: String, arguments: [RESPValue]) { + self.init(keyword: keyword, arguments: arguments, mapValueToResult: { + switch $0 { + case let .simpleString(buffer), + let .bulkString(.some(buffer)): + guard let value = String(fromRESP: $0) else { return "\(buffer)" } // default to ByteBuffer's representation + return value + + // .integer, .error, and .bulkString(.none) conversions to String always succeed + case .integer, + .bulkString(.none): + return String(fromRESP: $0)! + + case .null, .error: return nil + case let .array(elements): return "[\(elements.map({ $0.description }).joined(separator: ","))]" + } + }) + } + + public static func hget(_ key: String, field:String) -> RedisCommand { + return .init(keyword: "HGET", arguments: [.init(from: key), .init(from: field)]) + } +} + +// MARK: - String +extension RedisCommand where ResultType == String { + @usableFromInline + internal init(keyword: String, arguments: [RESPValue]) { + self.init(keyword: keyword, arguments: arguments, mapValueToResult: { + return $0.description + }) + } + + public static func type(_ key: String) -> RedisCommand { + return .init(keyword: "TYPE", arguments: [.init(from: key)]) + } + + public static func getConfig(_ key: String) -> RedisCommand { + return .init(keyword: "CONFIG", arguments: [.init(from: "GET"), .init(from: key)]) + } +} + +// MARK: - Int +extension RedisCommand where ResultType == Int { + public static func renamenx(_ key: String, newKey:String) -> RedisCommand { + return .init(keyword: "RENAMENX", arguments: [.init(from: key), .init(from: newKey)]) + } + + + public static func databases() -> RedisCommand { + return .init(keyword: "CONFIG", arguments: [.init(from: "GET"), .init(from: "databases")], mapValueToResult: { + let dbs = $0.array + return NumberHelper.toInt(dbs?[1], defaultValue: 16) + }) + } + + public static func dbsize() -> RedisCommand { + return .init(keyword: "DBSIZE", arguments: [], mapValueToResult: { + $0.int ?? 0 + }) + } + + public static func slowlogLen() -> RedisCommand { + return .init(keyword: "SLOWLOG", arguments: [.init(from: "LEN")], mapValueToResult: { + $0.int ?? 0 + }) + } +} + + +// MARK: - Hash +extension RedisCommand where ResultType == (Int, [(String, String?)]) { + public static func _hscan(_ key: String, keywords:String?, cursor:Int, count:Int = 100) -> RedisCommand<(Int, [(String, String?)])> { + var args: [RESPValue] = [.init(from: key), .init(from: cursor)] + + if let m = keywords { + args.append(.init(from: "MATCH")) + args.append(.init(from: m)) + } + + args.append(.init(from: "COUNT")) + args.append(.init(from: count)) + + return .init(keyword: "HSCAN", arguments: args, mapValueToResult: { r in + guard let scanR:[RESPValue] = r.array else { + return (0, []) + } + + let cursor = Int(scanR[0].string!) ?? 0 + + let elements:[(String, String?)] = _mapRes(scanR[1].array) + return (cursor, elements) + }) + } + + static func _mapRes(_ values: [RESPValue]?) -> [(String, String?)]{ + guard let values = values else { return [] } + guard values.count > 0 else { return [] } + + var result: [(String, String?)] = [] + + var index = 0 + repeat { + result.append((values[index].string!, values[index + 1].string)) + index += 2 + } while (index < values.count) + + return result + } +} + + + +// MARK: - Client +extension RedisCommand where ResultType == [ClientModel] { + + public static func clientList() -> RedisCommand<[ClientModel]> { + return .init(keyword: "CLIENT", arguments: [.init(from: "LIST")], mapValueToResult: { r in + let resStr = r.string ?? "" + let lines = resStr.components(separatedBy: "\n") + + var resArray = [ClientModel]() + + lines.forEach({ line in + if !line.contains("=") { + return + } + resArray.append(ClientModel(line: line)) + }) + return resArray + }) + } +} + + +// MARK: - Info +extension RedisCommand where ResultType == [RedisInfoModel] { + public static func info() -> RedisCommand<[RedisInfoModel]> { + return self.init(keyword: "INFO", arguments: [], mapValueToResult: { r in + // self.logger.info("query redis server info success: \(r.string ?? "")") + let infoStr = r.string ?? "" + let lines = infoStr.components(separatedBy: "\n") + + var redisInfoModels = [RedisInfoModel]() + var item:RedisInfoModel? + + lines.forEach({ line in + if line.starts(with: "#") { + if item != nil { + redisInfoModels.append(item!) + } + + let section = line.replacingOccurrences(of: "#", with: "").trimmingCharacters(in: .whitespacesAndNewlines) + item = RedisInfoModel(section: section) + } + if line.contains(":") { + let infoArr = line.components(separatedBy: ":") + let redisInfoItemModel = RedisInfoItemModel(section: item?.section ?? "", key: infoArr[0].trimmingCharacters(in: .whitespacesAndNewlines), value: infoArr[1].trimmingCharacters(in: .whitespacesAndNewlines)) + item?.infos.append(redisInfoItemModel) + } }) + + return redisInfoModels + }) + } +} + + + +// MARK: - Config +extension RedisCommand where ResultType == [RedisConfigItemModel] { + public static func configList(_ pattern: String = "*") -> RedisCommand<[RedisConfigItemModel]> { + var _pattern = pattern + if pattern.isEmpty { + _pattern = "*" + } + + return self.init(keyword: "CONFIG", arguments: [.init(from: "GET"), .init(from: _pattern)], mapValueToResult: { r in + + logger.info("get redis config list res: \(r)") + let configs = r.array ?? [] + + var configList = [RedisConfigItemModel]() + + let max:Int = configs.count / 2 + + for index in (0.. RedisCommand<[SlowLogModel]> { + + return self.init(keyword: "SLOWLOG", arguments: [.init(from: "GET"), .init(from: size)], mapValueToResult: { r in + + logger.info("get slow log res: \(r)") + + var slowLogs = [SlowLogModel]() + r.array?.forEach({ item in + let itemArray = item.array + + let cmd = itemArray?[3].array!.map({ + $0.string ?? MTheme.NULL_STRING + }).joined(separator: " ") + + slowLogs.append(SlowLogModel(id: itemArray?[0].string, timestamp: itemArray?[1].int, execTime: itemArray?[2].string, cmd: cmd, client: itemArray?[4].string, clientName: itemArray?[5].string)) + }) + return slowLogs + }) + } +} diff --git a/redis-pro/Common/SSH/SSHRediStackClient.swift b/redis-pro/Common/SSH/SSHRediStackClient.swift index 0154754..cae45b8 100644 --- a/redis-pro/Common/SSH/SSHRediStackClient.swift +++ b/redis-pro/Common/SSH/SSHRediStackClient.swift @@ -11,6 +11,7 @@ import NIO import NIOSSH import Logging + extension RediStackClient { func getSSHConn() async throws -> RedisConnection { @@ -34,8 +35,8 @@ extension RediStackClient { let bootstrap = ClientBootstrap(group: group) .channelInitializer { channel in - let _ = channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: UserPasswordDelegate(username: sshUser, password: sshPass), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) - return channel.addBaseRedisHandlers() + return channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: UserPasswordDelegate(username: sshUser, password: sshPass), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) +// return channel.pipeline.addBaseRedisHandlers() } .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) diff --git a/redis-pro/Common/SSH/SSHTunnel.swift b/redis-pro/Common/SSH/SSHTunnel.swift new file mode 100644 index 0000000..956da45 --- /dev/null +++ b/redis-pro/Common/SSH/SSHTunnel.swift @@ -0,0 +1,219 @@ +// +// SSHTunnel.swift +// redis-pro +// +// Created by chengpan on 2022/8/6. +// + +import Foundation +import RediStack +import NIO +import NIOSSH +import Logging + +class SSHTunnel { + private let logger = Logger(label: "ssh-tunnel") + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + private var sshChannel:Channel? + private var localForwardingChannel:Channel? + private var forwardingServer:PortForwardingServer? + + private let bindHost = "127.0.0.1" + private let bindPort = 0 + + var sshHost:String + var sshPort:Int + var user:String + var pass:String + var targetHost:String + var targetPort:Int + + init(sshHost: String, sshPort:Int, user:String, pass:String, targetHost:String, targetPort:Int) { + self.sshHost = sshHost + self.sshPort = sshPort + self.user = user + self.pass = pass + self.targetHost = targetHost + self.targetPort = targetPort + } + + + func openSSHTunnel() async throws -> Channel { + logger.info("create new ssh connection...") + + return try await withCheckedThrowingContinuation { continuation in + self.logger.info("init ssh tunnel start...") + + + let bootstrap = ClientBootstrap(group: self.group) + .channelInitializer { channel in + return channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: UserPasswordDelegate(username: self.user, password: self.pass), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) +// return channel.pipeline.addBaseRedisHandlers() + } + .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) + + logger.info("connecting to ssh server, host: \(self.sshHost), user: \(self.user)") + let channelFuture = bootstrap.connect(host: self.sshHost, port: self.sshPort) + + channelFuture.whenFailure { error in + self.logger.error("connect ssh server error: \(error)") + continuation.resume(throwing: error) + } + + + channelFuture.whenSuccess { channel in + self.logger.info("connect ssh server success, host: \(self.sshHost), user: \(self.user)") + self.sshChannel = channel + let server = PortForwardingServer(group: self.group, + bindHost: self.bindHost, + bindPort: self.bindPort) { inboundChannel in + // This block executes whenever a new inbound channel is received. We want to forward it to the peer. + // To do that, we have to begin by creating a new SSH channel of the appropriate type. + channel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in + let promise = inboundChannel.eventLoop.makePromise(of: Channel.self) + let directTCPIP = SSHChannelType.DirectTCPIP(targetHost: self.targetHost, + targetPort: self.targetPort, + originatorAddress: inboundChannel.remoteAddress!) + sshHandler.createChannel(promise, + channelType: .directTCPIP(directTCPIP)) { childChannel, channelType in + guard case .directTCPIP = channelType else { + return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType) + } + + // Attach a pair of glue handlers, one in the inbound channel and one in the outbound one. + // We also add an error handler to both channels, and a wrapper handler to the SSH child channel to + // encapsulate the data in SSH messages. + // When the glue handlers are in, we can create both channels. + let (ours, theirs) = GlueHandler.matchedPair() + return childChannel.pipeline.addHandlers([SSHWrapperHandler(), ours, ErrorHandler()]).flatMap { + inboundChannel.pipeline.addHandlers([theirs, ErrorHandler()]) + } + } + + self.logger.info("init new forwarding channel success...") + // We need to erase the channel here: we just want success or failure info. + return promise.futureResult.map { _ in } + } + } + + self.forwardingServer = server + + // Run the server until complete + self.logger.info("forwarding servier start...") + let f:EventLoopFuture = server.run() + f.whenFailure { error in + self.logger.error("init forwarding channel error: \(error)") + self.close() + continuation.resume(throwing: error) + } + f.whenSuccess { localForwardingChannel in + self.localForwardingChannel = localForwardingChannel + continuation.resume(returning: localForwardingChannel) + } + } + } + + } + +// func openSSH() async throws -> Channel { +// return try await withCheckedThrowingContinuation { continuation in +// let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) +// +// let bootstrap = ClientBootstrap(group: group) +// .channelInitializer { channel in +// return channel.pipeline.addHandlers([NIOSSHHandler(role: .client(.init(userAuthDelegate: UserPasswordDelegate(username: self.user, password: self.pass), serverAuthDelegate: AcceptAllHostKeysDelegate())), allocator: channel.allocator, inboundChildChannelInitializer: nil), ErrorHandler()]) +//// return channel.pipeline.addBaseRedisHandlers() +// } +// .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) +// .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1) +// +// logger.info("connecting to ssh server, host: \(sshHost), user: \(user)") +// let channelFuture = bootstrap.connect(host: self.sshHost, port: self.sshPort) +// +// channelFuture.whenFailure { error in +// self.logger.error("connect ssh server, error \(error)") +// continuation.resume(throwing: error) +// } +// +// +// channelFuture.whenSuccess { channel in +// self.logger.info("connect ssh server success, host: \(self.sshHost), user: \(self.user)") +// self.sshChannel = channel +// continuation.resume(returning: channel) +// } +// } +// } + + +// func open() async throws -> Channel { +// logger.info("create ssh tunnel start...") +// let group = MultiThreadedEventLoopGroup(numberOfThreads: 2) +// let channel = try await openSSH() +// return try await withCheckedThrowingContinuation { continuation in +// let server = PortForwardingServer(group: group, +// bindHost: self.bindHost, +// bindPort: self.bindPort) { inboundChannel in +// // This block executes whenever a new inbound channel is received. We want to forward it to the peer. +// // To do that, we have to begin by creating a new SSH channel of the appropriate type. +// channel.pipeline.handler(type: NIOSSHHandler.self).flatMap { sshHandler in +// let promise = inboundChannel.eventLoop.makePromise(of: Channel.self) +// let directTCPIP = SSHChannelType.DirectTCPIP(targetHost: self.targetHost, +// targetPort: self.targetPort, +// originatorAddress: inboundChannel.remoteAddress!) +// sshHandler.createChannel(promise, +// channelType: .directTCPIP(directTCPIP)) { childChannel, channelType in +// guard case .directTCPIP = channelType else { +// return channel.eventLoop.makeFailedFuture(SSHClientError.invalidChannelType) +// } +// +// // Attach a pair of glue handlers, one in the inbound channel and one in the outbound one. +// // We also add an error handler to both channels, and a wrapper handler to the SSH child channel to +// // encapsulate the data in SSH messages. +// // When the glue handlers are in, we can create both channels. +// let (ours, theirs) = GlueHandler.matchedPair() +// return childChannel.pipeline.addHandlers([SSHWrapperHandler(), ours, ErrorHandler()]).flatMap { +// inboundChannel.pipeline.addHandlers([theirs, ErrorHandler()]) +// } +// } +// +// self.logger.info("init new forwarding channel success...") +// // We need to erase the channel here: we just want success or failure info. +// return promise.futureResult.map { _ in } +// } +// } +// +// self.forwardingServer = server +// +// // Run the server until complete +// self.logger.info("create forwarding server success...") +// let localForwardingChannelFuture:EventLoopFuture = server.run() +// localForwardingChannelFuture.whenFailure { error in +// self.logger.error("bind local server error: \(error)") +// +// self.close() +// continuation.resume(throwing: error) +// } +// localForwardingChannelFuture.whenSuccess { localForwardingChannel in +// let localForwardingPort:Int = localForwardingChannel.localAddress?.port ?? 0 +// self.logger.info("ssh tunnel forwarding server start success, port: \(localForwardingPort)") +// self.localForwardingChannel = localForwardingChannel +// continuation.resume(returning: localForwardingChannel) +// } +// } +// +// } + + func close() { + let _ = self.localForwardingChannel?.close() + let _ = self.forwardingServer?.close() + let _ = self.sshChannel?.close() + } +} + + +struct SSHTunnelAddress { + var host:String + var port:Int +} diff --git a/redis-pro/Common/UserDefaults/UserDefaultsKeysEnum.swift b/redis-pro/Common/UserDefaults/UserDefaultsKeysEnum.swift index b2e4d69..3e95c1c 100644 --- a/redis-pro/Common/UserDefaults/UserDefaultsKeysEnum.swift +++ b/redis-pro/Common/UserDefaults/UserDefaultsKeysEnum.swift @@ -15,5 +15,7 @@ enum UserDefaulsKeysEnum: String { case RedisFavoriteDefaultSelectType = "User.defaultFavorite" // color scheme case AppColorScheme = "App.ColorScheme" + // keepalive second + case AppKeepalive = "App.Keepalive" } diff --git a/redis-pro/Model/ClientModel.swift b/redis-pro/Model/ClientModel.swift index 8c1f081..3f9acb6 100644 --- a/redis-pro/Model/ClientModel.swift +++ b/redis-pro/Model/ClientModel.swift @@ -8,8 +8,8 @@ import Foundation -class ClientModel:NSObject, Identifiable { - @objc var id:String = "" +public class ClientModel:NSObject, Identifiable { + @objc public var id:String = "" @objc var name:String = "" @objc var addr:String = "" @objc var laddr:String = "" diff --git a/redis-pro/Model/RedisConfigItemModel.swift b/redis-pro/Model/RedisConfigItemModel.swift index 152c19e..dba4fda 100644 --- a/redis-pro/Model/RedisConfigItemModel.swift +++ b/redis-pro/Model/RedisConfigItemModel.swift @@ -7,7 +7,7 @@ import Foundation -class RedisConfigItemModel:NSObject, Identifiable { +public class RedisConfigItemModel:NSObject, Identifiable { @objc var key:String = "" @objc var value:String = "" diff --git a/redis-pro/Model/RedisInfoItemModel.swift b/redis-pro/Model/RedisInfoItemModel.swift index 103b9f6..847a3d5 100644 --- a/redis-pro/Model/RedisInfoItemModel.swift +++ b/redis-pro/Model/RedisInfoItemModel.swift @@ -7,7 +7,7 @@ import Foundation -class RedisInfoItemModel: NSObject { +public class RedisInfoItemModel: NSObject { @objc var section:String = "" @objc var key:String = "" @objc var value:String = "" diff --git a/redis-pro/Model/RedisInfoModel.swift b/redis-pro/Model/RedisInfoModel.swift index 94af394..910f81a 100644 --- a/redis-pro/Model/RedisInfoModel.swift +++ b/redis-pro/Model/RedisInfoModel.swift @@ -6,8 +6,8 @@ // import Foundation -class RedisInfoModel:NSObject, Identifiable { - var id = UUID() +public class RedisInfoModel:NSObject, Identifiable { + public var id = UUID() var section:String = "" var infos:[RedisInfoItemModel] = [RedisInfoItemModel]() diff --git a/redis-pro/Model/RedisInsanceModel.swift b/redis-pro/Model/RedisInsanceModel.swift index d4eb401..5e11f04 100644 --- a/redis-pro/Model/RedisInsanceModel.swift +++ b/redis-pro/Model/RedisInsanceModel.swift @@ -33,41 +33,47 @@ class RedisInstanceModel:ObservableObject, Identifiable { ) } -// func setGlobalStore(_ viewStore: ViewStore?) { -// guard let viewStore = viewStore else { -// return -// } -// self.viewStore = viewStore -// } - func setAppStore(_ appStore: Store) { let globalStore = appStore.scope(state: \.globalState, action: AppAction.globalAction) self.viewStore = ViewStore(globalStore) } + // get client func getClient() -> RediStackClient { - if rediStackClient != nil { - return rediStackClient! + if let client = rediStackClient { + return client } - logger.info("get new redis client ...") - rediStackClient = RediStackClient(redisModel:redisModel) - rediStackClient?.setGlobalStore(self.viewStore) - return rediStackClient! + return initRedisClient(self.redisModel) } - func connect(redisModel:RedisModel) async -> Bool { - logger.info("connect to redis server: \(redisModel)") + // init redis client + func initRedisClient(_ redisModel: RedisModel) -> RediStackClient { + + logger.info("init new redis client, redisModel: \(redisModel)") self.redisModel = redisModel - let r = await self.getClient().initConnection() - return r + let client = RediStackClient(redisModel) + client.setGlobalStore(self.viewStore) + + self.rediStackClient = client + return client + } + + func connect(_ redisModel:RedisModel) async -> Bool { + logger.info("connect to redis server: \(redisModel)") + + let client = initRedisClient(redisModel) + + return await client.ping() } func testConnect(_ redisModel:RedisModel) async -> Bool { - self.redisModel = redisModel - let pong = await self.getClient().ping() - self.close() - return pong + defer { + self.close() + } + + logger.info("test connect to redis server: \(redisModel)") + return await initRedisClient(redisModel).ping() } func close() -> Void { diff --git a/redis-pro/Model/SlowLogModel.swift b/redis-pro/Model/SlowLogModel.swift index 7c8d599..91774c9 100644 --- a/redis-pro/Model/SlowLogModel.swift +++ b/redis-pro/Model/SlowLogModel.swift @@ -7,8 +7,8 @@ import Foundation -class SlowLogModel:NSObject, Identifiable { - @objc var id:String = "" +public class SlowLogModel:NSObject, Identifiable { + @objc public var id:String = "" @objc var timestamp:Int = -1 @objc var execTime:String = "" @objc var cmd:String = "" diff --git a/redis-pro/Store/ClientListStore.swift b/redis-pro/Store/ClientListStore.swift index cf874c0..9ffe39a 100644 --- a/redis-pro/Store/ClientListStore.swift +++ b/redis-pro/Store/ClientListStore.swift @@ -12,32 +12,36 @@ import ComposableArchitecture private let logger = Logger(label: "client-list-store") struct ClientListState: Equatable { - - var tableState: TableState = TableState(columns: [ - .init(title: "id", key: "id", width: 60), - .init(title: "name", key: "name", width: 60), - .init(title: "addr", key: "addr", width: 140), - .init(title: "laddr", key: "laddr", width: 140), - .init(title: "fd", key: "fd", width: 60), - .init(title: "age", key: "age", width: 60), - .init(title: "idle", key: "idle", width: 60), - .init(title: "flags", key: "flags", width: 60), - .init(title: "db", key: "db", width: 60), - .init(title: "sub", key: "sub", width: 60), - .init(title: "psub", key: "psub", width: 60), - .init(title: "multi", key: "multi", width: 60), - .init(title: "qbuf", key: "qbuf", width: 60), - .init(title: "qbuf_free", key: "qbuf_free", width: 60), - .init(title: "obl", key: "obl", width: 60), - .init(title: "oll", key: "oll", width: 60), - .init(title: "omem", key: "omem", width: 60), - .init(title: "events", key: "events", width: 60), - .init(title: "cmd", key: "cmd", width: 100), - .init(title: "argv_mem", key: "argv_mem", width: 60), - .init(title: "tot_mem", key: "tot_mem", width: 60), - .init(title: "redir", key: "redir", width: 60), - .init(title: "user", key: "user", width: 60) - ], datasource: [], contextMenus: ["Kill"], selectIndex: -1) + + var tableState: TableState = TableState( + columns: [ + .init(title: "id", key: "id", width: 60), + .init(title: "name", key: "name", width: 60), + .init(title: "addr", key: "addr", width: 140), + .init(title: "laddr", key: "laddr", width: 140), + .init(title: "fd", key: "fd", width: 60), + .init(title: "age", key: "age", width: 60), + .init(title: "idle", key: "idle", width: 60), + .init(title: "flags", key: "flags", width: 60), + .init(title: "db", key: "db", width: 60), + .init(title: "sub", key: "sub", width: 60), + .init(title: "psub", key: "psub", width: 60), + .init(title: "multi", key: "multi", width: 60), + .init(title: "qbuf", key: "qbuf", width: 60), + .init(title: "qbuf_free", key: "qbuf_free", width: 60), + .init(title: "obl", key: "obl", width: 60), + .init(title: "oll", key: "oll", width: 60), + .init(title: "omem", key: "omem", width: 60), + .init(title: "events", key: "events", width: 60), + .init(title: "cmd", key: "cmd", width: 100), + .init(title: "argv_mem", key: "argv_mem", width: 60), + .init(title: "tot_mem", key: "tot_mem", width: 60), + .init(title: "redir", key: "redir", width: 60), + .init(title: "user", key: "user", width: 60) + ] + , datasource: [] + , contextMenus: [.KILL] + , selectIndex: -1) init() { logger.info("client list state init ...") diff --git a/redis-pro/Store/FavoriteStore.swift b/redis-pro/Store/FavoriteStore.swift index 5161f13..2151b67 100644 --- a/redis-pro/Store/FavoriteStore.swift +++ b/redis-pro/Store/FavoriteStore.swift @@ -128,7 +128,7 @@ let favoriteReducer = Reducer.task { - let r = await env.redisInstanceModel.connect(redisModel:redisModel) + let r = await env.redisInstanceModel.connect(redisModel) logger.info("on connect to redis server: \(redisModel) , result: \(r)") RedisDefaults.saveLastUse(redisModel) return r ? .connectSuccess(redisModel) : .none @@ -136,6 +136,10 @@ let favoriteReducer = Reducer) } @@ -100,11 +100,11 @@ let loginReducer = Reducer { return Effect.task { let r = await env.redisInstanceModel.testConnect(redis) - return .ping(r) + return .setPingR(r) } .receive(on: env.mainQueue) .eraseToEffect() - case let .ping(r): + case let .setPingR(r): state.pingR = r ? "Connect successed!" : "Connect fail! " state.loading = false return .none diff --git a/redis-pro/Store/LuaStore.swift b/redis-pro/Store/LuaStore.swift index 9db28f3..7a8a5d5 100644 --- a/redis-pro/Store/LuaStore.swift +++ b/redis-pro/Store/LuaStore.swift @@ -66,8 +66,7 @@ let luaReducer = Reducer> let lua = state.lua return .task { - let r = await env.redisInstanceModel.getClient().scriptLoad(lua) - return .setLuaSHA(r) + return .setLuaSHA("") } .receive(on: env.mainQueue) .eraseToEffect() diff --git a/redis-pro/Store/RedisConfigStore.swift b/redis-pro/Store/RedisConfigStore.swift index 54971dc..788f0e8 100644 --- a/redis-pro/Store/RedisConfigStore.swift +++ b/redis-pro/Store/RedisConfigStore.swift @@ -20,8 +20,11 @@ struct RedisConfigState: Equatable { var editKey:String = "" var editIndex = 0 - var tableState: TableState = TableState(columns: [.init(title: "Key", key: "key", width: 200), .init(title: "Value", key: "value", width: 800)] - , datasource: [], contextMenus: ["Edit"], selectIndex: -1) + var tableState: TableState = TableState( + columns: [.init(title: "Key", key: "key", width: 200), .init(title: "Value", key: "value", width: 800)] + , datasource: [] + , contextMenus: [.EDIT] + , selectIndex: -1) init() { logger.info("redis config state init ...") diff --git a/redis-pro/Store/RedisKeysStore.swift b/redis-pro/Store/RedisKeysStore.swift index a4ce9b7..4238700 100644 --- a/redis-pro/Store/RedisKeysStore.swift +++ b/redis-pro/Store/RedisKeysStore.swift @@ -19,8 +19,10 @@ struct RedisKeysState: Equatable { var searchGroup = 0 var mainViewType: MainViewTypeEnum = .EDITOR - var tableState: TableState = TableState(columns: [.init(type: .KEY_TYPE,title: "Type", key: "type", width: 40), .init(title: "Key", key: "key", width: 50)] - , datasource: [], contextMenus: ["Rename", "Delete"], selectIndex: -1) + var tableState: TableState = TableState( + columns: [.init(type: .KEY_TYPE,title: "Type", key: "type", width: 40), .init(title: "Key", key: "key", width: 50)] + , datasource: [], contextMenus: [.COPY, .RENAME, .DELETE] + , selectIndex: -1) var redisSystemState:RedisSystemState = RedisSystemState() var valueState: ValueState = ValueState() var databaseState: DatabaseState = DatabaseState() @@ -299,7 +301,12 @@ let redisKeysReducer = Reducer = [] - var contextMenus: [String] = [] + var contextMenus: [TableContextMenu] = [] // 一定要设置-1, 其它值会在view 刷新时, 陷入无限循环 var selectIndex:Int = -1 var defaultSelectIndex:Int = -1 @@ -24,6 +24,7 @@ enum TableAction:Equatable { case selectionChange(Int) case double(Int) case delete(Int) + case copy(Int) case contextMenu(String, Int) case refresh case reset @@ -51,7 +52,11 @@ let tableReducer = Reducer { case let .delete(index): logger.info("table view on delete action, index: \(index)") return .none - + + case let .copy(index): + logger.info("table view on copy action, index: \(index)") + return .none + case let .contextMenu(sender, index): logger.info("table view on context menu action, sender: \(sender), index: \(index)") return .none @@ -62,11 +67,21 @@ let tableReducer = Reducer { return .none case let .dragComplete(from, to): - state.selectIndex = to let f = state.datasource[from] - state.datasource[from] = state.datasource[to] - state.datasource[to] = f + // 先删除原有的 + state.datasource.remove(at: from) + + +// state.datasource[from] = state.datasource[to] + + if from > to { + state.datasource.insert(f, at: to) + state.selectIndex = to + } else { + state.datasource.insert(f, at: to - 1) + state.selectIndex = to - 1 + } return .none } diff --git a/redis-pro/Store/ZSetValueStore.swift b/redis-pro/Store/ZSetValueStore.swift index cbd4a11..23e258a 100644 --- a/redis-pro/Store/ZSetValueStore.swift +++ b/redis-pro/Store/ZSetValueStore.swift @@ -11,6 +11,8 @@ import SwiftyJSON import ComposableArchitecture private let logger = Logger(label: "zset-value-store") + +// MARK: - state struct ZSetValueState: Equatable { @BindableState var editModalVisible:Bool = false @BindableState var editValue:String = "" @@ -20,8 +22,10 @@ struct ZSetValueState: Equatable { var isNew:Bool = false var redisKeyModel:RedisKeyModel? var pageState: PageState = PageState(showTotal: true) - var tableState: TableState = TableState(columns: [.init(title: "Score", key: "score", width: 80), .init(title: "Value", key: "value", width: 200)] - , datasource: [], contextMenus: ["Edit", "Delete"], selectIndex: -1) + var tableState: TableState = TableState( + columns: [.init(title: "Score", key: "score", width: 80), .init(title: "Value", key: "value", width: 200)] + , datasource: [], contextMenus: [.COPY, .EDIT, .DELETE] + , selectIndex: -1) init() { logger.info("zset value state init ...") @@ -29,6 +33,7 @@ struct ZSetValueState: Equatable { } } +// MARK: - action enum ZSetValueAction:BindableAction, Equatable { case initial @@ -57,6 +62,7 @@ struct ZSetValueEnvironment { var mainQueue: AnySchedulerOf = .main } +// MARK: - reducer let zsetValueReducer = Reducer.combine( tableReducer.pullback( state: \.tableState, @@ -220,7 +226,7 @@ let zsetValueReducer = Reducer Void)? - var disabled:Bool = false // 是否有编辑过,编辑过才会触commit @State private var isEdited:Bool = false @@ -44,7 +43,6 @@ struct MDoubleField: View { field .labelsHidden() .lineLimit(1) - .disabled(disabled) .multilineTextAlignment(.leading) .font(.body) .disableAutocorrection(true) @@ -57,7 +55,7 @@ struct MDoubleField: View { .background(Color.init(NSColor.textBackgroundColor)) .cornerRadius(MTheme.CORNER_RADIUS) .overlay( - RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(!disabled && isEditing ? 0.4 : 0.2), lineWidth: 1) + RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) ) } diff --git a/redis-pro/Views/Components/Form/MIntField.swift b/redis-pro/Views/Components/Form/MIntField.swift index eab6610..24d5ab6 100644 --- a/redis-pro/Views/Components/Form/MIntField.swift +++ b/redis-pro/Views/Components/Form/MIntField.swift @@ -11,10 +11,8 @@ import Logging struct MIntField: View { @Binding var value:Int var placeholder:String? - var suffix:String? @State private var isEditing = false var onCommit: (() -> Void)? - var disabled:Bool = false // 是否有编辑过,编辑过才会触commit @State private var isEdited:Bool = false @@ -43,7 +41,6 @@ struct MIntField: View { field .labelsHidden() .lineLimit(1) - .disabled(disabled) .multilineTextAlignment(.leading) .font(.body) .disableAutocorrection(true) @@ -51,17 +48,12 @@ struct MIntField: View { .onHover { inside in self.isEditing = inside } - - if suffix != nil { - MIcon(icon: suffix!, fontSize: MTheme.FONT_SIZE_BUTTON, action: doCommit) - .padding(0) - } } .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) .background(Color.init(NSColor.textBackgroundColor)) .cornerRadius(MTheme.CORNER_RADIUS) .overlay( - RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(!disabled && isEditing ? 0.4 : 0.2), lineWidth: 1) + RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) ) } diff --git a/redis-pro/Views/Components/Form/MPasswordField.swift b/redis-pro/Views/Components/Form/MPasswordField.swift index 233a06c..f0840f1 100644 --- a/redis-pro/Views/Components/Form/MPasswordField.swift +++ b/redis-pro/Views/Components/Form/MPasswordField.swift @@ -15,7 +15,6 @@ struct MPasswordField: View { var onCommit:(() -> Void)? @State private var visible:Bool = false - var disabled = false let logger = Logger(label: "pass-field") @@ -54,11 +53,10 @@ struct MPasswordField: View { } var body: some View { - ZStack(alignment: .trailing) { + HStack(alignment: .center) { field .labelsHidden() .lineLimit(1) - .disabled(disabled) .multilineTextAlignment(.leading) .font(.body) .disableAutocorrection(true) @@ -88,7 +86,7 @@ struct MPasswordField: View { .background(Color.init(NSColor.textBackgroundColor)) .cornerRadius(MTheme.CORNER_RADIUS) .overlay( - RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(!disabled && isEditing ? 0.4 : 0.2), lineWidth: 1) + RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEditing ? 0.4 : 0.2), lineWidth: 1) ) } diff --git a/redis-pro/Views/Components/Form/MSecureField.swift b/redis-pro/Views/Components/Form/MSecureField.swift index 4bd4c6c..70a0a9a 100644 --- a/redis-pro/Views/Components/Form/MSecureField.swift +++ b/redis-pro/Views/Components/Form/MSecureField.swift @@ -23,10 +23,7 @@ struct MSecureField: View { @ViewBuilder private var field: some View { if showPass { -// TextField(placeholder, text: $value, onEditingChanged: { isEditing in -// self.isEditing = isEditing -// }, onCommit: doCommit) - NTextField(stringValue: $value, placeholder: placeholder, onCommit: onCommit) + MTextField(value: $value, placeholder: placeholder, onCommit: onCommit) } else { SecureField(placeholder, text: $value, onCommit: doCommit) } diff --git a/redis-pro/Views/Components/Form/MTextField.swift b/redis-pro/Views/Components/Form/MTextField.swift index 5f7f257..961fa90 100644 --- a/redis-pro/Views/Components/Form/MTextField.swift +++ b/redis-pro/Views/Components/Form/MTextField.swift @@ -11,15 +11,14 @@ import Logging struct MTextField: View { @Binding var value:String var placeholder:String? - var suffix:String? - @State private var isEditing = false + var onCommit: (() -> Void)? var autoCommit:Bool = true + // allow edit + var editable: Bool = true - @Environment(\.isEnabled) var isEnabled + @State private var isEditing = false - // 是否有编辑过,编回过才会触commit - @State private var isEdited:Bool = false var autoTrim:Bool = false private var adapterValue: Binding { @@ -32,21 +31,35 @@ struct MTextField: View { let logger = Logger(label: "text-field") + @ViewBuilder + private var readonlyField: some View { + if #available(macOS 12.0, *) { + Text(self.value) + .textSelection(.enabled) + } else { + Text(self.value) + } + } @ViewBuilder - private var field: some View { + private var editField: some View { if #available(macOS 12.0, *) { TextField("", text: adapterValue, prompt: Text(placeholder ?? "")) .onSubmit { doCommit() } } else { - TextField(placeholder ?? "", text: adapterValue, onEditingChanged: { isEditing in - self.isEditing = isEditing - if isEditing { - self.isEdited = true - } - }, onCommit: doCommit) + TextField(placeholder ?? "", text: adapterValue, onCommit: doCommit) + } + } + + + @ViewBuilder + private var field: some View { + if editable { + editField + } else { + readonlyField } } @@ -62,26 +75,19 @@ struct MTextField: View { .onHover { inside in self.isEditing = inside } - - if suffix != nil { - MIcon(icon: suffix!, fontSize: MTheme.FONT_SIZE_BUTTON, action: doCommit) - .padding(0) - } + .frame(maxWidth: .infinity, alignment: .leading) } .padding(EdgeInsets(top: 3, leading: 4, bottom: 3, trailing: 4)) - .background(Color.init(NSColor.textBackgroundColor)) + .background(editable ? Color.init(NSColor.textBackgroundColor) : Color.gray.opacity(0.05)) .cornerRadius(MTheme.CORNER_RADIUS) .overlay( - RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(isEnabled && isEditing ? 0.4 : 0.2), lineWidth: 1) + RoundedRectangle(cornerRadius: MTheme.CORNER_RADIUS).stroke(Color.gray.opacity(editable && isEditing ? 0.4 : 0.2), lineWidth: 1) ) } func doCommit() -> Void { - if autoCommit && self.isEdited { - self.isEdited = false - logger.info("on textField commit, value: \(value)") - onCommit?() - } + logger.info("on textField commit, value: \(value)") + onCommit?() } } @@ -90,7 +96,7 @@ struct MTextField_Previews: PreviewProvider { static var previews: some View { VStack { - MTextField(value: $text, suffix: "magnifyingglass") + MTextField(value: $text) Text(text) MButton(text: "test") } diff --git a/redis-pro/Views/Components/Form/NSearchField.swift b/redis-pro/Views/Components/Form/NSearchField.swift index b6f0d2e..98bbd91 100644 --- a/redis-pro/Views/Components/Form/NSearchField.swift +++ b/redis-pro/Views/Components/Form/NSearchField.swift @@ -12,7 +12,6 @@ import Logging struct NSearchField: NSViewRepresentable { @Binding var value: String var placeholder: String - var disable = false var onCommit: ((String) -> Void)? private let logger = Logger(label: "search-field") @@ -24,8 +23,6 @@ struct NSearchField: NSViewRepresentable { textField.delegate = context.coordinator logger.info("search field init \(value)") - - textField.isEnabled = !disable // textField.bezelStyle = .roundedBezel return textField @@ -85,10 +82,25 @@ struct NSearchField: NSViewRepresentable { } } + // enter + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + + if (commandSelector == #selector(NSResponder.insertNewline(_:))) { + // Do something against ENTER key + logger.debug("on search field enter commit, text: \(parent.value)") + parent.onCommit?(parent.value) + editing = false + + return true + } + + return false + } + // func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { // let value = fieldEditor.string.trimmingCharacters(in: .whitespacesAndNewlines) // parent.value = value -// logger.debug("on search field commit, text: \(parent.value)") +// logger.debug("on search field enter commit, text: \(parent.value)") // parent.onCommit?(value) // editing = false // return true diff --git a/redis-pro/Views/Components/Form/NTextField.swift b/redis-pro/Views/Components/Form/NTextField.swift index beb5123..f7ce5c9 100644 --- a/redis-pro/Views/Components/Form/NTextField.swift +++ b/redis-pro/Views/Components/Form/NTextField.swift @@ -41,7 +41,7 @@ struct NTextField: NSViewRepresentable { // textField.alignment = .center // textField.bezelStyle = .roundedBezel textField.tag = tag - textField.isEnabled = !disabled + textField.isEditable = !disabled style(textField) return textField @@ -122,7 +122,6 @@ struct NTextField: NSViewRepresentable { // MARK: - NSTextFieldDelegate Methods - func controlTextDidChange(_ obj: Notification) { guard let textField = obj.object as? NSTextField else { return } parent.stringValue = textField.stringValue diff --git a/redis-pro/Views/Components/Table/NTable.swift b/redis-pro/Views/Components/Table/NTable.swift index 0084beb..b9efd24 100644 --- a/redis-pro/Views/Components/Table/NTable.swift +++ b/redis-pro/Views/Components/Table/NTable.swift @@ -147,11 +147,13 @@ class NTableController: NSViewController{ }) .store(in: &self.cancellables) - // 初始化右键菜单 + // init context menu if !viewStore.contextMenus.isEmpty { let menu = NSMenu() viewStore.contextMenus.forEach { item in - menu.addItem(NSMenuItem(title: item, action: #selector(contextMenuAction(_:)), keyEquivalent: "")) + let menuItem = NSMenuItem(title: item.rawValue, action: #selector(contextMenuAction(_:)), keyEquivalent: item.ext.keyEquivalent) + + menu.addItem(menuItem) } tableView.menu = menu @@ -224,17 +226,35 @@ class NTableController: NSViewController{ + //MARK: table event handler // 监听键盘删除事件 override func keyDown(with event: NSEvent) { + + let selectIndex = tableView.selectedRow if event.specialKey == NSEvent.SpecialKey.delete { - logger.info("on delete key down, delete index: \(tableView.selectedRow)") - let selectIndex = tableView.selectedRow + logger.info("on delete key down, delete index: \(selectIndex)") if selectIndex > -1 { - // self.onDeleteAction?(selectIndex, self.datasource[selectIndex]) self.viewStore.send(.delete(selectIndex)) } } + // cmd + else if event.modifierFlags.intersection(.deviceIndependentFlagsMask) == .command { + if event.charactersIgnoringModifiers == "c" { + logger.info("on table keyboard event: copy, index: \(selectIndex)") + + if selectIndex > -1 { + self.viewStore.send(.copy(selectIndex)) + } + } + else if event.charactersIgnoringModifiers == "e" { + logger.info("on table keyboard event: edit, index: \(selectIndex)") + + if selectIndex > -1 { + self.viewStore.send(.double(selectIndex)) + } + } + } } @@ -260,13 +280,18 @@ class NTableController: NSViewController{ if index < 0 { return } - logger.info("context menu action, index: \(index)") - self.viewStore.send(.contextMenu(menuItem.title, index)) + + logger.info("context menu action, menu: \(menuItem.title), index: \(index)") + if menuItem.title == "Copy" { + self.viewStore.send(.copy(index)) + } else { + self.viewStore.send(.contextMenu(menuItem.title, index)) + } } } -//NSTableViewDelegate +//MARK: - NSTableViewDelegate basic opration extension NTableController: NSTableViewDelegate { // 构建单元格 @@ -292,24 +317,19 @@ extension NTableController: NSTableViewDelegate { let selectIndex = tableView.selectedRow self.logger.info("table coordinator selection did change, selectedRow: \(selectIndex)") - // guard self.datasource.count > selectIndex && selectIndex > -1 else {return} - - // self.selectIndex = tableView.selectedRow - // self.onChangeAction?(tableView.selectedRow, self.datasource[selectIndex]) - self.viewStore.send(.selectionChange(selectIndex)) } } -// NSTableViewDataSource +//MARK: - NSTableViewDelegate drag opration extension NTableController: NSTableViewDataSource { // 获取id // For the source table view func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { - let rowAnyObj = self.viewStore.datasource[self.viewStore.selectIndex] + let rowAnyObj = self.viewStore.datasource[row] let value = "\(rowAnyObj.hashValue)" @@ -332,16 +352,13 @@ extension NTableController: NSTableViewDataSource { guard let that = info.draggingPasteboard.pasteboardItems?.first, let theString = that.string(forType: pasteboardType), -// let index = self.viewStore.datasource.first(where: { "\($0.hashValue)" == theString }), let originalRow = self.viewStore.datasource.firstIndex(where: { item in return "\(item.hashValue)" == theString }) else { return false } - var newRow = row - // When you drag an item downwards, the "new row" index is actually --1. Remember dragging operation is `.above`. - if originalRow < newRow { - newRow = row - 1 + if originalRow == row { + return false } // Animate the rows @@ -351,8 +368,8 @@ extension NTableController: NSTableViewDataSource { // Persist the ordering by saving your data model // saveAccountsReordered(at: originalRow, to: newRow) - self.viewStore.send(.dragComplete(originalRow, newRow)) - logger.info("drad complete, at: \(originalRow), to: \(newRow)") + self.viewStore.send(.dragComplete(originalRow, row)) + logger.info("drad complete, at: \(originalRow), to: \(row)") return true } diff --git a/redis-pro/Views/RedisEditorView/HashEditorView.swift b/redis-pro/Views/RedisEditorView/HashEditorView.swift index 584fc78..05a1988 100644 --- a/redis-pro/Views/RedisEditorView/HashEditorView.swift +++ b/redis-pro/Views/RedisEditorView/HashEditorView.swift @@ -43,7 +43,7 @@ struct HashEditorView: View { }) { ModalView("Edit hash entry", action: {viewStore.send(.submit)}) { VStack(alignment:.leading, spacing: 8) { - FormItemText(placeholder: "Field", value: viewStore.binding(\.$field)).disabled(!viewStore.isNew) + FormItemText(placeholder: "Field", editable: viewStore.isNew, value: viewStore.binding(\.$field)) FormItemTextArea(placeholder: "Value", value: viewStore.binding(\.$value)) } } diff --git a/redis-pro/Views/RedisEditorView/RedisValueHeaderView.swift b/redis-pro/Views/RedisEditorView/RedisValueHeaderView.swift index ce5ee87..2071c64 100644 --- a/redis-pro/Views/RedisEditorView/RedisValueHeaderView.swift +++ b/redis-pro/Views/RedisEditorView/RedisValueHeaderView.swift @@ -27,10 +27,11 @@ struct RedisValueHeaderView: View { WithViewStore(store) {viewStore in HStack(alignment: .center, spacing: 6) { - FormItemText(label: "Key", labelWidth: 40, required: true, value: viewStore.binding(get: \.key, send: KeyAction.setKey)).disabled(!viewStore.isNew) - RedisKeyTypePicker(label: "Type", value: viewStore.binding(get: \.type, send: KeyAction.setType), disabled: !viewStore.isNew) + FormItemText(label: "Key", labelWidth: 40, required: true, editable: viewStore.isNew, value: viewStore.binding(get: \.key, send: KeyAction.setKey)) + .frame(maxWidth: .infinity) + Spacer() - + RedisKeyTypePicker(label: "Type", value: viewStore.binding(get: \.type, send: KeyAction.setType), disabled: !viewStore.isNew) ttlView(viewStore) } } diff --git a/redis-pro/WindowController.swift b/redis-pro/WindowController.swift deleted file mode 100644 index 10db4e0..0000000 --- a/redis-pro/WindowController.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// WindowController.swift -// redis-pro -// -// Created by chengpan on 2022/5/16. -// - -import Foundation -import Cocoa - -class WindowController: NSWindowController { - @IBAction override func newWindowForTab(_ sender: Any?) { - let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController - let newWindow = newWindowController.window! - - // Add this line: - newWindow.windowController = self - - self.window!.addTabbedWindow(newWindow, ordered: .above) - } -} diff --git a/redis-pro/redis_proApp.swift b/redis-pro/redis_proApp.swift index 993c29d..53905da 100644 --- a/redis-pro/redis_proApp.swift +++ b/redis-pro/redis_proApp.swift @@ -105,8 +105,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillBecomeActive(_ notification: Notification) { logger.info("redis applicationWillBecomeActive...") -// NSApp.mainWindow?.makeKeyAndOrderFront(self) -// (notification.object as? NSApplication)?.windows.first?.makeKeyAndOrderFront(self) } func applicationWillResignActive(_:Notification) { @@ -129,9 +127,3 @@ class AppDelegate: NSObject, NSApplicationDelegate { // } } - -//extension AppDelegate: NSWindowDelegate { -// -// func windowDidMiniaturize(_: Notification) { -// } -//}