diff --git a/changelog.d/294.fixed.md b/changelog.d/294.fixed.md new file mode 100644 index 00000000..2a4f4430 --- /dev/null +++ b/changelog.d/294.fixed.md @@ -0,0 +1 @@ +Add support for using python docker interpreter. diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt index c032963c..3e441f4b 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt @@ -101,6 +101,12 @@ data class MirrordExecution( @SerializedName("uses_operator") val usesOperator: Boolean? ) +data class MirrordContainerExecution( + val runtime: String, + @SerializedName("extra_args") val extraArgs: MutableList, + @SerializedName("uses_operator") val usesOperator: Boolean? +) + /** * Wrapper around Gson for parsing messages from the mirrord binary. */ @@ -243,6 +249,66 @@ class MirrordApi(private val service: MirrordProjectService, private val project } } + private class MirrordContainerExtTask(cli: String, projectEnvVars: Map?) : MirrordCliTask(cli, "container-ext", null, projectEnvVars) { + override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordContainerExecution { + val parser = SafeParser() + val bufferedReader = process.inputStream.reader().buffered() + + val warningHandler = MirrordWarningHandler(project.service()) + + setText("mirrord is starting...") + for (line in bufferedReader.lines()) { + val message = parser.parse(line, Message::class.java) + when { + message.name == "mirrord preparing to launch" && message.type == MessageType.FinishedTask -> { + val success = message.success + ?: throw MirrordError("invalid message received from the mirrord binary") + if (success) { + val innerMessage = message.message + ?: throw MirrordError("invalid message received from the mirrord binary") + val executionInfo = parser.parse(innerMessage as String, MirrordContainerExecution::class.java) + setText("mirrord is running") + return executionInfo + } + } + + message.type == MessageType.Info -> { + val service = project.service() + message.message?.let { service.notifier.notifySimple(it as String, NotificationType.INFORMATION) } + } + + message.type == MessageType.Warning -> { + message.message?.let { warningHandler.handle(it as String) } + } + + message.type == MessageType.IdeMessage -> { + message.message?.run { + val ideMessage = Gson().fromJson(Gson().toJsonTree(this), IdeMessage::class.java) + val service = project.service() + ideMessage?.handleIdeMessage(service) + } + } + + else -> { + var displayMessage = message.name + message.message?.let { + displayMessage += ": $it" + } + setText(displayMessage) + } + } + } + + process.waitFor() + if (process.exitValue() != 0) { + val processStdError = process.errorStream.bufferedReader().readText() + throw MirrordError.fromStdErr(processStdError) + } else { + throw MirrordError("invalid output of the mirrord binary") + } + } + } + /** * Interacts with the `mirrord verify-config [path]` cli command. * @@ -310,6 +376,27 @@ class MirrordApi(private val service: MirrordProjectService, private val project return result } + fun containerExec(cli: String, target: String?, configFile: String?, wslDistribution: WSLDistribution?): MirrordContainerExecution { + bumpRunCounter() + + val task = MirrordContainerExtTask(cli, projectEnvVars).apply { + this.target = target + this.configFile = configFile + this.wslDistribution = wslDistribution + } + + val result = task.run(service.project) + service.notifier.notifySimple("mirrord starting...", NotificationType.INFORMATION) + + result.usesOperator?.let { usesOperator -> + if (usesOperator) { + MirrordSettingsState.instance.mirrordState.operatorUsed = true + } + } + + return result + } + /** * Increments the mirrord run counter. * Can display some notifications (asking for feedback, discord invite, mirrord for Teams invite). diff --git a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt index 40719110..7ea1343f 100644 --- a/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt +++ b/modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordExecManager.kt @@ -84,20 +84,12 @@ class MirrordExecManager(private val service: MirrordProjectService) { return wslDistribution?.getWslPath(path) ?: path } - /** - * Starts mirrord, shows dialog for selecting pod if target is not set and returns env to set. - * - * @param envVars Contains both system env vars, and (active) launch settings, see `Wrapper`. - * @return extra environment variables to set for the executed process and path to the patched executable. - * null if mirrord service is disabled - * @throws ProcessCanceledException if the user cancelled - */ - private fun start( + private fun prepareStart( wslDistribution: WSLDistribution?, - executable: String?, product: String, - projectEnvVars: Map? - ): MirrordExecution? { + projectEnvVars: Map?, + mirrordApi: MirrordApi + ): Pair? { MirrordLogger.logger.debug("MirrordExecManager.start") val mirrordActiveValue = projectEnvVars?.get("MIRRORD_ACTIVE") val explicitlyEnabled = mirrordActiveValue == "1" @@ -122,8 +114,6 @@ class MirrordExecManager(private val service: MirrordProjectService) { ) } - val mirrordApi = service.mirrordApi(projectEnvVars) - val mirrordConfigPath = projectEnvVars?.get(CONFIG_ENV_NAME)?.let { if (it.contains("\$ProjectPath\$")) { val projectFile = service.configApi.getProjectDir() @@ -185,6 +175,27 @@ class MirrordExecManager(private val service: MirrordProjectService) { null } + return Pair(configPath, target) + } + + /** + * Starts mirrord, shows dialog for selecting pod if target is not set and returns env to set. + * + * @param envVars Contains both system env vars, and (active) launch settings, see `Wrapper`. + * @return extra environment variables to set for the executed process and path to the patched executable. + * null if mirrord service is disabled + * @throws ProcessCanceledException if the user cancelled + */ + private fun start( + wslDistribution: WSLDistribution?, + executable: String?, + product: String, + projectEnvVars: Map? + ): MirrordExecution? { + val mirrordApi = service.mirrordApi(projectEnvVars) + val (configPath, target) = this.prepareStart(wslDistribution, product, projectEnvVars, mirrordApi) ?: return null + val cli = cliPath(wslDistribution, product) + val executionInfo = mirrordApi.exec( cli, target, @@ -198,6 +209,29 @@ class MirrordExecManager(private val service: MirrordProjectService) { return executionInfo } + private fun containerStart( + wslDistribution: WSLDistribution?, + product: String, + projectEnvVars: Map? + ): MirrordContainerExecution? { + val mirrordApi = service.mirrordApi(projectEnvVars) + val (configPath, target) = this.prepareStart(wslDistribution, product, projectEnvVars, mirrordApi) ?: return null + val cli = cliPath(wslDistribution, product) + + val executionInfo = mirrordApi.containerExec( + cli, + target, + configPath, + wslDistribution + ) + MirrordLogger.logger.debug("MirrordExecManager.start: executionInfo: $executionInfo") + + executionInfo.extraArgs.add("-e") + executionInfo.extraArgs.add("MIRRORD_IGNORE_DEBUGGER_PORTS=\"35000-65535\"") + + return executionInfo + } + /** * Wrapper around `MirrordExecManager` that is called by each IDE, or language variant. * @@ -222,6 +256,22 @@ class MirrordExecManager(private val service: MirrordProjectService) { throw e } } + + fun containerStart(): MirrordContainerExecution? { + return try { + manager.containerStart(wsl, product, extraEnvVars) + } catch (e: MirrordError) { + e.showHelp(manager.service.project) + throw e + } catch (e: ProcessCanceledException) { + manager.service.notifier.notifySimple("mirrord was cancelled", NotificationType.WARNING) + throw e + } catch (e: Throwable) { + val mirrordError = MirrordError(e.toString(), e) + mirrordError.showHelp(manager.service.project) + throw e + } + } } /** diff --git a/modules/products/pycharm/src/main/kotlin/com/metalbear/mirrord/products/pycharm/PythonCommandLineProvider.kt b/modules/products/pycharm/src/main/kotlin/com/metalbear/mirrord/products/pycharm/PythonCommandLineProvider.kt index fd96b7a3..6bacf7e7 100644 --- a/modules/products/pycharm/src/main/kotlin/com/metalbear/mirrord/products/pycharm/PythonCommandLineProvider.kt +++ b/modules/products/pycharm/src/main/kotlin/com/metalbear/mirrord/products/pycharm/PythonCommandLineProvider.kt @@ -1,5 +1,6 @@ package com.metalbear.mirrord.products.pycharm +import com.intellij.execution.target.TargetEnvironmentRequest import com.intellij.execution.wsl.target.WslTargetEnvironmentRequest import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -11,6 +12,38 @@ import com.jetbrains.python.run.target.PythonCommandLineTargetEnvironmentProvide import com.metalbear.mirrord.MirrordProjectService class PythonCommandLineProvider : PythonCommandLineTargetEnvironmentProvider { + // Wrapper for docker variant of TargetEnvironmentRequest because the variant is dynamically loaded from another + // plugin so we need to perform our operations via reflection api + private class DockerRuntimeConfig(val inner: TargetEnvironmentRequest) { + var runCliOptions: String? + get() { + val runCliOptionsField = inner.javaClass.getDeclaredField("myRunCliOptions") + return if (runCliOptionsField.trySetAccessible()) { + runCliOptionsField.get(inner) as String + } else { + null + } + } + set(value) { + inner + .javaClass + .getMethod("setRunCliOptions", Class.forName("java.lang.String")) + .invoke(inner, value) + } + } + + private fun extendContainerTargetEnvironment(project: Project, runParams: PythonRunParams, docker: DockerRuntimeConfig) { + val service = project.service() + + service.execManager.wrapper("pycharm", runParams.getEnvs()).containerStart()?.let { executionInfo -> + docker.runCliOptions?.let { + executionInfo.extraArgs.add(it) + } + + docker.runCliOptions = executionInfo.extraArgs.joinToString(" ") + } + } + override fun extendTargetEnvironment( project: Project, helpersAwareTargetRequest: HelpersAwareTargetEnvironmentRequest, @@ -20,26 +53,38 @@ class PythonCommandLineProvider : PythonCommandLineTargetEnvironmentProvider { val service = project.service() if (runParams is AbstractPythonRunConfiguration<*>) { - val wsl = helpersAwareTargetRequest.targetEnvironmentRequest.let { - if (it is WslTargetEnvironmentRequest) { - it.configuration.distribution + val docker = helpersAwareTargetRequest.targetEnvironmentRequest.let { + if (it.javaClass.name.startsWith("com.intellij.docker")) { + DockerRuntimeConfig(it) } else { null } } - service.execManager.wrapper("pycharm", runParams.getEnvs()).apply { - this.wsl = wsl - }.start()?.let { executionInfo -> - for (entry in executionInfo.environment.entries.iterator()) { - pythonExecution.addEnvironmentVariable(entry.key, entry.value) + if (docker != null) { + extendContainerTargetEnvironment(project, runParams, docker) + } else { + val wsl = helpersAwareTargetRequest.targetEnvironmentRequest.let { + if (it is WslTargetEnvironmentRequest) { + it.configuration.distribution + } else { + null + } } - for (key in executionInfo.envToUnset.orEmpty()) { - pythonExecution.envs.remove(key) - } + service.execManager.wrapper("pycharm", runParams.getEnvs()).apply { + this.wsl = wsl + }.start()?.let { executionInfo -> + for (entry in executionInfo.environment.entries.iterator()) { + pythonExecution.addEnvironmentVariable(entry.key, entry.value) + } - pythonExecution.addEnvironmentVariable("MIRRORD_DETECT_DEBUGGER_PORT", "pydevd") + for (key in executionInfo.envToUnset.orEmpty()) { + pythonExecution.envs.remove(key) + } + + pythonExecution.addEnvironmentVariable("MIRRORD_DETECT_DEBUGGER_PORT", "pydevd") + } } } }