Skip to content

Commit 3091479

Browse files
Adding the ShellCommandFileObserverExecutorServer.
PiperOrigin-RevId: 534888794
1 parent d54ae42 commit 3091479

File tree

6 files changed

+443
-2
lines changed

6 files changed

+443
-2
lines changed

services/proguard_library.cfg

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,34 @@
2020
-dontwarn kotlin.annotation.Retention
2121
-dontwarn kotlin.annotation.Target
2222
-dontwarn kotlin.Deprecated
23-
-dontwarn kotlin.ReplaceWith
23+
-dontwarn kotlin.ReplaceWith
24+
25+
# ServiceLoader support
26+
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
27+
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
28+
29+
# Most of volatile fields are updated with AFU and should not be mangled
30+
-keepclassmembers class kotlinx.coroutines.** {
31+
volatile <fields>;
32+
}
33+
34+
# Same story for the standard library's SafeContinuation that also uses AtomicReferenceFieldUpdater
35+
-keepclassmembers class kotlin.coroutines.SafeContinuation {
36+
volatile <fields>;
37+
}
38+
39+
# These classes are only required by kotlinx.coroutines.debug.AgentPremain, which is only loaded when
40+
# kotlinx-coroutines-core is used as a Java agent, so these are not needed in contexts where ProGuard is used.
41+
-dontwarn java.lang.instrument.ClassFileTransformer
42+
-dontwarn sun.misc.SignalHandler
43+
-dontwarn java.lang.instrument.Instrumentation
44+
-dontwarn sun.misc.Signal
45+
46+
# Only used in `kotlinx.coroutines.internal.ExceptionsConstructor`.
47+
# The case when it is not available is hidden in a `try`-`catch`, as well as a check for Android.
48+
-dontwarn java.lang.ClassValue
49+
50+
# An annotation used for build tooling, won't be directly accessed.
51+
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
52+
53+
-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
@@ -35,16 +35,20 @@ kt_android_library(
3535
"ShellCommand.java",
3636
"ShellCommandExecutor.java",
3737
"ShellCommandExecutorServer.java",
38+
"ShellCommandFileObserverExecutorServer.kt",
3839
"ShellExecSharedConstants.java",
3940
"ShellMain.java",
4041
],
4142
idl_srcs = ["Command.aidl"],
4243
visibility = [":export"],
4344
deps = [
45+
":coroutine_file_observer",
46+
":file_observer_protocol",
4447
"//services/speakeasy/java/androidx/test/services/speakeasy:protocol",
4548
"//services/speakeasy/java/androidx/test/services/speakeasy/client",
4649
"//services/speakeasy/java/androidx/test/services/speakeasy/client:tool_connection",
4750
"@maven//:com_google_guava_guava",
51+
"@maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core",
4852
],
4953
)
5054

