aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/android/flag_changer.py
diff options
context:
space:
mode:
Diffstat (limited to 'catapult/devil/devil/android/flag_changer.py')
-rw-r--r--catapult/devil/devil/android/flag_changer.py263
1 files changed, 177 insertions, 86 deletions
diff --git a/catapult/devil/devil/android/flag_changer.py b/catapult/devil/devil/android/flag_changer.py
index 5de54eaa..2c8cc3c2 100644
--- a/catapult/devil/devil/android/flag_changer.py
+++ b/catapult/devil/devil/android/flag_changer.py
@@ -3,12 +3,19 @@
# found in the LICENSE file.
import logging
-
-from devil.android import device_errors
+import posixpath
+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.
+_QUOTES = '"\'' # Either a single or a double quote.
+_ESCAPE = '\\' # A backslash.
+
+
class FlagChanger(object):
"""Changes the flags Chrome runs with.
@@ -22,26 +29,50 @@ class FlagChanger(object):
Args:
device: A DeviceUtils instance.
- cmdline_file: Path to the command line file on the device.
+ cmdline_file: Name of the command line file where to store flags.
"""
self._device = device
- # Unrooted devices have limited access to the file system.
- # Place files in /data/local/tmp/ rather than /data/local/
- if not device.HasRoot() and not '/data/local/tmp/' in cmdline_file:
- self._cmdline_file = cmdline_file.replace('/data/local/',
- '/data/local/tmp/')
+ unused_dir, basename = posixpath.split(cmdline_file)
+ self._cmdline_path = posixpath.join(_CMDLINE_DIR, basename)
+
+ # TODO(catapult:#3112): Make this fail instead of warn after all clients
+ # have been switched.
+ if unused_dir:
+ logging.warning(
+ 'cmdline_file argument of %s() should be a file name only (not a'
+ ' full path).', type(self).__name__)
+ if cmdline_file != self._cmdline_path:
+ logging.warning(
+ 'Client supplied %r, but %r will be used instead.',
+ cmdline_file, self._cmdline_path)
+
+ cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, basename)
+ if self._device.PathExists(cmdline_path_legacy):
+ logging.warning(
+ 'Removing legacy command line file %r.', cmdline_path_legacy)
+ self._device.RemovePath(cmdline_path_legacy, as_root=True)
+
+ 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):
+ command_line = self._device.ReadFile(self._cmdline_path).strip()
else:
- self._cmdline_file = cmdline_file
-
- stored_flags = ''
- if self._device.PathExists(self._cmdline_file):
- try:
- stored_flags = self._device.ReadFile(self._cmdline_file).strip()
- except device_errors.CommandFailedError:
- pass
+ 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.
@@ -52,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.
@@ -64,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.
@@ -80,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
@@ -94,91 +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/.
- use_root = '/data/local/tmp/' not in self._cmdline_file
- 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_file, cmd_line, as_root=use_root)
- file_contents = self._device.ReadFile(
- self._cmdline_file, as_root=use_root).rstrip()
- assert file_contents == cmd_line, (
- 'Failed to set the command line file at %s' % self._cmdline_file)
- else:
- self._device.RunShellCommand('rm ' + self._cmdline_file,
- as_root=use_root)
- assert not self._device.FileExists(self._cmdline_file), (
- 'Failed to remove the command line file at %s' % self._cmdline_file)
-
- @staticmethod
- def _TokenizeFlags(line):
- """Changes the string containing the command line into a list of 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.
+ """Writes out the command line to the file, or removes it if empty.
- Args:
- line: A string containing the entire command line. The first token is
- assumed to be the program name.
+ Returns:
+ A list with the flags now stored on the device.
"""
- 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
+ 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)
+
+ current_flags = self.GetCurrentFlags()
+ logger.info('Flags now set on the device: %s', current_flags)
+ return current_flags
- # Tack on the last flag.
- if not current_flag:
- if within_quotations:
- logger.warn('Unterminated quoted argument: ' + line)
+
+def _ParseFlags(line):
+ """Parse the string containing the command line into a list of flags.
+
+ It's a direct port of CommandLine.java::tokenizeQuotedArguments.
+
+ 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.
+
+ 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'\"')
- # Return everything but the program name.
- return tokenized_flags[1:]
+ 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])