Move "adb shell" over to getopt(3), and allow -tt on old devices.
From the bug:
Say we run a new adb against an old device (like KitKat). Even with a new
client ADB, in this configuration, "adb shell" will create a remove tty
unconditionally. So if the user runs "adb shell -t -t", we shouldn't fail
with a message about the remote device not supporting -tT options --- the
user asked to create a tty unconditionally, and since we're going to create
a tty unconditionally, we should just succeed. (That it's going to succeed
due to protocol inadequacy instead of succeeding on purpose is irrelevant.)
That adb fails in this case makes scripts more complicated, since they can't
just pass "-t -t" unconditionally if they want a tty and to work on all
device versions, even if the script requires a new-ish adb locally.
Bug: http://b/32216152
Bug: http://b/32219151
Test: test_device.py
Change-Id: I8ab7c8dfa212209a7ab43c1f0832eeac26d2e42f
diff --git a/adb/commandline.cpp b/adb/commandline.cpp
index 54e254a..fe5d280 100644
--- a/adb/commandline.cpp
+++ b/adb/commandline.cpp
@@ -686,100 +686,100 @@
static int adb_shell(int argc, const char** argv) {
FeatureSet features;
std::string error;
-
if (!adb_get_feature_set(&features, &error)) {
fprintf(stderr, "error: %s\n", error.c_str());
return 1;
}
- bool use_shell_protocol = CanUseFeature(features, kFeatureShell2);
- if (!use_shell_protocol) {
- D("shell protocol not supported, using raw data transfer");
- } else {
- D("using shell protocol");
- }
+ enum PtyAllocationMode { kPtyAuto, kPtyNo, kPtyYes, kPtyDefinitely };
+
+ // Defaults.
+ char escape_char = '~'; // -e
+ bool use_shell_protocol = CanUseFeature(features, kFeatureShell2); // -x
+ PtyAllocationMode tty = use_shell_protocol ? kPtyAuto : kPtyDefinitely; // -t/-T
// Parse shell-specific command-line options.
- // argv[0] is always "shell".
- --argc;
- ++argv;
- int t_arg_count = 0;
- char escape_char = '~';
- while (argc) {
- if (!strcmp(argv[0], "-e")) {
- if (argc < 2 || !(strlen(argv[1]) == 1 || strcmp(argv[1], "none") == 0)) {
- fprintf(stderr, "error: -e requires a single-character argument or 'none'\n");
+ argv[0] = "adb shell"; // So getopt(3) error messages start "adb shell".
+ optind = 1; // argv[0] is always "shell", so set `optind` appropriately.
+ int opt;
+ while ((opt = getopt(argc, const_cast<char**>(argv), "+e:ntTx")) != -1) {
+ switch (opt) {
+ case 'e':
+ if (!(strlen(optarg) == 1 || strcmp(optarg, "none") == 0)) {
+ fprintf(stderr, "error: -e requires a single-character argument or 'none'\n");
+ return 1;
+ }
+ escape_char = (strcmp(optarg, "none") == 0) ? 0 : optarg[0];
+ break;
+ case 'n':
+ close_stdin();
+ break;
+ case 'x':
+ // This option basically asks for historical behavior, so set options that
+ // correspond to the historical defaults. This is slightly weird in that -Tx
+ // is fine (because we'll undo the -T) but -xT isn't, but that does seem to
+ // be our least worst choice...
+ use_shell_protocol = false;
+ tty = kPtyDefinitely;
+ escape_char = '~';
+ break;
+ case 't':
+ // Like ssh, -t arguments are cumulative so that multiple -t's
+ // are needed to force a PTY.
+ tty = (tty >= kPtyYes) ? kPtyDefinitely : kPtyYes;
+ break;
+ case 'T':
+ tty = kPtyNo;
+ break;
+ default:
+ // getopt(3) already printed an error message for us.
return 1;
- }
- escape_char = (strcmp(argv[1], "none") == 0) ? 0 : argv[1][0];
- argc -= 2;
- argv += 2;
- } else if (!strcmp(argv[0], "-T") || !strcmp(argv[0], "-t")) {
- // Like ssh, -t arguments are cumulative so that multiple -t's
- // are needed to force a PTY.
- if (argv[0][1] == 't') {
- ++t_arg_count;
- } else {
- t_arg_count = -1;
- }
- --argc;
- ++argv;
- } else if (!strcmp(argv[0], "-x")) {
- use_shell_protocol = false;
- --argc;
- ++argv;
- } else if (!strcmp(argv[0], "-n")) {
- close_stdin();
-
- --argc;
- ++argv;
- } else {
- break;
}
}
- // Legacy shell protocol requires a remote PTY to close the subprocess properly which creates
- // some weird interactions with -tT.
- if (!use_shell_protocol && t_arg_count != 0) {
- if (!CanUseFeature(features, kFeatureShell2)) {
- fprintf(stderr, "error: target doesn't support PTY args -Tt\n");
- } else {
- fprintf(stderr, "error: PTY args -Tt cannot be used with -x\n");
- }
- return 1;
- }
+ bool is_interactive = (optind == argc);
- std::string shell_type_arg;
- if (CanUseFeature(features, kFeatureShell2)) {
- if (t_arg_count < 0) {
+ std::string shell_type_arg = kShellServiceArgPty;
+ if (tty == kPtyNo) {
+ shell_type_arg = kShellServiceArgRaw;
+ } else if (tty == kPtyAuto) {
+ // If stdin isn't a TTY, default to a raw shell; this lets
+ // things like `adb shell < my_script.sh` work as expected.
+ // Non-interactive shells should also not have a pty.
+ if (!unix_isatty(STDIN_FILENO) || !is_interactive) {
shell_type_arg = kShellServiceArgRaw;
- } else if (t_arg_count == 0) {
- // If stdin isn't a TTY, default to a raw shell; this lets
- // things like `adb shell < my_script.sh` work as expected.
- // Otherwise leave |shell_type_arg| blank which uses PTY for
- // interactive shells and raw for non-interactive.
- if (!unix_isatty(STDIN_FILENO)) {
- shell_type_arg = kShellServiceArgRaw;
- }
- } else if (t_arg_count == 1) {
- // A single -t arg isn't enough to override implicit -T.
- if (!unix_isatty(STDIN_FILENO)) {
- fprintf(stderr,
- "Remote PTY will not be allocated because stdin is not a terminal.\n"
- "Use multiple -t options to force remote PTY allocation.\n");
- shell_type_arg = kShellServiceArgRaw;
- } else {
- shell_type_arg = kShellServiceArgPty;
- }
+ }
+ } else if (tty == kPtyYes) {
+ // A single -t arg isn't enough to override implicit -T.
+ if (!unix_isatty(STDIN_FILENO)) {
+ fprintf(stderr,
+ "Remote PTY will not be allocated because stdin is not a terminal.\n"
+ "Use multiple -t options to force remote PTY allocation.\n");
+ shell_type_arg = kShellServiceArgRaw;
+ }
+ }
+
+ D("shell -e 0x%x t=%d use_shell_protocol=%s shell_type_arg=%s\n",
+ escape_char, tty,
+ use_shell_protocol ? "true" : "false",
+ (shell_type_arg == kShellServiceArgPty) ? "pty" : "raw");
+
+ // Raw mode is only supported when talking to a new device *and* using the shell protocol.
+ if (!use_shell_protocol) {
+ if (shell_type_arg != kShellServiceArgPty) {
+ fprintf(stderr, "error: %s only supports allocating a pty\n",
+ !CanUseFeature(features, kFeatureShell2) ? "device" : "-x");
+ return 1;
} else {
- shell_type_arg = kShellServiceArgPty;
+ // If we're not using the shell protocol, the type argument must be empty.
+ shell_type_arg = "";
}
}
std::string command;
- if (argc) {
+ if (optind < argc) {
// We don't escape here, just like ssh(1). http://b/20564385.
- command = android::base::Join(std::vector<const char*>(argv, argv + argc), ' ');
+ command = android::base::Join(std::vector<const char*>(argv + optind, argv + argc), ' ');
}
return RemoteShell(use_shell_protocol, shell_type_arg, escape_char, command);
@@ -1401,7 +1401,7 @@
return !CanUseFeature(features, kFeatureCmd);
}
-int adb_commandline(int argc, const char **argv) {
+int adb_commandline(int argc, const char** argv) {
int no_daemon = 0;
int is_daemon = 0;
int is_server = 0;
diff --git a/adb/test_device.py b/adb/test_device.py
index b12bf88..4c5563f 100644
--- a/adb/test_device.py
+++ b/adb/test_device.py
@@ -371,15 +371,8 @@
def test_pty_logic(self):
"""Tests that a PTY is allocated when it should be.
- PTY allocation behavior should match ssh; some behavior requires
- a terminal stdin to test so this test will be skipped if stdin
- is not a terminal.
+ PTY allocation behavior should match ssh.
"""
- if not self.device.has_shell_protocol():
- raise unittest.SkipTest('PTY arguments unsupported on this device')
- if not os.isatty(sys.stdin.fileno()):
- raise unittest.SkipTest('PTY tests require stdin terminal')
-
def check_pty(args):
"""Checks adb shell PTY allocation.
@@ -409,16 +402,34 @@
# -T: never allocate PTY.
self.assertEqual((False, False), check_pty(['-T']))
- # No args: PTY only if stdin is a terminal and shell is interactive,
- # which is difficult to reliably test from a script.
- self.assertEqual((False, False), check_pty([]))
+ # These tests require a new device.
+ if self.device.has_shell_protocol() and os.isatty(sys.stdin.fileno()):
+ # No args: PTY only if stdin is a terminal and shell is interactive,
+ # which is difficult to reliably test from a script.
+ self.assertEqual((False, False), check_pty([]))
- # -t: PTY if stdin is a terminal.
- self.assertEqual((True, False), check_pty(['-t']))
+ # -t: PTY if stdin is a terminal.
+ self.assertEqual((True, False), check_pty(['-t']))
# -t -t: always allocate PTY.
self.assertEqual((True, True), check_pty(['-t', '-t']))
+ # -tt: always allocate PTY, POSIX style (http://b/32216152).
+ self.assertEqual((True, True), check_pty(['-tt']))
+
+ # -ttt: ssh has weird even/odd behavior with multiple -t flags, but
+ # we follow the man page instead.
+ self.assertEqual((True, True), check_pty(['-ttt']))
+
+ # -ttx: -x and -tt aren't incompatible (though -Tx would be an error).
+ self.assertEqual((True, True), check_pty(['-ttx']))
+
+ # -Ttt: -tt cancels out -T.
+ self.assertEqual((True, True), check_pty(['-Ttt']))
+
+ # -ttT: -T cancels out -tt.
+ self.assertEqual((False, False), check_pty(['-ttT']))
+
def test_shell_protocol(self):
"""Tests the shell protocol on the device.