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