diff options
-rw-r--r-- | tools/android/OWNERS | 6 | ||||
-rw-r--r-- | tools/android/common/adb_connection.cc | 105 | ||||
-rw-r--r-- | tools/android/common/adb_connection.h | 19 | ||||
-rw-r--r-- | tools/android/common/common.gyp | 27 | ||||
-rw-r--r-- | tools/android/common/daemon.cc | 70 | ||||
-rw-r--r-- | tools/android/common/daemon.h | 31 | ||||
-rw-r--r-- | tools/android/common/net.cc | 25 | ||||
-rw-r--r-- | tools/android/common/net.h | 21 | ||||
-rw-r--r-- | tools/android/forwarder/forwarder.cc | 405 | ||||
-rw-r--r-- | tools/android/forwarder/forwarder.gyp | 47 |
10 files changed, 756 insertions, 0 deletions
diff --git a/tools/android/OWNERS b/tools/android/OWNERS new file mode 100644 index 0000000..26eb81a --- /dev/null +++ b/tools/android/OWNERS @@ -0,0 +1,6 @@ +set noparent +jnd@chromium.org +jrg@chromium.org +tonyg@chromium.org +wangxianzhu@chromium.org +yfriedman@chromium.org diff --git a/tools/android/common/adb_connection.cc b/tools/android/common/adb_connection.cc new file mode 100644 index 0000000..c542d16 --- /dev/null +++ b/tools/android/common/adb_connection.cc @@ -0,0 +1,105 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tools/android/common/adb_connection.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#include "base/eintr_wrapper.h" +#include "base/logging.h" +#include "tools/android/common/net.h" + +namespace tools { + +int ConnectAdbHostSocket(const char* forward_to) { + // ADB port forward request format: HHHHtcp:port:address. + // HHHH is the hexidecimal length of the "tcp:port:address" part. + const size_t kBufferMaxLength = 30; + const size_t kLengthOfLength = 4; + const size_t kAddressMaxLength = kBufferMaxLength - kLengthOfLength; + + const char kAddressPrefix[] = { 't', 'c', 'p', ':' }; + size_t address_length = arraysize(kAddressPrefix) + strlen(forward_to); + if (address_length > kBufferMaxLength - kLengthOfLength) { + LOG(ERROR) << "Forward to address is too long: " << forward_to; + return -1; + } + + char request[kBufferMaxLength]; + memcpy(request + kLengthOfLength, kAddressPrefix, arraysize(kAddressPrefix)); + memcpy(request + kLengthOfLength + arraysize(kAddressPrefix), + forward_to, strlen(forward_to)); + + char length_buffer[kLengthOfLength + 1]; + snprintf(length_buffer, arraysize(length_buffer), "%04X", + static_cast<int>(address_length)); + memcpy(request, length_buffer, kLengthOfLength); + + int host_socket = socket(AF_INET, SOCK_STREAM, 0); + if (host_socket < 0) { + LOG(ERROR) << "Failed to create adb socket: " << strerror(errno); + return -1; + } + + DisableNagle(host_socket); + + const int kAdbPort = 5037; + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(kAdbPort); + if (HANDLE_EINTR(connect(host_socket, reinterpret_cast<sockaddr*>(&addr), + sizeof(addr))) < 0) { + LOG(ERROR) << "Failed to connect adb socket: " << strerror(errno); + HANDLE_EINTR(close(host_socket)); + return -1; + } + + size_t bytes_remaining = address_length + kLengthOfLength; + size_t bytes_sent = 0; + while (bytes_remaining > 0) { + int ret = HANDLE_EINTR(send(host_socket, request + bytes_sent, + bytes_remaining, 0)); + if (ret < 0) { + LOG(ERROR) << "Failed to send request: " << strerror(errno); + HANDLE_EINTR(close(host_socket)); + return -1; + } + + bytes_sent += ret; + bytes_remaining -= ret; + } + + const size_t kAdbStatusLength = 4; + char response[kBufferMaxLength]; + int response_length = HANDLE_EINTR(recv(host_socket, response, + kBufferMaxLength, 0)); + if (response_length < kAdbStatusLength || + strncmp("OKAY", response, kAdbStatusLength) != 0) { + char fail_msg_buffer[kBufferMaxLength * 3 + 1]; + char* p = fail_msg_buffer; + for (int i = 0; i < response_length; ++i) { + snprintf(p, 3, "%02x,", static_cast<unsigned char>(response[i])); + p += 3; + } + + if (p > fail_msg_buffer) + *(--p) = 0; // Eliminate the last comma. + LOG(ERROR) << "Bad response from ADB: length: " << response_length + << " data: " << fail_msg_buffer; + HANDLE_EINTR(close(host_socket)); + return -1; + } + + return host_socket; +} + +} // namespace tools + diff --git a/tools/android/common/adb_connection.h b/tools/android/common/adb_connection.h new file mode 100644 index 0000000..48a1a6f --- /dev/null +++ b/tools/android/common/adb_connection.h @@ -0,0 +1,19 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TOOLS_ANDROID_COMMON_ADB_CONNECTION_H__ +#define TOOLS_ANDROID_COMMON_ADB_CONNECTION_H__ +#pragma once + +namespace tools { + +// Creates a socket that can forward to a host socket through ADB. +// The format of forward_to is <port>:<ip_address>. +// Returns the socket handle, or -1 on any error. +int ConnectAdbHostSocket(const char* forward_to); + +} // namespace tools + +#endif // TOOLS_ANDROID_COMMON_ADB_CONNECTION_H__ + diff --git a/tools/android/common/common.gyp b/tools/android/common/common.gyp new file mode 100644 index 0000000..54e2976 --- /dev/null +++ b/tools/android/common/common.gyp @@ -0,0 +1,27 @@ +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'targets': [ + { + 'target_name': 'android_tools_common', + 'type': '<(library)', + 'include_dirs': [ + '..', + '../../..', + ], + 'sources': [ + 'adb_connection.cc', + 'daemon.cc', + 'net.cc', + ], + }, + ], +} + +# Local Variables: +# tab-width:2 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=2 shiftwidth=2: diff --git a/tools/android/common/daemon.cc b/tools/android/common/daemon.cc new file mode 100644 index 0000000..c332b1f --- /dev/null +++ b/tools/android/common/daemon.cc @@ -0,0 +1,70 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tools/android/common/daemon.h" + +#include <signal.h> +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> + +#include "base/command_line.h" +#include "base/eintr_wrapper.h" +#include "base/logging.h" + +namespace { + +const char kNoSpawnDaemon[] = "D"; + +int g_exit_status = 0; + +void Exit(int unused) { + _exit(g_exit_status); +} + +} // namespace + +namespace tools { + +bool HasHelpSwitch(const CommandLine& command_line) { + return command_line.HasSwitch("h") || command_line.HasSwitch("help"); +} + +bool HasNoSpawnDaemonSwitch(const CommandLine& command_line) { + return command_line.HasSwitch(kNoSpawnDaemon); +} + +void ShowHelp(const char* program, + const char* extra_title, + const char* extra_descriptions) { + printf("Usage: %s [-%s] %s\n" + " -%s stops from spawning a daemon process\n%s", + program, kNoSpawnDaemon, extra_title, kNoSpawnDaemon, + extra_descriptions); +} + +void SpawnDaemon(int exit_status) { + g_exit_status = exit_status; + signal(SIGUSR1, Exit); + + if (fork()) { + // In parent process. + sleep(10); // Wait for the child process to finish setsid(). + NOTREACHED(); + } + + // In child process. + setsid(); // Detach the child process from its parent. + kill(getppid(), SIGUSR1); // Inform the parent process to exit. + + // Close the standard input and outputs, otherwise the process may block + // adbd when the shell exits. + // Comment out these lines if you want to see outputs for debugging. + HANDLE_EINTR(close(0)); + HANDLE_EINTR(close(1)); + HANDLE_EINTR(close(2)); +} + +} // namespace tools + diff --git a/tools/android/common/daemon.h b/tools/android/common/daemon.h new file mode 100644 index 0000000..2bd25d4 --- /dev/null +++ b/tools/android/common/daemon.h @@ -0,0 +1,31 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TOOLS_ANDROID_COMMON_DAEMON_H__ +#define TOOLS_ANDROID_COMMON_DAEMON_H__ +#pragma once + +#include <string> +#include <vector> + +class CommandLine; + +namespace tools { + +bool HasHelpSwitch(const CommandLine& command_line); + +bool HasNoSpawnDaemonSwitch(const CommandLine& command_line); + +void ShowHelp(const char* program, + const char* extra_title, + const char* extra_descriptions); + +// Spawns a daemon process and exit the current process. +// Any code after this function will be executed in the spawned daemon process. +void SpawnDaemon(int exit_status); + +} // namespace tools + +#endif // TOOLS_ANDROID_COMMON_DAEMON_H__ + diff --git a/tools/android/common/net.cc b/tools/android/common/net.cc new file mode 100644 index 0000000..e50a18a --- /dev/null +++ b/tools/android/common/net.cc @@ -0,0 +1,25 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "tools/android/common/net.h" + +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <sys/socket.h> +#include <sys/types.h> + +namespace tools { + +int DisableNagle(int socket) { + int on = 1; + return setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); +} + +int DeferAccept(int socket) { + int on = 1; + return setsockopt(socket, IPPROTO_TCP, TCP_DEFER_ACCEPT, &on, sizeof(on)); +} + +} // namespace tools + diff --git a/tools/android/common/net.h b/tools/android/common/net.h new file mode 100644 index 0000000..d17b6f4 --- /dev/null +++ b/tools/android/common/net.h @@ -0,0 +1,21 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TOOLS_ANDROID_COMMON_NET_H_ +#define TOOLS_ANDROID_COMMON_NET_H_ +#pragma once + +namespace tools { + +// DisableNagle can improve TCP transmission performance. Both Chrome net stack +// and adb tool use it. +int DisableNagle(int socket); + +// Wake up listener only when data arrive. +int DeferAccept(int socket); + +} // namespace tools + +#endif // TOOLS_ANDROID_COMMON_NET_H_ + diff --git a/tools/android/forwarder/forwarder.cc b/tools/android/forwarder/forwarder.cc new file mode 100644 index 0000000..e19fe65 --- /dev/null +++ b/tools/android/forwarder/forwarder.cc @@ -0,0 +1,405 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <errno.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <pthread.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/select.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "base/command_line.h" +#include "base/eintr_wrapper.h" +#include "base/logging.h" +#include "tools/android/common/adb_connection.h" +#include "tools/android/common/daemon.h" +#include "tools/android/common/net.h" + +namespace { + +const pthread_t kInvalidThread = static_cast<pthread_t>(-1); +volatile bool g_killed = false; + +class Buffer { + public: + Buffer() + : bytes_read_(0), + write_offset_(0) { + } + + bool CanRead() { + return bytes_read_ == 0; + } + + bool CanWrite() { + return write_offset_ < bytes_read_; + } + + int Read(int fd) { + int ret = -1; + if (CanRead()) { + ret = HANDLE_EINTR(read(fd, buffer_, kBufferSize)); + if (ret > 0) + bytes_read_ = ret; + } + return ret; + } + + int Write(int fd) { + int ret = -1; + if (CanWrite()) { + ret = HANDLE_EINTR(write(fd, buffer_ + write_offset_, + bytes_read_ - write_offset_)); + if (ret > 0) { + write_offset_ += ret; + if (write_offset_ == bytes_read_) { + write_offset_ = 0; + bytes_read_ = 0; + } + } + } + return ret; + } + + private: + // A big buffer to let our file-over-http bridge work more like real file. + static const int kBufferSize = 1024 * 128; + int bytes_read_; + int write_offset_; + char buffer_[kBufferSize]; + + DISALLOW_COPY_AND_ASSIGN(Buffer); +}; + +class Server; + +struct ForwarderThreadInfo { + ForwarderThreadInfo(Server* a_server, int a_forwarder_index) + : server(a_server), + forwarder_index(a_forwarder_index) { + } + Server* server; + int forwarder_index; +}; + +struct ForwarderInfo { + time_t start_time; + int socket1; + time_t socket1_last_byte_time; + size_t socket1_bytes; + int socket2; + time_t socket2_last_byte_time; + size_t socket2_bytes; +}; + +class Server { + public: + Server() + : thread_(kInvalidThread), + socket_(-1) { + memset(forward_to_, 0, sizeof(forward_to_)); + memset(&forwarders_, 0, sizeof(forwarders_)); + } + + int GetFreeForwarderIndex() { + for (int i = 0; i < kMaxForwarders; i++) { + if (forwarders_[i].start_time == 0) + return i; + } + return -1; + } + + void DisposeForwarderInfo(int index) { + forwarders_[index].start_time = 0; + } + + ForwarderInfo* GetForwarderInfo(int index) { + return &forwarders_[index]; + } + + void DumpInformation() { + LOG(INFO) << "Server information: " << forward_to_; + LOG(INFO) << "No.: age up(bytes,idle) down(bytes,idle)"; + int count = 0; + time_t now = time(NULL); + for (int i = 0; i < kMaxForwarders; i++) { + const ForwarderInfo& info = forwarders_[i]; + if (info.start_time) { + count++; + LOG(INFO) << count << ": " << now - info.start_time << " up(" + << info.socket1_bytes << "," + << now - info.socket1_last_byte_time << " down(" + << info.socket2_bytes << "," + << now - info.socket2_last_byte_time << ")"; + } + } + } + + void Shutdown() { + if (socket_ >= 0) + shutdown(socket_, SHUT_RDWR); + } + + bool InitSocket(const char* arg); + + void StartThread() { + pthread_create(&thread_, NULL, ServerThread, this); + } + + void JoinThread() { + if (thread_ != kInvalidThread) + pthread_join(thread_, NULL); + } + + private: + static void* ServerThread(void* arg); + + // There are 3 kinds of threads that will access the array: + // 1. Server thread will get a free ForwarderInfo and initialize it; + // 2. Forwarder threads will dispose the ForwarderInfo when it finishes; + // 3. Main thread will iterate and print the forwarders. + // Using an array is not optimal, but can avoid locks or other complex + // inter-thread communication. + static const int kMaxForwarders = 512; + ForwarderInfo forwarders_[kMaxForwarders]; + + pthread_t thread_; + int socket_; + char forward_to_[40]; + + DISALLOW_COPY_AND_ASSIGN(Server); +}; + +// Forwards all outputs from one socket to another socket. +void* ForwarderThread(void* arg) { + ForwarderThreadInfo* thread_info = + reinterpret_cast<ForwarderThreadInfo*>(arg); + Server* server = thread_info->server; + int index = thread_info->forwarder_index; + delete thread_info; + ForwarderInfo* info = server->GetForwarderInfo(index); + int socket1 = info->socket1; + int socket2 = info->socket2; + int nfds = socket1 > socket2 ? socket1 + 1 : socket2 + 1; + fd_set read_fds; + fd_set write_fds; + Buffer buffer1; + Buffer buffer2; + + while (!g_killed) { + FD_ZERO(&read_fds); + if (buffer1.CanRead()) + FD_SET(socket1, &read_fds); + if (buffer2.CanRead()) + FD_SET(socket2, &read_fds); + + FD_ZERO(&write_fds); + if (buffer1.CanWrite()) + FD_SET(socket2, &write_fds); + if (buffer2.CanWrite()) + FD_SET(socket1, &write_fds); + + if (HANDLE_EINTR(select(nfds, &read_fds, &write_fds, NULL, NULL)) <= 0) { + LOG(ERROR) << "Select error: " << strerror(errno); + break; + } + + int now = time(NULL); + if (FD_ISSET(socket1, &read_fds)) { + info->socket1_last_byte_time = now; + int bytes = buffer1.Read(socket1); + if (bytes <= 0) + break; + info->socket1_bytes += bytes; + } + if (FD_ISSET(socket2, &read_fds)) { + info->socket2_last_byte_time = now; + int bytes = buffer2.Read(socket2); + if (bytes <= 0) + break; + info->socket2_bytes += bytes; + } + if (FD_ISSET(socket1, &write_fds)) { + if (buffer2.Write(socket1) <= 0) + break; + } + if (FD_ISSET(socket2, &write_fds)) { + if (buffer1.Write(socket2) <= 0) + break; + } + } + + HANDLE_EINTR(close(socket1)); + HANDLE_EINTR(close(socket2)); + server->DisposeForwarderInfo(index); + return NULL; +} + +// Listens to a server socket. On incoming request, forward it to the host. +// static +void* Server::ServerThread(void* arg) { + Server* server = reinterpret_cast<Server*>(arg); + while (!g_killed) { + int forwarder_index = server->GetFreeForwarderIndex(); + if (forwarder_index < 0) { + LOG(ERROR) << "Too many forwarders"; + continue; + } + + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + int socket = HANDLE_EINTR(accept(server->socket_, + reinterpret_cast<sockaddr*>(&addr), + &addr_len)); + if (socket < 0) { + LOG(ERROR) << "Failed to accept: " << strerror(errno); + break; + } + tools::DisableNagle(socket); + + int host_socket = tools::ConnectAdbHostSocket(server->forward_to_); + if (host_socket >= 0) { + // Set NONBLOCK flag because we use select(). + fcntl(socket, F_SETFL, fcntl(socket, F_GETFL) | O_NONBLOCK); + fcntl(host_socket, F_SETFL, fcntl(host_socket, F_GETFL) | O_NONBLOCK); + + ForwarderInfo* forwarder_info = server->GetForwarderInfo(forwarder_index); + time_t now = time(NULL); + forwarder_info->start_time = now; + forwarder_info->socket1 = socket; + forwarder_info->socket1_last_byte_time = now; + forwarder_info->socket1_bytes = 0; + forwarder_info->socket2 = host_socket; + forwarder_info->socket2_last_byte_time = now; + forwarder_info->socket2_bytes = 0; + + pthread_t thread; + pthread_create(&thread, NULL, ForwarderThread, + new ForwarderThreadInfo(server, forwarder_index)); + } else { + // Close the unused client socket which is failed to connect to host. + HANDLE_EINTR(close(socket)); + } + } + + HANDLE_EINTR(close(server->socket_)); + server->socket_ = -1; + return NULL; +} + +// Format of arg: <Device port>[:<Forward to port>:<Forward to address>] +bool Server::InitSocket(const char* arg) { + char* endptr; + int local_port = static_cast<int>(strtol(arg, &endptr, 10)); + if (local_port <= 0) + return false; + + if (*endptr != ':') { + snprintf(forward_to_, sizeof(forward_to_), "%d:127.0.0.1", local_port); + } else { + strncpy(forward_to_, endptr + 1, sizeof(forward_to_) - 1); + } + + socket_ = socket(AF_INET, SOCK_STREAM, 0); + if (socket_ < 0) { + perror("server socket"); + return false; + } + tools::DisableNagle(socket_); + + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(local_port); + int reuse_addr = 1; + setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, + &reuse_addr, sizeof(reuse_addr)); + tools::DeferAccept(socket_); + if (HANDLE_EINTR(bind(socket_, reinterpret_cast<sockaddr*>(&addr), + sizeof(addr))) < 0 || + HANDLE_EINTR(listen(socket_, 5)) < 0) { + perror("server bind"); + HANDLE_EINTR(close(socket_)); + socket_ = -1; + return false; + } + + printf("Forwarding device port %d to host %s\n", local_port, forward_to_); + return true; +} + +int g_server_count = 0; +Server* g_servers = NULL; + +void KillHandler(int unused) { + g_killed = true; + for (int i = 0; i < g_server_count; i++) + g_servers[i].Shutdown(); +} + +void DumpInformation(int unused) { + for (int i = 0; i < g_server_count; i++) + g_servers[i].DumpInformation(); +} + +} // namespace + +int main(int argc, char** argv) { + printf("Android device to host TCP forwarder\n"); + printf("Like 'adb forward' but in the reverse direction\n"); + + CommandLine command_line(argc, argv); + CommandLine::StringVector server_args = command_line.GetArgs(); + if (tools::HasHelpSwitch(command_line) || server_args.empty()) { + tools::ShowHelp( + argv[0], + "<Device port>[:<Forward to port>:<Forward to address>] ...", + " <Forward to port> default is <Device port>\n" + " <Forward to address> default is 127.0.0.1\n"); + return 0; + } + + g_servers = new Server[server_args.size()]; + g_server_count = 0; + int failed_count = 0; + for (size_t i = 0; i < server_args.size(); i++) { + if (!g_servers[g_server_count].InitSocket(server_args[i].c_str())) { + printf("Couldn't start forwarder server for port spec: %s\n", + server_args[i].c_str()); + ++failed_count; + } else { + ++g_server_count; + } + } + + if (g_server_count == 0) { + printf("No forwarder servers could be started. Exiting.\n"); + delete [] g_servers; + return failed_count; + } + + if (!tools::HasNoSpawnDaemonSwitch(command_line)) + tools::SpawnDaemon(failed_count); + + signal(SIGTERM, KillHandler); + signal(SIGUSR2, DumpInformation); + + for (int i = 0; i < g_server_count; i++) + g_servers[i].StartThread(); + for (int i = 0; i < g_server_count; i++) + g_servers[i].JoinThread(); + g_server_count = 0; + delete [] g_servers; + + return 0; +} + diff --git a/tools/android/forwarder/forwarder.gyp b/tools/android/forwarder/forwarder.gyp new file mode 100644 index 0000000..75390c3 --- /dev/null +++ b/tools/android/forwarder/forwarder.gyp @@ -0,0 +1,47 @@ +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +{ + 'targets': [ + { + 'target_name': 'forwarder', + 'type': 'none', + 'dependencies': [ + 'forwarder_symbols', + ], + 'actions': [ + { + 'action_name': 'strip_forwarder', + 'inputs': ['<(PRODUCT_DIR)/forwarder_symbols'], + 'outputs': ['<(PRODUCT_DIR)/forwarder'], + 'action': [ + '<!(/bin/echo -n $STRIP)', + '--strip-unneeded', + '<@(_inputs)', + '-o', + '<@(_outputs)', + ], + }, + ], + }, { + 'target_name': 'forwarder_symbols', + 'type': 'executable', + 'dependencies': [ + '../../../base/base.gyp:base', + '../common/common.gyp:android_tools_common', + ], + 'include_dirs': [ + '../../..', + ], + 'sources': [ + 'forwarder.cc', + ], + }, + ], +} + +# Local Variables: +# tab-width:2 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=2 shiftwidth=2: |