Skip to content

Commit d54ae42

Browse files
Adding the ShellCommandFileObserverClient.
PiperOrigin-RevId: 534599021
1 parent c565344 commit d54ae42

File tree

5 files changed

+294
-1
lines changed

5 files changed

+294
-1
lines changed

runner/android_test_orchestrator/stubapp/proguard_binary.cfg

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,34 @@
77
-keepnames class androidx.test.**
88

99
# for 'library class android.test.* extends or implements program class'
10-
-dontwarn android.test.**
10+
-dontwarn android.test.**
11+
12+
# ServiceLoader support
13+
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
14+
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
15+
16+
# Most of volatile fields are updated with AFU and should not be mangled
17+
-keepclassmembers class kotlinx.coroutines.** {
18+
volatile <fields>;
19+
}
20+
21+
# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater
22+
-keepclassmembers class kotlin.coroutines.SafeContinuation {
23+
volatile <fields>;
24+
}
25+
26+
# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
27+
# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
28+
-dontwarn java.lang.instrument.ClassFileTransformer
29+
-dontwarn sun.misc.SignalHandler
30+
-dontwarn java.lang.instrument.Instrumentation
31+
-dontwarn sun.misc.Signal
32+
33+
# Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`.
34+
# The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android.
35+
-dontwarn java.lang.ClassValue
36+
37+
# An annotation used for build tooling, won't be directly accessed.
38+
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
39+
40+
-dontwarn kotlinx.coroutines.internal.ClassValueCtorCache

services/shellexecutor/java/androidx/test/services/shellexecutor/BUILD

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ kt_android_library(
5555
"ClientNotConnected.java",
5656
"ShellCommand.java",
5757
"ShellCommandClient.java",
58+
"ShellCommandFileObserverClient.kt",
5859
"ShellExecSharedConstants.java",
5960
"ShellExecutor.java",
6061
"ShellExecutorFactory.java",
@@ -63,9 +64,12 @@ kt_android_library(
6364
idl_srcs = ["Command.aidl"],
6465
visibility = [":export"],
6566
deps = [
67+
":coroutine_file_observer",
68+
":file_observer_protocol",
6669
"//services/speakeasy/java/androidx/test/services/speakeasy:protocol",
6770
"//services/speakeasy/java/androidx/test/services/speakeasy/client",
6871
"//services/speakeasy/java/androidx/test/services/speakeasy/client:tool_connection",
72+
"@maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core",
6973
],
7074
)
7175

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
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+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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 androidx.test.services.shellexecutor
18+
19+
import java.io.File
20+
import java.io.InputStream
21+
import java.io.PipedInputStream
22+
import java.io.PipedOutputStream
23+
import java.util.concurrent.Executors
24+
import kotlinx.coroutines.CoroutineScope
25+
import kotlinx.coroutines.Job
26+
import kotlinx.coroutines.asCoroutineDispatcher
27+
import kotlinx.coroutines.launch
28+
import kotlinx.coroutines.runBlocking
29+
import kotlinx.coroutines.sync.Semaphore
30+
31+
/**
32+
* Client that sends requests to the ShellCommandFileObserverExecutorServer.
33+
*
34+
* This client is designed to be callable from Java.
35+
*/
36+
public final class ShellCommandFileObserverClient {
37+
private val scope = CoroutineScope(Executors.newCachedThreadPool().asCoroutineDispatcher())
38+
39+
public final fun run(secret: String, message: Messages.Command): Execution {
40+
val execution = Execution(File(secret), message)
41+
execution.start()
42+
return execution
43+
}
44+
45+
public inner class Execution
46+
internal constructor(val exchangeDir: File, val message: Messages.Command) {
47+
private val messageWritten: Semaphore = Semaphore(1, 1)
48+
private val client = Client(exchangeDir, messageWritten, message)
49+
private lateinit var clientJob: Job
50+
51+
internal fun start() {
52+
runBlocking { clientJob = scope.launch { client.run() } }
53+
}
54+
55+
/** Blocks until the message has been written. */
56+
public fun waitForMessageWritten() {
57+
runBlocking { messageWritten.acquire() }
58+
}
59+
60+
/** Standard method for obtaining the response. */
61+
public fun await(): Messages.CommandResult {
62+
runBlocking { clientJob.join() }
63+
return client.result
64+
}
65+
66+
/** Alternative method for compatibility with methods that expect only an InputStream. */
67+
public fun asStream(): InputStream {
68+
val output = PipedOutputStream()
69+
val input = PipedInputStream(output)
70+
runBlocking {
71+
scope.launch {
72+
clientJob.join()
73+
output.use { it.write(client.result.stdout) }
74+
}
75+
}
76+
return input
77+
}
78+
}
79+
80+
private inner class Client(
81+
val exchangeDir: File,
82+
val messageWritten: Semaphore,
83+
val message: Messages.Command
84+
) : CoroutineFileObserver(exchangeDir) {
85+
private lateinit var response: File
86+
public lateinit var result: Messages.CommandResult
87+
88+
init {
89+
// Uncomment this line to see the event-level chatter.
90+
// logLevel = Log.INFO
91+
logTag = "${TAG}.Client"
92+
}
93+
94+
override fun onWatching() {
95+
// Wait to write the request file until we're sure we'll see the response.
96+
response = FileObserverProtocol.writeRequestFile(exchangeDir, message)
97+
// Make sure any interested parties are notified that we've finished creating the request.
98+
runBlocking { messageWritten.release() }
99+
}
100+
101+
override suspend fun onCloseWrite(file: File) {
102+
super.onCloseWrite(file)
103+
if (file != response) return
104+
result = FileObserverProtocol.readResponseFile(response)
105+
stop()
106+
}
107+
}
108+
109+
private companion object {
110+
const val TAG = "SCFOC"
111+
}
112+
}

services/shellexecutor/javatests/androidx/test/services/shellexecutor/BUILD

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ axt_android_library_test(
6767
],
6868
)
6969

