Skip to content

Commit

Permalink
Updating the ShellExecutorFactory to allow the ShellExecutor to talk …
Browse files Browse the repository at this point in the history
…to the ShellMain over a LocalSocket.

PiperOrigin-RevId: 693912332
  • Loading branch information
copybara-androidxtest committed Nov 7, 2024
1 parent ca8b28e commit 8047902
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ kt_android_library(
"ShellExecutorFactory.java",
"ShellExecutorFileObserverImpl.kt",
"ShellExecutorImpl.java",
"ShellExecutorLocalSocketImpl.kt",
],
idl_srcs = ["Command.aidl"],
visibility = [":export"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ public ShellExecutorFactory(Context context, String binderKey) {

public ShellExecutor create() {
// Binder keys for SpeakEasy are a string of hex digits. Binder keys for the FileObserver
// protocol are the absolute path of the directory that the server is watching.
if (binderKey.startsWith("/")) {
// protocol are the absolute path of the directory that the server is watching. Binder keys for
// the LocalSocket protocol start and end with a colon.
if (LocalSocketProtocol.isBinderKey(binderKey)) {
return new ShellExecutorLocalSocketImpl(binderKey);
} else if (binderKey.startsWith("/")) {
return new ShellExecutorFileObserverImpl(binderKey);
} else {
return new ShellExecutorImpl(context, binderKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.test.services.shellexecutor

import java.io.InputStream
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/** ShellExecutor that talks to LocalSocketShellMain. */
class ShellExecutorLocalSocketImpl(private val binderKey: String) : ShellExecutor {

/** {@inheritDoc} */
override fun getBinderKey() = binderKey

/** {@inheritDoc} */
@kotlin.time.ExperimentalTime
override fun executeShellCommandSync(
command: String?,
parameters: List<String>?,
shellEnv: Map<String, String>?,
executeThroughShell: Boolean,
timeoutMs: Long,
): String =
executeShellCommand(command, parameters, shellEnv, executeThroughShell, timeoutMs).use {
it.readBytes().toString(Charsets.UTF_8)
}

/** {@inheritDoc} */
@kotlin.time.ExperimentalTime
override fun executeShellCommandSync(
command: String?,
parameters: List<String>?,
shellEnv: Map<String, String>?,
executeThroughShell: Boolean,
) = executeShellCommandSync(command, parameters, shellEnv, executeThroughShell, TIMEOUT_FOREVER)

/** {@inheritDoc} */
@kotlin.time.ExperimentalTime
override fun executeShellCommand(
command: String?,
parameters: List<String>?,
shellEnv: Map<String, String>?,
executeThroughShell: Boolean,
timeoutMs: Long,
): InputStream {
if (command == null || command.isEmpty()) {
throw IllegalArgumentException("Null or empty command")
}
val client = ShellCommandLocalSocketClient(binderKey)
val timeout =
if (timeoutMs > 0) {
timeoutMs.milliseconds
} else {
Duration.INFINITE
}
return client.request(command, parameters, shellEnv, executeThroughShell, timeout)
}

/** {@inheritDoc} */
@kotlin.time.ExperimentalTime
override fun executeShellCommand(
command: String?,
parameters: List<String>?,
shellEnv: Map<String, String>?,
executeThroughShell: Boolean,
) = executeShellCommand(command, parameters, shellEnv, executeThroughShell, TIMEOUT_FOREVER)

private companion object {
const val TAG = "ShellExecutorLocalSocketImpl"
const val TIMEOUT_FOREVER = -1L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ axt_android_library_test(
"//runner/android_junit_runner",
"//services/shellexecutor:exec_client",
"//services/shellexecutor:exec_server",
"//services/shellexecutor/java/androidx/test/services/shellexecutor:local_socket_protocol",
"@maven//:com_google_code_findbugs_jsr305",
"@maven//:com_google_guava_guava",
"@maven//:com_google_truth_truth",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
Expand All @@ -48,14 +50,52 @@ private static String getSecret() {
private static String execShellCommand(
String command, List<String> params, Map<String, String> env, boolean executeThroughShell)
throws Exception {
return ShellCommandClient.execOnServerSync(
InstrumentationRegistry.getInstrumentation().getContext(),
getSecret(),
command,
params,
env,
executeThroughShell,
0L);
if (LocalSocketProtocol.isBinderKey(getSecret())) {
ShellCommandLocalSocketClient client = new ShellCommandLocalSocketClient(getSecret());
InputStream is =
client.request(command, params, env, executeThroughShell, Duration.ofSeconds(10));
ByteArrayOutputStream result = new ByteArrayOutputStream();
try {
byte[] buffer = new byte[ShellExecSharedConstants.BUFFER_SIZE];
int length;
while ((length = is.read(buffer)) != -1) {
result.write(buffer, 0, length);
}
} finally {
if (is != null) {
is.close();
}
}
return result.toString("UTF-8");

} else {
return ShellCommandClient.execOnServerSync(
InstrumentationRegistry.getInstrumentation().getContext(),
getSecret(),
command,
params,
env,
executeThroughShell,
0L);
}
}

private static InputStream execShellCommandAsync(
String command, List<String> params, Map<String, String> env, boolean executeThroughShell)
throws Exception {
if (LocalSocketProtocol.isBinderKey(getSecret())) {
ShellCommandLocalSocketClient client = new ShellCommandLocalSocketClient(getSecret());
return client.request(command, params, env, executeThroughShell, Duration.ofMinutes(2));
} else {
return ShellCommandClient.execOnServer(
InstrumentationRegistry.getInstrumentation().getContext(),
getSecret(),
command,
params,
env,
executeThroughShell,
0L);
}
}

@Test
Expand Down Expand Up @@ -104,7 +144,7 @@ public void run() {
}
});

spinlock.run();
spinlock.start();
execShellCommand("setprop testing 1", null, null, true);

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

InputStream stream =
ShellCommandClient.execOnServer(
InstrumentationRegistry.getInstrumentation().getContext(),
getSecret(),
"dd if=/dev/urandom bs=2048 count=16384",
null,
null,
true,
0L);
execShellCommandAsync("dd if=/dev/urandom bs=2048 count=16384", null, null, true);

boolean weReadSomething = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ public class ShellExecutorTest {

@Before
public void initShellExec() {
this.shellExecutor =
new ShellExecutorImpl(
ShellExecutorFactory factory =
new ShellExecutorFactory(
InstrumentationRegistry.getInstrumentation().getContext(),
InstrumentationRegistry.getArguments().getString(ShellExecSharedConstants.BINDER_KEY));
this.shellExecutor = factory.create();
}

@Test
Expand Down

0 comments on commit 8047902

Please sign in to comment.