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 # On Android N and above, we need to temporarily set SELinux to permissive 40 # so that Chrome is allowed to read the command line file. 41 # TODO(crbug.com/699082): Remove when a solution to avoid this is implemented. 42 needs_permissive = ( 43 device.build_version_sdk >= version_codes.NOUGAT and 44 device.GetEnforce()) 45 if needs_permissive: 46 device.SetEnforce(enabled=False) 47 try: 48 changer = FlagChanger(device, cmdline_name) 49 try: 50 changer.ReplaceFlags(flags) 51 yield 52 finally: 53 changer.Restore() 54 finally: 55 if needs_permissive: 56 device.SetEnforce(enabled=True) 57 58 59class FlagChanger(object): 60 """Changes the flags Chrome runs with. 61 62 Flags can be temporarily set for a particular set of unit tests. These 63 tests should call Restore() to revert the flags to their original state 64 once the tests have completed. 65 """ 66 67 def __init__(self, device, cmdline_file): 68 """Initializes the FlagChanger and records the original arguments. 69 70 Args: 71 device: A DeviceUtils instance. 72 cmdline_file: Name of the command line file where to store flags. 73 """ 74 self._device = device 75 76 if posixpath.sep in cmdline_file: 77 raise ValueError( 78 'cmdline_file should be a file name only, do not include path' 79 ' separators in: %s' % cmdline_file) 80 self._cmdline_path = posixpath.join(_CMDLINE_DIR, cmdline_file) 81 82 cmdline_path_legacy = posixpath.join(_CMDLINE_DIR_LEGACY, cmdline_file) 83 if self._device.PathExists(cmdline_path_legacy): 84 logging.warning( 85 'Removing legacy command line file %r.', cmdline_path_legacy) 86 self._device.RemovePath(cmdline_path_legacy, as_root=True) 87 88 self._state_stack = [None] # Actual state is set by GetCurrentFlags(). 89 self.GetCurrentFlags() 90 91 def GetCurrentFlags(self): 92 """Read the current flags currently stored in the device. 93 94 Also updates the internal state of the flag_changer. 95 96 Returns: 97 A list of flags. 98 """ 99 if self._device.PathExists(self._cmdline_path): 100 command_line = self._device.ReadFile(self._cmdline_path).strip() 101 else: 102 command_line = '' 103 flags = _ParseFlags(command_line) 104 105 # Store the flags as a set to facilitate adding and removing flags. 106 self._state_stack[-1] = set(flags) 107 return flags 108 109 def ReplaceFlags(self, flags): 110 """Replaces the flags in the command line with the ones provided. 111 Saves the current flags state on the stack, so a call to Restore will 112 change the state back to the one preceeding the call to ReplaceFlags. 113 114 Args: 115 flags: A sequence of command line flags to set, eg. ['--single-process']. 116 Note: this should include flags only, not the name of a command 117 to run (ie. there is no need to start the sequence with 'chrome'). 118 119 Returns: 120 A list with the flags now stored on the device. 121 """ 122 new_flags = set(flags) 123 self._state_stack.append(new_flags) 124 return self._UpdateCommandLineFile() 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 Restore(self): 181 """Restores the flags to their state prior to the last AddFlags or 182 RemoveFlags call. 183 184 Returns: 185 A list with the flags now stored on the device. 186 """ 187 # The initial state must always remain on the stack. 188 assert len(self._state_stack) > 1, ( 189 "Mismatch between calls to Add/RemoveFlags and Restore") 190 self._state_stack.pop() 191 return self._UpdateCommandLineFile() 192 193 def _UpdateCommandLineFile(self): 194 """Writes out the command line to the file, or removes it if empty. 195 196 Returns: 197 A list with the flags now stored on the device. 198 """ 199 command_line = _SerializeFlags(self._state_stack[-1]) 200 if command_line is not None: 201 self._device.WriteFile(self._cmdline_path, command_line) 202 else: 203 self._device.RemovePath(self._cmdline_path, force=True) 204 205 current_flags = self.GetCurrentFlags() 206 logger.info('Flags now set on the device: %s', current_flags) 207 return current_flags 208 209 210def _ParseFlags(line): 211 """Parse the string containing the command line into a list of flags. 212 213 It's a direct port of CommandLine.java::tokenizeQuotedArguments. 214 215 The first token is assumed to be the (unused) program name and stripped off 216 from the list of flags. 217 218 Args: 219 line: A string containing the entire command line. The first token is 220 assumed to be the program name. 221 222 Returns: 223 A list of flags, with quoting removed. 224 """ 225 flags = [] 226 current_quote = None 227 current_flag = None 228 229 for c in line: 230 # Detect start or end of quote block. 231 if (current_quote is None and c in _QUOTES) or c == current_quote: 232 if current_flag is not None and current_flag[-1] == _ESCAPE: 233 # Last char was a backslash; pop it, and treat c as a literal. 234 current_flag = current_flag[:-1] + c 235 else: 236 current_quote = c if current_quote is None else None 237 elif current_quote is None and c.isspace(): 238 if current_flag is not None: 239 flags.append(current_flag) 240 current_flag = None 241 else: 242 if current_flag is None: 243 current_flag = '' 244 current_flag += c 245 246 if current_flag is not None: 247 if current_quote is not None: 248 logger.warning('Unterminated quoted argument: ' + current_flag) 249 flags.append(current_flag) 250 251 # Return everything but the program name. 252 return flags[1:] 253 254 255def _SerializeFlags(flags): 256 """Serialize a sequence of flags into a command line string. 257 258 Args: 259 flags: A sequence of strings with individual flags. 260 261 Returns: 262 A line with the command line contents to save; or None if the sequence of 263 flags is empty. 264 """ 265 if flags: 266 # The first command line argument doesn't matter as we are not actually 267 # launching the chrome executable using this command line. 268 args = ['_'] 269 args.extend(_QuoteFlag(f) for f in flags) 270 return ' '.join(args) 271 else: 272 return None 273 274 275def _QuoteFlag(flag): 276 """Validate and quote a single flag. 277 278 Args: 279 A string with the flag to quote. 280 281 Returns: 282 A string with the flag quoted so that it can be parsed by the algorithm 283 in _ParseFlags; or None if the flag does not appear to be valid. 284 """ 285 if '=' in flag: 286 key, value = flag.split('=', 1) 287 else: 288 key, value = flag, None 289 290 if not flag or _RE_NEEDS_QUOTING.search(key): 291 # Probably not a valid flag, but quote the whole thing so it can be 292 # parsed back correctly. 293 return '"%s"' % flag.replace('"', r'\"') 294 295 if value is None: 296 return key 297 298 if _RE_NEEDS_QUOTING.search(value): 299 value = '"%s"' % value.replace('"', r'\"') 300 return '='.join([key, value]) 301