EelLocalExecApiTest: use Kotlin helper instead of python.

There might be no Python, but java is always available.

Helper is now written in Kotlin in `intellij.platform.tests.eelHelper` module.

This module is executed by test.

I couldn't use platform jline because old version doesn't play well with signals and new version comes with ffm compiled against jvm21 and can't be used in our platform.

I had to pick up versions carefully skipping ffm.

GitOrigin-RevId: e5d997f23122e7fa8b6703847cc324170fa2d382
This commit is contained in:
Ilya.Kazakevich
2024-10-05 05:24:56 +02:00
committed by intellij-monorepo-bot
parent 41c068e2c0
commit 1b76e04151
19 changed files with 444 additions and 91 deletions

38
.idea/libraries/jline_terminal_jansi.xml generated Normal file
View File

@@ -0,0 +1,38 @@
<component name="libraryTable">
<library name="jline.terminal.jansi" type="repository">
<properties maven-id="org.jline:jline-terminal-jansi:3.27.0">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jansi/3.27.0/jline-terminal-jansi-3.27.0.jar">
<sha256sum>a7b369a2e697215fed314a58b736f10ae8bdedf218096916a6bdb7bc886fe470</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/fusesource/jansi/jansi/2.4.1/jansi-2.4.1.jar">
<sha256sum>2e5e775a9dc58ffa6bbd6aa6f099d62f8b62dcdeb4c3c3bbbe5cf2301bc2dcc1</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar">
<sha256sum>2f461c75091ec3810a42184afc80c96910f54e9dd2110e9ecb9902a5ee6c244b</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar">
<sha256sum>d8bc77bc80edc7d9a02a06a069b2d7085629421db0843aba7c153a869ffe525d</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jansi/3.27.0/jline-terminal-jansi-3.27.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/fusesource/jansi/jansi/2.4.1/jansi-2.4.1.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jansi/3.27.0/jline-terminal-jansi-3.27.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/fusesource/jansi/jansi/2.4.1/jansi-2.4.1-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jansi/3.27.0/jline-terminal-jansi-3.27.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/fusesource/jansi/jansi/2.4.1/jansi-2.4.1-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-sources.jar!/" />
</SOURCES>
</library>
</component>

38
.idea/libraries/jline_terminal_jna.xml generated Normal file
View File

@@ -0,0 +1,38 @@
<component name="libraryTable">
<library name="jline.terminal.jna" type="repository">
<properties maven-id="org.jline:jline-terminal-jna:3.27.0">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.27.0/jline-terminal-jna-3.27.0.jar">
<sha256sum>e7322e6d8dc1bd69f507b91865af741af4a95c649ccab36e79e8be89696bb432</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar">
<sha256sum>a564158d28ab5127fc6a958028ed54279fe0999662c46425b6a3b09a2a52094d</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar">
<sha256sum>2f461c75091ec3810a42184afc80c96910f54e9dd2110e9ecb9902a5ee6c244b</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar">
<sha256sum>d8bc77bc80edc7d9a02a06a069b2d7085629421db0843aba7c153a869ffe525d</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.27.0/jline-terminal-jna-3.27.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.27.0/jline-terminal-jna-3.27.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.15.0/jna-5.15.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal-jna/3.27.0/jline-terminal-jna-3.27.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.15.0/jna-5.15.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-sources.jar!/" />
</SOURCES>
</library>
</component>

1
.idea/modules.xml generated
View File

@@ -647,6 +647,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/eel/intellij.platform.eel.iml" filepath="$PROJECT_DIR$/platform/eel/intellij.platform.eel.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/eelProvider/intellij.platform.eel.provider.iml" filepath="$PROJECT_DIR$/platform/eelProvider/intellij.platform.eel.provider.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/eel/intellij.platform.eel.tests.iml" filepath="$PROJECT_DIR$/platform/eel/intellij.platform.eel.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/platform-tests/eel-helper/intellij.platform.eelHelper.iml" filepath="$PROJECT_DIR$/platform/platform-tests/eel-helper/intellij.platform.eelHelper.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/execution/intellij.platform.execution.iml" filepath="$PROJECT_DIR$/platform/execution/intellij.platform.execution.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/execution.dashboard/intellij.platform.execution.dashboard.iml" filepath="$PROJECT_DIR$/platform/execution.dashboard/intellij.platform.execution.dashboard.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/execution-impl/intellij.platform.execution.impl.iml" filepath="$PROJECT_DIR$/platform/execution-impl/intellij.platform.execution.impl.iml" />

