Compare commits

...

7 Commits

Author SHA1 Message Date
fef84849bc Show a friendly hint for adb installation
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-01-26 15:07:53 +01:00
96bd2c974d Do not report workarounds errors
Some workarounds are needed on some devices. But applying them may cause
exceptions on other devices, where they are not necessary anyway.

Do not report these errors in release builds.

Closes #994 <https://github.com/Genymobile/scrcpy/issues/994>
2020-01-19 15:52:24 +01:00
dc7fcf3c7a Accept port range
Accept a range of ports to listen to, so that it does not fail if
another instance of scrcpy is currently starting.

The range can be passed via the command line:

    scrcpy -p 27183:27186
    scrcpy -p 27183  # implicitly 27183:27183, as before

The default is 27183:27199.

Closes #951 <https://github.com/Genymobile/scrcpy/issues/951>
2020-01-18 17:21:00 +01:00
2a3a9d4ea9 Add util function to parse a list of integers
This will help parsing arguments like '1234:5678' into a list of
integers.
2020-01-18 17:21:00 +01:00
ca0031cbde Refactor server tunnel initialization
Start the server socket in enable_tunnel() directly.

For the caller point of view, enabling the tunnel opens a port (either
the server socket locally or the "adb forward" process).
2020-01-18 17:21:00 +01:00
d1a9a76cc6 Reorder functions
Move functions so that they can be called from enable_tunnel() (in the
following commit).
2020-01-18 17:21:00 +01:00
a8ceaf5284 Fix include order 2020-01-17 21:00:14 +01:00
17 changed files with 336 additions and 72 deletions

View File

