1645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez# Use of this source code is governed by a BSD-style license that can be
3645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez# found in the LICENSE file.
4645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
5645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavezimport logging
6645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
7645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavezfrom devil.android import device_errors
8645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
9645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
10645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavezclass FlagChanger(object):
11645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  """Changes the flags Chrome runs with.
12645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
13645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Flags can be temporarily set for a particular set of unit tests.  These
14645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    tests should call Restore() to revert the flags to their original state
15645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    once the tests have completed.
16645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  """
17645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
18645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def __init__(self, device, cmdline_file):
19645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Initializes the FlagChanger and records the original arguments.
20645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
21645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
22645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      device: A DeviceUtils instance.
23645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      cmdline_file: Path to the command line file on the device.
24645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
25645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._device = device
26645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
27645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Unrooted devices have limited access to the file system.
28645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Place files in /data/local/tmp/ rather than /data/local/
29645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if not device.HasRoot() and not '/data/local/tmp/' in cmdline_file:
30645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      self._cmdline_file = cmdline_file.replace('/data/local/',
31645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez                                                '/data/local/tmp/')
32645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    else:
33645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      self._cmdline_file = cmdline_file
34645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
35645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    stored_flags = ''
36645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if self._device.PathExists(self._cmdline_file):
37645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      try:
38645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        stored_flags = self._device.ReadFile(self._cmdline_file).strip()
39645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      except device_errors.CommandFailedError:
40645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        pass
41645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Store the flags as a set to facilitate adding and removing flags.
42645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._state_stack = [set(self._TokenizeFlags(stored_flags))]
43645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
44645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def ReplaceFlags(self, flags):
45645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Replaces the flags in the command line with the ones provided.
46645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       Saves the current flags state on the stack, so a call to Restore will
47645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       change the state back to the one preceeding the call to ReplaceFlags.
48645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
49645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
50645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      flags: A sequence of command line flags to set, eg. ['--single-process'].
51645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez             Note: this should include flags only, not the name of a command
52645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez             to run (ie. there is no need to start the sequence with 'chrome').
53645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
54645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    new_flags = set(flags)
55645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._state_stack.append(new_flags)
56645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._UpdateCommandLineFile()
57645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
58645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def AddFlags(self, flags):
59645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Appends flags to the command line if they aren't already there.
60645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       Saves the current flags state on the stack, so a call to Restore will
61645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       change the state back to the one preceeding the call to AddFlags.
62645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
63645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
64645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      flags: A sequence of flags to add on, eg. ['--single-process'].
65645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
66645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self.PushFlags(add=flags)
67645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
68645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def RemoveFlags(self, flags):
69645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Removes flags from the command line, if they exist.
70645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       Saves the current flags state on the stack, so a call to Restore will
71645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       change the state back to the one preceeding the call to RemoveFlags.
72645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
73645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       Note that calling RemoveFlags after AddFlags will result in having
74645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       two nested states.
75645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
76645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
77645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      flags: A sequence of flags to remove, eg. ['--single-process'].  Note
78645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez             that we expect a complete match when removing flags; if you want
79645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez             to remove a switch with a value, you must use the exact string
80645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez             used to add it in the first place.
81645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
82645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self.PushFlags(remove=flags)
83645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
84645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def PushFlags(self, add=None, remove=None):
85645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Appends and removes flags to/from the command line if they aren't already
86645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       there. Saves the current flags state on the stack, so a call to Restore
87645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       will change the state back to the one preceeding the call to PushFlags.
88645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
89645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
90645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      add: A list of flags to add on, eg. ['--single-process'].
91645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      remove: A list of flags to remove, eg. ['--single-process'].  Note that we
92645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez              expect a complete match when removing flags; if you want to remove
93645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez              a switch with a value, you must use the exact string used to add
94645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez              it in the first place.
95645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
96645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    new_flags = self._state_stack[-1].copy()
97645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if add:
98645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      new_flags.update(add)
99645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if remove:
100645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      new_flags.difference_update(remove)
101645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self.ReplaceFlags(new_flags)
102645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
103645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def Restore(self):
104645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Restores the flags to their state prior to the last AddFlags or
105645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez       RemoveFlags call.
106645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
107645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # The initial state must always remain on the stack.
108645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    assert len(self._state_stack) > 1, (
109645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      "Mismatch between calls to Add/RemoveFlags and Restore")
110645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._state_stack.pop()
111645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    self._UpdateCommandLineFile()
112645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
113645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def _UpdateCommandLineFile(self):
114645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Writes out the command line to the file, or removes it if empty."""
115645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    current_flags = list(self._state_stack[-1])
116645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    logging.info('Current flags: %s', current_flags)
117645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Root is not required to write to /data/local/tmp/.
118645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    use_root = '/data/local/tmp/' not in self._cmdline_file
119645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if current_flags:
120645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      # The first command line argument doesn't matter as we are not actually
121645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      # launching the chrome executable using this command line.
122645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      cmd_line = ' '.join(['_'] + current_flags)
123645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      self._device.WriteFile(
124645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          self._cmdline_file, cmd_line, as_root=use_root)
125645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      file_contents = self._device.ReadFile(
126645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          self._cmdline_file, as_root=use_root).rstrip()
127645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      assert file_contents == cmd_line, (
128645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          'Failed to set the command line file at %s' % self._cmdline_file)
129645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    else:
130645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      self._device.RunShellCommand('rm ' + self._cmdline_file,
131645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez                                   as_root=use_root)
132645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      assert not self._device.FileExists(self._cmdline_file), (
133645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          'Failed to remove the command line file at %s' % self._cmdline_file)
134645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
135645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  @staticmethod
136645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez  def _TokenizeFlags(line):
137645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """Changes the string containing the command line into a list of flags.
138645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
139645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Follows similar logic to CommandLine.java::tokenizeQuotedArguments:
140645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    * Flags are split using whitespace, unless the whitespace is within a
141645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      pair of quotation marks.
142645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    * Unlike the Java version, we keep the quotation marks around switch
143645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      values since we need them to re-create the file when new flags are
144645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      appended.
145645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
146645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    Args:
147645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      line: A string containing the entire command line.  The first token is
148645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez            assumed to be the program name.
149645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    """
150645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if not line:
151645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      return []
152645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
153645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    tokenized_flags = []
154645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    current_flag = ""
155645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    within_quotations = False
156645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
157645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Move through the string character by character and build up each flag
158645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # along the way.
159645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    for c in line.strip():
160645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      if c is '"':
161645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        if len(current_flag) > 0 and current_flag[-1] == '\\':
162645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          # Last char was a backslash; pop it, and treat this " as a literal.
163645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          current_flag = current_flag[0:-1] + '"'
164645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        else:
165645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          within_quotations = not within_quotations
166645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          current_flag += c
167645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      elif not within_quotations and (c is ' ' or c is '\t'):
168645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        if current_flag is not "":
169645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          tokenized_flags.append(current_flag)
170645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez          current_flag = ""
171645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      else:
172645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        current_flag += c
173645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
174645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Tack on the last flag.
175645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    if not current_flag:
176645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      if within_quotations:
177645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez        logging.warn('Unterminated quoted argument: ' + line)
178645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    else:
179645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez      tokenized_flags.append(current_flag)
180645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez
181645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    # Return everything but the program name.
182645501c2ab19a559ce82a1d5a29ced159a4c30fbLuis Hector Chavez    return tokenized_flags[1:]
183