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