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