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.
97 changes: 92 additions & 5 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 @@ -144,7 +150,7 @@ private const val MIRRORD_FOR_TEAMS_INVITE_EVERY = 30
* Interact with mirrord CLI using this API.
*/
class MirrordApi(private val service: MirrordProjectService, private val projectEnvVars: Map<String, String>?) {
private class MirrordLsTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<List<String>>(cli, "ls", null, projectEnvVars) {
private class MirrordLsTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<List<String>>(cli, listOf("ls"), null, projectEnvVars) {
override fun compute(project: Project, process: Process, setText: (String) -> Unit): List<String> {
setText("mirrord is listing targets...")

Expand Down Expand Up @@ -183,7 +189,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project
return task.run(service.project)
}

private class MirrordExtTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<MirrordExecution>(cli, "ext", null, projectEnvVars) {
private class MirrordExtTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<MirrordExecution>(cli, listOf("ext"), null, projectEnvVars) {
override fun compute(project: Project, process: Process, setText: (String) -> Unit): MirrordExecution {
val parser = SafeParser()
val bufferedReader = process.inputStream.reader().buffered()
Expand Down Expand Up @@ -243,13 +249,73 @@ class MirrordApi(private val service: MirrordProjectService, private val project
}
}

private class MirrordContainerExtTask(cli: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<MirrordContainerExecution>(cli, listOf("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.
*
* Reads the output (json) from stdout which contain either a success + warnings, or the errors from the verify
* command.
*/
private class MirrordVerifyConfigTask(cli: String, path: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<String>(cli, "verify-config", listOf("--ide", path), projectEnvVars) {
private class MirrordVerifyConfigTask(cli: String, path: String, projectEnvVars: Map<String, String>?) : MirrordCliTask<String>(cli, listOf("verify-config"), listOf("--ide", path), projectEnvVars) {
override fun compute(project: Project, process: Process, setText: (String) -> Unit): String {
setText("mirrord is verifying the config options...")
process.waitFor()
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 Expand Up @@ -342,7 +429,7 @@ class MirrordApi(private val service: MirrordProjectService, private val project
*
* @param args: An extra list of arguments (used by `verify-config`).
*/
private abstract class MirrordCliTask<T>(private val cli: String, private val command: String, private val args: List<String>?, private val projectEnvVars: Map<String, String>?) {
private abstract class MirrordCliTask<T>(private val cli: String, private val command: List<String>, private val args: List<String>?, private val projectEnvVars: Map<String, String>?) {
var target: String? = null
var configFile: String? = null
var executable: String? = null
Expand All @@ -353,7 +440,7 @@ private abstract class MirrordCliTask<T>(private val cli: String, private val co
* Returns command line for execution.
*/
private fun prepareCommandLine(project: Project): GeneralCommandLine {
return GeneralCommandLine(cli, command).apply {
return GeneralCommandLine(cli, *command.toTypedArray()).apply {
// Merge our `environment` vars with what's set in the current launch run configuration.
if (projectEnvVars != null) {
environment.putAll(projectEnvVars)
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,
cli: String
): 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 All @@ -140,7 +130,6 @@ class MirrordExecManager(private val service: MirrordProjectService) {
it
}
}
val cli = cliPath(wslDistribution, product)

MirrordLogger.logger.debug("MirrordExecManager.start: mirrord cli path is $cli")
// Find the mirrord config path, then call `mirrord verify-config {path}` so we can display warnings/errors
Expand Down Expand Up @@ -185,6 +174,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 cli = cliPath(wslDistribution, product)
val mirrordApi = service.mirrordApi(projectEnvVars)
val (configPath, target) = this.prepareStart(wslDistribution, projectEnvVars, mirrordApi, cli) ?: return null

val executionInfo = mirrordApi.exec(
cli,
target,
Expand All @@ -198,6 +208,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 cli = cliPath(wslDistribution, product)
val mirrordApi = service.mirrordApi(projectEnvVars)
val (configPath, target) = this.prepareStart(wslDistribution, projectEnvVars, mirrordApi, cli) ?: return null

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 +255,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