70+
axt_android_library_test(
71+
name = "ShellCommandFileObserverClientTest",
72+
srcs = [
73+
"ShellCommandFileObserverClientTest.kt",
74+
],
75+
deps = [
76+
"//runner/monitor",
77+
"//services/shellexecutor:exec_client",
78+
"//services/shellexecutor/java/androidx/test/services/shellexecutor:file_observer_protocol",
79+
"@maven//:com_google_truth_truth",
80+
"@maven//:junit_junit",
81+
],
82+
)
83+
7084
axt_android_library_test(
7185
name = "ShellExecutorTest",
7286
srcs = [
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
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+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
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 androidx.test.services.shellexecutor
18+
19+
import android.content.Context
20+
import androidx.test.platform.app.InstrumentationRegistry
21+
import com.google.common.truth.Expect
22+
import com.google.common.truth.Truth.assertThat
23+
import java.io.File
24+
import java.io.FileNotFoundException
25+
import java.io.FilenameFilter
26+
import org.junit.Before
27+
import org.junit.Rule
28+
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import org.junit.runners.JUnit4
31+
32+
@RunWith(JUnit4::class)
33+
class ShellCommandFileObserverClientTest {
34+
@get:Rule final var expect = Expect.create()
35+
val exchangeDir =
36+
InstrumentationRegistry.getInstrumentation().getContext().getDir("SCFOCT", Context.MODE_PRIVATE)
37+
lateinit var client: ShellCommandFileObserverClient
38+
39+
@Before
40+
fun setUp() {
41+
client = ShellCommandFileObserverClient()
42+
}
43+
44+
private fun getRequestFile(): File {
45+
val files =
46+
exchangeDir.listFiles(
47+
object : FilenameFilter {
48+
override fun accept(dir: File, name: String) = name.endsWith(".request")
49+
}
50+
)
51+
if (files == null) throw FileNotFoundException()
52+
return files[0]
53+
}
54+
55+
@Test
56+
fun success_await() {
57+
val execution =
58+
client.run(
59+
exchangeDir.toString(),
60+
Messages.Command(
61+
"command",
62+
listOf("parameters"),
63+
mapOf("name" to "value"),
64+
true,
65+
true,
66+
1234L
67+
)
68+
)
69+
execution.waitForMessageWritten()
70+
val requestFile = getRequestFile()
71+
val request = FileObserverProtocol.readRequestFile(requestFile)
72+
val responseFile = FileObserverProtocol.calculateResponseFile(requestFile)
73+
FileObserverProtocol.writeResponseFile(
74+
responseFile,
75+
Messages.CommandResult(
76+
Messages.ResultType.EXITED,
77+
123,
78+
"stdout".toByteArray(Charsets.UTF_8),
79+
"stderr".toByteArray(Charsets.UTF_8)
80+
)
81+
)
82+
83+
val response = execution.await()
84+
85+
expect.that(request.command).isEqualTo("command")
86+
expect.that(request.parameters).containsExactly("parameters")
87+
expect.that(request.shellEnv).containsExactlyEntriesIn(mapOf("name" to "value"))
88+
expect.that(request.executeThroughShell).isTrue()
89+
expect.that(request.redirectErrorStream).isTrue()
90+
expect.that(request.timeoutMs).isEqualTo(1234L)
91+
expect.that(response.resultType).isEqualTo(Messages.ResultType.EXITED)
92+
expect.that(response.stdout.toString(Charsets.UTF_8)).isEqualTo("stdout")
93+
expect.that(response.stderr.toString(Charsets.UTF_8)).isEqualTo("stderr")
94+
expect.that(response.exitCode).isEqualTo(123)
95+
}
96+
97+
@Test
98+
fun success_asStream() {
99+
val execution = client.run(exchangeDir.toString(), Messages.Command("command", timeoutMs = 0L))
100+
execution.waitForMessageWritten()
101+
val requestFile = getRequestFile()
102+
FileObserverProtocol.readRequestFile(requestFile)
103+
val responseFile = FileObserverProtocol.calculateResponseFile(requestFile)
104+
105+
FileObserverProtocol.writeResponseFile(
106+
responseFile,
107+
Messages.CommandResult(
108+
Messages.ResultType.EXITED,
109+
0,
110+
"foo\nbar\nbaz\n".toByteArray(Charsets.UTF_8)
111+
)
112+
)
113+
114+
val output = execution.asStream().readBytes()
115+
116+
assertThat(output.toString(Charsets.UTF_8)).isEqualTo("foo\nbar\nbaz\n")
117+
}
118+
119+
@Test
120+
fun failure_malformedResponse() {
121+
val execution = client.run(exchangeDir.toString(), Messages.Command("command", timeoutMs = 0L))
122+
execution.waitForMessageWritten()
123+
val requestFile = getRequestFile()
124+
FileObserverProtocol.readRequestFile(requestFile)
125+
val responseFile = FileObserverProtocol.calculateResponseFile(requestFile)
126+
responseFile.writeText("Potrzebie!")
127+
val response = execution.await()
128+
expect.that(response.resultType).isEqualTo(Messages.ResultType.CLIENT_ERROR)
129+
expect
130+
.that(response.stderr.toString(Charsets.UTF_8))
131+
.startsWith("java.io.StreamCorruptedException")
132+
}
133+
}

0 commit comments

Comments
 (0)