Skip to content

Commit 9b982db

Browse files
Adding the FileObserverProtocol and the Messages it transmits.
PiperOrigin-RevId: 533572400
1 parent df62a91 commit 9b982db

File tree

3 files changed

+301
-4
lines changed

3 files changed

+301
-4
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
load("@build_bazel_rules_android//android:rules.bzl", "android_library")
1+
load("//build_extensions:kt_android_library.bzl", "kt_android_library")
22

33
# A shell command execution server to allow shell commands to be run at elevated permissions
44

55
package(default_applicable_licenses = ["//services:license"])
66

77
licenses(["notice"])
88

9-
android_library(
9+
kt_android_library(
1010
name = "coroutine_file_observer",
1111
srcs = [
1212
"CoroutineFileObserver.kt",
@@ -17,7 +17,18 @@ android_library(
1717
],
1818
)
1919

20-
android_library(
20+
kt_android_library(
21+
name = "file_observer_protocol",
22+
srcs = [
23+
"FileObserverProtocol.kt",
24+
"Messages.kt",
25+
],
26+
visibility = [
27+
"//services/shellexecutor/javatests/androidx/test/services/shellexecutor:__pkg__",
28+
],
29+
)
30+
31+
kt_android_library(
2132
name = "exec_server",
2233
srcs = [
2334
"BlockingPublish.java",
@@ -37,7 +48,7 @@ android_library(
3748
],
3849
)
3950

40-
android_library(
51+
kt_android_library(
4152
name = "exec_client",
4253
srcs = [
4354
"BlockingFind.java",
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.ByteArrayOutputStream
20+
import java.io.File
21+
import java.io.IOException
22+
import java.io.OutputStreamWriter
23+
import java.io.PrintWriter
24+
import java.util.UUID
25+
26+
/**
27+
* The protocol for communicating by FileObserver is:
28+
* 1. The server creates the server directory in /data/local/tmp.
29+
* 2. The client creates [UUID].request in the server directory.
30+
* 3. The server reads and deletes [UUID].request, then writes [UUID].response.
31+
* 4. The client reads and deletes [UUID].response.
32+
*
33+
* The underlying communication is handled by inotify, which only generates events for the
34+
* directories it is explicitly watching. (The FileObserver documentation makes it sound like it can
35+
* pick things up in subdirectories; this is erroneous.)
36+
*
37+
* The underlying directory and file are set world-readable and -writable so the client can write
38+
* the request and read the response. Because this only works when someone is already running
39+
* FileObserverShellMain, there is very little threat here; if someone is able to put a program onto
40+
* your test device that can watch /data/local/tmp for the appearance of the exchange directory, you
41+
* have bigger problems than whatever it's going to do with root privileges.
42+
*/
43+
@Suppress("SetWorldReadable", "SetWorldWritable")
44+
object FileObserverProtocol {
45+
const val REQUEST = "request"
46+
const val RESPONSE = "response"
47+
48+
/** Creates the exchange directory with appropriate permissions. */
49+
fun createExchangeDir(commonDir: File): File {
50+
val exchangeDir = File.createTempFile("androidx", ".tmp", commonDir)
51+
exchangeDir.delete()
52+
exchangeDir.mkdir()
53+
exchangeDir.setReadable(/* readable= */ true, /* ownerOnly= */ false)
54+
exchangeDir.setWritable(/* writable= */ true, /* ownerOnly= */ false)
55+
exchangeDir.setExecutable(/* executable= */ true, /* ownerOnly= */ false)
56+
return exchangeDir
57+
}
58+
59+
/**
60+
* Writes a request file to the exchange directory.
61+
*
62+
* @return the location for the response file
63+
*/
64+
fun writeRequestFile(exchangeDir: File, message: Messages.Command): File {
65+
val stem = UUID.randomUUID().toString()
66+
val request = File(exchangeDir, "${stem}.$REQUEST")
67+
request.outputStream().use {
68+
request.setReadable(/* readable= */ true, /* ownerOnly= */ false)
69+
request.setWritable(/* writable= */ true, /* ownerOnly= */ false)
70+
message.writeTo(it)
71+
}
72+
return File(exchangeDir, "${stem}.response")
73+
}
74+
75+
fun isRequestFile(file: File) = file.name.endsWith(".$REQUEST")
76+
77+
fun calculateResponseFile(requestFile: File) =
78+
File(requestFile.parentFile, "${requestFile.name.split(".").first()}.$RESPONSE")
79+
80+
/** Reads and deletes the request file */
81+
fun readRequestFile(request: File): Messages.Command {
82+
val command: Messages.Command
83+
request.inputStream().use { command = Messages.Command.readFrom(it) }
84+
request.delete()
85+
return command
86+
}
87+
88+
/** Writes the response file */
89+
fun writeResponseFile(path: File, result: Messages.CommandResult) {
90+
path.outputStream().use {
91+
path.setReadable(/* readable= */ true, /* ownerOnly= */ false)
92+
path.setWritable(/* writable= */ true, /* ownerOnly= */ false)
93+
result.writeTo(it)
94+
}
95+
}
96+
97+
/** Reads and deletes the response file. */
98+
fun readResponseFile(response: File): Messages.CommandResult {
99+
try {
100+
val result: Messages.CommandResult
101+
response.inputStream().use { result = Messages.CommandResult.readFrom(it) }
102+
response.delete()
103+
return result
104+
} catch (x: IOException) {
105+
return Messages.CommandResult(
106+
resultType = Messages.ResultType.CLIENT_ERROR,
107+
stderr = x.toByteArray()
108+
)
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Writes an exception stack trace to a ByteArray as UTF-8, to make them easy to pass through
115+
* Messages.CommandResult.
116+
*/
117+
internal fun Exception.toByteArray(): ByteArray {
118+
val bos = ByteArrayOutputStream()
119+
val pw = PrintWriter(OutputStreamWriter(bos, Charsets.UTF_8))
120+
printStackTrace(pw)
121+
pw.close()
122+
return bos.toByteArray()
123+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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.InputStream
20+
import java.io.ObjectInputStream
21+
import java.io.ObjectOutputStream
22+
import java.io.OutputStream
23+
24+
/**
25+
* Messages used for communication between the ShellCommandFileObserverClient and the
26+
* ShellCommandFileObserverExecutorServer.
27+
*
28+
* These are not protobufs because on APIs 15-19, desugaring causes the FileObserverShellMain to
29+
* crash with ClassNotFound: j$.util.concurrent.ConcurrentHashMap, and GeneratedMessageLite needs
30+
* that class. The fix for this (starting multidex earlier) is not possible in a class with no
31+
* Context.
32+
*
33+
* They aren't PersistableBundles because those get introduced in API 21. So we resort to the
34+
* ObjectInputStream/ObjectOutputStream, working around the BanSerializableRead.
35+
*/
36+
object Messages {
37+
data class Command(
38+
val command: String,
39+
val parameters: List<String> = emptyList(),
40+
val shellEnv: Map<String, String> = emptyMap(),
41+
val executeThroughShell: Boolean = false,
42+
val redirectErrorStream: Boolean = false,
43+
val timeoutMs: Long,
44+
) {
45+
fun writeTo(outputStream: OutputStream) {
46+
ObjectOutputStream(outputStream).use {
47+
it.apply {
48+
writeUTF(command)
49+
write(parameters)
50+
write(shellEnv)
51+
writeBoolean(executeThroughShell)
52+
writeBoolean(redirectErrorStream)
53+
writeLong(timeoutMs)
54+
}
55+
}
56+
}
57+
58+
override fun toString(): String {
59+
val env = shellEnv.asSequence().map { "${it.key}=${it.value}" }.joinToString(", ")
60+
val ets = if (executeThroughShell) " executeThroughShell" else ""
61+
val res = if (redirectErrorStream) " redirectErrorStream" else ""
62+
return "[$command] [${parameters.joinToString("] [")}] ($env)$ets$res ${timeoutMs}ms"
63+
}
64+
65+
companion object {
66+
fun readFrom(inputStream: InputStream): Command {
67+
ObjectInputStream(inputStream).use {
68+
it.apply {
69+
return Command(
70+
command = readUTF(),
71+
parameters = readStringList(),
72+
shellEnv = readStringMap(),
73+
executeThroughShell = readBoolean(),
74+
redirectErrorStream = readBoolean(),
75+
timeoutMs = readLong()
76+
)
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
enum class ResultType {
84+
EXITED,
85+
TIMED_OUT,
86+
SERVER_ERROR,
87+
CLIENT_ERROR
88+
}
89+
90+
data class CommandResult(
91+
val resultType: ResultType,
92+
val exitCode: Int = -1,
93+
val stdout: ByteArray = ByteArray(0),
94+
val stderr: ByteArray = ByteArray(0),
95+
) {
96+
fun writeTo(outputStream: OutputStream) {
97+
ObjectOutputStream(outputStream).use {
98+
it.apply {
99+
write(resultType)
100+
writeInt(exitCode)
101+
writeInt(stdout.size)
102+
write(stdout)
103+
writeInt(stderr.size)
104+
write(stderr)
105+
}
106+
}
107+
}
108+
109+
override fun toString(): String {
110+
val out = stdout.toString(Charsets.UTF_8)
111+
val err = stderr.toString(Charsets.UTF_8)
112+
return "${resultType.name} $exitCode stdout=[$out] stderr=[$err]"
113+
}
114+
115+
companion object {
116+
fun readFrom(inputStream: InputStream): CommandResult {
117+
ObjectInputStream(inputStream).use {
118+
it.apply {
119+
return CommandResult(
120+
resultType = readResultType(),
121+
exitCode = readInt(),
122+
stdout = ByteArray(readInt()).also { readFully(it) },
123+
stderr = ByteArray(readInt()).also { readFully(it) }
124+
)
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
private fun ObjectOutputStream.write(resultType: ResultType) {
132+
writeUTF(resultType.name)
133+
}
134+
135+
private fun ObjectInputStream.readResultType() = ResultType.valueOf(readUTF())
136+
137+
private fun ObjectOutputStream.write(list: List<String>) {
138+
writeInt(list.size)
139+
for (s in list) writeUTF(s)
140+
}
141+
142+
private fun ObjectInputStream.readStringList(): List<String> {
143+
val count = readInt()
144+
val result = mutableListOf<String>()
145+
for (i in 1..count) result.add(readUTF())
146+
return result
147+
}
148+
149+
private fun ObjectOutputStream.write(map: Map<String, String>) {
150+
writeInt(map.size)
151+
for (entry in map) {
152+
writeUTF(entry.key)
153+
writeUTF(entry.value)
154+
}
155+
}
156+
157+
private fun ObjectInputStream.readStringMap(): Map<String, String> {
158+
val count = readInt()
159+
val result = mutableMapOf<String, String>()
160+
for (i in 1..count) result.put(readUTF(), readUTF())
161+
return result
162+
}
163+
}

0 commit comments

Comments
 (0)