Home | History | Annotate | Download | only in android
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import contextlib
      6 import logging
      7 import posixpath
      8 import re
      9 
     10 from devil.android.sdk import version_codes
     11 
     12 
     13 logger = logging.getLogger(__name__)
     14 
     15 
     16 _CMDLINE_DIR = '/data/local/tmp'
     17 _CMDLINE_DIR_LEGACY = '/data/local'
     18 _RE_NEEDS_QUOTING = re.compile(r'[^\w-]')  # Not in: alphanumeric or hyphens.
     19 _QUOTES = '"\''  # Either a single or a double quote.
     20 _ESCAPE = '\\'  # A backslash.
     21 
     22 
     23 @contextlib.contextmanager
     24 def CustomCommandLineFlags(device, cmdline_name, flags):
     25   """Context manager to change Chrome's command line temporarily.
     26 
     27   Example:
     28 
     29       with flag_changer.TemporaryCommandLineFlags(device, name, flags):
     30         # Launching Chrome will use the provided flags.
     31 
     32       # Previous set of flags on the device is now restored.
     33 
     34   Args:
     35     device: A DeviceUtils instance.
     36     cmdline_name: Name of the command line file where to store flags.
     37     flags: A sequence of command line flags to set.
     38   """
     39   # On Android N and above, we need to temporarily set SELinux to permissive
     40   # so that Chrome is allowed to read the command line file.
     41   # TODO(crbug.com/699082): Remove when a solution to avoid this is implemented.
     42   needs_permissive = (
     43       device.build_version_sdk >= version_codes.NOUGAT and
     44       device.GetEnforce())
     45   if needs_permissive:
     46     device.SetEnforce(enabled=False)
     47   try:
     48     changer = FlagChanger(device, cmdline_name)
     49     try:
     50       changer.ReplaceFlags(flags)
     51       yield
     52     finally:
     53       changer.Restore()
     54   finally:
     55     if needs_permissive:
     56       device.SetEnforce(enabled=True)
     57 
     58 
     59 class FlagChanger(object):
     60   """Changes the flags Chrome runs with.
     61 
     62     Flags can be temporarily set for a particular set of unit tests.  These
     63     tests should call Restore() to revert the flags to their original state
     64     once the tests have completed.
     65   """
     66 
     67   def __init__(self, device, cmdline_file):
     68     """Initializes the FlagChanger and records the original arguments.
     69 
     70     Args:
     71       device: A DeviceUtils instance.
     72       cmdline_file: Name of the command line file where to store flags.
     73     """
     74     self._device = device
     75 
     76     if posixpath.sep in cmdline_file:
     77       raise ValueError(
     78           'cmdline_file should be a file name only, do not include path'
     79           ' separators in: %s' % cmdline_file)
     80     self._cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file)
     81 
     82     cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file)
     83     if self._device.PathExists(cmdline_path_legacy):
     84       logging.warning(
     85             'Removing legacy command line file %r.', cmdline_path_legacy)
     86       self._device.RemovePath(cmdline_path_legacy, as_root=True)
     87 
     88     self._state_stack = [None]  # Actual state is set by GetCurrentFlags().
     89     self.GetCurrentFlags()
     90 
     91   def GetCurrentFlags(self):
     92     """Read the current flags currently stored in the device.
     93 
     94     Also updates the internal state of the flag_changer.
     95 
     96     Returns:
     97       A list of flags.
     98     """
     99     if self._device.PathExists(self._cmdline_path):
    100       command_line = self._device.ReadFile(self._cmdline_path).strip()
    101     else:
    102       command_line = ''
    103     flags = _ParseFlags(command_line)
    104 
    105     # Store the flags as a set to facilitate adding and removing flags.
    106     self._state_stack[-1] = set(flags)
    107     return flags
    108 
    109   def ReplaceFlags(self, flags):
    110     """Replaces the flags in the command line with the ones provided.
    111        Saves the current flags state on the stack, so a call to Restore will
    112        change the state back to the one preceeding the call to ReplaceFlags.
    113 
    114     Args:
    115       flags: A sequence of command line flags to set, eg. ['--single-process'].
    116              Note: this should include flags only, not the name of a command
    117              to run (ie. there is no need to start the sequence with 'chrome').
    118 
    119     Returns:
    120       A list with the flags now stored on the device.
    121     """
    122     new_flags = set(flags)
    123     self._state_stack.append(new_flags)
    124     return self._UpdateCommandLineFile()
    125 
    126   def AddFlags(self, flags):
    127     """Appends flags to the command line if they aren't already there.
    128        Saves the current flags state on the stack, so a call to Restore will
    129        change the state back to the one preceeding the call to AddFlags.
    130 
    131     Args:
    132       flags: A sequence of flags to add on, eg. ['--single-process'].
    133 
    134     Returns:
    135       A list with the flags now stored on the device.
    136     """
    137     return self.PushFlags(add=flags)
    138 
    139   def RemoveFlags(self, flags):
    140     """Removes flags from the command line, if they exist.
    141        Saves the current flags state on the stack, so a call to Restore will
    142        change the state back to the one preceeding the call to RemoveFlags.
    143 
    144        Note that calling RemoveFlags after AddFlags will result in having
    145        two nested states.
    146 
    147     Args:
    148       flags: A sequence of flags to remove, eg. ['--single-process'].  Note
    149              that we expect a complete match when removing flags; if you want
    150              to remove a switch with a value, you must use the exact string
    151              used to add it in the first place.
    152 
    153     Returns:
    154       A list with the flags now stored on the device.
    155     """
    156     return self.PushFlags(remove=flags)
    157 
    158   def PushFlags(self, add=None, remove=None):
    159     """Appends and removes flags to/from the command line if they aren't already
    160        there. Saves the current flags state on the stack, so a call to Restore
    161        will change the state back to the one preceeding the call to PushFlags.
    162 
    163     Args:
    164       add: A list of flags to add on, eg. ['--single-process'].
    165       remove: A list of flags to remove, eg. ['--single-process'].  Note that we
    166               expect a complete match when removing flags; if you want to remove
    167               a switch with a value, you must use the exact string used to add
    168               it in the first place.
    169 
    170     Returns:
    171       A list with the flags now stored on the device.
    172     """
    173     new_flags = self._state_stack[-1].copy()
    174     if add:
    175       new_flags.update(add)
    176     if remove:
    177       new_flags.difference_update(remove)
    178     return self.ReplaceFlags(new_flags)
    179 
    180   def Restore(self):
    181     """Restores the flags to their state prior to the last AddFlags or
    182        RemoveFlags call.
    183 
    184     Returns:
    185       A list with the flags now stored on the device.
    186     """
    187     # The initial state must always remain on the stack.
    188     assert len(self._state_stack) > 1, (
    189       "Mismatch between calls to Add/RemoveFlags and Restore")
    190     self._state_stack.pop()
    191     return self._UpdateCommandLineFile()
    192 
    193   def _UpdateCommandLineFile(self):
    194     """Writes out the command line to the file, or removes it if empty.
    195 
    196     Returns:
    197       A list with the flags now stored on the device.
    198     """
    199     command_line = _SerializeFlags(self._state_stack[-1])
    200     if command_line is not None:
    201       self._device.WriteFile(self._cmdline_path, command_line)
    202     else:
    203       self._device.RemovePath(self._cmdline_path, force=True)
    204 
    205     current_flags = self.GetCurrentFlags()
    206     logger.info('Flags now set on the device: %s', current_flags)
    207     return current_flags
    208 
    209 
    210 def _ParseFlags(line):
    211   """Parse the string containing the command line into a list of flags.
    212 
    213   It's a direct port of CommandLine.java::tokenizeQuotedArguments.
    214 
    215   The first token is assumed to be the (unused) program name and stripped off
    216   from the list of flags.
    217 
    218   Args:
    219     line: A string containing the entire command line.  The first token is
    220           assumed to be the program name.
    221 
    222   Returns:
    223      A list of flags, with quoting removed.
    224   """
    225   flags = []
    226   current_quote = None
    227   current_flag = None
    228 
    229   for c in line:
    230     # Detect start or end of quote block.
    231     if (current_quote is None and c in _QUOTES) or c == current_quote:
    232       if current_flag is not None and current_flag[-1] == _ESCAPE:
    233         # Last char was a backslash; pop it, and treat c as a literal.
    234         current_flag = current_flag[:-1] + c
    235       else:
    236         current_quote = c if current_quote is None else None
    237     elif current_quote is None and c.isspace():
    238       if current_flag is not None:
    239         flags.append(current_flag)
    240         current_flag = None
    241     else:
    242       if current_flag is None:
    243         current_flag = ''
    244       current_flag += c
    245 
    246   if current_flag is not None:
    247     if current_quote is not None:
    248       logger.warning('Unterminated quoted argument: ' + current_flag)
    249     flags.append(current_flag)
    250 
    251   # Return everything but the program name.
    252   return flags[1:]
    253 
    254 
    255 def _SerializeFlags(flags):
    256   """Serialize a sequence of flags into a command line string.
    257 
    258   Args:
    259     flags: A sequence of strings with individual flags.
    260 
    261   Returns:
    262     A line with the command line contents to save; or None if the sequence of
    263     flags is empty.
    264   """
    265   if flags:
    266     # The first command line argument doesn't matter as we are not actually
    267     # launching the chrome executable using this command line.
    268     args = ['_']
    269     args.extend(_QuoteFlag(f) for f in flags)
    270     return ' '.join(args)
    271   else:
    272     return None
    273 
    274 
    275 def _QuoteFlag(flag):
    276   """Validate and quote a single flag.
    277 
    278   Args:
    279     A string with the flag to quote.
    280 
    281   Returns:
    282     A string with the flag quoted so that it can be parsed by the algorithm
    283     in _ParseFlags; or None if the flag does not appear to be valid.
    284   """
    285   if '=' in flag:
    286     key, value = flag.split('=', 1)
    287   else:
    288     key, value = flag, None
    289 
    290   if not flag or _RE_NEEDS_QUOTING.search(key):
    291     # Probably not a valid flag, but quote the whole thing so it can be
    292     # parsed back correctly.
    293     return '"%s"' % flag.replace('"', r'\"')
    294 
    295   if value is None:
    296     return key
    297 
    298   if _RE_NEEDS_QUOTING.search(value):
    299     value = '"%s"' % value.replace('"', r'\"')
    300   return '='.join([key, value])
    301