View File

@@ -639,6 +639,19 @@ object CommunityLibraryLicenses {
.newBsd("https://opensource.org/license/bsd-3-clause/")
.suppliedByOrganizations("Thai Open Source Software Center Ltd"),
LibraryLicense(null, libraryName = "jline.terminal", url = "https://github.com/jline/jline3")
.newBsd("https://github.com/jline/jline3/blob/master/LICENSE.txt")
.suppliedByPersons("Guillaume Nodet"),
LibraryLicense(null, libraryName = "jline.terminal.jansi", url = "https://github.com/jline/jline3")
.newBsd("https://github.com/jline/jline3/blob/master/LICENSE.txt")
.suppliedByPersons("Guillaume Nodet"),
LibraryLicense(null, libraryName = "jline.terminal.jna", url = "https://github.com/jline/jline3")
.newBsd("https://github.com/jline/jline3/blob/master/LICENSE.txt")
.suppliedByPersons("Guillaume Nodet"),
LibraryLicense("JNA", libraryName = "jna", url = "https://github.com/java-native-access/jna")
.apache("https://github.com/java-native-access/jna/blob/master/LICENSE"),

View File

@@ -0,0 +1,5 @@
Helper for EEL local execution test
1. prints hello into stderr
2. prints tty and its size to stdout
3. waits for command exit (exit 0) or sleep (sleep 15_000)
4. installs signal for SIGINT to return 42

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.rt" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module-library">
<library name="jline.terminal" type="repository">
<properties maven-id="org.jline:jline-terminal:3.27.0">
<verification>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar">
<sha256sum>2f461c75091ec3810a42184afc80c96910f54e9dd2110e9ecb9902a5ee6c244b</sha256sum>
</artifact>
<artifact url="file://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar">
<sha256sum>d8bc77bc80edc7d9a02a06a069b2d7085629421db0843aba7c153a869ffe525d</sha256sum>
</artifact>
</verification>
</properties>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-terminal/3.27.0/jline-terminal-3.27.0-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jline/jline-native/3.27.0/jline-native-3.27.0-sources.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="library" name="jline.terminal.jansi" level="project" />
<orderEntry type="library" name="jline.terminal.jna" level="project" />
</component>
</module>

View File

@@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: com.intellij.platform.tests.eelHelper.EelHelper

View File

@@ -0,0 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper
import org.jetbrains.annotations.TestOnly
/**
* Commands to be sent from test to this helper
*/
@TestOnly
enum class Command {
/**
* Exit with [GRACEFUL_EXIT_CODE]
*/
EXIT,
/**
* Sleep for a long time
*/
SLEEP
}

View File

@@ -0,0 +1,20 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper;
import org.jetbrains.annotations.TestOnly;
import static com.intellij.platform.tests.eelHelper.ImplKt.startHelper;
/**
* Helper that is run by EEL test: should react on signals and commands
*/
@TestOnly
public final class EelHelper {
private EelHelper() {
}
@TestOnly
public static void main(String[] args) {
startHelper();
}
}

View File

@@ -0,0 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper
data class Size(val cols:Int, val rows: Int)

View File

@@ -0,0 +1,19 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.jetbrains.annotations.TestOnly
@TestOnly
data class TTYState(val size: Size?) {
companion object {
private val mapper = ObjectMapper().registerKotlinModule()
@TestOnly
fun deserialize(str: String): TTYState = mapper.readValue(str, TTYState::class.java)
}
@TestOnly
fun serialize(): String = mapper.writeValueAsString(this)
}

View File

@@ -0,0 +1,23 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper
import org.jetbrains.annotations.TestOnly
/**
* Messages to be printed to stderr by this helper
*/
@TestOnly
const val HELLO: String = "hello"
/**
* Exit code to be used on `SIGINT`
*/
@TestOnly
const val INTERRUPT_EXIT_CODE: Int = 42
/**
* Exit code for [Command.EXIT]
*/
@TestOnly
const val GRACEFUL_EXIT_CODE: Int = 0

View File

@@ -0,0 +1,57 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.tests.eelHelper
import org.jetbrains.annotations.TestOnly
import org.jline.terminal.Terminal
import org.jline.terminal.TerminalBuilder
import sun.misc.Signal
import sun.misc.SignalHandler
import kotlin.system.exitProcess
@TestOnly
private object OnSigint : SignalHandler, Terminal.SignalHandler {
override fun handle(sig: Signal?) {
onSigInt()
}
override fun handle(signal: Terminal.Signal?) {
onSigInt()
}
private fun onSigInt() {
exitProcess(INTERRUPT_EXIT_CODE)
}
}
@TestOnly
internal fun startHelper() {
// Exit once SIGINT sent without terminal
Signal.handle(Signal("INT"), OnSigint)
val terminal = TerminalBuilder.terminal()
val terminalSize = if (terminal.type == Terminal.TYPE_DUMB) null else terminal.size
// Exit once SIGINT sent with terminal
terminal.handle(Terminal.Signal.INT, OnSigint)
// First thing to do is to print this to the stderr
System.err.print(HELLO + "\n")
System.err.flush()
val ttyState = TTYState(terminalSize?.let{ Size(cols = it.columns, rows = it.rows )})
// Then, print terminal info to the stdout
println(ttyState.serialize())
System.out.flush()
when (Command.valueOf(readln().trim().uppercase())) {
Command.EXIT -> {
exitProcess(GRACEFUL_EXIT_CODE)
}
Command.SLEEP -> {
Thread.sleep(15_000)
}
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@ApiStatus.Internal
package com.intellij.platform.tests.eelHelper;
import org.jetbrains.annotations.ApiStatus;

View File

@@ -107,5 +107,6 @@
<orderEntry type="library" scope="TEST" name="rhino" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="library" scope="TEST" name="JUnit5Pioneer" level="project" />
<orderEntry type="module" module-name="intellij.platform.eelHelper" scope="TEST" />
</component>
</module>

View File

@@ -1,64 +1,46 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.eel
import com.google.gson.Gson
import com.intellij.execution.process.UnixSignal
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.platform.eel.EelExecApi
import com.intellij.platform.eel.EelExecApi.Pty
import com.intellij.platform.eel.EelProcess
import com.intellij.platform.eel.EelResult
import com.intellij.platform.eel.impl.local.EelLocalExecApi
import com.intellij.testFramework.UsefulTestCase.IS_UNDER_TEAMCITY
import com.intellij.platform.tests.eelHelper.*
import com.intellij.platform.tests.eelHelper.Size
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.util.io.write
import io.ktor.util.decodeString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assumptions.assumeFalse
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.io.TempDir
import org.junitpioneer.jupiter.cartesian.CartesianTest
import java.nio.file.Path
import kotlin.io.path.isExecutable
import java.nio.ByteBuffer
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
@TestApplication
class EelLocalExecApiTest {
companion object {
const val PYTHON_ENV = "PYTHON"
private const val PTY_COLS = 42
private const val PTY_ROWS = 24
private val NEW_LINES = Regex("\r?\n")
// TODO: Remove as soon as we migrate to kotlin script from the python
private lateinit var executor: JavaMainClassExecutor
@BeforeAll
@JvmStatic
fun skipTestOnTcWindows() {
assumeFalse(SystemInfoRt.isWindows && IS_UNDER_TEAMCITY, "Test is disabled on TC@WIN as there is no python by default there")
}
}
private data class TtySize(val cols: Int, val rows: Int)
private data class PythonOutput(
val tty: Boolean,
val size: TtySize?,
)
private val helperContent = EelLocalExecApiTest::class.java.classLoader.getResource("helper.py")!!.readBytes()
// TODO: This tests depends on python interpreter. Rewrite to kotlin script
private val python = Path.of(System.getenv(PYTHON_ENV)
?: if (!SystemInfoRt.isWindows) "/usr/bin/python3" else error("Provide $PYTHON_ENV env var with path to python"))
@BeforeEach
fun setUp() {
assert(python.isExecutable()) {
"Can't find python or $python isn't executable. Please set $PYTHON_ENV env var to the path to python binary"
fun createExecutor() {
executor = JavaMainClassExecutor(EelHelper::class.java)
}
}
@@ -73,18 +55,15 @@ class EelLocalExecApiTest {
/**
* Test runs `helper.py` checking stdin/stdout iteration, exit code, tty and signal/termination handling.
* Test runs [EelHelper] checking stdin/stdout iteration, exit code, tty and signal/termination handling.
*/
@CartesianTest
fun testOutput(
@CartesianTest.Enum exitType: ExitType,
@CartesianTest.Enum ptyManagement: PTYManagement,
@TempDir tempDir: Path,
): Unit = timeoutRunBlocking {
val helperScript = tempDir.resolve("helper.py")
helperScript.write(helperContent)
): Unit = timeoutRunBlocking(1.minutes) {
val builder = EelExecApi.executeProcessBuilder(python.toString()).args(listOf(helperScript.toString()))
val builder = executor.createBuilderToExecuteMain()
builder.pty(when (ptyManagement) {
PTYManagement.NO_PTY -> null
PTYManagement.PTY_SIZE_FROM_START -> Pty(PTY_COLS, PTY_ROWS, true)
@@ -111,19 +90,29 @@ class EelLocalExecApiTest {
}
}
Assertions.assertEquals("hello", process.stderr.receive().decodeToString().trim())
withContext(Dispatchers.Default) {
val text = ByteBuffer.allocate(1024)
withTimeoutOrNull(10.seconds) {
for (chunk in process.stderr) {
text.put(chunk)
if (HELLO in chunk.decodeToString()) break
}
}
text.limit(text.position()).rewind()
assertThat("No $HELLO reported in stderr", text.decodeString(), CoreMatchers.containsString(HELLO))
}
// Test tty api
// tty might insert "\r\n", we need to remove them. Hence, NEW_LINES.
val pyOutputStr = process.stdout.receive().decodeToString().replace(NEW_LINES, "")
val pyOutputObj = Gson().fromJson<PythonOutput>(pyOutputStr, PythonOutput::class.java)
val outputStr = process.stdout.receive().decodeToString().replace(NEW_LINES, "")
val pyOutputObj = TTYState.deserialize(outputStr)
when (ptyManagement) {
PTYManagement.PTY_SIZE_FROM_START, PTYManagement.PTY_RESIZE_LATER -> {
Assertions.assertTrue(pyOutputObj.tty)
Assertions.assertEquals(TtySize(PTY_COLS, PTY_ROWS), pyOutputObj.size, "size must be set for tty")
Assertions.assertNotNull(pyOutputObj.size)
Assertions.assertEquals(Size(PTY_COLS, PTY_ROWS), pyOutputObj.size, "size must be set for tty")
}
PTYManagement.NO_PTY -> {
Assertions.assertFalse(pyOutputObj.tty)
Assertions.assertNull(pyOutputObj.size, "size must not be set if no tty")
}
}
@@ -135,13 +124,12 @@ class EelLocalExecApiTest {
ExitType.TERMINATE -> process.terminate()
ExitType.INTERRUPT -> {
// Terminate sleep with interrupt/CTRL+C signal
process.stdin.send("sleep\n".encodeToByteArray())
assertEquals("sleeping", process.stdout.receive().decodeToString().trim())
process.sendCommand(Command.SLEEP)
process.interrupt()
}
ExitType.EXIT_WITH_COMMAND -> {
// Just command to ask script return gracefully
process.stdin.send("exit\n".encodeToByteArray())
process.sendCommand(Command.EXIT)
}
}
val exitCode = process.exitCode.await()
@@ -161,13 +149,27 @@ class EelLocalExecApiTest {
}
}
ExitType.INTERRUPT -> {
assertEquals(42, exitCode) // CTRL+C/SIGINT handler returns 42, see script
when (ptyManagement) {
PTYManagement.NO_PTY -> Unit // SIGINT is doubtful without PTY especially without console on Windows
PTYManagement.PTY_SIZE_FROM_START, PTYManagement.PTY_RESIZE_LATER -> {
// CTRL+C/SIGINT handler returns 42, see script
assertEquals(INTERRUPT_EXIT_CODE, exitCode)
}
}
}
ExitType.EXIT_WITH_COMMAND -> {
assertEquals(0, exitCode) // Graceful exit
assertEquals(GRACEFUL_EXIT_CODE, exitCode) // Graceful exit
}
}
}
}
}
/**
* Sends [command] to the helper and flush
*/
private suspend fun EelProcess.sendCommand(command: Command) {
val text = command.name + "\n"
stdin.send(text.encodeToByteArray())
}
}

View File

@@ -0,0 +1,100 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.execution.eel
import com.intellij.application.options.PathMacrosImpl
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.util.SystemInfoRt
import com.intellij.platform.eel.EelExecApi
import com.intellij.util.SystemProperties
import org.jetbrains.jps.model.java.JpsJavaExtensionService
import org.jetbrains.jps.model.library.JpsOrderRootType
import org.jetbrains.jps.model.module.JpsModule
import org.jetbrains.jps.model.serialization.JpsSerializationManager
import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.name
import kotlin.io.path.pathString
/**
* Searches for module with [clazz] in [PathManager.ourHomePath] an executes [clazz] `main` with all dependencies
*/
internal class JavaMainClassExecutor(clazz: Class<*>) {
private val exe = Path(ProcessHandle.current().info().command().get()).toString()
private val env = mapOf("CLASSPATH" to getClassPathForClass(clazz))
private val args = listOf(clazz.canonicalName)
/**
* Execute `main` method
*/
fun createBuilderToExecuteMain(): EelExecApi.ExecuteProcessBuilder = EelExecApi.executeProcessBuilder(exe).env(env).args(args)
private companion object {
private fun getClassPathForClass(clazz: Class<*>): String {
val mavenPath = Path(SystemProperties.getUserHome()).resolve(".m2").resolve("repository").toString()
val helperModuleOutputPath = getJpsModuleOutputPathForClass(clazz)
logger.value.info("helper module path: $helperModuleOutputPath")
val helperModuleName = helperModuleOutputPath.name
val helperModule = module(helperModuleName)
var dependencies = JpsJavaExtensionService
.dependencies(helperModule)
.recursively()
val libraries = dependencies
.libraries
.flatMap { it.getPaths(JpsOrderRootType.COMPILED) }
.map { Path(it.pathString.replace('$' + PathMacrosImpl.MAVEN_REPOSITORY + '$', mavenPath)) }
val modulesOutputPath = helperModuleOutputPath.parent
val modules = dependencies
.modules
.map { module -> modulesOutputPath.resolve(module.name) }
return (modules + libraries)
.filter { path ->
path.exists().also {
if (!it) {
logger.value.info("$path doesn't exist")
}
}
}
.joinToString(File.pathSeparator)
}
private fun module(helperModuleName: String): JpsModule {
for (homePath in arrayOf(PathManager.getHomePath(), PathManager.getCommunityHomePath())) {
val jpsProject = JpsSerializationManager.getInstance().loadProject(homePath, mapOf())
val helperModule = jpsProject.modules.firstOrNull { module -> module.name == helperModuleName }
if (helperModule != null) return helperModule
logger.value.warn("$helperModuleName not found in $homePath modules. Checked: ${jpsProject.modules}")
}
throw AssertionError("Couldn't find module $helperModuleName")
}
private fun getJpsModuleOutputPathForClass(clazz: Class<*>): Path {
val classFile = clazz.name.replace(".", "/") + ".class"
val absoluteClassPath = clazz.classLoader.getResource(classFile)!!.path.let { path ->
Path(if (SystemInfoRt.isWindows) path.trimStart('/') else path)
}
logger.value.info("Looking for class file $absoluteClassPath")
val relativeClassPath = Path(classFile)
var pathToModule = absoluteClassPath
while (pathToModule.toList().size > 1) {
if (pathToModule.resolve(relativeClassPath).exists()) break
pathToModule = pathToModule.parent
}
return pathToModule
}
private val logger = lazy { fileLogger() }
}
}

View File

@@ -1,42 +0,0 @@
# Script for EEL local execution test
# 1.prints tty and its size
# 2.waits for command exit (exit 0) or sleep (sleep 10_000)
# 3. installs signal for SIGINT to return 42
import os
import signal
import sys
import json
from time import sleep
def exit_42(*_):
exit(42)
signal.signal(signal.SIGINT, exit_42)
is_tty = sys.stdin.isatty()
terminal_size = None
try:
terminal_size = os.get_terminal_size()
except OSError:
pass
sys.stderr.write("hello\n")
sys.stderr.flush()
json.dump({
"tty": is_tty,
"size": {"cols": terminal_size.columns, "rows":terminal_size.lines} if terminal_size else None
}, sys.stdout)
sys.stdout.flush()
command = input().strip()
if command == "exit":
exit(0)
elif command == "sleep":
print("sleeping")
sys.stdout.flush()
sleep(10_000)