Commit c2e31ff5 authored by perezju's avatar perezju Committed by Commit bot

Reland of 'New run shell implementation for DeviceUtils'

This is a reland of https://codereview.chromium.org/659533002/

Previous CL broke telemetry unittests and perf unittests because it
didn't handle properly shell commands which may end with a newline
character when check_rc is not None.

This revised patch strips any whitespace at the end of shell commands
to prevent this.

BUG=267773

Review URL: https://codereview.chromium.org/669573003

Cr-Commit-Position: refs/heads/master@{#300322}
parent ae18399f
...@@ -9,6 +9,7 @@ import os ...@@ -9,6 +9,7 @@ import os
import pipes import pipes
import select import select
import signal import signal
import string
import StringIO import StringIO
import subprocess import subprocess
import time import time
...@@ -19,6 +20,52 @@ try: ...@@ -19,6 +20,52 @@ try:
except ImportError: except ImportError:
fcntl = None fcntl = None
_SafeShellChars = frozenset(string.ascii_letters + string.digits + '@%_-+=:,./')
def SingleQuote(s):
"""Return an shell-escaped version of the string using single quotes.
Reliably quote a string which may contain unsafe characters (e.g. space,
quote, or other special characters such as '$').
The returned value can be used in a shell command line as one token that gets
to be interpreted literally.
Args:
s: The string to quote.
Return:
The string quoted using single quotes.
"""
return pipes.quote(s)
def DoubleQuote(s):
"""Return an shell-escaped version of the string using double quotes.
Reliably quote a string which may contain unsafe characters (e.g. space
or quote characters), while retaining some shell features such as variable
interpolation.
The returned value can be used in a shell command line as one token that gets
to be further interpreted by the shell.
The set of characters that retain their special meaning may depend on the
shell implementation. This set usually includes: '$', '`', '\', '!', '*',
and '@'.
Args:
s: The string to quote.
Return:
The string quoted using double quotes.
"""
if not s:
return '""'
elif all(c in _SafeShellChars for c in s):
return s
else:
return '"' + s.replace('"', '\\"') + '"'
def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None): def Popen(args, stdout=None, stderr=None, shell=None, cwd=None, env=None):
return subprocess.Popen( return subprocess.Popen(
...@@ -88,7 +135,7 @@ def GetCmdStatusAndOutput(args, cwd=None, shell=False): ...@@ -88,7 +135,7 @@ def GetCmdStatusAndOutput(args, cwd=None, shell=False):
elif shell: elif shell:
raise Exception('array args must be run with shell=False') raise Exception('array args must be run with shell=False')
else: else:
args_repr = ' '.join(map(pipes.quote, args)) args_repr = ' '.join(map(SingleQuote, args))
s = '[host]' s = '[host]'
if cwd: if cwd:
......
# Copyright 2013 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.
"""Tests for the cmd_helper module."""
import unittest
from pylib import cmd_helper
# TODO(jbudorick) Make these tests run on the bots.
class CmdHelperSingleQuoteTest(unittest.TestCase):
def testSingleQuote_basic(self):
self.assertEquals('hello',
cmd_helper.SingleQuote('hello'))
def testSingleQuote_withSpaces(self):
self.assertEquals("'hello world'",
cmd_helper.SingleQuote('hello world'))
def testSingleQuote_withUnsafeChars(self):
self.assertEquals("""'hello'"'"'; rm -rf /'""",
cmd_helper.SingleQuote("hello'; rm -rf /"))
def testSingleQuote_dontExpand(self):
test_string = 'hello $TEST_VAR'
cmd = 'TEST_VAR=world; echo %s' % cmd_helper.SingleQuote(test_string)
self.assertEquals(test_string,
cmd_helper.GetCmdOutput(cmd, shell=True).rstrip())
class CmdHelperDoubleQuoteTest(unittest.TestCase):
def testDoubleQuote_basic(self):
self.assertEquals('hello',
cmd_helper.DoubleQuote('hello'))
def testDoubleQuote_withSpaces(self):
self.assertEquals('"hello world"',
cmd_helper.DoubleQuote('hello world'))
def testDoubleQuote_withUnsafeChars(self):
self.assertEquals('''"hello\\"; rm -rf /"''',
cmd_helper.DoubleQuote('hello"; rm -rf /'))
def testSingleQuote_doExpand(self):
test_string = 'hello $TEST_VAR'
cmd = 'TEST_VAR=world; echo %s' % cmd_helper.DoubleQuote(test_string)
self.assertEquals('hello world',
cmd_helper.GetCmdOutput(cmd, shell=True).rstrip())
...@@ -171,18 +171,27 @@ class AdbWrapper(object): ...@@ -171,18 +171,27 @@ class AdbWrapper(object):
if expect_rc is None: if expect_rc is None:
actual_command = command actual_command = command
else: else:
actual_command = '%s; echo $?;' % command actual_command = '%s; echo %%$?;' % command.rstrip()
output = self._DeviceAdbCmd( output = self._DeviceAdbCmd(
['shell', actual_command], timeout, retries, check_error=False) ['shell', actual_command], timeout, retries, check_error=False)
if expect_rc is not None: if expect_rc is not None:
output_end = output.rstrip().rfind('\n') + 1 output_end = output.rfind('%')
rc = output[output_end:].strip() if output_end < 0:
output = output[:output_end] # causes the string for rc to become empty and also raise a ValueError
if int(rc) != expect_rc: output_end = len(output)
try:
rc = int(output[output_end+1:])
except ValueError:
raise device_errors.AdbCommandFailedError( raise device_errors.AdbCommandFailedError(
['shell', command], ['shell'], 'command %r on device produced output %r where no'
'shell command exited with code: %s' % rc, ' valid return code was found' % (actual_command, output),
self._device_serial) self._device_serial)
output = output[:output_end]
if rc != expect_rc:
raise device_errors.AdbShellCommandFailedError(
command, rc, output, self._device_serial)
return output return output
def Logcat(self, filter_spec=None, timeout=_DEFAULT_TIMEOUT, def Logcat(self, filter_spec=None, timeout=_DEFAULT_TIMEOUT,
......
...@@ -41,8 +41,8 @@ class TestAdbWrapper(unittest.TestCase): ...@@ -41,8 +41,8 @@ class TestAdbWrapper(unittest.TestCase):
self.assertEqual(output.strip(), 'test') self.assertEqual(output.strip(), 'test')
output = self._adb.Shell('echo test') output = self._adb.Shell('echo test')
self.assertEqual(output.strip(), 'test') self.assertEqual(output.strip(), 'test')
self.assertRaises(device_errors.AdbCommandFailedError, self._adb.Shell, self.assertRaises(device_errors.AdbShellCommandFailedError,
'echo test', expect_rc=1) self._adb.Shell, 'echo test', expect_rc=1)
def testPushPull(self): def testPushPull(self):
path = self._MakeTempFile('foo') path = self._MakeTempFile('foo')
......
...@@ -24,10 +24,23 @@ class AdbCommandFailedError(CommandFailedError): ...@@ -24,10 +24,23 @@ class AdbCommandFailedError(CommandFailedError):
def __init__(self, cmd, msg, device=None): def __init__(self, cmd, msg, device=None):
super(AdbCommandFailedError, self).__init__( super(AdbCommandFailedError, self).__init__(
'adb command \'%s\' failed with message: \'%s\'' % (' '.join(cmd), msg), 'adb command %r failed with message: %s' % (' '.join(cmd), msg),
device=device) device=device)
class AdbShellCommandFailedError(AdbCommandFailedError):
"""Exception for adb shell command failing with non-zero return code."""
def __init__(self, cmd, return_code, output, device=None):
super(AdbShellCommandFailedError, self).__init__(
['shell'],
'command %r on device failed with return code %d and output %r'
% (cmd, return_code, output),
device=device)
self.return_code = return_code
self.output = output
class CommandTimeoutError(BaseError): class CommandTimeoutError(BaseError):
"""Exception for command timeouts.""" """Exception for command timeouts."""
pass pass
......
This diff is collapsed.
...@@ -358,8 +358,8 @@ class TestRunner(base_test_runner.BaseTestRunner): ...@@ -358,8 +358,8 @@ class TestRunner(base_test_runner.BaseTestRunner):
cmd = ['am', 'instrument', '-r'] cmd = ['am', 'instrument', '-r']
for k, v in self._GetInstrumentationArgs().iteritems(): for k, v in self._GetInstrumentationArgs().iteritems():
cmd.extend(['-e', k, "'%s'" % v]) cmd.extend(['-e', k, v])
cmd.extend(['-e', 'class', "'%s'" % test]) cmd.extend(['-e', 'class', test])
cmd.extend(['-w', instrumentation_path]) cmd.extend(['-w', instrumentation_path])
return self.device.RunShellCommand(cmd, timeout=timeout, retries=0) return self.device.RunShellCommand(cmd, timeout=timeout, retries=0)
......
...@@ -261,8 +261,8 @@ class InstrumentationTestRunnerTest(unittest.TestCase): ...@@ -261,8 +261,8 @@ class InstrumentationTestRunnerTest(unittest.TestCase):
self.instance._RunTest('test.package.TestClass#testMethod', 100) self.instance._RunTest('test.package.TestClass#testMethod', 100)
self.instance.device.RunShellCommand.assert_called_with( self.instance.device.RunShellCommand.assert_called_with(
['am', 'instrument', '-r', ['am', 'instrument', '-r',
'-e', 'test_arg_key', "'test_arg_value'", '-e', 'test_arg_key', 'test_arg_value',
'-e', 'class', "'test.package.TestClass#testMethod'", '-e', 'class', 'test.package.TestClass#testMethod',
'-w', 'test.package/MyTestRunner'], '-w', 'test.package/MyTestRunner'],
timeout=100, retries=0) timeout=100, retries=0)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment