Merge "Disable watchdog on OSX using compile-time checks"
diff --git a/include/perfetto/ext/tracing/core/BUILD.gn b/include/perfetto/ext/tracing/core/BUILD.gn
index 5dc8c88..b263ec0 100644
--- a/include/perfetto/ext/tracing/core/BUILD.gn
+++ b/include/perfetto/ext/tracing/core/BUILD.gn
@@ -28,6 +28,7 @@
"shared_memory_abi.h",
"shared_memory_arbiter.h",
"slice.h",
+ "sliced_protobuf_input_stream.h",
"startup_trace_writer.h",
"startup_trace_writer_registry.h",
"trace_packet.h",
diff --git a/src/tracing/core/sliced_protobuf_input_stream.h b/include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
similarity index 89%
rename from src/tracing/core/sliced_protobuf_input_stream.h
rename to include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
index fe743d6..d305cf4 100644
--- a/src/tracing/core/sliced_protobuf_input_stream.h
+++ b/include/perfetto/ext/tracing/core/sliced_protobuf_input_stream.h
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-#ifndef SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
-#define SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#ifndef INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#define INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
#include "perfetto/ext/tracing/core/slice.h"
@@ -60,4 +60,4 @@
} // namespace perfetto
-#endif // SRC_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
+#endif // INCLUDE_PERFETTO_EXT_TRACING_CORE_SLICED_PROTOBUF_INPUT_STREAM_H_
diff --git a/include/perfetto/ext/tracing/core/trace_packet.h b/include/perfetto/ext/tracing/core/trace_packet.h
index 80f1018..d695165 100644
--- a/include/perfetto/ext/tracing/core/trace_packet.h
+++ b/include/perfetto/ext/tracing/core/trace_packet.h
@@ -21,7 +21,6 @@
#include <memory>
#include <tuple>
-#include <google/protobuf/io/zero_copy_stream.h>
#include "perfetto/base/export.h"
#include "perfetto/base/logging.h"
#include "perfetto/ext/tracing/core/slice.h"
@@ -43,7 +42,6 @@
class PERFETTO_EXPORT TracePacket {
public:
using const_iterator = Slices::const_iterator;
- using ZeroCopyInputStream = ::google::protobuf::io::ZeroCopyInputStream;
// The field id of protos::Trace::packet, static_assert()-ed in the unittest.
static constexpr uint32_t kPacketFieldNumber = 1;
@@ -56,20 +54,6 @@
// Accesses all the raw slices in the packet, for saving them to file/network.
const Slices& slices() const { return slices_; }
- // Decodes the packet. This function requires that the caller:
- // 1) Does #include "protos/perfetto/trace/trace_packet.pb.h"
- // 2) Links against the //protos/trace:lite target.
- // The core service code deliberately doesn't link against that in order to
- // avoid binary bloat. This is the reason why this is a templated function.
- // It doesn't need to be (i.e. the caller should not specify the template
- // argument) but doing so prevents the compiler trying to resolve the
- // TracePacket type until it's needed, in which case the caller needs (1).
- template <typename TracePacketType = protos::TracePacket>
- bool Decode(TracePacketType* packet) const {
- std::unique_ptr<ZeroCopyInputStream> istr = CreateSlicedInputStream();
- return packet->ParseFromZeroCopyStream(istr.get());
- }
-
// Mutator, used only by the service and tests.
void AddSlice(Slice);
@@ -86,12 +70,14 @@
// and its size.
std::tuple<char*, size_t> GetProtoPreamble();
+ // Returns the raw protobuf bytes of the slices, all stitched together into
+ // a string. Only for testing.
+ std::string GetRawBytesForTesting();
+
private:
TracePacket(const TracePacket&) = delete;
TracePacket& operator=(const TracePacket&) = delete;
- std::unique_ptr<ZeroCopyInputStream> CreateSlicedInputStream() const;
-
Slices slices_; // Not owned.
size_t size_ = 0; // SUM(slice.size for slice in slices_).
char preamble_[8]; // Deliberately not initialized.
diff --git a/src/ipc/buffered_frame_deserializer.cc b/src/ipc/buffered_frame_deserializer.cc
index 9974654..dc107f7 100644
--- a/src/ipc/buffered_frame_deserializer.cc
+++ b/src/ipc/buffered_frame_deserializer.cc
@@ -22,7 +22,6 @@
#include <type_traits>
#include <utility>
-#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/utils.h"
@@ -167,9 +166,7 @@
if (size == 0)
return;
std::unique_ptr<Frame> frame(new Frame);
- const int sz = static_cast<int>(size);
- ::google::protobuf::io::ArrayInputStream stream(data, sz);
- if (frame->ParseFromBoundedZeroCopyStream(&stream, sz))
+ if (frame->ParseFromArray(data, static_cast<int>(size)))
decoded_frames_.push_back(std::move(frame));
}
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index 5067570..4a13c32 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -55,7 +55,6 @@
"core/shared_memory_arbiter_impl.cc",
"core/shared_memory_arbiter_impl.h",
"core/sliced_protobuf_input_stream.cc",
- "core/sliced_protobuf_input_stream.h",
"core/startup_trace_writer.cc",
"core/startup_trace_writer_registry.cc",
"core/test_config.cc",
diff --git a/src/tracing/core/packet_stream_validator.h b/src/tracing/core/packet_stream_validator.h
index 8494f4e..af3d477 100644
--- a/src/tracing/core/packet_stream_validator.h
+++ b/src/tracing/core/packet_stream_validator.h
@@ -17,7 +17,8 @@
#ifndef SRC_TRACING_CORE_PACKET_STREAM_VALIDATOR_H_
#define SRC_TRACING_CORE_PACKET_STREAM_VALIDATOR_H_
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/base/export.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
namespace perfetto {
@@ -30,7 +31,7 @@
//
// Note that we only validate top-level fields in the trace proto; sub-messages
// are simply skipped.
-class PacketStreamValidator {
+class PERFETTO_EXPORT PacketStreamValidator {
public:
PacketStreamValidator() = delete;
diff --git a/src/tracing/core/sliced_protobuf_input_stream.cc b/src/tracing/core/sliced_protobuf_input_stream.cc
index bf81d89..3301e63 100644
--- a/src/tracing/core/sliced_protobuf_input_stream.cc
+++ b/src/tracing/core/sliced_protobuf_input_stream.cc
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
#include <algorithm>
diff --git a/src/tracing/core/sliced_protobuf_input_stream_unittest.cc b/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
index 59f52f2..fc0c075 100644
--- a/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
+++ b/src/tracing/core/sliced_protobuf_input_stream_unittest.cc
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
+#include "perfetto/ext/tracing/core/sliced_protobuf_input_stream.h"
#include "perfetto/ext/base/utils.h"
#include "test/gtest_and_gmock.h"
diff --git a/src/tracing/core/startup_trace_writer_unittest.cc b/src/tracing/core/startup_trace_writer_unittest.cc
index 139456e..a7cd419 100644
--- a/src/tracing/core/startup_trace_writer_unittest.cc
+++ b/src/tracing/core/startup_trace_writer_unittest.cc
@@ -23,7 +23,6 @@
#include "src/base/test/test_task_runner.h"
#include "src/tracing/core/patch_list.h"
#include "src/tracing/core/shared_memory_arbiter_impl.h"
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
#include "src/tracing/core/trace_buffer.h"
#include "src/tracing/test/aligned_buffer_test.h"
#include "src/tracing/test/fake_producer_endpoint.h"
@@ -147,15 +146,10 @@
EXPECT_EQ(static_cast<uid_t>(1),
sequence_properties.producer_uid_trusted);
- SlicedProtobufInputStream stream(&packet.slices());
- size_t size = 0;
- for (const Slice& slice : packet.slices())
- size += slice.size;
protos::TracePacket parsed_packet;
- bool success = parsed_packet.ParseFromBoundedZeroCopyStream(
- &stream, static_cast<int>(size));
- EXPECT_TRUE(success);
- if (!success)
+ bool res = parsed_packet.ParseFromString(packet.GetRawBytesForTesting());
+ EXPECT_TRUE(res);
+ if (!res)
break;
// If the buffer size was exceeded, the data loss packet should be the
diff --git a/src/tracing/core/trace_packet.cc b/src/tracing/core/trace_packet.cc
index c3891c5..0dea150 100644
--- a/src/tracing/core/trace_packet.cc
+++ b/src/tracing/core/trace_packet.cc
@@ -18,7 +18,6 @@
#include "perfetto/base/logging.h"
#include "perfetto/protozero/proto_utils.h"
-#include "src/tracing/core/sliced_protobuf_input_stream.h"
namespace perfetto {
@@ -63,10 +62,16 @@
return std::make_tuple(&preamble_[0], preamble_size);
}
-std::unique_ptr<TracePacket::ZeroCopyInputStream>
-TracePacket::CreateSlicedInputStream() const {
- return std::unique_ptr<ZeroCopyInputStream>(
- new SlicedProtobufInputStream(&slices_));
+std::string TracePacket::GetRawBytesForTesting() {
+ std::string data;
+ data.resize(size());
+ size_t pos = 0;
+ for (const Slice& slice : slices()) {
+ PERFETTO_CHECK(pos + slice.size <= data.size());
+ memcpy(&data[pos], slice.start, slice.size);
+ pos += slice.size;
+ }
+ return data;
}
} // namespace perfetto
diff --git a/src/tracing/core/trace_packet_unittest.cc b/src/tracing/core/trace_packet_unittest.cc
index f2aaa45..0469ae8 100644
--- a/src/tracing/core/trace_packet_unittest.cc
+++ b/src/tracing/core/trace_packet_unittest.cc
@@ -63,7 +63,7 @@
ASSERT_EQ(tp.slices().end(), ++slice);
protos::TracePacket decoded_packet;
- ASSERT_TRUE(tp.Decode(&decoded_packet));
+ ASSERT_TRUE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
ASSERT_EQ(proto.for_testing().str(), decoded_packet.for_testing().str());
}
@@ -94,7 +94,7 @@
ASSERT_EQ(tp.slices().end(), ++slice);
protos::TracePacket decoded_packet;
- ASSERT_TRUE(tp.Decode(&decoded_packet));
+ ASSERT_TRUE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
ASSERT_EQ(proto.for_testing().str(), decoded_packet.for_testing().str());
}
@@ -105,7 +105,7 @@
TracePacket tp;
tp.AddSlice({ser_buf.data(), ser_buf.size() - 2}); // corrupted.
protos::TracePacket decoded_packet;
- ASSERT_FALSE(tp.Decode(&decoded_packet));
+ ASSERT_FALSE(decoded_packet.ParseFromString(tp.GetRawBytesForTesting()));
}
// Tests that the GetProtoPreamble() logic returns a valid preamble that allows
diff --git a/src/tracing/test/mock_consumer.cc b/src/tracing/test/mock_consumer.cc
index 867d98c..17a983d 100644
--- a/src/tracing/test/mock_consumer.cc
+++ b/src/tracing/test/mock_consumer.cc
@@ -109,7 +109,7 @@
for (TracePacket& packet : *packets) {
decoded_packets.emplace_back();
protos::TracePacket* decoded_packet = &decoded_packets.back();
- packet.Decode(decoded_packet);
+ decoded_packet->ParseFromString(packet.GetRawBytesForTesting());
}
if (!has_more)
on_read_buffers();
diff --git a/src/tracing/test/tracing_integration_test.cc b/src/tracing/test/tracing_integration_test.cc
index 56cc2c9..799b614 100644
--- a/src/tracing/test/tracing_integration_test.cc
+++ b/src/tracing/test/tracing_integration_test.cc
@@ -308,7 +308,8 @@
for (auto& encoded_packet : *packets) {
protos::TracePacket packet;
- ASSERT_TRUE(encoded_packet.Decode(&packet));
+ ASSERT_TRUE(packet.ParseFromString(
+ encoded_packet.GetRawBytesForTesting()));
if (packet.has_for_testing()) {
char buf[8];
sprintf(buf, "evt_%zu", num_pack_rx++);
@@ -516,7 +517,8 @@
std::vector<TracePacket>* packets, bool has_more) {
for (auto& encoded_packet : *packets) {
protos::TracePacket packet;
- ASSERT_TRUE(encoded_packet.Decode(&packet));
+ ASSERT_TRUE(packet.ParseFromString(
+ encoded_packet.GetRawBytesForTesting()));
if (packet.has_for_testing()) {
num_test_pack_rx++;
}
diff --git a/test/test_helper.cc b/test/test_helper.cc
index e4af816..57e8b10 100644
--- a/test/test_helper.cc
+++ b/test/test_helper.cc
@@ -61,7 +61,8 @@
void TestHelper::OnTraceData(std::vector<TracePacket> packets, bool has_more) {
for (auto& encoded_packet : packets) {
protos::TracePacket packet;
- PERFETTO_CHECK(encoded_packet.Decode(&packet));
+ PERFETTO_CHECK(
+ packet.ParseFromString(encoded_packet.GetRawBytesForTesting()));
if (packet.has_clock_snapshot() || packet.has_trace_config() ||
packet.has_trace_stats() || !packet.synchronization_marker().empty() ||
packet.has_system_info()) {
diff --git a/ui/src/base/string_utils.ts b/ui/src/base/string_utils.ts
new file mode 100644
index 0000000..0ad7dd3
--- /dev/null
+++ b/ui/src/base/string_utils.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 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.
+
+export function uint8ArrayToBase64(buffer: Uint8Array): string {
+ return btoa(uint8ArrayToString(buffer));
+}
+
+export function uint8ArrayToString(buffer: Uint8Array): string {
+ return String.fromCharCode.apply(null, Array.from(buffer));
+}
+
+export function stringToUint8Array(str: string): Uint8Array {
+ const bufView = new Uint8Array(new ArrayBuffer(str.length));
+ const strLen = str.length;
+ for (let i = 0; i < strLen; i++) {
+ bufView[i] = str.charCodeAt(i);
+ }
+ return bufView;
+}
\ No newline at end of file
diff --git a/ui/src/base/string_utils_jsdomtest.ts b/ui/src/base/string_utils_jsdomtest.ts
new file mode 100644
index 0000000..e8bae93
--- /dev/null
+++ b/ui/src/base/string_utils_jsdomtest.ts
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 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.
+
+import {
+ stringToUint8Array,
+ uint8ArrayToBase64,
+ uint8ArrayToString
+} from './string_utils';
+
+test('uint8ArrayToBase64', () => {
+ const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
+ const buffer = new Uint8Array(bytes);
+ expect(uint8ArrayToBase64(buffer)).toEqual('SGVsbG8sIHdvcmxk');
+});
+
+test('stringToBufferToString', () => {
+ const testString = 'Hello world!';
+ const buffer = stringToUint8Array(testString);
+ const convertedBack = uint8ArrayToString(buffer);
+ expect(testString).toEqual(convertedBack);
+});
+
+test('bufferToStringToBuffer', () => {
+ const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
+ const buffer = new Uint8Array(bytes);
+ const toString = uint8ArrayToString(buffer);
+ const convertedBack = stringToUint8Array(toString);
+ expect(convertedBack).toEqual(buffer);
+});
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 512e633..d46f6b7 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -22,13 +22,15 @@
GetTraceStatsResponse,
ReadBuffersResponse
} from '../controller/consumer_port_types';
+import {extractTraceConfig} from '../controller/record_controller';
+import {RpcConsumerPort} from '../controller/record_controller_interfaces';
import {perfetto} from '../gen/protos';
import {DevToolsSocket} from './devtools_socket';
const CHUNK_SIZE: number = 1024 * 1024 * 64;
-export class ChromeTracingController {
+export class ChromeTracingController extends RpcConsumerPort {
private streamHandle: string|undefined = undefined;
private uiPort: chrome.runtime.Port;
private api: ProtocolProxyApi.ProtocolApi;
@@ -36,6 +38,16 @@
private lastBufferUsageEvent: Protocol.Tracing.BufferUsageEvent|undefined;
constructor(port: chrome.runtime.Port) {
+ super({
+ onConsumerPortResponse: (message: ConsumerPortResponse) =>
+ this.uiPort.postMessage(message),
+
+ onError: (error: string) =>
+ this.uiPort.postMessage({type: 'ChromeExtensionError', error}),
+
+ onStatus: (status) =>
+ this.uiPort.postMessage({type: 'ChromeExtensionStatus', status})
+ });
this.uiPort = port;
this.devtoolsSocket = new DevToolsSocket();
this.devtoolsSocket.on('close', () => this.resetState());
@@ -45,18 +57,10 @@
this.api.Tracing.on('bufferUsage', this.onBufferUsage.bind(this));
}
- sendMessage(message: ConsumerPortResponse) {
- this.uiPort.postMessage(message);
- }
-
- sendErrorMessage(error: string) {
- this.uiPort.postMessage({type: 'ErrorResponse', result: {error}});
- }
-
- onMessage(request: {method: string, traceConfig: Uint8Array}) {
- switch (request.method) {
+ handleCommand(methodName: string, requestData: Uint8Array) {
+ switch (methodName) {
case 'EnableTracing':
- this.enableTracing(request);
+ this.enableTracing(requestData);
break;
case 'FreeBuffers':
this.freeBuffers();
@@ -74,19 +78,25 @@
this.getCategories();
break;
default:
- this.sendErrorMessage('Action not recognised');
- console.log('Received not recognized message: ', request.method);
+ this.sendErrorMessage('Action not recognized');
+ console.log('Received not recognized message: ', methodName);
break;
}
}
- enableTracing(request: {method: string, traceConfig: Uint8Array}) {
+ enableTracing(enableTracingRequest: Uint8Array) {
this.resetState();
- const traceConfig = TraceConfig.decode(new Uint8Array(request.traceConfig));
+ const traceConfigProto = extractTraceConfig(enableTracingRequest);
+ if (!traceConfigProto) {
+ this.sendErrorMessage('Invalid trace config');
+ return;
+ }
+ const traceConfig = TraceConfig.decode(traceConfigProto);
const chromeConfig = this.extractChromeConfig(traceConfig);
this.handleStartTracing(chromeConfig);
}
+ // TODO(nicomazz): write unit test for this
extractChromeConfig(perfettoConfig: TraceConfig):
Protocol.Tracing.TraceConfig {
for (const ds of perfettoConfig.dataSources) {
@@ -106,7 +116,6 @@
}
async readBuffers(offset = 0) {
- // TODO(nicomazz): Add error handling also in the frontend.
if (!this.devtoolsSocket.isAttached() || this.streamHandle === undefined) {
this.sendErrorMessage('No tracing session to read from');
return;
diff --git a/ui/src/chrome_extension/index.ts b/ui/src/chrome_extension/index.ts
index 1bc5e36..5bce4ec 100644
--- a/ui/src/chrome_extension/index.ts
+++ b/ui/src/chrome_extension/index.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {stringToUint8Array} from '../base/string_utils';
import {ChromeTracingController} from './chrome_tracing_controller';
let chromeTraceController: ChromeTracingController|undefined = undefined;
@@ -24,14 +25,19 @@
});
function onUIMessage(
- message: {method: string, traceConfig: Uint8Array},
- port: chrome.runtime.Port) {
+ message: {method: string, requestData: string}, port: chrome.runtime.Port) {
if (message.method === 'ExtensionVersion') {
port.postMessage({version: chrome.runtime.getManifest().version});
return;
}
- // In the future, more targets will be supported.
- if (chromeTraceController) chromeTraceController.onMessage(message);
+ console.assert(chromeTraceController !== undefined);
+ if (!chromeTraceController) return;
+ // ChromeExtensionConsumerPort sends the request data as string because
+ // chrome.runtime.port doesn't support ArrayBuffers.
+ const requestDataArray: Uint8Array = message.requestData ?
+ stringToUint8Array(message.requestData) :
+ new Uint8Array();
+ chromeTraceController.handleCommand(message.method, requestDataArray);
}
function enableOnlyOnPerfettoHost() {
diff --git a/ui/src/controller/adb.ts b/ui/src/controller/adb.ts
index 15c9139..b44c654 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/controller/adb.ts
@@ -24,6 +24,8 @@
RSAPUBLICKEY = 3,
}
+const DEVICE_NOT_SET_ERROR = 'Device not set.';
+
// This class is a basic TypeScript implementation of adb that only supports
// shell commands. It is used to send the start tracing command to the connected
// android device, and to automatically pull the trace after the end of the
@@ -100,6 +102,15 @@
return new Promise<void>((resolve, _) => this.onConnected = resolve);
}
+ async disconnect(): Promise<void> {
+ if (!this.dev) {
+ console.error('adb disconnect() called with no device connected');
+ return;
+ }
+ this.dev.close();
+ this.dev = undefined;
+ }
+
async startAuthentication() {
// USB connected, now let's authenticate.
const VERSION =
@@ -109,7 +120,7 @@
}
findInterfaceAndEndpoint() {
- if (!this.dev) throw Error('Device not set');
+ if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
for (const config of this.dev.configurations) {
for (const interface_ of config.interfaces) {
for (const alt of interface_.alternates) {
@@ -140,18 +151,18 @@
}
receiveDeviceMessages() {
- // TODO(nicomazz): find the best (and correct) way to stop this.
- try {
- this.recv().then(msg => {
- this.onMessage(msg);
- this.receiveDeviceMessages();
- });
- } catch (e) {
- // Then the usb connection is not available anymore, the recv will throw
- // an exception here.
- // TODO(nicomazz): Propagate this unconnected state to the UI.
- console.log('exception on recv: ', e);
- }
+ this.recv()
+ .then(msg => {
+ this.onMessage(msg);
+ this.receiveDeviceMessages();
+ })
+ .catch(e => {
+ // Ignore error with "DEVICE_NOT_SET_ERROR" message since it is always
+ // thrown after the device disconnects.
+ if (e.message !== DEVICE_NOT_SET_ERROR) {
+ console.error(`Exception in recv: ${e.name}. error: ${e.message}`);
+ }
+ });
}
async onMessage(msg: AdbMsg) {
@@ -244,6 +255,16 @@
});
}
+ async shellOutputAsString(cmd: string): Promise<string> {
+ const shell = await this.shell(cmd);
+
+ return new Promise<string>((resolve, _) => {
+ const output: string[] = [];
+ shell.onData = (str, _) => output.push(str);
+ shell.onClose = () => resolve(output.join());
+ });
+ }
+
async send(
cmd: CmdType, arg0: number, arg1: number, data?: Uint8Array|string) {
await this.sendMsg(AdbMsgImpl.create(
@@ -304,12 +325,12 @@
sendRaw(buf: Uint8Array): Promise<USBOutTransferResult> {
console.assert(buf.length <= this.maxPayload);
- if (!this.dev) throw Error('Device not set');
+ if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
return this.dev.transferOut(this.usbWriteEpEndpoint, buf.buffer);
}
recvRaw(dataLen: number): Promise<USBInTransferResult> {
- if (!this.dev) throw Error('Device not set');
+ if (!this.dev) throw Error(DEVICE_NOT_SET_ERROR);
return this.dev.transferIn(this.usbReadEndpoint, dataLen);
}
}
@@ -363,7 +384,6 @@
}
this.adb.send('CLSE', this.localStreamId, this.remoteStreamId);
- this.doClose();
}
async write(msg: string|Uint8Array) {
diff --git a/ui/src/controller/adb_interfaces.ts b/ui/src/controller/adb_interfaces.ts
index 91dd653..2f22a2f 100644
--- a/ui/src/controller/adb_interfaces.ts
+++ b/ui/src/controller/adb_interfaces.ts
@@ -15,12 +15,16 @@
export interface Adb {
connect(device: USBDevice): Promise<void>;
+ disconnect(): Promise<void>;
shell(cmd: string): Promise<AdbStream>;
+ shellOutputAsString(cmd: string): Promise<string>;
}
export interface AdbStream {
onMessage(message: AdbMsg): void;
onData: (str: string, raw: Uint8Array) => void;
+ close(): void;
+
onConnect: VoidCallback;
onClose: VoidCallback;
}
@@ -29,9 +33,18 @@
connect(_: USBDevice): Promise<void> {
return Promise.resolve();
}
+
+ disconnect(): Promise<void> {
+ return Promise.resolve();
+ }
+
shell(_: string): Promise<AdbStream> {
return Promise.resolve(new MockAdbStream());
}
+
+ shellOutputAsString(_: string): Promise<string> {
+ return Promise.resolve('');
+ }
}
export class MockAdbStream implements AdbStream {
@@ -39,6 +52,7 @@
onConnect = () => {};
onClose = () => {};
onMessage = (_: AdbMsg) => {};
+ close() {}
}
export declare type CmdType =
diff --git a/ui/src/controller/adb_record_controller.ts b/ui/src/controller/adb_record_controller.ts
index f3d17cb..990053d 100644
--- a/ui/src/controller/adb_record_controller.ts
+++ b/ui/src/controller/adb_record_controller.ts
@@ -12,10 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {uint8ArrayToBase64} from '../base/string_utils';
+
import {Adb, AdbStream} from './adb_interfaces';
-import {ConsumerPortResponse, ReadBuffersResponse} from './consumer_port_types';
+import {ReadBuffersResponse} from './consumer_port_types';
import {globals} from './globals';
-import {RecordControllerMessage, uint8ArrayToBase64} from './record_controller';
+import {
+ extractDurationFromTraceConfig,
+ extractTraceConfig
+} from './record_controller';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
enum AdbState {
READY,
@@ -24,39 +30,20 @@
}
const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace';
-export class AdbRecordController {
+export class AdbConsumerPort extends RpcConsumerPort {
// public for testing
traceDestFile = DEFAULT_DESTINATION_FILE;
private state = AdbState.READY;
private adb: Adb;
private device: USBDevice|undefined = undefined;
- private mainControllerCallback:
- (_: {data: ConsumerPortResponse|RecordControllerMessage}) => void;
+ private recordShell?: AdbStream;
- constructor(adb: Adb, mainControllerCallback: (_: {
- data: ConsumerPortResponse
- }) => void) {
- this.mainControllerCallback = mainControllerCallback;
+ constructor(adb: Adb, consumerPortListener: Consumer) {
+ super(consumerPortListener);
this.adb = adb;
}
- sendMessage(message: ConsumerPortResponse|RecordControllerMessage) {
- this.mainControllerCallback({data: message});
- }
-
- sendErrorMessage(message: string) {
- console.error('Error in adb record controller: ', message);
- this.sendMessage({type: 'RecordControllerError', message});
- }
-
- sendStatus(status: string) {
- this.sendMessage({type: 'RecordControllerStatus', status});
- }
-
handleCommand(method: string, params: Uint8Array) {
- // TODO(nicomazz): after having implemented the connection to the consumer
- // port socket through adb (on a real device), this class will be a simple
- // proxy.
switch (method) {
case 'EnableTracing':
this.enableTracing(params);
@@ -64,9 +51,11 @@
case 'ReadBuffers':
this.readBuffers();
break;
+ case 'DisableTracing':
+ this.disableTracing();
+ break;
case 'FreeBuffers': // no-op
case 'GetTraceStats':
- case 'DisableTracing':
break;
default:
this.sendErrorMessage(`Method not recognized: ${method}`);
@@ -74,12 +63,9 @@
}
}
- async enableTracing(configProto: Uint8Array) {
+ async enableTracing(enableTracingProto: Uint8Array) {
try {
- if (this.state !== AdbState.READY) {
- console.error('Current state of AdbRecordController is not READY');
- return;
- }
+ console.assert(this.state === AdbState.READY);
this.device = await this.findDevice();
if (this.device === undefined) {
@@ -89,9 +75,17 @@
this.sendStatus(
'Check the screen of your device and allow USB debugging.');
await this.adb.connect(this.device);
- await this.startRecording(configProto);
- this.sendStatus('Recording in progress...');
+ const traceConfigProto = extractTraceConfig(enableTracingProto);
+ if (!traceConfigProto) {
+ this.sendErrorMessage('Invalid config.');
+ return;
+ }
+
+ await this.startRecording(traceConfigProto);
+ const duration = extractDurationFromTraceConfig(traceConfigProto);
+ this.sendStatus(`Recording in progress${
+ duration ? ' for ' + duration.toString() + ' ms' : ''}...`);
} catch (e) {
this.sendErrorMessage(e.message);
}
@@ -100,15 +94,17 @@
async startRecording(configProto: Uint8Array) {
this.state = AdbState.RECORDING;
const recordCommand = this.generateStartTracingCommand(configProto);
- const recordShell: AdbStream = await this.adb.shell(recordCommand);
- let response = '';
- recordShell.onData = (str, _) => response += str;
- recordShell.onClose = () => {
+ this.recordShell = await this.adb.shell(recordCommand);
+ const output: string[] = [];
+ this.recordShell.onData = (str, _) => output.push(str);
+ this.recordShell.onClose = () => {
+ const response = output.join();
if (!this.tracingEndedSuccessfully(response)) {
this.sendErrorMessage(response);
this.state = AdbState.READY;
return;
}
+ this.sendStatus('Recording ended successfully. Fetching the trace..');
this.sendMessage({type: 'EnableTracingResponse'});
};
}
@@ -138,6 +134,9 @@
// things are not working, the chunks should be sent as they are received,
// like in the following line.
// this.sendMessage(this.generateChunkReadResponse(str));
+ // EDIT: we should send back a response as if it was a real
+ // ReadBufferResponse, with trace packets. Here we are only sending the
+ // trace split in several pieces.
trace += str;
};
readTraceShell.onClose = () => {
@@ -145,10 +144,34 @@
this.sendMessage(
this.generateChunkReadResponse(decoded, /* last */ true));
+ this.adb.disconnect();
this.state = AdbState.READY;
};
}
+ // TODO(nicomazz): Implement cancel/reset recording.
+ async disableTracing() {
+ console.assert(this.recordShell !== undefined);
+ if (!this.recordShell) return;
+
+ // We are not using 'pidof perfetto' so that we can use more filters. 'ps -u
+ // shell' is meant to catch processes started from shell, so if there are
+ // other ongoing tracing sessions started by others, we are not killing
+ // them.
+ const pid = await this.adb.shellOutputAsString(
+ `ps -u shell | grep perfetto | awk '{print $2}'`);
+ if (pid.length === 0 || isNaN(Number(pid))) {
+ this.sendErrorMessage(
+ 'Unexpected error, impossible to stop the recording');
+ console.error('Perfetto pid not found. Command output: ', pid);
+ return;
+ }
+ // Perfetto stops and finalizes the tracing session on SIGINT.
+ const killOutput =
+ await this.adb.shellOutputAsString(`kill -SIGINT ${pid}`);
+ console.assert(killOutput.length === 0);
+ }
+
generateChunkReadResponse(data: string, last = false): ReadBuffersResponse {
return {
type: 'ReadBuffersResponse',
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/controller/adb_record_controller_jsdomtest.ts
index dffb563..503f92d 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/controller/adb_record_controller_jsdomtest.ts
@@ -14,14 +14,29 @@
import {dingus} from 'dingusjs';
+import {perfetto} from '../gen/protos';
import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
-import {AdbRecordController} from './adb_record_controller';
+import {AdbConsumerPort} from './adb_record_controller';
+import {Consumer} from './record_controller_interfaces';
-const mainCallback = jest.fn();
+function generateMockConsumer(): Consumer {
+ return {
+ onConsumerPortResponse: jest.fn(),
+ onError: jest.fn(),
+ onStatus: jest.fn()
+ };
+}
+const mainCallback = generateMockConsumer();
const adbMock = new MockAdb();
-const adbController = new AdbRecordController(adbMock, mainCallback);
+const adbController = new AdbConsumerPort(adbMock, mainCallback);
const mockIntArray = new Uint8Array();
+const enableTracingRequest = new perfetto.protos.EnableTracingRequest();
+enableTracingRequest.traceConfig = new perfetto.protos.TraceConfig();
+const enableTracingRequestProto =
+ perfetto.protos.EnableTracingRequest.encode(enableTracingRequest).finish();
+
+
test('handleCommand', () => {
adbController.findDevice = () => {
return Promise.resolve(dingus<USBDevice>());
@@ -44,9 +59,9 @@
});
test('enableTracing', async () => {
- const mainCallback = jest.fn();
+ const mainCallback = generateMockConsumer();
const adbMock = new MockAdb();
- const adbController = new AdbRecordController(adbMock, mainCallback);
+ const adbController = new AdbConsumerPort(adbMock, mainCallback);
adbController.sendErrorMessage =
jest.fn().mockImplementation(s => console.error(s));
@@ -71,13 +86,11 @@
adbController.generateStartTracingCommand = (_) => 'CMD';
- await adbController.enableTracing(mockIntArray);
+ await adbController.enableTracing(enableTracingRequestProto);
expect(adbShell).toBeCalledWith('CMD');
expect(findDevice).toHaveBeenCalledTimes(1);
expect(connectToDevice).toHaveBeenCalledTimes(1);
- // Two messages: RecordControllerStatus asking for allow the debug, and
- // another status to clear that message.
- expect(sendMessage).toHaveBeenCalledTimes(2);
+ expect(sendMessage).toHaveBeenCalledTimes(0);
stream.onData('starting tracing Wrote 123 bytes', mockIntArray);
@@ -110,4 +123,4 @@
'Connected to the Perfetto traced service, starting tracing for \
0 ms'))
.toBe(false);
-});
\ No newline at end of file
+});
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/controller/chrome_proxy_record_controller.ts
new file mode 100644
index 0000000..c2ce289
--- /dev/null
+++ b/ui/src/controller/chrome_proxy_record_controller.ts
@@ -0,0 +1,69 @@
+// Copyright (C) 2019 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.
+
+import {uint8ArrayToString} from '../base/string_utils';
+
+import {ConsumerPortResponse, Typed} from './consumer_port_types';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
+
+export interface ChromeExtensionError extends Typed {
+ error: string;
+}
+
+export interface ChromeExtensionStatus extends Typed {
+ status: string;
+}
+
+export type ChromeExtensionMessage =
+ ChromeExtensionError|ChromeExtensionStatus|ConsumerPortResponse;
+
+function isError(obj: Typed): obj is ChromeExtensionError {
+ return obj.type === 'ChromeExtensionError';
+}
+
+function isStatus(obj: Typed): obj is ChromeExtensionStatus {
+ return obj.type === 'ChromeExtensionStatus';
+}
+
+// This class acts as a proxy from the record controller (running in a worker),
+// to the frontend. This is needed because we can't directly talk with the
+// extension from a web-worker, so we use a MessagePort to communicate with the
+// frontend, that will consecutively forward it to the extension.
+export class ChromeExtensionConsumerPort extends RpcConsumerPort {
+ private extensionPort: MessagePort;
+
+ constructor(extensionPort: MessagePort, consumerPortListener: Consumer) {
+ super(consumerPortListener);
+ this.extensionPort = extensionPort;
+ this.extensionPort.onmessage = this.onExtensionMessage.bind(this);
+ }
+
+ onExtensionMessage(message: {data: ChromeExtensionMessage}) {
+ if (isError(message.data)) {
+ this.sendErrorMessage(message.data.error);
+ } else if (isStatus(message.data)) {
+ this.sendStatus(message.data.status);
+ } else {
+ this.sendMessage(message.data);
+ }
+ }
+
+ handleCommand(method: string, requestData: Uint8Array): void {
+ const buffer = uint8ArrayToString(requestData);
+ // We need to encode the buffer as a string because the message port doesn't
+ // fully support sending ArrayBuffers (they are converted to objects with
+ // indexes as keys).
+ this.extensionPort.postMessage({method, requestData: buffer});
+ }
+}
diff --git a/ui/src/controller/record_controller.ts b/ui/src/controller/record_controller.ts
index 34feb53..71b04d5 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/controller/record_controller.ts
@@ -15,6 +15,7 @@
import {ungzip} from 'pako';
import {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
+import {stringToUint8Array, uint8ArrayToBase64} from '../base/string_utils';
import {Actions} from '../common/actions';
import {
AndroidLogConfig,
@@ -34,47 +35,27 @@
isAndroidTarget,
isChromeTarget,
MAX_TIME,
- RecordConfig
+ RecordConfig,
+ TargetOs
} from '../common/state';
+import {perfetto} from '../gen/protos';
import {AdbOverWebUsb} from './adb';
-import {AdbRecordController} from './adb_record_controller';
+import {AdbConsumerPort} from './adb_record_controller';
+import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller';
import {
ConsumerPortResponse,
GetTraceStatsResponse,
isEnableTracingResponse,
isGetTraceStatsResponse,
isReadBuffersResponse,
- Typed,
} from './consumer_port_types';
import {Controller} from './controller';
import {App, globals} from './globals';
+import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>);
-export interface RecordControllerError extends Typed {
- message: string;
-}
-
-export interface RecordControllerStatus extends Typed {
- status: string;
-}
-
-export type RecordControllerMessage =
- RecordControllerError|RecordControllerStatus;
-
-function isError(obj: Typed): obj is RecordControllerError {
- return obj.type === 'RecordControllerError';
-}
-
-function isStatus(obj: Typed): obj is RecordControllerStatus {
- return obj.type === 'RecordControllerStatus';
-}
-
-export function uint8ArrayToBase64(buffer: Uint8Array): string {
- return btoa(String.fromCharCode.apply(null, Array.from(buffer)));
-}
-
export function genConfigProto(uiCfg: RecordConfig): Uint8Array {
return TraceConfig.encode(genConfig(uiCfg)).finish();
}
@@ -436,7 +417,28 @@
return [...message(json, 0)].join('');
}
-export class RecordController extends Controller<'main'> {
+export function extractTraceConfig(enableTracingRequest: Uint8Array):
+ Uint8Array|undefined {
+ try {
+ const enableTracingObject =
+ perfetto.protos.EnableTracingRequest.decode(enableTracingRequest);
+ if (!enableTracingObject.traceConfig) return undefined;
+ return perfetto.protos.TraceConfig.encode(enableTracingObject.traceConfig)
+ .finish();
+ } catch (e) { // This catch is for possible proto encoding/decoding issues.
+ console.error('Error extracting the config: ', e.message);
+ return undefined;
+ }
+}
+
+export function extractDurationFromTraceConfig(traceConfigProto: Uint8Array) {
+ try {
+ return perfetto.protos.TraceConfig.decode(traceConfigProto).durationMs;
+ } catch (e) { // This catch is for possible proto encoding/decoding issues.
+ return undefined;
+ }
+}
+export class RecordController extends Controller<'main'> implements Consumer {
private app: App;
private config: RecordConfig|null = null;
private extensionPort: MessagePort;
@@ -445,14 +447,14 @@
private traceBuffer = '';
private bufferUpdateInterval: ReturnType<typeof setTimeout>|undefined;
- private adbRecordController = new AdbRecordController(
- new AdbOverWebUsb(), this.onConsumerPortMessage.bind(this));
+ // We have a different controller for each targetOS. The correct one will be
+ // created when needed, and stored here.
+ private controllers = new Map<TargetOs, RpcConsumerPort>();
constructor(args: {app: App, extensionPort: MessagePort}) {
super('main');
this.app = args.app;
this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
this.extensionPort = args.extensionPort;
- this.extensionPort.onmessage = this.onConsumerPortMessage.bind(this);
}
run() {
@@ -510,13 +512,11 @@
this.consumerPort.readBuffers({});
}
- onConsumerPortMessage({data}: {
- data: ConsumerPortResponse|RecordControllerMessage
- }) {
+ onConsumerPortResponse(data: ConsumerPortResponse) {
if (data === undefined) return;
-
if (isReadBuffersResponse(data)) {
if (!data.slices) return;
+ // TODO(nicomazz): handle this as intended by consumer_port.proto.
this.traceBuffer += data.slices[0].data;
// TODO(nicomazz): Stream the chunks directly in the trace processor.
if (data.slices[0].lastSliceForPacket) this.openTraceInUI();
@@ -527,77 +527,76 @@
if (percentage) {
globals.publish('BufferUsage', {percentage});
}
- } else if (isError(data)) {
- this.handleError(data.message);
- } else if (isStatus(data)) {
- this.handleStatus(data.status);
+ } else {
+ console.error('Unrecognized consumer port response:', data);
}
}
openTraceInUI() {
this.consumerPort.freeBuffers({});
globals.dispatch(Actions.setRecordingStatus({status: undefined}));
- const trace = ungzip(this.stringToArrayBuffer(this.traceBuffer));
+ const trace = ungzip(stringToUint8Array(this.traceBuffer));
globals.dispatch(Actions.openTraceFromBuffer({buffer: trace.buffer}));
this.traceBuffer = '';
}
- stringToArrayBuffer(str: string): Uint8Array {
- const buf = new ArrayBuffer(str.length);
- const bufView = new Uint8Array(buf);
- for (let i = 0, strLen = str.length; i < strLen; i++) {
- bufView[i] = str.charCodeAt(i);
- }
- return bufView;
- }
-
getBufferUsagePercentage(data: GetTraceStatsResponse): number {
if (!data.traceStats || !data.traceStats.bufferStats) return 0.0;
- let used = 0.0, total = 0.0;
+ let maximumUsage = 0;
for (const buffer of data.traceStats.bufferStats) {
- used += buffer.bytesWritten as number;
- total += buffer.bufferSize as number;
+ const used = buffer.bytesWritten as number;
+ const total = buffer.bufferSize as number;
+ maximumUsage = Math.max(maximumUsage, used / total);
}
- if (total === 0.0) return 0;
- return used / total;
+ return maximumUsage;
}
- handleError(message: string) {
+ onError(message: string) {
globals.dispatch(
Actions.setLastRecordingError({error: message.substr(0, 150)}));
globals.dispatch(Actions.stopRecording({}));
}
- handleStatus(message: string) {
+ onStatus(message: string) {
globals.dispatch(Actions.setRecordingStatus({status: message}));
}
// Depending on the recording target, different implementation of the
// consumer_port will be used.
// - Chrome target: This forwards the messages that have to be sent
- // to the extension to the frontend. This is necessary because this controller
- // is running in a separate worker, that can't directly send messages to the
- // extension.
+ // to the extension to the frontend. This is necessary because this
+ // controller is running in a separate worker, that can't directly send
+ // messages to the extension.
// - Android device target: WebUSB is used to communicate using the adb
- // protocol. Actually, there is no full consumer_port implementation, but only
- // the support to start tracing and fetch the file.
+ // protocol. Actually, there is no full consumer_port implementation, but
+ // only the support to start tracing and fetch the file.
+ getTargetController(target: TargetOs): RpcConsumerPort {
+ let controller = this.controllers.get(target);
+ if (controller) return controller;
+
+ if (isChromeTarget(target)) {
+ controller = new ChromeExtensionConsumerPort(this.extensionPort, this);
+ } else if (isAndroidTarget(target)) {
+ // TODO(nicomazz): create the correct controller also based on the
+ // selected android device.
+ controller = new AdbConsumerPort(new AdbOverWebUsb(), this);
+ }
+
+ if (!controller) throw Error(`Unknown target: ${target}`);
+
+ this.controllers.set(target, controller);
+ return controller;
+ }
+
private rpcImpl(
method: RPCImplMethod, requestData: Uint8Array,
_callback: RPCImplCallback) {
- const target = this.app.state.recordConfig.targetOS;
- if (isChromeTarget(target) && method !== null && method.name !== null &&
- this.config !== null) {
- this.extensionPort.postMessage(
- {method: method.name, traceConfig: requestData});
- } else if (isAndroidTarget(target)) {
- // TODO(nicomazz): In theory requestData should contain the configuration
- // proto, but in practice there are missing fields. As a temporary
- // workaround I'm directly passing the configuration.
- this.adbRecordController.handleCommand(
- method.name, genConfigProto(this.config!));
- } else {
- console.error(`Target ${target} not supported!`);
+ try {
+ this.getTargetController(this.app.state.recordConfig.targetOS)
+ .handleCommand(method.name, requestData);
+ } catch (e) {
+ console.error(`error invoking ${method}: ${e.message}`);
}
}
}
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/controller/record_controller_interfaces.ts
new file mode 100644
index 0000000..afd1d8b
--- /dev/null
+++ b/ui/src/controller/record_controller_interfaces.ts
@@ -0,0 +1,36 @@
+import {ConsumerPortResponse} from './consumer_port_types';
+
+export type ConsumerPortCallback = (_: ConsumerPortResponse) => void;
+export type ErrorCallback = (_: string) => void;
+export type StatusCallback = (_: string) => void;
+
+export abstract class RpcConsumerPort {
+ // The responses of the call invocations should be sent through this listener.
+ // This is done by the 3 "send" methods in this abstract class.
+ private consumerPortListener: Consumer;
+
+ constructor(consumerPortListener: Consumer) {
+ this.consumerPortListener = consumerPortListener;
+ }
+
+ // RequestData is the proto representing the arguments of the function call.
+ abstract handleCommand(methodName: string, requestData: Uint8Array): void;
+
+ sendMessage(data: ConsumerPortResponse) {
+ this.consumerPortListener.onConsumerPortResponse(data);
+ }
+
+ sendErrorMessage(message: string) {
+ this.consumerPortListener.onError(message);
+ }
+
+ sendStatus(status: string) {
+ this.consumerPortListener.onStatus(status);
+ }
+}
+
+export interface Consumer {
+ onConsumerPortResponse(data: ConsumerPortResponse): void;
+ onError: ErrorCallback;
+ onStatus: StatusCallback;
+}
\ No newline at end of file
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/controller/record_controller_jsdomtest.ts
index 3455b52..8fb9983 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/controller/record_controller_jsdomtest.ts
@@ -19,18 +19,7 @@
import {createEmptyRecordConfig, RecordConfig} from '../common/state';
import {App} from './globals';
-import {
- genConfigProto,
- RecordController,
- toPbtxt,
- uint8ArrayToBase64
-} from './record_controller';
-
-test('uint8ArrayToBase64', () => {
- const bytes = [...'Hello, world'].map(c => c.charCodeAt(0));
- const buffer = new Uint8Array(bytes);
- expect(uint8ArrayToBase64(buffer)).toEqual('SGVsbG8sIHdvcmxk');
-});
+import {genConfigProto, RecordController, toPbtxt} from './record_controller';
test('encodeConfig', () => {
const config = createEmptyRecordConfig();
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index f11e74a..6c4f26f 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -768,11 +768,11 @@
const realDeviceTarget = state.androidDeviceConnected !== undefined;
const recInProgress = state.recordingInProgress;
- const startButton =
+ const start =
m(`button${recInProgress ? '.selected' : ''}`,
{onclick: onStartRecordingPressed},
'Start Recording');
- const showCmdButton =
+ const showCmd =
m(`button`,
{
onclick: () => {
@@ -781,7 +781,7 @@
}
},
'Show Command');
- const stopButton =
+ const stop =
m(`button${recInProgress ? '' : '.disabled'}`,
{onclick: () => globals.dispatch(Actions.stopRecording({}))},
'Stop Recording');
@@ -790,14 +790,13 @@
const targetOs = state.recordConfig.targetOS;
if (isAndroidTarget(targetOs)) {
- buttons.push(showCmdButton);
- if (realDeviceTarget) buttons.push(startButton);
- // TODO(nicomazz): Support stop recording on Android devices.
+ buttons.push(showCmd);
+ if (realDeviceTarget) buttons.push(recInProgress ? stop : start);
} else if (isChromeTarget(targetOs) && state.extensionInstalled) {
- buttons.push(startButton);
- if (recInProgress) buttons.push(stopButton);
+ buttons.push(start);
+ if (recInProgress) buttons.push(stop);
} else if (isLinuxTarget(targetOs)) {
- buttons.push(showCmdButton);
+ buttons.push(showCmd);
}
return m('.button', buttons);