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 logging 6 7 from devil.android import device_errors 8 9 10 class FlagChanger(object): 11 """Changes the flags Chrome runs with. 12 13 Flags can be temporarily set for a particular set of unit tests. These 14 tests should call Restore() to revert the flags to their original state 15 once the tests have completed. 16 """ 17 18 def __init__(self, device, cmdline_file): 19 """Initializes the FlagChanger and records the original arguments. 20 21 Args: 22 device: A DeviceUtils instance. 23 cmdline_file: Path to the command line file on the device. 24 """ 25 self._device = device 26 27 # Unrooted devices have limited access to the file system. 28 # Place files in /data/local/tmp/ rather than /data/local/ 29 if not device.HasRoot() and not '/data/local/tmp/' in cmdline_file: 30 self._cmdline_file = cmdline_file.replace('/data/local/', 31 '/data/local/tmp/') 32 else: 33 self._cmdline_file = cmdline_file 34 35 stored_flags = '' 36 if self._device.PathExists(self._cmdline_file): 37 try: 38 stored_flags = self._device.ReadFile(self._cmdline_file).strip() 39 except device_errors.CommandFailedError: 40 pass 41 # Store the flags as a set to facilitate adding and removing flags. 42 self._state_stack = [set(self._TokenizeFlags(stored_flags))] 43 44 def ReplaceFlags(self, flags): 45 """Replaces the flags in the command line with the ones provided. 46 Saves the current flags state on the stack, so a call to Restore will 47 change the state back to the one preceeding the call to ReplaceFlags. 48 49 Args: 50 flags: A sequence of command line flags to set, eg. ['--single-process']. 51 Note: this should include flags only, not the name of a command 52 to run (ie. there is no need to start the sequence with 'chrome'). 53 """ 54 new_flags = set(flags) 55 self._state_stack.append(new_flags) 56 self._UpdateCommandLineFile() 57 58 def AddFlags(self, flags): 59 """Appends flags to the command line if they aren't already there. 60 Saves the current flags state on the stack, so a call to Restore will 61 change the state back to the one preceeding the call to AddFlags. 62 63 Args: 64 flags: A sequence of flags to add on, eg. ['--single-process']. 65 """ 66 self.PushFlags(add=flags) 67 68 def RemoveFlags(self, flags): 69 """Removes flags from the command line, if they exist. 70 Saves the current flags state on the stack, so a call to Restore will 71 change the state back to the one preceeding the call to RemoveFlags. 72 73 Note that calling RemoveFlags after AddFlags will result in having 74 two nested states. 75 76 Args: 77 flags: A sequence of flags to remove, eg. ['--single-process']. Note 78 that we expect a complete match when removing flags; if you want 79 to remove a switch with a value, you must use the exact string 80 used to add it in the first place. 81 """ 82 self.PushFlags(remove=flags) 83 84 def PushFlags(self, add=None, remove=None): 85 """Appends and removes flags to/from the command line if they aren't already 86 there. Saves the current flags state on the stack, so a call to Restore 87 will change the state back to the one preceeding the call to PushFlags. 88 89 Args: 90 add: A list of flags to add on, eg. ['--single-process']. 91 remove: A list of flags to remove, eg. ['--single-process']. Note that we 92 expect a complete match when removing flags; if you want to remove 93 a switch with a value, you must use the exact string used to add 94 it in the first place. 95 """ 96 new_flags = self._state_stack[-1].copy() 97 if add: 98 new_flags.update(add) 99 if remove: 100 new_flags.difference_update(remove) 101 self.ReplaceFlags(new_flags) 102 103 def Restore(self): 104 """Restores the flags to their state prior to the last AddFlags or 105 RemoveFlags call. 106 """ 107 # The initial state must always remain on the stack. 108 assert len(self._state_stack) > 1, ( 109 "Mismatch between calls to Add/RemoveFlags and Restore") 110 self._state_stack.pop() 111 self._UpdateCommandLineFile() 112 113 def _UpdateCommandLineFile(self): 114 """Writes out the command line to the file, or removes it if empty.""" 115 current_flags = list(self._state_stack[-1]) 116 logging.info('Current flags: %s', current_flags) 117 # Root is not required to write to /data/local/tmp/. 118 use_root = '/data/local/tmp/' not in self._cmdline_file 119 if current_flags: 120 # The first command line argument doesn't matter as we are not actually 121 # launching the chrome executable using this command line. 122 cmd_line = ' '.join(['_'] + current_flags) 123 self._device.WriteFile( 124 self._cmdline_file, cmd_line, as_root=use_root) 125 file_contents = self._device.ReadFile( 126 self._cmdline_file, as_root=use_root).rstrip() 127 assert file_contents == cmd_line, ( 128 'Failed to set the command line file at %s' % self._cmdline_file) 129 else: 130 self._device.RunShellCommand('rm ' + self._cmdline_file, 131 as_root=use_root) 132 assert not self._device.FileExists(self._cmdline_file), ( 133 'Failed to remove the command line file at %s' % self._cmdline_file) 134 135 @staticmethod 136 def _TokenizeFlags(line): 137 """Changes the string containing the command line into a list of flags. 138 139 Follows similar logic to CommandLine.java::tokenizeQuotedArguments: 140 * Flags are split using whitespace, unless the whitespace is within a 141 pair of quotation marks. 142 * Unlike the Java version, we keep the quotation marks around switch 143 values since we need them to re-create the file when new flags are 144 appended. 145 146 Args: 147 line: A string containing the entire command line. The first token is 148 assumed to be the program name. 149 """ 150 if not line: 151 return [] 152 153 tokenized_flags = [] 154 current_flag = "" 155 within_quotations = False 156 157 # Move through the string character by character and build up each flag 158 # along the way. 159 for c in line.strip(): 160 if c is '"': 161 if len(current_flag) > 0 and current_flag[-1] == '\\': 162 # Last char was a backslash; pop it, and treat this " as a literal. 163 current_flag = current_flag[0:-1] + '"' 164 else: 165 within_quotations = not within_quotations 166 current_flag += c 167 elif not within_quotations and (c is ' ' or c is '\t'): 168 if current_flag is not "": 169 tokenized_flags.append(current_flag) 170 current_flag = "" 171 else: 172 current_flag += c 173 174 # Tack on the last flag. 175 if not current_flag: 176 if within_quotations: 177 logging.warn('Unterminated quoted argument: ' + line) 178 else: 179 tokenized_flags.append(current_flag) 180 181 # Return everything but the program name. 182 return tokenized_flags[1:] 183