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 logging
      6 
      7 from devil.android import device_errors
      8 
      9 
     10 class FlagChanger(object):
     11   """Changes the flags Chrome runs with.
     12 
     13     Flags can be temporarily set for a particular set of unit tests.  These
     14     tests should call Restore() to revert the flags to their original state
     15     once the tests have completed.
     16   """
     17 
     18   def __init__(self, device, cmdline_file):
     19     """Initializes the FlagChanger and records the original arguments.
     20 
     21     Args:
     22       device: A DeviceUtils instance.
     23       cmdline_file: Path to the command line file on the device.
     24     """
     25     self._device = device
     26 
     27     # Unrooted devices have limited access to the file system.
     28     # Place files in /data/local/tmp/ rather than /data/local/
     29     if not device.HasRoot() and not '/data/local/tmp/' in cmdline_file:
     30       self._cmdline_file = cmdline_file.replace('/data/local/',
     31                                                 '/data/local/tmp/')
     32     else:
     33       self._cmdline_file = cmdline_file
     34 
     35     stored_flags = ''
     36     if self._device.PathExists(self._cmdline_file):
     37       try:
     38         stored_flags = self._device.ReadFile(self._cmdline_file).strip()
     39       except device_errors.CommandFailedError:
     40         pass
     41     # Store the flags as a set to facilitate adding and removing flags.
     42     self._state_stack = [set(self._TokenizeFlags(stored_flags))]
     43 
     44   def ReplaceFlags(self, flags):
     45     """Replaces the flags in the command line with the ones provided.
     46        Saves the current flags state on the stack, so a call to Restore will
     47        change the state back to the one preceeding the call to ReplaceFlags.
     48 
     49     Args:
     50       flags: A sequence of command line flags to set, eg. ['--single-process'].
     51              Note: this should include flags only, not the name of a command
     52              to run (ie. there is no need to start the sequence with 'chrome').
     53     """
     54     new_flags = set(flags)
     55     self._state_stack.append(new_flags)
     56     self._UpdateCommandLineFile()
     57 
     58   def AddFlags(self, flags):
     59     """Appends flags to the command line if they aren't already there.
     60        Saves the current flags state on the stack, so a call to Restore will
     61        change the state back to the one preceeding the call to AddFlags.
     62 
     63     Args:
     64       flags: A sequence of flags to add on, eg. ['--single-process'].
     65     """
     66     self.PushFlags(add=flags)
     67 
     68   def RemoveFlags(self, flags):
     69     """Removes flags from the command line, if they exist.
     70        Saves the current flags state on the stack, so a call to Restore will
     71        change the state back to the one preceeding the call to RemoveFlags.
     72 
     73        Note that calling RemoveFlags after AddFlags will result in having
     74        two nested states.
     75 
     76     Args:
     77       flags: A sequence of flags to remove, eg. ['--single-process'].  Note
     78              that we expect a complete match when removing flags; if you want
     79              to remove a switch with a value, you must use the exact string
     80              used to add it in the first place.
     81     """
     82     self.PushFlags(remove=flags)
     83 
     84   def PushFlags(self, add=None, remove=None):
     85     """Appends and removes flags to/from the command line if they aren't already
     86        there. Saves the current flags state on the stack, so a call to Restore
     87        will change the state back to the one preceeding the call to PushFlags.
     88 
     89     Args:
     90       add: A list of flags to add on, eg. ['--single-process'].
     91       remove: A list of flags to remove, eg. ['--single-process'].  Note that we
     92               expect a complete match when removing flags; if you want to remove
     93               a switch with a value, you must use the exact string used to add
     94               it in the first place.
     95     """
     96     new_flags = self._state_stack[-1].copy()
     97     if add:
     98       new_flags.update(add)
     99     if remove:
    100       new_flags.difference_update(remove)
    101     self.ReplaceFlags(new_flags)
    102 
    103   def Restore(self):
    104     """Restores the flags to their state prior to the last AddFlags or
    105        RemoveFlags call.
    106     """
    107     # The initial state must always remain on the stack.
    108     assert len(self._state_stack) > 1, (
    109       "Mismatch between calls to Add/RemoveFlags and Restore")
    110     self._state_stack.pop()
    111     self._UpdateCommandLineFile()
    112 
    113   def _UpdateCommandLineFile(self):
    114     """Writes out the command line to the file, or removes it if empty."""
    115     current_flags = list(self._state_stack[-1])
    116     logging.info('Current flags: %s', current_flags)
    117     # Root is not required to write to /data/local/tmp/.
    118     use_root = '/data/local/tmp/' not in self._cmdline_file
    119     if current_flags:
    120       # The first command line argument doesn't matter as we are not actually
    121       # launching the chrome executable using this command line.
    122       cmd_line = ' '.join(['_'] + current_flags)
    123       self._device.WriteFile(
    124           self._cmdline_file, cmd_line, as_root=use_root)
    125       file_contents = self._device.ReadFile(
    126           self._cmdline_file, as_root=use_root).rstrip()
    127       assert file_contents == cmd_line, (
    128           'Failed to set the command line file at %s' % self._cmdline_file)
    129     else:
    130       self._device.RunShellCommand('rm ' + self._cmdline_file,
    131                                    as_root=use_root)
    132       assert not self._device.FileExists(self._cmdline_file), (
    133           'Failed to remove the command line file at %s' % self._cmdline_file)
    134 
    135   @staticmethod
    136   def _TokenizeFlags(line):
    137     """Changes the string containing the command line into a list of flags.
    138 
    139     Follows similar logic to CommandLine.java::tokenizeQuotedArguments:
    140     * Flags are split using whitespace, unless the whitespace is within a
    141       pair of quotation marks.
    142     * Unlike the Java version, we keep the quotation marks around switch
    143       values since we need them to re-create the file when new flags are
    144       appended.
    145 
    146     Args:
    147       line: A string containing the entire command line.  The first token is
    148             assumed to be the program name.
    149     """
    150     if not line:
    151       return []
    152 
    153     tokenized_flags = []
    154     current_flag = ""
    155     within_quotations = False
    156 
    157     # Move through the string character by character and build up each flag
    158     # along the way.
    159     for c in line.strip():
    160       if c is '"':
    161         if len(current_flag) > 0 and current_flag[-1] == '\\':
    162           # Last char was a backslash; pop it, and treat this " as a literal.
    163           current_flag = current_flag[0:-1] + '"'
    164         else:
    165           within_quotations = not within_quotations
    166           current_flag += c
    167       elif not within_quotations and (c is ' ' or c is '\t'):
    168         if current_flag is not "":
    169           tokenized_flags.append(current_flag)
    170           current_flag = ""
    171       else:
    172         current_flag += c
    173 
    174     # Tack on the last flag.
    175     if not current_flag:
    176       if within_quotations:
    177         logging.warn('Unterminated quoted argument: ' + line)
    178     else:
    179       tokenized_flags.append(current_flag)
    180 
    181     # Return everything but the program name.
    182     return tokenized_flags[1:]
    183