Skip to content

Commit 8047902

Browse files
Updating the ShellExecutorFactory to allow the ShellExecutor to talk to the ShellMain over a LocalSocket.
PiperOrigin-RevId: 693912332
1 parent ca8b28e commit 8047902

File tree

6 files changed

+146
-21
lines changed

6 files changed

+146
-21
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ kt_android_library(
9898
"ShellExecutorFactory.java",
9999
"ShellExecutorFileObserverImpl.kt",
100100
"ShellExecutorImpl.java",
101+
"ShellExecutorLocalSocketImpl.kt",
101102
],
102103
idl_srcs = ["Command.aidl"],
103104
visibility = [":export"],

services/shellexecutor/java/androidx/test/services/shellexecutor/ShellExecutorFactory.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ public ShellExecutorFactory(Context context, String binderKey) {
3131

3232
public ShellExecutor create() {
3333
// Binder keys for SpeakEasy are a string of hex digits. Binder keys for the FileObserver
34-
// protocol are the absolute path of the directory that the server is watching.
35-
if (binderKey.startsWith("/")) {
34+
// protocol are the absolute path of the directory that the server is watching. Binder keys for
35+
// the LocalSocket protocol start and end with a colon.
36+
if (LocalSocketProtocol.isBinderKey(binderKey)) {
37+
return new ShellExecutorLocalSocketImpl(binderKey);
38+
} else if (binderKey.startsWith("/")) {
3639
return new ShellExecutorFileObserverImpl(binderKey);
3740
} else {
3841
return new ShellExecutorImpl(context, binderKey);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (C) 2024 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 kotlin.time.Duration
21+
import kotlin.time.Duration.Companion.milliseconds
22+
23+
/** ShellExecutor that talks to LocalSocketShellMain. */
24+
class ShellExecutorLocalSocketImpl(private val binderKey: String) : ShellExecutor {
25+
26+
/** {@inheritDoc} */
27+
override fun getBinderKey() = binderKey
28+
29+
/** {@inheritDoc} */
30+
@kotlin.time.ExperimentalTime
31+
override fun executeShellCommandSync(
32+
command: String?,
33+
parameters: List<String>?,
34+
shellEnv: Map<String, String>?,
35+
executeThroughShell: Boolean,
36+
timeoutMs: Long,
37+
): String =
38+
executeShellCommand(command, parameters, shellEnv, executeThroughShell, timeoutMs).use {
39+
it.readBytes().toString(Charsets.UTF_8)
40+
}
41+
42+
/** {@inheritDoc} */
43+
@kotlin.time.ExperimentalTime
44+
override fun executeShellCommandSync(
45+
command: String?,
46+
parameters: List<String>?,
47+
shellEnv: Map<String, String>?,
48+
executeThroughShell: Boolean,
49+
) = executeShellCommandSync(command, parameters, shellEnv, executeThroughShell, TIMEOUT_FOREVER)
50+
51+
/** {@inheritDoc} */
52+
@kotlin.time.ExperimentalTime
53+
override fun executeShellCommand(
54+
command: String?,
55+
parameters: List<String>?,
56+
shellEnv: Map<String, String>?,
57+
executeThroughShell: Boolean,
58+
timeoutMs: Long,
59+
): InputStream {
60+
if (command == null || command.isEmpty()) {
61+
throw IllegalArgumentException("Null or empty command")
62+
}
63+
val client = ShellCommandLocalSocketClient(binderKey)
64+
val timeout =
65+
if (timeoutMs > 0) {
66+
timeoutMs.milliseconds
67+
} else {
68+
Duration.INFINITE
69+
}
70+
return client.request(command, parameters, shellEnv, executeThroughShell, timeout)
71+
}
72+
73+
/** {@inheritDoc} */
74+
@kotlin.time.ExperimentalTime
75+
override fun executeShellCommand(
76+
command: String?,
77+
parameters: List<String>?,
78+
shellEnv: Map<String, String>?,
79+
executeThroughShell: Boolean,
80+
) = executeShellCommand(command, parameters, shellEnv, executeThroughShell, TIMEOUT_FOREVER)
81+
82+
private companion object {
83+
const val TAG = "ShellExecutorLocalSocketImpl"
84+
const val TIMEOUT_FOREVER = -1L
85+
}
86+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ axt_android_library_test(
3636
"//runner/android_junit_runner",
3737
"//services/shellexecutor:exec_client",
3838
"//services/shellexecutor:exec_server",
39+
"//services/shellexecutor/java/androidx/test/services/shellexecutor:local_socket_protocol",
3940
"@maven//:com_google_code_findbugs_jsr305",
4041
"@maven//:com_google_guava_guava",
4142
"@maven//:com_google_truth_truth",

services/shellexecutor/javatests/androidx/test/services/shellexecutor/ShellCommandTest.java

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import androidx.test.ext.junit.rules.ActivityScenarioRule;
2525
import androidx.test.ext.junit.runners.AndroidJUnit4;
2626
import androidx.test.platform.app.InstrumentationRegistry;
27+
import java.io.ByteArrayOutputStream;
2728
import java.io.InputStream;
29+
import java.time.Duration;
2830
import java.util.Arrays;
2931
import java.util.HashMap;
3032
import java.util.List;
@@ -48,14 +50,52 @@ private static String getSecret() {
4850
private static String execShellCommand(
4951
String command, List<String> params, Map<String, String> env, boolean executeThroughShell)
5052
throws Exception {
51-
return ShellCommandClient.execOnServerSync(
52-
InstrumentationRegistry.getInstrumentation().getContext(),
53-
getSecret(),
54-
command,
55-
params,
56-
env,
57-
executeThroughShell,
58-
0L);
53+
if (LocalSocketProtocol.isBinderKey(getSecret())) {
54+
ShellCommandLocalSocketClient client = new ShellCommandLocalSocketClient(getSecret());
55+
InputStream is =
56+
client.request(command, params, env, executeThroughShell, Duration.ofSeconds(10));
57+
ByteArrayOutputStream result = new ByteArrayOutputStream();
58+
try {
59+
byte[] buffer = new byte[ShellExecSharedConstants.BUFFER_SIZE];
60+
int length;
61+
while ((length = is.read(buffer)) != -1) {
62+
result.write(buffer, 0, length);
63+
}
64+
} finally {
65+
if (is != null) {
66+
is.close();
67+
}
68+
}
69+
return result.toString("UTF-8");
70+
71+
} else {
72+
return ShellCommandClient.execOnServerSync(
73+
InstrumentationRegistry.getInstrumentation().getContext(),
74+
getSecret(),
75+
command,
76+
params,
77+
env,
78+
executeThroughShell,
79+
0L);
80+
}
81+
}
82+
83+
private static InputStream execShellCommandAsync(
84+
String command, List<String> params, Map<String, String> env, boolean executeThroughShell)
85+
throws Exception {
86+
if (LocalSocketProtocol.isBinderKey(getSecret())) {
87+
ShellCommandLocalSocketClient client = new ShellCommandLocalSocketClient(getSecret());
88+
return client.request(command, params, env, executeThroughShell, Duration.ofMinutes(2));
89+
} else {
90+
return ShellCommandClient.execOnServer(
91+
InstrumentationRegistry.getInstrumentation().getContext(),
92+
getSecret(),
93+
command,
94+
params,
95+
env,
96+
executeThroughShell,
97+
0L);
98+
}
5999
}
60100

61101
@Test
@@ -104,7 +144,7 @@ public void run() {
104144
}
105145
});
106146

107-
spinlock.run();
147+
spinlock.start();
108148
execShellCommand("setprop testing 1", null, null, true);
109149

110150
try {
@@ -123,14 +163,7 @@ public void testLargeFileDump() throws Exception {
123163
// handle. If the buffer blocks and overflows this test will timeout.
124164

125165
InputStream stream =
126-
ShellCommandClient.execOnServer(
127-
InstrumentationRegistry.getInstrumentation().getContext(),
128-
getSecret(),
129-
"dd if=/dev/urandom bs=2048 count=16384",
130-
null,
131-
null,
132-
true,
133-
0L);
166+
execShellCommandAsync("dd if=/dev/urandom bs=2048 count=16384", null, null, true);
134167

135168
boolean weReadSomething = false;
136169

services/shellexecutor/javatests/androidx/test/services/shellexecutor/ShellExecutorTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ public class ShellExecutorTest {
3636

3737
@Before
3838
public void initShellExec() {
39-
this.shellExecutor =
40-
new ShellExecutorImpl(
39+
ShellExecutorFactory factory =
40+
new ShellExecutorFactory(
4141
InstrumentationRegistry.getInstrumentation().getContext(),
4242
InstrumentationRegistry.getArguments().getString(ShellExecSharedConstants.BINDER_KEY));
43+
this.shellExecutor = factory.create();
4344
}
4445

4546
@Test

0 commit comments

Comments
 (0)