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