1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.command; 17 18 import com.android.tradefed.log.LogUtil.CLog; 19 import com.android.tradefed.util.IRunUtil; 20 import com.android.tradefed.util.RunUtil; 21 22 import com.google.common.annotations.VisibleForTesting; 23 24 import java.io.File; 25 import java.util.ArrayList; 26 import java.util.Collection; 27 import java.util.Collections; 28 import java.util.HashSet; 29 import java.util.Hashtable; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Set; 33 34 /** 35 * A simple class to watch a set of command files for changes, and to trigger a 36 * reload of _all_ manually-loaded command files when such a change happens. 37 */ 38 class CommandFileWatcher extends Thread { 39 private static final long POLL_TIME_MS = 20 * 1000; // 20 seconds 40 // thread-safe (for read-writes, not write during iteration) structure holding all commands 41 // being watched. map of absolute file system path to command file 42 private Map<String, CommandFile> mCmdFileMap = new Hashtable<>(); 43 boolean mCancelled = false; 44 private final ICommandFileListener mListener; 45 46 static interface ICommandFileListener { notifyFileChanged(File cmdFile, List<String> extraArgs)47 public void notifyFileChanged(File cmdFile, List<String> extraArgs); 48 } 49 50 /** 51 * A simple struct to store a command file as well as its extra args 52 */ 53 static class CommandFile { 54 public final File file; 55 public final long modTime; 56 public final List<String> extraArgs; 57 public final List<CommandFile> dependencies; 58 59 /** 60 * Construct a CommandFile with no arguments and no dependencies 61 * 62 * @param cmdFile a {@link File} representing the command file path 63 */ CommandFile(File cmdFile)64 public CommandFile(File cmdFile) { 65 if (cmdFile == null) { 66 throw new NullPointerException(); 67 } 68 69 this.file = cmdFile; 70 this.modTime = cmdFile.lastModified(); 71 72 this.extraArgs = Collections.emptyList(); 73 this.dependencies = Collections.emptyList(); 74 } 75 76 /** 77 * Construct a CommandFile 78 * 79 * @param cmdFile a {@link File} representing the command file path 80 * @param extraArgs A {@link List} of extra arguments that should be 81 * used when the command is rerun. 82 * @param dependencies The command files that this command file 83 * requires as transitive dependencies. A change in any of the 84 * dependencies will trigger a reload, but none of the 85 * dependencies themselves will be reloaded directly, only the 86 * main command file, {@code cmdFile}. 87 */ CommandFile(File cmdFile, List<String> extraArgs, List<File> dependencies)88 public CommandFile(File cmdFile, List<String> extraArgs, List<File> dependencies) { 89 if (cmdFile == null) { 90 throw new NullPointerException(); 91 } 92 93 this.file = cmdFile; 94 this.modTime = cmdFile.lastModified(); 95 96 if (extraArgs == null) { 97 this.extraArgs = Collections.emptyList(); 98 } else { 99 this.extraArgs = extraArgs; 100 } 101 if (dependencies == null) { 102 this.dependencies = Collections.emptyList(); 103 104 } else { 105 this.dependencies = new ArrayList<CommandFile>(dependencies.size()); 106 for (File f: dependencies) { 107 this.dependencies.add(new CommandFile(f)); 108 } 109 } 110 } 111 } 112 CommandFileWatcher(ICommandFileListener listener)113 public CommandFileWatcher(ICommandFileListener listener) { 114 super("CommandFileWatcher"); // set the thread name 115 mListener = listener; 116 setDaemon(true); // Don't keep the JVM alive for this thread 117 } 118 119 /** 120 * {@inheritDoc} 121 */ 122 @Override run()123 public void run() { 124 while (!isCancelled()) { 125 checkForUpdates(); 126 getRunUtil().sleep(POLL_TIME_MS); 127 } 128 } 129 130 /** 131 * Same as {@link #addCmdFile(File, List, Collection)} but accepts a list of {@link File}s 132 * as dependencies 133 */ 134 @VisibleForTesting addCmdFile(File cmdFile, List<String> extraArgs, List<File> dependencies)135 void addCmdFile(File cmdFile, List<String> extraArgs, List<File> dependencies) { 136 CommandFile f = new CommandFile(cmdFile, extraArgs, dependencies); 137 mCmdFileMap.put(cmdFile.getAbsolutePath(), f); 138 } 139 140 /** 141 * <p> 142 * Add a command file to watch, as well as its dependencies. When either 143 * the command file itself or any of its dependencies changes, notify the registered 144 * {@link ICommandFileListener} 145 * </p> 146 * if the cmdFile is already being watching, this call will replace the current entry 147 */ addCmdFile(File cmdFile, List<String> extraArgs, Collection<String> includedFiles)148 public void addCmdFile(File cmdFile, List<String> extraArgs, Collection<String> includedFiles) { 149 List<File> includesAsFiles = new ArrayList<File>(includedFiles.size()); 150 for (String p : includedFiles) { 151 includesAsFiles.add(new File(p)); 152 } 153 addCmdFile(cmdFile, extraArgs, includesAsFiles); 154 } 155 156 /** 157 * Returns true if given command gile path is currently being watched 158 */ isFileWatched(File cmdFile)159 public boolean isFileWatched(File cmdFile) { 160 return mCmdFileMap.containsKey(cmdFile.getAbsolutePath()); 161 } 162 163 /** 164 * Terminate the watcher thread 165 */ cancel()166 public void cancel() { 167 mCancelled = true; 168 interrupt(); 169 } 170 171 /** 172 * Check if the thread has been signalled to stop. 173 */ isCancelled()174 public boolean isCancelled() { 175 return mCancelled; 176 } 177 178 /** 179 * Poll the filesystem to see if any of the files of interest have 180 * changed 181 * <p /> 182 * Exposed for unit testing 183 */ checkForUpdates()184 void checkForUpdates() { 185 final Set<File> checkedFiles = new HashSet<File>(); 186 187 // iterate through a copy of the command list to limit time lock needs to be held 188 List<CommandFile> cmdCopy; 189 synchronized (mCmdFileMap) { 190 cmdCopy = new ArrayList<CommandFile>(mCmdFileMap.values()); 191 } 192 for (CommandFile cmd : cmdCopy) { 193 if (checkCommandFileForUpdate(cmd, checkedFiles)) { 194 mListener.notifyFileChanged(cmd.file, cmd.extraArgs); 195 } 196 } 197 } 198 checkCommandFileForUpdate(CommandFile cmd, Set<File> checkedFiles)199 boolean checkCommandFileForUpdate(CommandFile cmd, Set<File> checkedFiles) { 200 if (checkedFiles.contains(cmd.file)) { 201 return false; 202 } else { 203 checkedFiles.add(cmd.file); 204 } 205 206 final long curModTime = cmd.file.lastModified(); 207 if (curModTime == 0L) { 208 // File doesn't exist, or had an IO error. Don't do anything. If a change occurs 209 // that we should pay attention to, then we'll see the file actually updated, which 210 // implies that the modtime will be non-zero and will also be different from what 211 // we stored before. 212 } else if (curModTime != cmd.modTime) { 213 // Note that we land on this case if the original modtime was 0 and the modtime is 214 // now non-zero, so there's a race-condition if an IO error causes us to fail to 215 // read the modtime initially. This should be okay. 216 CLog.w("Found update in monitored cmdfile %s (%d -> %d)", cmd.file, cmd.modTime, 217 curModTime); 218 return true; 219 } 220 221 // Now check dependencies 222 for (CommandFile dep : cmd.dependencies) { 223 if (checkCommandFileForUpdate(dep, checkedFiles)) { 224 // dependency changed 225 return true; 226 } 227 } 228 229 // We didn't change, and nor did any of our dependencies 230 return false; 231 } 232 233 /** 234 * Factory method for creating a {@link CommandFileParser}. 235 * <p/> 236 * Exposed for unit testing. 237 */ createCommandFileParser()238 CommandFileParser createCommandFileParser() { 239 return new CommandFileParser(); 240 } 241 242 /** 243 * Utility method to fetch the default {@link IRunUtil} singleton 244 * <p /> 245 * Exposed for unit testing. 246 */ getRunUtil()247 IRunUtil getRunUtil() { 248 return RunUtil.getDefault(); 249 } 250 251 /** 252 * Remove all files from the watched list 253 */ removeAllFiles()254 public void removeAllFiles() { 255 mCmdFileMap.clear(); 256 } 257 258 /** 259 * Retrieves the extra arguments associated with given file being watched. 260 * <p> 261 * TODO: extra args list should likely be stored elsewhere, and have this class just operate 262 * as a generic file watcher with dependencies 263 * </p> 264 * @return the list of extra arguments associated with command file. Returns empty list if 265 * command path is not recognized 266 */ getExtraArgsForFile(String cmdPath)267 public List<String> getExtraArgsForFile(String cmdPath) { 268 CommandFile cmdFile = mCmdFileMap.get(cmdPath); 269 if (cmdFile != null) { 270 return cmdFile.extraArgs; 271 } 272 CLog.w("Could not find cmdfile %s", cmdPath); 273 return Collections.<String>emptyList(); 274 } 275 } 276