Index: devil/devil/android/flag_changer.py |
diff --git a/devil/devil/android/flag_changer.py b/devil/devil/android/flag_changer.py |
index a5bda0779c053ac0b78ce3945dbfbbfc935c8848..2c8cc3c2b1d4ef5fc7fe1ddc43d5d12c22d05ac2 100644 |
--- a/devil/devil/android/flag_changer.py |
+++ b/devil/devil/android/flag_changer.py |
@@ -4,14 +4,16 @@ |
import logging |
import posixpath |
- |
-from devil.android import device_errors |
+import re |
logger = logging.getLogger(__name__) |
_CMDLINE_DIR = '/data/local/tmp' |
_CMDLINE_DIR_LEGACY = '/data/local' |
+_RE_NEEDS_QUOTING = re.compile(r'[^\w-]') # Not in: alphanumeric or hyphens. |
jbudorick
2017/01/18 14:56:28
I'm surprised that this doesn't also include under
perezju
2017/01/18 15:19:31
\w already includes underscores, from the docs: "t
|
+_QUOTES = '"\'' # Either a single or a double quote. |
+_ESCAPE = '\\' # A backslash. |
class FlagChanger(object): |
@@ -39,7 +41,7 @@ class FlagChanger(object): |
if unused_dir: |
logging.warning( |
'cmdline_file argument of %s() should be a file name only (not a' |
- ' full path).', type(self)) |
+ ' full path).', type(self).__name__) |
if cmdline_file != self._cmdline_path: |
logging.warning( |
'Client supplied %r, but %r will be used instead.', |
@@ -51,14 +53,26 @@ class FlagChanger(object): |
'Removing legacy command line file %r.', cmdline_path_legacy) |
self._device.RemovePath(cmdline_path_legacy, as_root=True) |
- stored_flags = '' |
+ self._state_stack = [None] # Actual state is set by GetCurrentFlags(). |
+ self.GetCurrentFlags() |
+ |
+ def GetCurrentFlags(self): |
+ """Read the current flags currently stored in the device. |
+ |
+ Also updates the internal state of the flag_changer. |
+ |
+ Returns: |
+ A list of flags. |
+ """ |
if self._device.PathExists(self._cmdline_path): |
- try: |
- stored_flags = self._device.ReadFile(self._cmdline_path).strip() |
- except device_errors.CommandFailedError: |
- pass |
+ command_line = self._device.ReadFile(self._cmdline_path).strip() |
+ else: |
+ command_line = '' |
+ flags = _ParseFlags(command_line) |
+ |
# Store the flags as a set to facilitate adding and removing flags. |
- self._state_stack = [set(self._TokenizeFlags(stored_flags))] |
+ self._state_stack[-1] = set(flags) |
+ return flags |
def ReplaceFlags(self, flags): |
"""Replaces the flags in the command line with the ones provided. |
@@ -69,10 +83,13 @@ class FlagChanger(object): |
flags: A sequence of command line flags to set, eg. ['--single-process']. |
Note: this should include flags only, not the name of a command |
to run (ie. there is no need to start the sequence with 'chrome'). |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
""" |
new_flags = set(flags) |
self._state_stack.append(new_flags) |
- self._UpdateCommandLineFile() |
+ return self._UpdateCommandLineFile() |
def AddFlags(self, flags): |
"""Appends flags to the command line if they aren't already there. |
@@ -81,8 +98,11 @@ class FlagChanger(object): |
Args: |
flags: A sequence of flags to add on, eg. ['--single-process']. |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
""" |
- self.PushFlags(add=flags) |
+ return self.PushFlags(add=flags) |
def RemoveFlags(self, flags): |
"""Removes flags from the command line, if they exist. |
@@ -97,8 +117,11 @@ class FlagChanger(object): |
that we expect a complete match when removing flags; if you want |
to remove a switch with a value, you must use the exact string |
used to add it in the first place. |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
""" |
- self.PushFlags(remove=flags) |
+ return self.PushFlags(remove=flags) |
def PushFlags(self, add=None, remove=None): |
"""Appends and removes flags to/from the command line if they aren't already |
@@ -111,89 +134,142 @@ class FlagChanger(object): |
expect a complete match when removing flags; if you want to remove |
a switch with a value, you must use the exact string used to add |
it in the first place. |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
""" |
new_flags = self._state_stack[-1].copy() |
if add: |
new_flags.update(add) |
if remove: |
new_flags.difference_update(remove) |
- self.ReplaceFlags(new_flags) |
+ return self.ReplaceFlags(new_flags) |
def Restore(self): |
"""Restores the flags to their state prior to the last AddFlags or |
RemoveFlags call. |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
""" |
# The initial state must always remain on the stack. |
assert len(self._state_stack) > 1, ( |
"Mismatch between calls to Add/RemoveFlags and Restore") |
self._state_stack.pop() |
- self._UpdateCommandLineFile() |
+ return self._UpdateCommandLineFile() |
def _UpdateCommandLineFile(self): |
- """Writes out the command line to the file, or removes it if empty.""" |
- current_flags = list(self._state_stack[-1]) |
- logger.info('Current flags: %s', current_flags) |
- # Root is not required to write to /data/local/tmp/. |
- if current_flags: |
- # The first command line argument doesn't matter as we are not actually |
- # launching the chrome executable using this command line. |
- cmd_line = ' '.join(['_'] + current_flags) |
- self._device.WriteFile( |
- self._cmdline_path, cmd_line) |
- file_contents = self._device.ReadFile( |
- self._cmdline_path).rstrip() |
- assert file_contents == cmd_line, ( |
- 'Failed to set the command line file at %s' % self._cmdline_path) |
+ """Writes out the command line to the file, or removes it if empty. |
+ |
+ Returns: |
+ A list with the flags now stored on the device. |
+ """ |
+ command_line = _SerializeFlags(self._state_stack[-1]) |
+ if command_line is not None: |
+ self._device.WriteFile(self._cmdline_path, command_line) |
else: |
self._device.RunShellCommand('rm ' + self._cmdline_path) |
- assert not self._device.FileExists(self._cmdline_path), ( |
- 'Failed to remove the command line file at %s' % self._cmdline_path) |
- @staticmethod |
- def _TokenizeFlags(line): |
- """Changes the string containing the command line into a list of flags. |
+ current_flags = self.GetCurrentFlags() |
+ logger.info('Flags now set on the device: %s', current_flags) |
+ return current_flags |
- Follows similar logic to CommandLine.java::tokenizeQuotedArguments: |
- * Flags are split using whitespace, unless the whitespace is within a |
- pair of quotation marks. |
- * Unlike the Java version, we keep the quotation marks around switch |
- values since we need them to re-create the file when new flags are |
- appended. |
- Args: |
- line: A string containing the entire command line. The first token is |
- assumed to be the program name. |
- """ |
- if not line: |
- return [] |
- |
- tokenized_flags = [] |
- current_flag = "" |
- within_quotations = False |
- |
- # Move through the string character by character and build up each flag |
- # along the way. |
- for c in line.strip(): |
- if c is '"': |
- if len(current_flag) > 0 and current_flag[-1] == '\\': |
- # Last char was a backslash; pop it, and treat this " as a literal. |
- current_flag = current_flag[0:-1] + '"' |
- else: |
- within_quotations = not within_quotations |
- current_flag += c |
- elif not within_quotations and (c is ' ' or c is '\t'): |
- if current_flag is not "": |
- tokenized_flags.append(current_flag) |
- current_flag = "" |
- else: |
- current_flag += c |
+def _ParseFlags(line): |
+ """Parse the string containing the command line into a list of flags. |
+ |
+ It's a direct port of CommandLine.java::tokenizeQuotedArguments. |
jbudorick
2017/01/18 14:56:28
somewhat amusingly, it's misspelled in CommandLine
perezju
2017/01/18 15:19:31
yeah :-/ noted that too, wasn't sure whether to sp
|
- # Tack on the last flag. |
- if not current_flag: |
- if within_quotations: |
- logger.warn('Unterminated quoted argument: ' + line) |
+ The first token is assumed to be the (unused) program name and stripped off |
+ from the list of flags. |
+ |
+ Args: |
+ line: A string containing the entire command line. The first token is |
+ assumed to be the program name. |
+ |
+ Returns: |
+ A list of flags, with quoting removed. |
+ """ |
+ flags = [] |
+ current_quote = None |
+ current_flag = None |
+ |
+ for c in line: |
+ # Detect start or end of quote block. |
+ if (current_quote is None and c in _QUOTES) or c == current_quote: |
+ if current_flag is not None and current_flag[-1] == _ESCAPE: |
+ # Last char was a backslash; pop it, and treat c as a literal. |
+ current_flag = current_flag[:-1] + c |
+ else: |
+ current_quote = c if current_quote is None else None |
+ elif current_quote is None and c.isspace(): |
+ if current_flag is not None: |
+ flags.append(current_flag) |
+ current_flag = None |
else: |
- tokenized_flags.append(current_flag) |
+ if current_flag is None: |
+ current_flag = '' |
+ current_flag += c |
+ |
+ if current_flag is not None: |
+ if current_quote is not None: |
+ logger.warning('Unterminated quoted argument: ' + current_flag) |
+ flags.append(current_flag) |
+ |
+ # Return everything but the program name. |
+ return flags[1:] |
+ |
+ |
+def _SerializeFlags(flags): |
+ """Serialize a sequence of flags into a command line string. |
+ |
+ Args: |
+ flags: A sequence of strings with individual flags. |
- # Return everything but the program name. |
- return tokenized_flags[1:] |
+ Returns: |
+ A line with the command line contents to save; or None if the sequence of |
+ flags is empty. |
+ """ |
+ if flags: |
+ # The first command line argument doesn't matter as we are not actually |
+ # launching the chrome executable using this command line. |
+ args = ['_'] |
+ args.extend(_QuoteFlag(f) for f in flags) |
+ return ' '.join(args) |
+ else: |
+ return None |
+ |
+ |
+def _QuoteFlag(flag): |
+ """Validate and quote a single flag. |
+ |
+ Args: |
+ A string with the flag to quote. |
+ |
+ Returns: |
+ A string with the flag quoted so that it can be parsed by the algorithm |
+ in _ParseFlags; or None if the flag does not appear to be valid. |
+ """ |
+ if '=' in flag: |
+ key, value = flag.split('=', 1) |
+ else: |
+ key, value = flag, None |
+ |
+ if not flag or _RE_NEEDS_QUOTING.search(key): |
+ # Probably not a valid flag, but quote the whole thing so it can be |
+ # parsed back correctly. |
+ return '"%s"' % flag.replace('"', r'\"') |
+ |
+ if value is None: |
+ return key |
+ else: |
+ # TODO(catapult:#3112): Remove this check when all clients comply. |
+ if value[0] in _QUOTES and value[0] == value[-1]: |
+ logging.warning( |
+ 'Flag %s appears to be quoted, so will be passed as-is.', flag) |
+ logging.warning( |
+ 'Note: this behavior will be changed in the future. ' |
+ 'Clients should pass values unquoted to prevent double-quoting.') |
+ elif _RE_NEEDS_QUOTING.search(value): |
+ value = '"%s"' % value.replace('"', r'\"') |
+ return '='.join([key, value]) |