• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import contextlib
6import logging
7import posixpath
8import re
9
10from devil.android.sdk import version_codes
11
12
13logger = 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
24def 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
59class 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
210def _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
255def _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
275def _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