Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.nio.file.InvalidPathException
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

Expand All @@ -44,24 +47,43 @@ class IdeCommandServiceImpl internal constructor(
when (spec) {
// Through the tooling server, never ./gradlew: a second daemon doubles Gradle's memory
// on the device, and its output would never reach the Build Output pane.
is CommandSpec.GradleTask -> GradleTaskExecution(executionId, spec, gradleTaskRunner, timeoutMs)
is CommandSpec.GradleTask -> {
GradleTaskExecution(executionId, spec, gradleTaskRunner, timeoutMs)
}

is CommandSpec.ShellCommand -> CommandExecutionImpl(executionId, shellProcess(spec), timeoutMs)
is CommandSpec.ShellCommand -> {
val workDir =
try {
resolvePluginWorkingDirectory(pluginId, projectRootProvider(), spec.workingDirectory)
} catch (e: InvalidPathException) {
return RejectedCommandExecution(executionId, "Invalid working directory: ${e.message}")
} catch (e: IOException) {
return RejectedCommandExecution(executionId, "Invalid working directory: ${e.message}")
}
val processBuilder =
try {
shellProcess(spec, workDir)
} catch (e: IllegalArgumentException) {
return RejectedCommandExecution(executionId, "Invalid environment: ${e.message}")
}
CommandExecutionImpl(executionId, processBuilder, timeoutMs)
}
}
runningCommands[executionId] = execution
execution.start { runningCommands.remove(executionId) }
return execution
}

private fun shellProcess(spec: CommandSpec.ShellCommand): ProcessBuilder {
val workDir = resolvePluginWorkingDirectory(pluginId, projectRootProvider(), spec.workingDirectory)
return ProcessBuilder(listOf(spec.executable) + spec.arguments).apply {
private fun shellProcess(
spec: CommandSpec.ShellCommand,
workDir: File?,
): ProcessBuilder =
ProcessBuilder(listOf(spec.executable) + spec.arguments).apply {
workDir?.let { directory(it) }
environment().putAll(spec.environment)
redirectErrorStream(false)
TermuxProcessEnvironment.applyTo(environment(), appFilesDir)
}
}

override fun isCommandRunning(executionId: String): Boolean = runningCommands[executionId]?.isRunning() == true

Expand Down Expand Up @@ -224,3 +246,16 @@ private class CommandExecutionImpl(
private const val MAX_OUTPUT_BYTES = 10 * 1024 * 1024
}
}

private class RejectedCommandExecution(
override val executionId: String,
error: String,
) : CommandExecution {
private val result = CommandResult.Failure(-1, "", "", error, 0)

override val output: Flow<CommandOutput> = emptyFlow()

override suspend fun await(): CommandResult = result

override fun cancel() = Unit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.itsaky.androidide.plugins.manager.services

import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.plugins.PluginPermission
import com.itsaky.androidide.plugins.extensions.CommandResult
import com.itsaky.androidide.plugins.extensions.CommandSpec
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

class IdeCommandServiceImplTest {
@get:Rule
val tmp = TemporaryFolder()

private fun resultFor(workingDirectory: String): CommandResult =
resultFor(CommandSpec.ShellCommand("true", workingDirectory = workingDirectory))

private fun resultFor(spec: CommandSpec): CommandResult {
val projectRoot = tmp.newFolder("project")
val service =
IdeCommandServiceImpl(
pluginId = "test.plugin",
permissions = setOf(PluginPermission.SYSTEM_COMMANDS),
projectRootProvider = { projectRoot },
appFilesDir = tmp.newFolder("files"),
)
val execution = service.executeCommand(spec, 5_000)
return runBlocking { execution.await() }
}

@Test
fun `a working directory with a NUL character fails the command`() {
val result = resultFor("sub\u0000dir")

assertThat(result).isInstanceOf(CommandResult.Failure::class.java)
assertThat((result as CommandResult.Failure).error).startsWith("Invalid working directory")
}

@Test
fun `a working directory too long to resolve fails the command`() {
val result = resultFor("a/".repeat(3000))

assertThat(result).isInstanceOf(CommandResult.Failure::class.java)
assertThat((result as CommandResult.Failure).error).startsWith("Invalid working directory")
}

@Test
fun `an environment variable with a NUL character fails the command`() {
val result = resultFor(CommandSpec.ShellCommand("true", environment = mapOf("KEY" to "a\u0000b")))

assertThat(result).isInstanceOf(CommandResult.Failure::class.java)
assertThat((result as CommandResult.Failure).error).startsWith("Invalid environment")
}
}
Loading