services/shellexecutor/java/androidx/test/services/shellexecutor/FileObserverProtocol.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ object FileObserverProtocol {
114114
* Writes an exception stack trace to a ByteArray as UTF-8, to make them easy to pass through
115115
* Messages.CommandResult.
116116
*/
117-
internal fun Exception.toByteArray(): ByteArray {
117+
public fun Exception.toByteArray(): ByteArray {
118118
val bos = ByteArrayOutputStream()
119119
val pw = PrintWriter(OutputStreamWriter(bos, Charsets.UTF_8))
120120
printStackTrace(pw)
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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.os.Build
20+
import android.util.Log
21+
import java.io.File
22+
import java.io.IOException
23+
import java.io.InputStream
24+
import java.util.concurrent.Executors
25+
import java.util.concurrent.TimeUnit
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Job
28+
import kotlinx.coroutines.TimeoutCancellationException
29+
import kotlinx.coroutines.asCoroutineDispatcher
30+
import kotlinx.coroutines.async
31+
import kotlinx.coroutines.coroutineScope
32+
import kotlinx.coroutines.launch
33+
import kotlinx.coroutines.runBlocking
34+
import kotlinx.coroutines.sync.Semaphore
35+
import kotlinx.coroutines.withTimeout
36+
37+
/**
38+
* Server that handles requests from the ShellCommandFileObserverClient.
39+
*
40+
* This server should be easily callable from Java.
41+
*
42+
* On API 28, System.getProperty("java.io.tmpdir") returns "/tmp", which does not exist on a
43+
* standard emulator! /data/local/tmp seems to be a reliable location.
44+
*/
45+
final class ShellCommandFileObserverExecutorServer
46+
@JvmOverloads
47+
constructor(
48+
private val commonDir: File = File("/data/local/tmp"),
49+
private val scope: CoroutineScope =
50+
CoroutineScope(Executors.newCachedThreadPool().asCoroutineDispatcher())
51+
) {
52+
@JvmField val exchangeDir: File
53+
54+
private lateinit var server: Server
55+
private lateinit var serverJob: Job
56+
57+
init {
58+
exchangeDir = FileObserverProtocol.createExchangeDir(commonDir)
59+
}
60+
61+
fun start() {
62+
server = Server(exchangeDir)
63+
runBlocking {
64+
serverJob = scope.launch { server.run() }
65+
server.waitForReady()
66+
}
67+
}
68+
69+
fun stop() {
70+
server.stop()
71+
runBlocking { serverJob.join() }
72+
try {
73+
exchangeDir.delete()
74+
} catch (x: IOException) {
75+
Log.e(TAG, "Couldn't delete $exchangeDir", x)
76+
}
77+
}
78+
79+
private final inner class Server(directory: File) : CoroutineFileObserver(directory) {
80+
81+
private val ready: Semaphore = Semaphore(1, 1)
82+
83+
init {
84+
// Uncomment this line to see the event-level chatter.
85+
// logLevel = Log.INFO
86+
logTag = "${TAG}.Server"
87+
}
88+
89+
override fun onWatching() {
90+
ready.release()
91+
}
92+
93+
suspend fun waitForReady() {
94+
ready.acquire()
95+
}
96+
97+
override suspend fun onCloseWrite(file: File) {
98+
super.onCloseWrite(file)
99+
if (!FileObserverProtocol.isRequestFile(file)) return
100+
if (file.isDirectory()) {
101+
Log.w(logTag, "$file is a directory")
102+
return
103+
}
104+
if (!file.canRead()) {
105+
Log.w(logTag, "$file cannot be read")
106+
return
107+
}
108+
coroutineScope { launch { handleCommand(file) } }
109+
}
110+
111+
suspend fun handleCommand(request: File) {
112+
val response = FileObserverProtocol.calculateResponseFile(request)
113+
val command: Messages.Command
114+
try {
115+
command = FileObserverProtocol.readRequestFile(request)
116+
} catch (x: IOException) {
117+
Log.e(logTag, "Couldn't parse command in $request", x)
118+
FileObserverProtocol.writeResponseFile(
119+
response,
120+
Messages.CommandResult(
121+
resultType = Messages.ResultType.SERVER_ERROR,
122+
stderr = x.toByteArray()
123+
)
124+
)
125+
return
126+
}
127+
128+
val process: Process
129+
try {
130+
val argv = mutableListOf<String>()
131+
if (command.executeThroughShell) argv.addAll(listOf("sh", "-c"))
132+
argv.add(command.command)
133+
argv.addAll(command.parameters)
134+
135+
val pb = ProcessBuilder(argv)
136+
pb.environment().putAll(command.shellEnv)
137+
pb.redirectErrorStream(command.redirectErrorStream)
138+
139+
process = pb.start()
140+
process.outputStream.close()
141+
142+
val exitCode =
143+
process.onTimeout(command.timeoutMs) {
144+
// The input streams are not yet closed, so don't try reading to EOF. Instead, read all
145+
// available bytes. (Calling process.destroy() first causes InputStream.available()
146+
// to throw "java.io.IOException: Stream closed", so doing the read after the destroy
147+
// won't work.)
148+
FileObserverProtocol.writeResponseFile(
149+
response,
150+
Messages.CommandResult(
151+
resultType = Messages.ResultType.TIMED_OUT,
152+
stdout = process.inputStream.availableToByteArray(),
153+
stderr =
154+
if (!command.redirectErrorStream) {
155+
process.errorStream.availableToByteArray()
156+
} else {
157+
ByteArray(0)
158+
}
159+
)
160+
)
161+
}
162+
163+
if (exitCode < 0) return // timed out
164+
165+
FileObserverProtocol.writeResponseFile(
166+
response,
167+
Messages.CommandResult(
168+
resultType = Messages.ResultType.EXITED,
169+
exitCode,
170+
stdout = process.inputStream.readBytes(),
171+
stderr =
172+
if (!command.redirectErrorStream) process.errorStream.readBytes() else ByteArray(0)
173+
)
174+
)
175+
} catch (x: Exception) {
176+
FileObserverProtocol.writeResponseFile(
177+
response,
178+
Messages.CommandResult(
179+
resultType = Messages.ResultType.SERVER_ERROR,
180+
stderr = x.toByteArray()
181+
)
182+
)
183+
}
184+
}
185+
}
186+
187+
/** Hide API differences in handling timeouts. */
188+
private fun Process.onTimeout(timeout: Long, onTimeout: () -> Unit): Int {
189+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
190+
if (!waitFor(timeout, TimeUnit.MILLISECONDS)) {
191+
onTimeout.invoke()
192+
destroy()
193+
return -1
194+
}
195+
return exitValue()
196+
} else {
197+
var exitCode: Int = -1
198+
runBlocking {
199+
try {
200+
val job = scope.async { waitFor() }
201+
withTimeout(timeout) { exitCode = job.await() }
202+
} catch (e: TimeoutCancellationException) {
203+
onTimeout.invoke()
204+
destroy()
205+
}
206+
}
207+
return exitCode
208+
}
209+
}
210+
211+
/** Use this instead of ByteString.readFrom when the stream has not yet been closed. */
212+
private fun InputStream.availableToByteArray(): ByteArray {
213+
val expected = available()
214+
if (expected == 0) return ByteArray(0)
215+
val bytes = ByteArray(expected)
216+
val amount = read(bytes)
217+
return bytes.sliceArray(0..amount - 1)
218+
}
219+
220+
private companion object {
221+
const val TAG = "SCFOES"
222+
}
223+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ axt_android_library_test(
8181
],
8282
)
8383

84+
axt_android_library_test(
85+
name = "ShellCommandFileObserverExecutorServerTest",
86+
srcs = [
87+
"ShellCommandFileObserverExecutorServerTest.kt",
88+
],
89+
deps = [
90+
"//runner/monitor",
91+
"//services/shellexecutor:exec_server",
92+
"//services/shellexecutor/java/androidx/test/services/shellexecutor:file_observer_protocol",
93+
"@maven//:com_google_truth_truth",
94+
"@maven//:junit_junit",
95+
],
96+
)
97+
8498
axt_android_library_test(
8599
name = "ShellExecutorTest",
86100
srcs = [

0 commit comments

Comments
 (0)