Skip to content

Commit

Permalink
Adding a LocalSocketProtocol for the ShellExecutor to talk to the She…
Browse files Browse the repository at this point in the history
…llMain.

PiperOrigin-RevId: 681170535
  • Loading branch information
copybara-androidxtest committed Oct 2, 2024
1 parent 21a81c6 commit 7ce5874
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 6 deletions.
25 changes: 19 additions & 6 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,30 @@ http_archive(
url = "https://github.com/bazelbuild/rules_jvm_external/archive/%s.zip" % RULES_JVM_EXTERNAL_TAG,
)

# rules_proto needs zlib.
http_archive(
name = "zlib",
build_file = "//build/bazel:zlib.BUILD",
sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1",
strip_prefix = "zlib-1.3.1",
urls = ["http://zlib.net/fossils/zlib-1.3.1.tar.gz"],
)

# rules_proto defines proto_library, as well as @com_google_protobuf_javalite
http_archive(
name = "rules_proto",
sha256 = "dc3fb206a2cb3441b485eb1e423165b231235a1ea9b031b4433cf7bc1fa460dd",
strip_prefix = "rules_proto-5.3.0-21.7",
urls = [
"https://github.com/bazelbuild/rules_proto/archive/refs/tags/5.3.0-21.7.tar.gz",
],
sha256 = "6fb6767d1bef535310547e03247f7518b03487740c11b6c6adb7952033fe1295",
strip_prefix = "rules_proto-6.0.2",
url = "https://github.com/bazelbuild/rules_proto/releases/download/6.0.2/rules_proto-6.0.2.tar.gz",
)
load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")

load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies")
rules_proto_dependencies()

load("@rules_proto//proto:setup.bzl", "rules_proto_setup")
rules_proto_setup()

load("@rules_proto//proto:toolchains.bzl", "rules_proto_toolchains")
rules_proto_toolchains()

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
Expand Down
2 changes: 2 additions & 0 deletions services/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

**New Features**

* Added a new protocol to replace SpeakEasy.

**Breaking Changes**

**API Changes**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ kt_android_library(
],
)

proto_library(
name = "local_socket_protocol_pb",
srcs = ["local_socket_protocol.proto"],
)

java_lite_proto_library(
name = "local_socket_protocol_pb_java_proto_lite",
visibility = [
"//services/shellexecutor/javatests/androidx/test/services/shellexecutor:__subpackages__",
],
deps = [":local_socket_protocol_pb"],
)

kt_android_library(
name = "local_socket_protocol",
srcs = ["LocalSocketProtocol.kt"],
visibility = [
"//services/shellexecutor/javatests/androidx/test/services/shellexecutor:__subpackages__",
],
deps = [
":local_socket_protocol_pb_java_proto_lite",
"@com_google_protobuf//:protobuf_javalite",
"@maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core",
],
)

kt_android_library(
name = "exec_server",
srcs = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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 android.net.LocalSocket
import android.net.LocalSocketAddress
import android.util.Log
import androidx.test.services.shellexecutor.LocalSocketProtocolProto.RunCommandRequest
import androidx.test.services.shellexecutor.LocalSocketProtocolProto.RunCommandResponse
import com.google.protobuf.ByteString
import java.io.IOException
import java.net.URLDecoder
import java.net.URLEncoder
import kotlin.time.Duration

/**
* Protocol for ShellCommandLocalSocketClient to talk to ShellCommandLocalSocketExecutorServer.
*
* Since androidx.test.services already includes the protobuf runtime, we aren't paying much extra
* for adding some more protos to ship back and forth, which is vastly easier to deal with than
* PersistableBundles (which don't even support ByteArray types).
*
* A conversation consists of a single RunCommandRequest from the client followed by a stream of
* RunCommandResponses from the server; the final response has an exit code.
*/
object LocalSocketProtocol {
/** Composes a RunCommandRequest and sends it over the LocalSocket. */
fun LocalSocket.sendRequest(
argv: List<String>,
env: Map<String, String>? = null,
timeout: Duration,
) {
val builder = RunCommandRequest.newBuilder()
builder.addAllArgv(argv)
env?.forEach { (k, v) -> builder.putEnvironment(k, v) }
builder.setTimeoutMs(timeout.inWholeMilliseconds)
builder.build().writeDelimitedTo(outputStream)
}

/** Reads a RunCommandRequest from the LocalSocket. */
fun LocalSocket.readRequest(): RunCommandRequest {
return RunCommandRequest.parseDelimitedFrom(inputStream)!!
}

/** Composes a RunCommandResponse and sends it over the LocalSocket. */
fun LocalSocket.sendResponse(
buffer: ByteArray? = null,
size: Int = 0,
exitCode: Int? = null,
): Boolean {
val builder = RunCommandResponse.newBuilder()
buffer?.let {
val bufferSize = if (size > 0) size else it.size
builder.buffer = ByteString.copyFrom(it, 0, bufferSize)
}
exitCode?.let { builder.exitCode = it }

try {
builder.build().writeDelimitedTo(outputStream)
} catch (x: IOException) {
// Sadly, the only way to discover that the client cut the connection is an exception that
// can only be distinguished by its text.
if (x.message.equals("Broken pipe")) {
Log.i(TAG, "LocalSocket stream closed early")
} else {
Log.w(TAG, "LocalSocket write failed", x)
}
return false
}
return true
}

/** Reads a RunCommandResponse from the LocalSocket. */
fun LocalSocket.readResponse(): RunCommandResponse? {
return RunCommandResponse.parseDelimitedFrom(inputStream)
}

/**
* The address can contain spaces, and since it gets passed through a command line, we need to
* encode it so it doesn't get split by argv. java.net.URLEncoder is conveniently available on all
* SDK versions.
*/
@JvmStatic fun LocalSocketAddress.asBinderKey() = ":${URLEncoder.encode(name, "UTF-8")}"

@JvmStatic
fun fromBinderKey(binderKey: String) =
LocalSocketAddress(URLDecoder.decode(binderKey.trimStart(':'), "UTF-8"))

@JvmStatic fun isBinderKey(maybeKey: String) = maybeKey.startsWith(':')

const val TAG = "LocalSocketProtocol"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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.

edition = "2023";
//syntax = "proto3"; // Maven on github doesn't like 'edition = "2023"'

package androidx.test.services.storage;

option java_package = "androidx.test.services.shellexecutor";
option java_outer_classname = 'LocalSocketProtocolProto';

// Message sent from client to server to start a process.
message RunCommandRequest {
repeated string argv = 1;
map<string, string> environment = 2;
int64 timeout_ms = 3;
}

// Multiple responses can be streamed back to the client. The one that has an
// exit code indicates the end of the stream.
message RunCommandResponse {
bytes buffer = 1;
int32 exit_code = 2;
}

0 comments on commit 7ce5874

Please sign in to comment.