Skip to content

Commit 60c28d3

Browse files
authored
Add ProcessUtil (#293)
1 parent ea3c7c0 commit 60c28d3

File tree

4 files changed

+139
-50
lines changed

4 files changed

+139
-50
lines changed

jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Dot.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ fun render(
4444
"-o",
4545
"$formatFile"
4646
)
47-
runProcess(cmd, 10.seconds)
48-
logger.info { "Generated ${format.uppercase()} file: ${formatFile.absolute()}" }
47+
val res = ProcessUtil.run(cmd, timeout = 30.seconds)
48+
if (res.isTimeout) {
49+
logger.error { "Rendering DOT to ${format.uppercase()} timed out" }
50+
} else {
51+
logger.info { "Generated ${format.uppercase()} file: ${formatFile.absolute()}" }
52+
}
4953
}
5054
}

jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/LoadEtsFile.kt

+19-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.jacodb.ets.utils
1818

19+
import mu.KotlinLogging
1920
import org.jacodb.ets.dto.EtsFileDto
2021
import org.jacodb.ets.dto.toEtsFile
2122
import org.jacodb.ets.model.EtsFile
@@ -26,14 +27,18 @@ import kotlin.io.path.Path
2627
import kotlin.io.path.PathWalkOption
2728
import kotlin.io.path.absolute
2829
import kotlin.io.path.createTempDirectory
30+
import kotlin.io.path.createTempFile
2931
import kotlin.io.path.exists
3032
import kotlin.io.path.extension
3133
import kotlin.io.path.inputStream
3234
import kotlin.io.path.nameWithoutExtension
3335
import kotlin.io.path.pathString
3436
import kotlin.io.path.walk
37+
import kotlin.time.Duration
3538
import kotlin.time.Duration.Companion.seconds
3639

40+
private val logger = KotlinLogging.logger {}
41+
3742
private const val ENV_VAR_ARK_ANALYZER_DIR = "ARKANALYZER_DIR"
3843
private const val DEFAULT_ARK_ANALYZER_DIR = "arkanalyzer"
3944