@ -96,9 +96,10 @@ conf.set_quoted('PREFIX', get_option('prefix'))
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))
# the default client TCP port for the "adb reverse" tunnel
# the default client TCP port range for the "adb reverse" tunnel
# overridden by option --port
conf.set('DEFAULT_LOCAL_PORT', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default max video size for both dimensions, in pixels
# overridden by option --max-size

View File

@ -60,10 +60,10 @@ Disable device control (mirror the device in read\-only).
Do not display device (only when screen recording is enabled).
.TP
.BI "\-p, \-\-port " port
Set the TCP port the client listens on.
.BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen.
Default is 27183.
Default is 27183:27199.
.TP
.B \-\-prefer\-text

View File

@ -58,9 +58,9 @@ scrcpy_print_usage(const char *arg0) {
" Do not display device (only when screen recording is\n"
" enabled).\n"
"\n"
" -p, --port port\n"
" Set the TCP port the client listens on.\n"
" Default is %d.\n"
" -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n"
"\n"
" --prefer-text\n"
" Inject alpha characters and space as text events instead of\n"
@ -193,7 +193,7 @@ scrcpy_print_usage(const char *arg0) {
arg0,
DEFAULT_BIT_RATE,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT);
DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST);
}
static bool
@ -221,6 +221,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
return true;
}
static size_t
parse_integers_arg(const char *s, size_t max_items, long *out, long min,
long max, const char *name) {
size_t count = parse_integers(s, ':', max_items, out);
if (!count) {
LOGE("Could not parse %s: %s", name, s);
return 0;
}
for (size_t i = 0; i < count; ++i) {
long value = out[i];
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return 0;
}
}
return count;
}
static bool
parse_bit_rate(const char *s, uint32_t *bit_rate) {
long value;
@ -286,14 +307,30 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
}
static bool
parse_port(const char *s, uint16_t *port) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port");
if (!ok) {
parse_port_range(const char *s, struct port_range *port_range) {
long values[2];
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
if (!count) {
return false;
}
*port = (uint16_t) value;
uint16_t v0 = (uint16_t) values[0];
if (count == 1) {
port_range->first = v0;
port_range->last = v0;
return true;
}
assert(count == 2);
uint16_t v1 = (uint16_t) values[1];
if (v0 < v1) {
port_range->first = v0;
port_range->last = v1;
} else {
port_range->first = v1;
port_range->last = v0;
}
return true;
}
@ -424,7 +461,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
opts->display = false;
break;
case 'p':
if (!parse_port(optarg, &opts->port)) {
if (!parse_port_range(optarg, &opts->port_range)) {
return false;
}
break;

View File

@ -55,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
return idx;
}
static void
show_adb_installation_msg() {
#ifndef __WINDOWS__
static const struct {
const char *binary;
const char *command;
} pkg_managers[] = {
{"apt", "apt install adb"},
{"apt-get", "apt-get install adb"},
{"brew", "brew cask install android-platform-tools"},
{"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"},
};
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
if (cmd_search(pkg_managers[i].binary)) {
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
return;
}
}
#endif
LOGI("You may download and install 'adb' from "
"https://developer.android.com/studio/releases/platform-tools");
}
static void
show_adb_err_msg(enum process_result err, const char *const argv[]) {
char buf[512];
@ -68,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
show_adb_installation_msg();
break;
case PROCESS_SUCCESS:
// do nothing

View File

@ -43,6 +43,11 @@ enum process_result {
PROCESS_ERROR_MISSING_BINARY,
};
#ifndef __WINDOWS__
bool
cmd_search(const char *file);
#endif
enum process_result
cmd_execute(const char *const argv[], process_t *process);

View File

@ -27,4 +27,9 @@ struct position {
struct point point;
};
struct port_range {
uint16_t first;
uint16_t last;
};
#endif

View File

@ -280,7 +280,7 @@ scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
struct server_params params = {
.crop = options->crop,
.local_port = options->port,
.port_range = options->port_range,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,

View File

@ -5,6 +5,7 @@
#include <stdint.h>
#include "config.h"
#include "common.h"
#include "input_manager.h"
#include "recorder.h"
@ -15,7 +16,7 @@ struct scrcpy_options {
const char *window_title;
const char *push_target;
enum recorder_format record_format;
uint16_t port;
struct port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
@ -41,7 +42,10 @@ struct scrcpy_options {
.window_title = NULL, \
.push_target = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \
.port = DEFAULT_LOCAL_PORT, \
.port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \
.max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \

View File

@ -126,17 +126,6 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
return process_check_success(process, "adb forward --remove");
}
static bool
enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return true;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = true;
return enable_tunnel_forward(server->serial, server->local_port);
}
static bool
disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
@ -145,6 +134,100 @@ disable_tunnel(struct server *server) {
return disable_tunnel_reverse(server->serial);
}
static socket_t
listen_on_port(uint16_t port) {
#define IPV4_LOCALHOST 0x7F000001
return net_listen(IPV4_LOCALHOST, port, 1);
}
static bool
enable_tunnel_reverse_any_port(struct server *server,
struct port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) {
// the command itself failed, it will fail on any port
return false;
}
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(port);
if (server->server_socket != INVALID_SOCKET) {
// success
server->local_port = port;
return true;
}
// failure, disable tunnel and try another port
if (!disable_tunnel_reverse(server->serial)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
// check before incrementing to avoid overflow on port 65535
if (port < port_range.last) {
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
port, port + 1);
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not listen on port %" PRIu16, port_range.first);
} else {
LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_forward_any_port(struct server *server,
struct port_range port_range) {
server->tunnel_forward = true;
uint16_t port = port_range.first;
for (;;) {
if (enable_tunnel_forward(server->serial, port)) {
// success
server->local_port = port;
return true;
}
if (port < port_range.last) {
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
port, port + 1);
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not forward port %" PRIu16, port_range.first);
} else {
LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
if (enable_tunnel_reverse_any_port(server, port_range)) {
return true;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
LOGW("'adb reverse' failed, fallback to 'adb forward'");
return enable_tunnel_forward_any_port(server, port_range);
}
static process_t
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
@ -187,13 +270,6 @@ execute_server(struct server *server, const struct server_params *params) {
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
#define IPV4_LOCALHOST 0x7F000001
static socket_t
listen_on_port(uint16_t port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t
connect_and_read_byte(uint16_t port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
@ -247,7 +323,7 @@ server_init(struct server *server) {
bool
server_start(struct server *server, const char *serial,
const struct server_params *params) {
server->local_port = params->local_port;
server->port_range = params->port_range;
if (serial) {
server->serial = SDL_strdup(serial);
@ -261,30 +337,11 @@ server_start(struct server *server, const char *serial,
return false;
}
if (!enable_tunnel(server)) {
if (!enable_tunnel_any_port(server, params->port_range)) {
SDL_free(server->serial);
return false;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!server->tunnel_forward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(params->local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, params->local_port);
disable_tunnel(server);
SDL_free(server->serial);
return false;
}
}
// server will connect to our server socket
server->process = execute_server(server, params);

View File

@ -6,6 +6,7 @@
#include "config.h"
#include "command.h"
#include "common.h"
#include "util/net.h"
struct server {
@ -14,25 +15,30 @@ struct server {
socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket;
socket_t control_socket;
uint16_t local_port;
struct port_range port_range;
uint16_t local_port; // selected from port_range
bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
};
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
.port_range = { \
.first = 0, \
.last = 0, \
}, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
}
struct server_params {
const char *crop;
uint16_t local_port;
struct port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;

View File

@ -14,6 +14,7 @@
#include <limits.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
@ -21,6 +22,42 @@
#include "util/log.h"
bool
cmd_search(const char *file) {
char *path = getenv("PATH");
if (!path)
return false;
path = strdup(path);
if (!path)
return false;
bool ret = false;
size_t file_len = strlen(file);
char *saveptr;
for (char *dir = strtok_r(path, ":", &saveptr); dir;
dir = strtok_r(NULL, ":", &saveptr)) {
size_t dir_len = strlen(dir);
char *fullpath = malloc(dir_len + file_len + 2);
if (!fullpath)
continue;
memcpy(fullpath, dir, dir_len);
fullpath[dir_len] = '/';
memcpy(fullpath + dir_len + 1, file, file_len + 1);
struct stat sb;
bool fullpath_executable = stat(fullpath, &sb) == 0 &&
sb.st_mode & S_IXUSR;
free(fullpath);
if (fullpath_executable) {
ret = true;
break;
}
}
free(path);
return ret;
}
enum process_result
cmd_execute(const char *const argv[], pid_t *pid) {
int fd[2];

View File

@ -1,11 +1,11 @@
#include "command.h"
#include <sys/stat.h>
#include "config.h"
#include "util/log.h"
#include "util/str_util.h"
#include <sys/stat.h>
static int
build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF:

View File

@ -81,6 +81,35 @@ parse_integer(const char *s, long *out) {
return true;
}
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out) {
size_t count = 0;
char *endptr;
do {
errno = 0;
long value = strtol(s, &endptr, 0);
if (errno == ERANGE) {
return 0;
}
if (endptr == s || (*endptr != sep && *endptr != '\0')) {
return 0;
}
out[count++] = value;
if (*endptr == sep) {
if (count >= max_items) {
// max items already reached, could not accept a new item
return 0;
}
// parse the next token during the next iteration
s = endptr + 1;
}
} while (*endptr != '\0');
return count;
}
bool
parse_integer_with_suffix(const char *s, long *out) {
char *endptr;

View File

@ -31,6 +31,11 @@ strquote(const char *src);
bool
parse_integer(const char *s, long *out);
// parse s as integers separated by sep (for example '1234:2000')
// returns the number of integers on success, 0 on failure
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out);
// parse s as an integer into value
// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
// suffix

View File

@ -50,7 +50,7 @@ static void test_options(void) {
"--max-size", "1024",
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen"
"--port", "1234",
"--port", "1234:1236",
"--push-target", "/sdcard/Movies",
"--record", "file",
"--record-format", "mkv",
@ -78,7 +78,8 @@ static void test_options(void) {
assert(opts->fullscreen);
assert(opts->max_fps == 30);
assert(opts->max_size == 1024);
assert(opts->port == 1234);
assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == RECORDER_FORMAT_MKV);

View File

@ -187,6 +187,55 @@ static void test_parse_integer(void) {
assert(!ok); // out-of-range
}
static void test_parse_integers(void) {
long values[5];
size_t count = parse_integers("1234", ':', 5, values);
assert(count == 1);
assert(values[0] == 1234);
count = parse_integers("1234:5678", ':', 5, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:-5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == -5678);
count = parse_integers("1:2:3:4:5", ':', 5, values);
assert(count == 5);
assert(values[0] == 1);
assert(values[1] == 2);
assert(values[2] == 3);
assert(values[3] == 4);
assert(values[4] == 5);
count = parse_integers("1234:5678", ':', 1, values);
assert(count == 0); // max_items == 1
count = parse_integers("1:2:3:4:5", ':', 3, values);
assert(count == 0); // max_items == 3
count = parse_integers(":1234", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 1, values);
assert(count == 0); // invalid, even when max_items == 1
count = parse_integers("1234::5678", ':', 5, values);
assert(count == 0); // invalid
}
static void test_parse_integer_with_suffix(void) {
long value;
bool ok = parse_integer_with_suffix("1234", &value);
@ -249,6 +298,7 @@ int main(void) {
test_strquote();
test_utf8_truncate();
test_parse_integer();
test_parse_integers();
test_parse_integer_with_suffix();
return 0;
}

View File

@ -73,7 +73,7 @@ public final class Workarounds {
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.w("Could not fill app info: " + throwable.getMessage());
Ln.d("Could not fill app info: " + throwable.getMessage());
}
}
}