adb: non-interactive shell stdin.
Non-interactive `adb shell` previously only read from the remote shell,
but we want it to write as well so interactive and non-interactive
shells can both send data. With this CL, we can now do:
$ echo foo | adb shell cat
foo
This is primarily usable with newer devices that support the shell_v2
features. Older devices will receive stdin but the shell will still
hang after all input has been sent, requiring user Ctrl+C. This seems
better than closing communication altogether which could potentially
miss an unpredictable amount of return data by closing too early.
Known issue: non-interactive stdin to a PTY shell isn't reliable.
However I don't think this is a common case as ssh doesn't seem to
handle it properly either. Examples:
* echo 'echo foo' | adb shell
* echo 'foo' | adb shell -t cat
Bug: http://b/24565284
Change-Id: I5b017fd12d8478765bb6e8400ea76d535c24ce42
diff --git a/adb/commandline.cpp b/adb/commandline.cpp
index 0531cf9..bc5ba38 100644
--- a/adb/commandline.cpp
+++ b/adb/commandline.cpp
@@ -425,6 +425,7 @@
// Used to pass multiple values to the stdin read thread.
struct StdinReadArgs {
int stdin_fd, write_fd;
+ bool raw_stdin;
std::unique_ptr<ShellProtocol> protocol;
};
@@ -452,26 +453,42 @@
D("stdin_read_thread(): pre unix_read(fdi=%d,...)", args->stdin_fd);
int r = unix_read(args->stdin_fd, buffer_ptr, buffer_size);
D("stdin_read_thread(): post unix_read(fdi=%d,...)", args->stdin_fd);
- if (r <= 0) break;
- for (int n = 0; n < r; n++){
- switch(buffer_ptr[n]) {
- case '\n':
- state = 1;
- break;
- case '\r':
- state = 1;
- break;
- case '~':
- if(state == 1) state++;
- break;
- case '.':
- if(state == 2) {
- fprintf(stderr,"\n* disconnect *\n");
- stdin_raw_restore(args->stdin_fd);
- exit(0);
+ if (r <= 0) {
+ // Only devices using the shell protocol know to close subprocess
+ // stdin. For older devices we want to just leave the connection
+ // open, otherwise an unpredictable amount of return data could
+ // be lost due to the FD closing before all data has been received.
+ if (args->protocol) {
+ args->protocol->Write(ShellProtocol::kIdCloseStdin, 0);
+ }
+ break;
+ }
+ // If we made stdin raw, check input for the "~." escape sequence. In
+ // this situation signals like Ctrl+C are sent remotely rather than
+ // interpreted locally so this provides an emergency out if the remote
+ // process starts ignoring the signal. SSH also does this, see the
+ // "escape characters" section on the ssh man page for more info.
+ if (args->raw_stdin) {
+ for (int n = 0; n < r; n++){
+ switch(buffer_ptr[n]) {
+ case '\n':
+ state = 1;
+ break;
+ case '\r':
+ state = 1;
+ break;
+ case '~':
+ if(state == 1) state++;
+ break;
+ case '.':
+ if(state == 2) {
+ stdin_raw_restore(args->stdin_fd);
+ fprintf(stderr,"\n* disconnect *\n");
+ exit(0);
+ }
+ default:
+ state = 0;
}
- default:
- state = 0;
}
}
if (args->protocol) {
@@ -488,8 +505,44 @@
return nullptr;
}
-static int interactive_shell(const std::string& service_string,
- bool use_shell_protocol) {
+// Returns a shell service string with the indicated arguments and command.
+static std::string ShellServiceString(bool use_shell_protocol,
+ const std::string& type_arg,
+ const std::string& command) {
+ std::vector<std::string> args;
+ if (use_shell_protocol) {
+ args.push_back(kShellServiceArgShellProtocol);
+ }
+ if (!type_arg.empty()) {
+ args.push_back(type_arg);
+ }
+
+ // Shell service string can look like: shell[,arg1,arg2,...]:[command].
+ return android::base::StringPrintf("shell%s%s:%s",
+ args.empty() ? "" : ",",
+ android::base::Join(args, ',').c_str(),
+ command.c_str());
+}
+
+// Connects to a shell on the device and read/writes data.
+//
+// Note: currently this function doesn't properly clean up resources; the
+// FD connected to the adb server is never closed and the stdin read thread
+// may never exit.
+//
+// On success returns the remote exit code if |use_shell_protocol| is true,
+// 0 otherwise. On failure returns 1.
+static int RemoteShell(bool use_shell_protocol, const std::string& type_arg,
+ const std::string& command) {
+ std::string service_string = ShellServiceString(use_shell_protocol,
+ type_arg, command);
+
+ // Make local stdin raw if the device allocates a PTY, which happens if:
+ // 1. We are explicitly asking for a PTY shell, or
+ // 2. We don't specify shell type and are starting an interactive session.
+ bool raw_stdin = (type_arg == kShellServiceArgPty ||
+ (type_arg.empty() && command.empty()));
+
std::string error;
int fd = adb_connect(service_string, &error);
if (fd < 0) {
@@ -502,13 +555,16 @@
LOG(ERROR) << "couldn't allocate StdinReadArgs object";
return 1;
}
- args->stdin_fd = 0;
+ args->stdin_fd = STDIN_FILENO;
args->write_fd = fd;
+ args->raw_stdin = raw_stdin;
if (use_shell_protocol) {
args->protocol.reset(new ShellProtocol(args->write_fd));
}
- stdin_raw_init(args->stdin_fd);
+ if (raw_stdin) {
+ stdin_raw_init(STDIN_FILENO);
+ }
int exit_code = 0;
if (!adb_thread_create(stdin_read_thread, args)) {
@@ -519,7 +575,12 @@
exit_code = read_and_dump(fd, use_shell_protocol);
}
- stdin_raw_restore(args->stdin_fd);
+ if (raw_stdin) {
+ stdin_raw_restore(STDIN_FILENO);
+ }
+
+ // TODO(dpursell): properly exit stdin_read_thread and close |fd|.
+
return exit_code;
}
@@ -795,25 +856,6 @@
return adb_command(cmd);
}
-// Returns a shell service string with the indicated arguments and command.
-static std::string ShellServiceString(bool use_shell_protocol,
- const std::string& type_arg,
- const std::string& command) {
- std::vector<std::string> args;
- if (use_shell_protocol) {
- args.push_back(kShellServiceArgShellProtocol);
- }
- if (!type_arg.empty()) {
- args.push_back(type_arg);
- }
-
- // Shell service string can look like: shell[,arg1,arg2,...]:[command].
- return android::base::StringPrintf("shell%s%s:%s",
- args.empty() ? "" : ",",
- android::base::Join(args, ',').c_str(),
- command.c_str());
-}
-
// Connects to the device "shell" service with |command| and prints the
// resulting output.
static int send_shell_command(TransportType transport_type, const char* serial,
@@ -1320,51 +1362,26 @@
}
}
+ std::string command;
+ if (argc) {
+ // We don't escape here, just like ssh(1). http://b/20564385.
+ command = android::base::Join(
+ std::vector<const char*>(argv, argv + argc), ' ');
+ }
+
if (h) {
printf("\x1b[41;33m");
fflush(stdout);
}
- if (!argc) {
- D("starting interactive shell");
- std::string service_string =
- ShellServiceString(use_shell_protocol, shell_type_arg, "");
- r = interactive_shell(service_string, use_shell_protocol);
- if (h) {
- printf("\x1b[0m");
- fflush(stdout);
- }
- return r;
+ r = RemoteShell(use_shell_protocol, shell_type_arg, command);
+
+ if (h) {
+ printf("\x1b[0m");
+ fflush(stdout);
}
- // We don't escape here, just like ssh(1). http://b/20564385.
- std::string command = android::base::Join(
- std::vector<const char*>(argv, argv + argc), ' ');
- std::string service_string =
- ShellServiceString(use_shell_protocol, shell_type_arg, command);
-
- while (true) {
- D("non-interactive shell loop. cmd=%s", service_string.c_str());
- std::string error;
- int fd = adb_connect(service_string, &error);
- int r;
- if (fd >= 0) {
- D("about to read_and_dump(fd=%d)", fd);
- r = read_and_dump(fd, use_shell_protocol);
- D("read_and_dump() done.");
- adb_close(fd);
- } else {
- fprintf(stderr,"error: %s\n", error.c_str());
- r = -1;
- }
-
- if (h) {
- printf("\x1b[0m");
- fflush(stdout);
- }
- D("non-interactive shell loop. return r=%d", r);
- return r;
- }
+ return r;
}
else if (!strcmp(argv[0], "exec-in") || !strcmp(argv[0], "exec-out")) {
int exec_in = !strcmp(argv[0], "exec-in");