diff --git a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt index bba73979..28a89004 100644 --- a/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt +++ b/src/main/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceImpl.kt @@ -20,10 +20,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.vfs.VirtualFileManager +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import org.zowe.explorer.config.ConfigService import org.zowe.explorer.config.connect.ConnectionConfig import org.zowe.explorer.config.connect.CredentialService @@ -180,7 +184,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } } catch (e: Exception) { - NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") + NotificationsService.errorNotification(e, project = myProject, custTitle = "Error with Zowe config file") return null } } @@ -239,42 +243,99 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } /** - * Checks the zowe connection using InfoOperation. It is also needed to load zos version and + * Checks all passed zowe connections using InfoOperation in parallel. It is also needed to load zos version and * real owner of the connection (acceptable for alias users). - * IMPORTANT!!! It modifies passed connection object by setting zVersion and owner. - * @param zoweConnection connection to check and prepare. - * @throws Throwable if something went wrong (connection was not established or one of requests was failed ...) + * IMPORTANT!!! It modifies passed connection objects by setting zVersion and owner. + * @param zoweConnections list of connection to check and prepare. + * @param type of zowe config + * @throws ProcessCanceledException if connection has been canceled by user + * @return a pair of lists with successfully tested connections and invalid connections */ - private fun testAndPrepareConnection(zoweConnection: ConnectionConfig) { - val throwable = runTask("Testing Connection to ${zoweConnection.url}", myProject) { indicator -> - return@runTask try { - runCatching { - DataOpsManager.getService().performOperation(InfoOperation(zoweConnection), indicator) - }.onSuccess { - indicator.text = "Retrieving z/OS information" - val systemInfo = DataOpsManager.getService().performOperation(ZOSInfoOperation(zoweConnection), indicator) - zoweConnection.zVersion = when (systemInfo.zosVersion) { - "04.25.00" -> ZVersion.ZOS_2_2 - "04.26.00" -> ZVersion.ZOS_2_3 - "04.27.00" -> ZVersion.ZOS_2_4 - "04.28.00" -> ZVersion.ZOS_2_5 - "04.29.00" -> ZVersion.ZOS_3_1 - else -> ZVersion.ZOS_2_1 + @Throws(ProcessCanceledException::class) + private fun testAndPrepareConnections( + zoweConnections: List, + type: ZoweConfigType + ): Pair, MutableList> { + val goodConnections = mutableListOf() + val failedConnections = mutableListOf() + val results = mutableListOf>() + var throwable: ProcessCanceledException? = null + runTask("Testing Connections...", myProject, cancellable = true) { indicator -> + runBlocking { + for (zosmfConnection in zoweConnections) { + results.add(async { + val zoweConnection: ConnectionConfig = prepareConnection(zosmfConnection, type) + try { + runCatching { + if (!indicator.isCanceled) { + indicator.text = if (zoweConnections.size == 1) + "Testing Connection to ${zoweConnection.url}" + else + "Testing Connection to ${zoweConnections.size} connections" + DataOpsManager.getService().performOperation(InfoOperation(zoweConnection), indicator) + } else { + throw ProcessCanceledException() + } + }.onSuccess { + if (!indicator.isCanceled) { + + indicator.text = if (zoweConnections.size == 1) + "Retrieving z/OS information for ${zoweConnection.url}" + else + "Retrieving z/OS information for ${zoweConnections.size} connections" + val systemInfo = + DataOpsManager.getService().performOperation( + ZOSInfoOperation(zoweConnection), + indicator + ) + zoweConnection.zVersion = when (systemInfo.zosVersion) { + "04.25.00" -> ZVersion.ZOS_2_2 + "04.26.00" -> ZVersion.ZOS_2_3 + "04.27.00" -> ZVersion.ZOS_2_4 + "04.28.00" -> ZVersion.ZOS_2_5 + "04.29.00" -> ZVersion.ZOS_3_1 + else -> ZVersion.ZOS_2_1 + } + } else { + throw ProcessCanceledException() + } + }.onSuccess { + if (!indicator.isCanceled) { + indicator.text = if (zoweConnections.size == 1) + "Retrieving user information for ${zoweConnection.url}" + else + "Retrieving user information for ${zoweConnections.size} connections" + zoweConnection.owner = whoAmI(zoweConnection) ?: "" + } else { + throw ProcessCanceledException() + } + }.onFailure { + if (indicator.isCanceled) + throw ProcessCanceledException() + else { + throw it + } + } + } catch (t: Throwable) { + if (t is ProcessCanceledException) + throwable = t + failedConnections.add(zoweConnection) + return@async zoweConnection + } + goodConnections.add(zoweConnection) + return@async zoweConnection } - }.onSuccess { - indicator.text = "Retrieving user information" - zoweConnection.owner = whoAmI(zoweConnection) ?: "" - }.onFailure { - throw it + ) } - null - } catch (t: Throwable) { - t } } if (throwable != null) { - throw throwable + throw ProcessCanceledException() + } + runBlocking { + results.map { it.await() } } + return goodConnections to failedConnections } /** @@ -290,7 +351,8 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService this.globalZoweConfig zoweConfig ?: throw Exception("Cannot get $type Zowe config") - val (allPreparedConn, failedConnections) = testAndPrepareAllZosmfConnections(zoweConfig, type) + val (goodConnections, failedConnections) = + testAndPrepareConnections(zoweConfig.getListOfZosmfConections(), type) if (checkConnection and failedConnections.isNotEmpty()) { val andMore = if (failedConnections.size > 3) "..." else "" notifyUiOnConnectionFailure( @@ -300,9 +362,9 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService ) } val conToAdd = if (checkConnection) - allPreparedConn.subtract(failedConnections.toSet()) + goodConnections else - allPreparedConn + goodConnections + failedConnections conToAdd.forEach { zosmfConnection -> val connectionOpt = configCrudable.addOrUpdate(zosmfConnection) if (!connectionOpt.isEmpty) { @@ -315,7 +377,14 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } catch (e: Exception) { - NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") + if (e !is ProcessCanceledException) + NotificationsService.errorNotification( + e, + project = myProject, + custTitle = "Error with Zowe config file" + ) + else { /* Nothing to do */ + } } } @@ -338,31 +407,6 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService return zoweConnection } - /** - * Convert all zosmf connections from zowe config file to ConnectionConfig and tests them - * @param zoweConfig - * @param type of zowe config - * @return pair of lists, one is the connections list, the second is the list of URLs which did not pass the test - */ - private fun testAndPrepareAllZosmfConnections( - zoweConfig: ZoweConfig, - type: ZoweConfigType - ): Pair, List> { - return zoweConfig.getListOfZosmfConections() - .fold( - mutableListOf() to mutableListOf() - ) { (allConnectionConfigs, failedURLs), zosmfConnection -> - val zoweConnection = prepareConnection(zosmfConnection, type) - try { - testAndPrepareConnection(zoweConnection) - } catch (t: Throwable) { - failedURLs.add(zoweConnection) - } - allConnectionConfigs.add(zoweConnection) - allConnectionConfigs to failedURLs - } - } - /** * @see ZoweConfigService.deleteZoweConfig */ @@ -398,7 +442,7 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService } } catch (e: Exception) { - NotificationsService.errorNotification(e, project = myProject, custTitle="Error with Zowe config file") + NotificationsService.errorNotification(e, project = myProject, custTitle = "Error with Zowe config file") } } @@ -421,13 +465,13 @@ class ZoweConfigServiceImpl(override val myProject: Project) : ZoweConfigService var host = "localhost" var port = "10443" if (matcher.matches()) { - if (matcher.group(2) != null) + if (!matcher.group(2).isNullOrBlank()) host = matcher.group(2) - if (matcher.group(3) != null) + if (!matcher.group(3).isNullOrBlank()) port = matcher.group(3).substring(1) } - val content = getResourceAsStreamWrappable(ZoweConfigServiceImpl::class.java.classLoader ,"files/${ZOWE_CONFIG_NAME}") + val content = getResourceAsStreamWrappable(ZoweConfigServiceImpl::class.java.classLoader, "files/${ZOWE_CONFIG_NAME}") .use { iS -> iS?.readAllBytes()?.let { String(it, charset) } } ?.replace("".toRegex(), port) ?.replace("".toRegex(), "\"$host\"") diff --git a/src/test/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceTestSpec.kt b/src/test/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceTestSpec.kt index 01da5bb8..fb9ea47e 100644 --- a/src/test/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceTestSpec.kt +++ b/src/test/kotlin/org/zowe/explorer/zowe/service/ZoweConfigServiceTestSpec.kt @@ -18,6 +18,7 @@ import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages @@ -316,6 +317,80 @@ class ZoweConfigServiceTestSpec : WithApplicationShouldSpec({ assertSoftly { addOrUpdateCalledCount shouldBe 1 } } + should("cancel testing Zowe config connections") { + val testFailProfileName5 = "test_profile_name_fail5" + var extractSecurePropertiesCalledCount = 0 + var cancelationCount = 0 + + dataOpsManagerServiceMock.testInstance = object : TestDataOpsManagerImpl() { + override fun performOperation(operation: Operation, progressIndicator: ProgressIndicator): R { + return when (operation) { + is InfoOperation -> { + infoOperationCount += 1 + if ((operation as InfoOperation).connectionConfig.uuid==("throw")){ + cancelationCount += 1 + throw ProcessCanceledException() + } + else { + mockk() as R + } + } + else -> { + mockk() as R + } + } + } + } + + val globalZoweConfig: ZoweConfig = mockk { + every { + extractSecureProperties(any>(), any()) + } answers { + extractSecurePropertiesCalledCount += 1 + } + every { + getListOfZosmfConections() + } returns listOf( + mockk { + every { user } returns "TSTUSR" + every { password } returns "TSTPWD" + every { profileName } returns testFailProfileName5 + every { basePath } returns "test/base/path/" + every { host } returns "testFailHost5" + every { zosmfPort } returns "1234" + every { protocol } returns "https" + every { rejectUnauthorized } returns null + } + ) + } + + every { parseConfigJsonRef(any()) } returns globalZoweConfig + + every { + configServiceCrudableMock.find(any>(), any>()) + } answers { + listOf( + mockk { + every { uuid } returns "throw" + every { zVersion } returns ZVersion.ZOS_2_4 + every { name } returns "$ZOWE_PROJECT_PREFIX${ZoweConfigType.GLOBAL}-$testFailProfileName5" + every { zoweConfigPath } returns System.getProperty("user.home").replace("((\\*)|(/*))$", "") + "/.zowe/" + ZOWE_CONFIG_NAME + } + ) + .filter(secondArg>()::test) + .stream() + } + + val zoweConfigService = ZoweConfigServiceImpl(projectMock) + + zoweConfigService + .addOrUpdateZoweConfig(scanProject = true, checkConnection = true, ZoweConfigType.GLOBAL) + assertSoftly { cancelationCount shouldBe 1 } + assertSoftly { setCredentialsCalledCount shouldBe 1 } + assertSoftly { infoOperationCount shouldBe 1 } + + } + should("add a new connection for the local Zowe config, scanning a project, with failed connections and their check") { val testSuccessProfileName = "test_profile_name_success" val testFailProfileName1 = "test_profile_name_fail1" @@ -543,6 +618,7 @@ class ZoweConfigServiceTestSpec : WithApplicationShouldSpec({ val testFailProfileName2 = "test_profile_name_fail2" val testFailProfileName3 = "test_profile_name_fail3" val testFailProfileName4 = "test_profile_name_fail4" + val testFailProfileName5 = "test_profile_name_fail5" val testFailHost1 = "test1.com" val testFailHost2 = "test2.com" val testFailHost3 = "test3.com"