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 @@ -198,6 +198,95 @@ 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? {
MirrordLogger.logger.debug("MirrordExecManager.startContainer")
val explicitlyEnabled = projectEnvVars?.any { (key, value) -> key == "MIRRORD_ACTIVE" && value == "1" } ?: false
if (!service.enabled && !explicitlyEnabled) {
DmitryDodzin marked this conversation as resolved.
Show resolved Hide resolved
MirrordLogger.logger.debug("disabled, returning")
return null
}

val mirrordApi = service.mirrordApi(projectEnvVars)

val mirrordConfigPath = projectEnvVars?.get(CONFIG_ENV_NAME)?.let {
if (it.contains("\$ProjectPath\$")) {
val projectFile = service.configApi.getProjectDir()
projectFile.canonicalPath?.let { path ->
it.replace("\$ProjectPath\$", path)
} ?: run {
service.notifier.notifySimple(
"Failed to evaluate `ProjectPath` macro used in `$CONFIG_ENV_NAME` environment variable",
NotificationType.WARNING
)
it
}
} else {
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
// from the config without relying on mirrord-layer.

val configPath = service.configApi.getConfigPath(mirrordConfigPath)
MirrordLogger.logger.debug("MirrordExecManager.start: config path is $configPath")

val verifiedConfig = configPath?.let {
val verifiedConfigOutput =
mirrordApi.verifyConfig(cli, wslDistribution?.getWslPath(it) ?: it, wslDistribution)
MirrordLogger.logger.debug("MirrordExecManager.start: verifiedConfigOutput: $verifiedConfigOutput")
MirrordVerifiedConfig(verifiedConfigOutput, service.notifier).apply {
MirrordLogger.logger.debug("MirrordExecManager.start: MirrordVerifiedConfig: $it")
if (isError()) {
MirrordLogger.logger.debug("MirrordExecManager.start: invalid config error")
throw InvalidConfigException(it, "Validation failed for config")
}
}
}

MirrordLogger.logger.debug("Verified Config: $verifiedConfig, Target selection.")

val targetSet = verifiedConfig?.let { isTargetSet(it.config) } ?: false
val target = if (!targetSet) {
// There is no config file or the config does not specify a target, so show dialog.
MirrordLogger.logger.debug("target not selected, showing dialog")

chooseTarget(cli, wslDistribution, configPath, mirrordApi)
.takeUnless { it == MirrordExecDialog.targetlessTargetName } ?: run {
MirrordLogger.logger.info("No target specified - running targetless")
service.notifier.notification(
"No target specified, mirrord running targetless.",
NotificationType.INFORMATION
)
.withDontShowAgain(MirrordSettingsState.NotificationId.RUNNING_TARGETLESS)
.fire()

null
}
} else {
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 +311,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,39 @@ import com.jetbrains.python.run.target.PythonCommandLineTargetEnvironmentProvide
import com.metalbear.mirrord.MirrordProjectService

class PythonCommandLineProvider : PythonCommandLineTargetEnvironmentProvider {
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()).apply {
this.wsl = null
}.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 +54,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