• 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      if not self._device.HasRoot():
78        raise ValueError('use_legacy_path requires a rooted device')
79    self._cmdline_path = cmdline_path
80
81    if self._device.PathExists(alternate_cmdline_path):
82      logger.warning(
83          'Removing alternate command line file %r.', alternate_cmdline_path)
84      self._device.RemovePath(alternate_cmdline_path, as_root=True)
85
86    self._state_stack = [None]  # Actual state is set by GetCurrentFlags().
87    self.GetCurrentFlags()
88
89  def GetCurrentFlags(self):
90    """Read the current flags currently stored in the device.
91
92    Also updates the internal state of the flag_changer.
93
94    Returns:
95      A list of flags.
96    """
97    if self._device.PathExists(self._cmdline_path):
98      command_line = self._device.ReadFile(
99          self._cmdline_path, as_root=True).strip()
100    else:
101      command_line = ''
102    flags = _ParseFlags(command_line)
103
104    # Store the flags as a set to facilitate adding and removing flags.
105    self._state_stack[-1] = set(flags)
106    return flags
107
108  def ReplaceFlags(self, flags, log_flags=True):
109    """Replaces the flags in the command line with the ones provided.
110       Saves the current flags state on the stack, so a call to Restore will
111       change the state back to the one preceeding the call to ReplaceFlags.
112
113    Args:
114      flags: A sequence of command line flags to set, eg. ['--single-process'].
115             Note: this should include flags only, not the name of a command
116             to run (ie. there is no need to start the sequence with 'chrome').
117
118    Returns:
119      A list with the flags now stored on the device.
120    """
121    new_flags = set(flags)
122    self._state_stack.append(new_flags)
123    self._SetPermissive()
124    return self._UpdateCommandLineFile(log_flags=log_flags)
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 _SetPermissive(self):
181    """Set SELinux to permissive, if needed.
182
183    On Android N and above this is needed in order to allow Chrome to read the
184    legacy command line file.
185
186    TODO(crbug.com/699082): Remove when a better solution exists.
187    """
188    # TODO(crbug.com/948578): figure out the exact scenarios where the lowered
189    # permissions are needed, and document them in the code.
190    if not self._device.HasRoot():
191      return
192    if (self._device.build_version_sdk >= version_codes.NOUGAT and
193        self._device.GetEnforce()):
194      self._device.SetEnforce(enabled=False)
195      self._should_reset_enforce = True
196
197  def _ResetEnforce(self):
198    """Restore SELinux policy if it had been previously made permissive."""
199    if self._should_reset_enforce:
200      self._device.SetEnforce(enabled=True)
201      self._should_reset_enforce = False
202
203  def Restore(self):
204    """Restores the flags to their state prior to the last AddFlags or
205       RemoveFlags call.
206
207    Returns:
208      A list with the flags now stored on the device.
209    """
210    # The initial state must always remain on the stack.
211    assert len(self._state_stack) > 1, (
212        'Mismatch between calls to Add/RemoveFlags and Restore')
213    self._state_stack.pop()
214    if len(self._state_stack) == 1:
215      self._ResetEnforce()
216    return self._UpdateCommandLineFile()
217
218  def _UpdateCommandLineFile(self, log_flags=True):
219    """Writes out the command line to the file, or removes it if empty.
220
221    Returns:
222      A list with the flags now stored on the device.
223    """
224    command_line = _SerializeFlags(self._state_stack[-1])
225    if command_line is not None:
226      self._device.WriteFile(self._cmdline_path, command_line, as_root=True)
227    else:
228      self._device.RemovePath(self._cmdline_path, force=True, as_root=True)
229
230    flags = self.GetCurrentFlags()
231    logging.info('Flags now written on the device to %s', self._cmdline_path)
232    if log_flags:
233      logging.info('Flags: %s', flags)
234    return flags
235
236
237def _ParseFlags(line):
238  """Parse the string containing the command line into a list of flags.
239
240  It's a direct port of CommandLine.java::tokenizeQuotedArguments.
241
242  The first token is assumed to be the (unused) program name and stripped off
243  from the list of flags.
244
245  Args:
246    line: A string containing the entire command line.  The first token is
247          assumed to be the program name.
248
249  Returns:
250     A list of flags, with quoting removed.
251  """
252  flags = []
253  current_quote = None
254  current_flag = None
255
256  # pylint: disable=unsubscriptable-object
257  for c in line:
258    # Detect start or end of quote block.
259    if (current_quote is None and c in _QUOTES) or c == current_quote:
260      if current_flag is not None and current_flag[-1] == _ESCAPE:
261        # Last char was a backslash; pop it, and treat c as a literal.
262        current_flag = current_flag[:-1] + c
263      else:
264        current_quote = c if current_quote is None else None
265    elif current_quote is None and c.isspace():
266      if current_flag is not None:
267        flags.append(current_flag)
268        current_flag = None
269    else:
270      if current_flag is None:
271        current_flag = ''
272      current_flag += c
273
274  if current_flag is not None:
275    if current_quote is not None:
276      logger.warning('Unterminated quoted argument: ' + current_flag)
277    flags.append(current_flag)
278
279  # Return everything but the program name.
280  return flags[1:]
281
282
283def _SerializeFlags(flags):
284  """Serialize a sequence of flags into a command line string.
285
286  Args:
287    flags: A sequence of strings with individual flags.
288
289  Returns:
290    A line with the command line contents to save; or None if the sequence of
291    flags is empty.
292  """
293  if flags:
294    # The first command line argument doesn't matter as we are not actually
295    # launching the chrome executable using this command line.
296    args = ['_']
297    args.extend(_QuoteFlag(f) for f in flags)
298    return ' '.join(args)
299  else:
300    return None
301
302
303def _QuoteFlag(flag):
304  """Validate and quote a single flag.
305
306  Args:
307    A string with the flag to quote.
308
309  Returns:
310    A string with the flag quoted so that it can be parsed by the algorithm
311    in _ParseFlags; or None if the flag does not appear to be valid.
312  """
313  if '=' in flag:
314    key, value = flag.split('=', 1)
315  else:
316    key, value = flag, None
317
318  if not flag or _RE_NEEDS_QUOTING.search(key):
319    # Probably not a valid flag, but quote the whole thing so it can be
320    # parsed back correctly.
321    return '"%s"' % flag.replace('"', r'\"')
322
323  if value is None:
324    return key
325
326  if _RE_NEEDS_QUOTING.search(value):
327    value = '"%s"' % value.replace('"', r'\"')
328  return '='.join([key, value])
329