@@ -48,6 +53,7 @@ fun generateEtsIR(
4853
isProject: Boolean = false,
4954
loadEntrypoints: Boolean = true,
5055
useArkAnalyzerTypeInference: Int? = null,
56+
timeout: Duration? = 10.seconds,
5157
): Path {
5258
val arkAnalyzerDir = Path(System.getenv(ENV_VAR_ARK_ANALYZER_DIR) ?: DEFAULT_ARK_ANALYZER_DIR)
5359
if (!arkAnalyzerDir.exists()) {
@@ -70,9 +76,9 @@ fun generateEtsIR(
7076

7177
val node = System.getenv(ENV_VAR_NODE_EXECUTABLE) ?: DEFAULT_NODE_EXECUTABLE
7278
val output = if (isProject) {
73-
createTempDirectory(prefix = projectPath.nameWithoutExtension)
79+
createTempDirectory(projectPath.nameWithoutExtension)
7480
} else {
75-
kotlin.io.path.createTempFile(prefix = projectPath.nameWithoutExtension, suffix = ".json")
81+
createTempFile(projectPath.nameWithoutExtension, suffix = ".json")
7682
}
7783

7884
val cmd = listOfNotNull(
@@ -83,8 +89,18 @@ fun generateEtsIR(
8389
useArkAnalyzerTypeInference?.let { "-t $it" },
8490
projectPath.pathString,
8591
output.pathString,
92+
"-v",
8693
)
87-
runProcess(cmd, 10.seconds)
94+
val res = ProcessUtil.run(cmd, timeout = timeout)
95+
if (res.exitCode != 0) {
96+
logger.error { "ARKANALYZER failed with exit code ${res.exitCode}" }
97+
logger.error { "STDOUT:\n${res.stdout}" }
98+
logger.error { "STDERR:\n${res.stderr}" }
99+
} else if (res.isTimeout) {
100+
logger.error { "ARKANALYZER timed out after $timeout" }
101+
logger.error { "STDOUT:\n${res.stdout}" }
102+
logger.error { "STDERR:\n${res.stderr}" }
103+
}
88104
return output
89105
}
90106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2022 UnitTestBot contributors (utbot.org)
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jacodb.ets.utils
18+
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.launch
22+
import kotlinx.coroutines.runBlocking
23+
import mu.KotlinLogging
24+
import java.io.Reader
25+
import java.util.concurrent.TimeUnit
26+
import kotlin.time.Duration
27+
28+
private val logger = KotlinLogging.logger {}
29+
30+
object ProcessUtil {
31+
data class Result(
32+
val exitCode: Int,
33+
val stdout: String,
34+
val stderr: String,
35+
val isTimeout: Boolean, // true if the process was terminated due to timeout
36+
)
37+
38+
fun run(
39+
command: List<String>,
40+
input: String? = null,
41+
timeout: Duration? = null,
42+
): Result {
43+
val reader = input?.reader() ?: "".reader()
44+
return run(command, reader, timeout)
45+
}
46+
47+
fun run(
48+
command: List<String>,
49+
input: Reader,
50+
timeout: Duration? = null,
51+
): Result {
52+
logger.debug { "Running command: $command" }
53+
val process = ProcessBuilder(command).start()
54+
return communicate(process, input, timeout)
55+
}
56+
57+
private fun communicate(
58+
process: Process,
59+
input: Reader,
60+
timeout: Duration? = null,
61+
): Result {
62+
val stdout = StringBuilder()
63+
val stderr = StringBuilder()
64+
65+
val scope = CoroutineScope(Dispatchers.IO)
66+
67+
// Handle process input
68+
val stdinJob = scope.launch {
69+
process.outputStream.bufferedWriter().use { writer ->
70+
input.copyTo(writer)
71+
}
72+
}
73+
74+
// Launch output capture coroutines
75+
val stdoutJob = scope.launch {
76+
process.inputStream.bufferedReader().useLines { lines ->
77+
lines.forEach { stdout.appendLine(it) }
78+
}
79+
}
80+
val stderrJob = scope.launch {
81+
process.errorStream.bufferedReader().useLines { lines ->
82+
lines.forEach { stderr.appendLine(it) }
83+
}
84+
}
85+
86+
// Wait for completion
87+
val isTimeout = if (timeout != null) {
88+
!process.waitFor(timeout.inWholeNanoseconds, TimeUnit.NANOSECONDS)
89+
} else {
90+
process.waitFor()
91+
false
92+
}
93+
runBlocking {
94+
stdinJob.join()
95+
stdoutJob.join()
96+
stderrJob.join()
97+
}
98+
99+
return Result(
100+
exitCode = process.exitValue(),
101+
stdout = stdout.toString(),
102+
stderr = stderr.toString(),
103+
isTimeout = isTimeout,
104+
)
105+
}
106+
}
107+
108+
fun main() {
109+
// Note: `ls -l /bin/` has big enough output to demonstrate the necessity
110+
// of separate output capture threads/coroutines.
111+
val result = ProcessUtil.run(listOf("ls", "-l", "/bin/"))
112+
println("STDOUT: ${result.stdout}")
113+
println("STDERR: ${result.stderr}")
114+
}

jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Utils.kt

-45
Original file line numberDiff line numberDiff line change
@@ -16,53 +16,8 @@
1616

1717
package org.jacodb.ets.utils
1818

19-
import mu.KotlinLogging
2019
import org.jacodb.ets.dto.EtsFileDto
2120
import org.jacodb.ets.model.EtsFile
22-
import java.nio.file.Path
23-
import java.util.concurrent.TimeUnit
24-
import kotlin.time.Duration
25-
26-
private val logger = KotlinLogging.logger {}
27-
28-
internal fun runProcess(cmd: List<String>, timeout: Duration? = null) {
29-
logger.info { "Running: '${cmd.joinToString(" ")}'" }
30-
val process = ProcessBuilder(cmd).start()
31-
val ok = if (timeout == null) {
32-
process.waitFor()
33-
true
34-
} else {
35-
process.waitFor(timeout.inWholeNanoseconds, TimeUnit.NANOSECONDS)
36-
}
37-
38-
val stdout = process.inputStream.bufferedReader().readText().trim()
39-
if (stdout.isNotBlank()) {
40-
logger.info { "STDOUT:\n$stdout" }
41-
}
42-
val stderr = process.errorStream.bufferedReader().readText().trim()
43-
if (stderr.isNotBlank()) {
44-
logger.info { "STDERR:\n$stderr" }
45-
}
46-
47-
if (!ok) {
48-
logger.info { "Timeout!" }
49-
process.destroy()
50-
}
51-
}
52-
53-
/**
54-
* Returns the path to the sibling of this path with the given name.
55-
*
56-
* Usage:
57-
* ```
58-
* val path = Path("foo/bar.jpeg")
59-
* val sibling = path.resolveSibling { it.nameWithoutExtension + ".png" }
60-
* println(sibling) // foo/bar.png
61-
* ```
62-
*/
63-
internal fun Path.resolveSibling(name: (Path) -> String): Path {
64-
return resolveSibling(name(this))
65-
}
6621

6722
fun EtsFileDto.toText(): String {
6823
val lines: MutableList<String> = mutableListOf()

0 commit comments

Comments
 (0)