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 12logger = logging.getLogger(__name__) 13 14_CMDLINE_DIR = '/data/local/tmp' 15_CMDLINE_DIR_LEGACY = '/data/local' 16_RE_NEEDS_QUOTING = re.compile(r'[^\w-]') # Not in: alphanumeric or hyphens. 17_QUOTES = '"\'' # Either a single or a double quote. 18_ESCAPE = '\\' # A backslash. 19 20 21@contextlib.contextmanager 22def CustomCommandLineFlags(device, cmdline_name, flags): 23 """Context manager to change Chrome's command line temporarily. 24 25 Example: 26 27 with flag_changer.TemporaryCommandLineFlags(device, name, flags): 28 # Launching Chrome will use the provided flags. 29 30 # Previous set of flags on the device is now restored. 31 32 Args: 33 device: A DeviceUtils instance. 34 cmdline_name: Name of the command line file where to store flags. 35 flags: A sequence of command line flags to set. 36 """ 37 changer = FlagChanger(device, cmdline_name) 38 try: 39 changer.ReplaceFlags(flags) 40 yield 41 finally: 42 changer.Restore() 43 44 45class FlagChanger(object): 46 """Changes the flags Chrome runs with. 47 48 Flags can be temporarily set for a particular set of unit tests. These 49 tests should call Restore() to revert the flags to their original state 50 once the tests have completed. 51 """ 52 53 def __init__(self, device, cmdline_file, use_legacy_path=False): 54 """Initializes the FlagChanger and records the original arguments. 55 56 Args: 57 device: A DeviceUtils instance. 58 cmdline_file: Name of the command line file where to store flags. 59 use_legacy_path: Whether to use the legacy commandline path (needed for 60 M54 and earlier) 61 """ 62 self._device = device 63 self._should_reset_enforce = False 64 65 if posixpath.sep in cmdline_file: 66 raise ValueError( 67 'cmdline_file should be a file name only, do not include path' 68 ' separators in: %s' % cmdline_file) 69 cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file) 70 alternate_cmdline_path = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file) 71 72 if use_legacy_path: 73 cmdline_path, alternate_cmdline_path = (alternate_cmdline_path, 74 cmdline_path) 75 if not self._device.HasRoot(): 76 raise ValueError('use_legacy_path requires a rooted device') 77 self._cmdline_path = cmdline_path 78 79 if self._device.PathExists(alternate_cmdline_path): 80 logger.warning('Removing alternate command line file %r.', 81 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, log_flags=True): 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(log_flags=log_flags) 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 legacy command line file. 183 184 TODO(crbug.com/699082): Remove when a better solution exists. 185 """ 186 # TODO(crbug.com/948578): figure out the exact scenarios where the lowered 187 # permissions are needed, and document them in the code. 188 if not self._device.HasRoot(): 189 return 190 if (self._device.build_version_sdk >= version_codes.NOUGAT 191 and self._device.GetEnforce()): 192 self._device.SetEnforce(enabled=False) 193 self._should_reset_enforce = True 194 195 def _ResetEnforce(self): 196 """Restore SELinux policy if it had been previously made permissive.""" 197 if self._should_reset_enforce: 198 self._device.SetEnforce(enabled=True) 199 self._should_reset_enforce = False 200 201 def Restore(self): 202 """Restores the flags to their state prior to the last AddFlags or 203 RemoveFlags call. 204 205 Returns: 206 A list with the flags now stored on the device. 207 """ 208 # The initial state must always remain on the stack. 209 assert len(self._state_stack) > 1, ( 210 'Mismatch between calls to Add/RemoveFlags and Restore') 211 self._state_stack.pop() 212 if len(self._state_stack) == 1: 213 self._ResetEnforce() 214 return self._UpdateCommandLineFile() 215 216 def _UpdateCommandLineFile(self, log_flags=True): 217 """Writes out the command line to the file, or removes it if empty. 218 219 Returns: 220 A list with the flags now stored on the device. 221 """ 222 command_line = _SerializeFlags(self._state_stack[-1]) 223 if command_line is not None: 224 self._device.WriteFile(self._cmdline_path, command_line, as_root=True) 225 else: 226 self._device.RemovePath(self._cmdline_path, force=True, as_root=True) 227 228 flags = self.GetCurrentFlags() 229 logging.info('Flags now written on the device to %s', self._cmdline_path) 230 if log_flags: 231 logging.info('Flags: %s', flags) 232 return flags 233 234 235def _ParseFlags(line): 236 """Parse the string containing the command line into a list of flags. 237 238 It's a direct port of CommandLine.java::tokenizeQuotedArguments. 239 240 The first token is assumed to be the (unused) program name and stripped off 241 from the list of flags. 242 243 Args: 244 line: A string containing the entire command line. The first token is 245 assumed to be the program name. 246 247 Returns: 248 A list of flags, with quoting removed. 249 """ 250 flags = [] 251 current_quote = None 252 current_flag = None 253 254 # pylint: disable=unsubscriptable-object 255 for c in line: 256 # Detect start or end of quote block. 257 if (current_quote is None and c in _QUOTES) or c == current_quote: 258 if current_flag is not None and current_flag[-1] == _ESCAPE: 259 # Last char was a backslash; pop it, and treat c as a literal. 260 current_flag = current_flag[:-1] + c 261 else: 262 current_quote = c if current_quote is None else None 263 elif current_quote is None and c.isspace(): 264 if current_flag is not None: 265 flags.append(current_flag) 266 current_flag = None 267 else: 268 if current_flag is None: 269 current_flag = '' 270 current_flag += c 271 272 if current_flag is not None: 273 if current_quote is not None: 274 logger.warning('Unterminated quoted argument: ' + current_flag) 275 flags.append(current_flag) 276 277 # Return everything but the program name. 278 return flags[1:] 279 280 281def _SerializeFlags(flags): 282 """Serialize a sequence of flags into a command line string. 283 284 Args: 285 flags: A sequence of strings with individual flags. 286 287 Returns: 288 A line with the command line contents to save; or None if the sequence of 289 flags is empty. 290 """ 291 if flags: 292 # The first command line argument doesn't matter as we are not actually 293 # launching the chrome executable using this command line. 294 args = ['_'] 295 args.extend(_QuoteFlag(f) for f in flags) 296 return ' '.join(args) 297 else: 298 return None 299 300 301def _QuoteFlag(flag): 302 """Validate and quote a single flag. 303 304 Args: 305 A string with the flag to quote. 306 307 Returns: 308 A string with the flag quoted so that it can be parsed by the algorithm 309 in _ParseFlags; or None if the flag does not appear to be valid. 310 """ 311 if '=' in flag: 312 key, value = flag.split('=', 1) 313 else: 314 key, value = flag, None 315 316 if not flag or _RE_NEEDS_QUOTING.search(key): 317 # Probably not a valid flag, but quote the whole thing so it can be 318 # parsed back correctly. 319 return '"%s"' % flag.replace('"', r'\"') 320 321 if value is None: 322 return key 323 324 if _RE_NEEDS_QUOTING.search(value): 325 value = '"%s"' % value.replace('"', r'\"') 326 return '='.join([key, value]) 327