Skip to content

Commit

Permalink
Use BeforeRunTaskProvider for Tomcat (#300)
Browse files Browse the repository at this point in the history
* wip

* Moved logic to a callback run in a before run task

* Changelog

* Doc extended

* Extra doc
  • Loading branch information
Razz4780 authored Nov 27, 2024
1 parent e83ecb7 commit a6e359f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 45 deletions.
1 change: 1 addition & 0 deletions changelog.d/230.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tomcat executions are now stopped when mirrord setup fails.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.metalbear.mirrord.products.tomcat

import com.intellij.execution.BeforeRunTask
import com.intellij.execution.BeforeRunTaskProvider
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.util.Key

val KEY: Key<TomcatBeforeRunTaskProvider.TomcatBeforeRunTask> = Key("MirrordTomcatBeforeRunTask")

class TomcatBeforeRunTaskProvider : BeforeRunTaskProvider<TomcatBeforeRunTaskProvider.TomcatBeforeRunTask>() {
/**
* Simple task that only executes the given callback.
* Must throw an [Exception] in case execution should be stopped.
*/
class TomcatBeforeRunTask(val callback: () -> Unit) : BeforeRunTask<TomcatBeforeRunTask>(KEY)

override fun getId(): Key<TomcatBeforeRunTask> = KEY

override fun getName(): String = "TomcatBeforeRunTask"

/**
* Always returns null. Otherwise, the task would be visible to the users in the run configuration dialog when they add their own before launch tasks.
*/
override fun createTask(runConfiguration: RunConfiguration): TomcatBeforeRunTask? {
return null
}

/**
* Returning `false` here prevents the execution.
*/
override fun executeTask(
context: DataContext,
configuration: RunConfiguration,
environment: ExecutionEnvironment,
task: TomcatBeforeRunTask
): Boolean {
return try {
task.callback.invoke()
true
} catch (_: Exception) {
// Exceptions already handled in `MirrordExecManager`
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package com.metalbear.mirrord.products.tomcat

import com.intellij.execution.ExecutionListener
import com.intellij.execution.configurations.RunConfigurationBase
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.target.createEnvironmentRequest
Expand Down Expand Up @@ -79,24 +80,27 @@ fun patchedScriptFromScript(scriptInfo: ScriptInfo, script: String, args: String
return PatchedScriptInfo(scriptInfo.scriptHelper, parent, startupNotShutdown, params.toTypedArray())
}

data class SavedConfigData(val envVars: List<EnvironmentVariable>, var scriptInfo: ScriptInfo?)
data class SavedConfigData(var envVars: List<EnvironmentVariable>? = null, var scriptInfo: ScriptInfo? = null)

data class CommandLineWithArgs(val command: String, val args: String?)

class TomcatExecutionListener : ExecutionListener {
private val savedEnvs: ConcurrentHashMap<String, SavedConfigData> = ConcurrentHashMap()

private fun getConfig(env: ExecutionEnvironment): RunnerSpecificLocalConfigurationBit? {
/**
* Attempts to downcast [ExecutionEnvironment] fields to known Tomcat-specific types.
*/
private fun getConfig(env: ExecutionEnvironment): Pair<RunnerSpecificLocalConfigurationBit, RunConfigurationBase<*>>? {
MirrordLogger.logger.debug("getConfig - env: $env")

val settings = env.configurationSettings ?: return null
MirrordLogger.logger.debug("getConfig - settings: $settings")

return if (settings is RunnerSpecificLocalConfigurationBit) {
settings
} else {
null
}
val configurationBit = settings as? RunnerSpecificLocalConfigurationBit ?: return null
configurationBit.parentConfiguration
val base = env.runProfile as? RunConfigurationBase<*> ?: return null

return Pair(configurationBit, base)
}

/**
Expand Down Expand Up @@ -134,6 +138,13 @@ class TomcatExecutionListener : ExecutionListener {
}
}

/**
* Verifies that [ExecutionEnvironment] is a Tomcat run. Extracts original environment and startup script info (SIP).
* Injects a [TomcatBeforeRunTaskProvider.TomcatBeforeRunTask] task into the run configuration.
*
* Main mirrord plugin logic (e.g. spawning intproxy) is done in a [com.intellij.execution.BeforeRunTask],
* because we can't stop execution from this [ExecutionListener].
*/
override fun processStartScheduled(executorId: String, env: ExecutionEnvironment) {
val service = env.project.service<MirrordProjectService>()

Expand All @@ -146,16 +157,16 @@ class TomcatExecutionListener : ExecutionListener {
}

MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: got config $config")
var envVars = config.envVariables
val envVarsMap = envVars.map { it.NAME to it.VALUE }.toMap()
var envVars = config.first.envVariables
val envVarsMap = envVars.associate { it.NAME to it.VALUE }

MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: wsl check")
val wsl = when (val request = createEnvironmentRequest(env.runProfile, env.project)) {
is WslTargetEnvironmentRequest -> request.configuration.distribution!!
else -> null
}

val startupInfo = config.startupInfo
val startupInfo = config.first.startupInfo
val scriptAndArgs = if (SystemInfo.isMac) {
val startScriptAndArgs = getStartScript(startupInfo, env)
if (startScriptAndArgs == null) {
Expand All @@ -170,17 +181,27 @@ class TomcatExecutionListener : ExecutionListener {
null
}

try {
config.second.beforeRunTasks = config.second.beforeRunTasks + TomcatBeforeRunTaskProvider.TomcatBeforeRunTask {
val savedData = SavedConfigData()
// Always inject `SavedConfigData` early.
// This allows for removing `TomcatBeforeRunTask` from the run configuration,
// even if mirrord is disabled, or we throw an exception in `MirrordExecManager`.
savedEnvs[executorId] = savedData

service.execManager.wrapper("tomcat", envVarsMap).apply {
this.wsl = wsl
this.executable = scriptAndArgs?.command
}.start()?.let { executionInfo ->
// `MIRRORD_IGNORE_DEBUGGER_PORTS` should allow clean shutdown of the app
// even if `outgoing` feature is enabled.
val mirrordEnv = executionInfo.environment + mapOf(Pair("MIRRORD_DETECT_DEBUGGER_PORT", "javaagent"), Pair("MIRRORD_IGNORE_DEBUGGER_PORTS", getTomcatServerPort()))
val mirrordEnv = executionInfo.environment + mapOf(
Pair("MIRRORD_DETECT_DEBUGGER_PORT", "javaagent"),
Pair("MIRRORD_IGNORE_DEBUGGER_PORTS", getTomcatServerPort())
)

MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: saving ${envVars.size} original environment variables")
val savedData = SavedConfigData(envVars.toList(), null)
savedData.envVars = envVars.toList()

MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: adding ${mirrordEnv.size} environment variables")
envVars.addAll(mirrordEnv.map { (k, v) -> EnvironmentVariable(k, v, false) })

Expand All @@ -189,68 +210,68 @@ class TomcatExecutionListener : ExecutionListener {
!envToUnset.contains(it.name)
}
}
config.setEnvironmentVariables(envVars)
config.first.setEnvironmentVariables(envVars)

if (SystemInfo.isMac) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: isMac, patching SIP.")
executionInfo.patchedPath?.let {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: patchedPath is not null: $it, meaning original was SIP")
savedData.scriptInfo = startupInfo
if (config.startupInfo.USE_DEFAULT) {
if (config.first.startupInfo.USE_DEFAULT) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: using default - handling SIP by replacing config.startupInfo")
val patchedStartupInfo = patchedScriptFromScript(startupInfo, it, scriptAndArgs?.args)
val startupInfoField = RunnerSpecificLocalConfigurationBit::class.java.getDeclaredField("myStartupInfo")
val startupInfoField =
RunnerSpecificLocalConfigurationBit::class.java.getDeclaredField("myStartupInfo")
startupInfoField.isAccessible = true
startupInfoField.set(config, patchedStartupInfo)
} else {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: NOT using default - patching by changing the non-default script")
config.startupInfo.SCRIPT = it
config.first.startupInfo.SCRIPT = it
}
}
}
savedEnvs[executorId] = savedData
}
} catch (e: Throwable) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStartScheduled: exception catched: ", e)
// Error notifications were already fired.
// We can't abort the execution here, so we let the app run without mirrord.
service.notifier.notifySimple(
"Cannot abort run due to platform limitations, running without mirrord",
NotificationType.WARNING
)
}

super.processStartScheduled(executorId, env)
}

private fun restoreConfig(executorId: String, config: RunnerSpecificLocalConfigurationBit) {
private fun restoreConfig(executorId: String, config: Pair<RunnerSpecificLocalConfigurationBit, RunConfigurationBase<*>>) {
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: $executorId $config")

val saved = savedEnvs.remove(executorId) ?: run {
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: no saved config data found")
return
}
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: found ${saved.envVars.size} saved original variables")

config.setEnvironmentVariables(saved.envVars)

if (SystemInfo.isMac) {
val scriptInfo = saved.scriptInfo ?: run {
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: no saved script info found")
return
}
val savedEnvVars = saved.envVars
if (savedEnvVars != null) {
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: found ${savedEnvVars.size} saved original variables")
config.first.setEnvironmentVariables(savedEnvVars)
}

val savedScriptInfo = saved.scriptInfo
if (SystemInfo.isMac && savedScriptInfo != null) {
MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: found saved script info")
val startupInfoField = RunnerSpecificLocalConfigurationBit::class.java.getDeclaredField("myStartupInfo")
startupInfoField.isAccessible = true
startupInfoField.set(config, scriptInfo)
startupInfoField.set(config, savedScriptInfo)
}

MirrordLogger.logger.debug("[${this.javaClass.name}] restoreConfig: removing before run task")
config.second.beforeRunTasks = config.second.beforeRunTasks.filter { it !is TomcatBeforeRunTaskProvider.TomcatBeforeRunTask }
}

override fun processTerminating(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processTerminating: $executorId $env $handler")
override fun processTerminated(
executorId: String,
env: ExecutionEnvironment,
handler: ProcessHandler,
exitCode: Int
) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processTerminated: $executorId $env $handler")

val config = getConfig(env) ?: run {
MirrordLogger.logger.debug("[${this.javaClass.name}] processTerminating: Tomcat not detected")
MirrordLogger.logger.debug("[${this.javaClass.name}] processTerminated: Tomcat not detected")
return
}

Expand All @@ -260,12 +281,15 @@ class TomcatExecutionListener : ExecutionListener {
}

override fun processNotStarted(executorId: String, env: ExecutionEnvironment) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processNotStarted (noop): $executorId $env")
super.processNotStarted(executorId, env)
}
MirrordLogger.logger.debug("[${this.javaClass.name}] processNotStarted: $executorId $env")

val config = getConfig(env) ?: run {
MirrordLogger.logger.debug("[${this.javaClass.name}] processNotStarted: Tomcat not detected")
return
}

override fun processStarted(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) {
MirrordLogger.logger.debug("[${this.javaClass.name}] processStarted (noop): $executorId $env $handler")
super.processStarted(executorId, env, handler)
restoreConfig(executorId, config)

super.processNotStarted(executorId, env)
}
}
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/mirrord-tomcat.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
<listener class="com.metalbear.mirrord.products.tomcat.TomcatExecutionListener"
topic="com.intellij.execution.ExecutionListener"/>
</projectListeners>

<extensions defaultExtensionNs="com.intellij">
<stepsBeforeRunProvider implementation="com.metalbear.mirrord.products.tomcat.TomcatBeforeRunTaskProvider"/>
</extensions>
</idea-plugin>

0 comments on commit a6e359f

Please sign in to comment.