Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python via docker #309

1 change: 1 addition & 0 deletions changelog.d/294.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for using python docker interpreter.
87 changes: 87 additions & 0 deletions modules/core/src/main/kotlin/com/metalbear/mirrord/MirrordApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
@SerializedName("uses_operator") val usesOperator: Boolean?
)

/**
* Wrapper around Gson for parsing messages from the mirrord binary.
*/
Expand Down Expand Up @@ -243,6 +249,66 @@ class MirrordApi(private val service: MirrordProjectService, private val project
}
}

private class MirrordContainerExtTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<MirrordContainerExecution>(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<MirrordProjectService>())

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<MirrordProjectService>()
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<MirrordProjectService>()
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.
*
Expand Down Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>?
): MirrordExecution? {
projectEnvVars: Map<String, String>?,
mirrordApi: MirrordApi
): Pair<String?, String?>? {
MirrordLogger.logger.debug("MirrordExecManager.start")
val mirrordActiveValue = projectEnvVars?.get("MIRRORD_ACTIVE")
val explicitlyEnabled = mirrordActiveValue == "1"
Expand All @@ -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()
Expand Down Expand Up @@ -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<String, String>?
): 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,
Expand All @@ -198,6 +209,29 @@ class MirrordExecManager(private val service: MirrordProjectService) {
return executionInfo
}

private fun containerStart(
DmitryDodzin marked this conversation as resolved.
Show resolved Hide resolved
wslDistribution: WSLDistribution?,
product: String,
projectEnvVars: Map<String, String>?
): 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.
*
Expand All @@ -222,6 +256,22 @@ class MirrordExecManager(private val service: MirrordProjectService) {
throw e
}
}

fun containerStart(): MirrordContainerExecution? {
DmitryDodzin marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
DmitryDodzin marked this conversation as resolved.
Show resolved Hide resolved
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<MirrordProjectService>()

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,
Expand All @@ -20,26 +53,38 @@ class PythonCommandLineProvider : PythonCommandLineTargetEnvironmentProvider {
val service = project.service<MirrordProjectService>()

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")
}
}
}
}
Expand Down