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) throw new NullPointerException(); 66 67 this.file = cmdFile; 68 this.modTime = cmdFile.lastModified(); 69 70 this.extraArgs = Collections.emptyList(); 71 this.dependencies = Collections.emptyList(); 72 } 73 74 /** 75 * Construct a CommandFile 76 * 77 * @param cmdFile a {@link File} representing the command file path 78 * @param extraArgs A {@link List} of extra arguments that should be 79 * used when the command is rerun. 80 * @param dependencies The command files that this command file 81 * requires as transitive dependencies. A change in any of the 82 * dependencies will trigger a reload, but none of the 83 * dependencies themselves will be reloaded directly, only the 84 * main command file, {@code cmdFile}. 85 */ CommandFile(File cmdFile, List<String> extraArgs, List<File> dependencies)86 public CommandFile(File cmdFile, List<String> extraArgs, List<File> dependencies) { 87 if (cmdFile == null) throw new NullPointerException(); 88 89 this.file = cmdFile; 90 this.modTime = cmdFile.lastModified(); 91 92 if (extraArgs == null) { 93 this.extraArgs = Collections.emptyList(); 94 } else { 95 this.extraArgs = extraArgs; 96 } 97 if (dependencies == null) { 98 this.dependencies = Collections.emptyList(); 99 100 } else { 101 this.dependencies = new ArrayList<CommandFile>(dependencies.size()); 102 for (File f: dependencies) { 103 this.dependencies.add(new CommandFile(f)); 104 } 105 } 106 } 107 } 108 CommandFileWatcher(ICommandFileListener listener)109 public CommandFileWatcher(ICommandFileListener listener) { 110 super("CommandFileWatcher"); // set the thread name 111 mListener = listener; 112 setDaemon(true); // Don't keep the JVM alive for this thread 113 } 114 115 /** 116 * {@inheritDoc} 117 */ 118 @Override run()119 public void run() { 120 while (!isCancelled()) { 121 checkForUpdates(); 122 getRunUtil().sleep(POLL_TIME_MS); 123 } 124 } 125 126 /** 127 * Same as {@link #addCmdFile(File, List, Collection)} but accepts a list of {@link File}s 128 * as dependencies 129 */ 130 @VisibleForTesting addCmdFile(File cmdFile, List<String> extraArgs, List<File> dependencies)131 void addCmdFile(File cmdFile, List<String> extraArgs, List<File> dependencies) { 132 CommandFile f = new CommandFile(cmdFile, extraArgs, dependencies); 133 mCmdFileMap.put(cmdFile.getAbsolutePath(), f); 134 } 135 136 /** 137 * Returns true if given command gile path is currently being watched 138 */ isFileWatched(File cmdFile)139 public boolean isFileWatched(File cmdFile) { 140 return mCmdFileMap.containsKey(cmdFile.getAbsolutePath()); 141 } 142 143 /** 144 * Terminate the watcher thread 145 */ cancel()146 public void cancel() { 147 mCancelled = true; 148 interrupt(); 149 } 150 151 /** 152 * Check if the thread has been signalled to stop. 153 */ isCancelled()154 public boolean isCancelled() { 155 return mCancelled; 156 } 157 158 /** 159 * Poll the filesystem to see if any of the files of interest have 160 * changed 161 * <p /> 162 * Exposed for unit testing 163 */ checkForUpdates()164 void checkForUpdates() { 165 final Set<File> checkedFiles = new HashSet<File>(); 166 167 // iterate through a copy of the command list to limit time lock needs to be held 168 List<CommandFile> cmdCopy; 169 synchronized (mCmdFileMap) { 170 cmdCopy = new ArrayList<CommandFile>(mCmdFileMap.values()); 171 } 172 for (CommandFile cmd : cmdCopy) { 173 if (checkCommandFileForUpdate(cmd, checkedFiles)) { 174 mListener.notifyFileChanged(cmd.file, cmd.extraArgs); 175 } 176 } 177 } 178 checkCommandFileForUpdate(CommandFile cmd, Set<File> checkedFiles)179 boolean checkCommandFileForUpdate(CommandFile cmd, Set<File> checkedFiles) { 180 if (checkedFiles.contains(cmd.file)) { 181 return false; 182 } else { 183 checkedFiles.add(cmd.file); 184 } 185 186 final long curModTime = cmd.file.lastModified(); 187 if (curModTime == 0L) { 188 // File doesn't exist, or had an IO error. Don't do anything. If a change occurs 189 // that we should pay attention to, then we'll see the file actually updated, which 190 // implies that the modtime will be non-zero and will also be different from what 191 // we stored before. 192 } else if (curModTime != cmd.modTime) { 193 // Note that we land on this case if the original modtime was 0 and the modtime is 194 // now non-zero, so there's a race-condition if an IO error causes us to fail to 195 // read the modtime initially. This should be okay. 196 CLog.w("Found update in monitored cmdfile %s (%d -> %d)", cmd.file, cmd.modTime, 197 curModTime); 198 return true; 199 } 200 201 // Now check dependencies 202 for (CommandFile dep : cmd.dependencies) { 203 if (checkCommandFileForUpdate(dep, checkedFiles)) { 204 // dependency changed 205 return true; 206 } 207 } 208 209 // We didn't change, and nor did any of our dependencies 210 return false; 211 } 212 213 /** 214 * Factory method for creating a {@link CommandFileParser}. 215 * <p/> 216 * Exposed for unit testing. 217 */ createCommandFileParser()218 CommandFileParser createCommandFileParser() { 219 return new CommandFileParser(); 220 } 221 222 /** 223 * Utility method to fetch the default {@link IRunUtil} singleton 224 * <p /> 225 * Exposed for unit testing. 226 */ getRunUtil()227 IRunUtil getRunUtil() { 228 return RunUtil.getDefault(); 229 } 230 231 /** 232 * <p> 233 * Add a command file to watch, as well as its dependencies. When either 234 * the command file itself or any of its dependencies changes, notify the registered 235 * {@link ICommandFileListener} 236 * </p> 237 * if the cmdFile is already being watching, this call will replace the current entry 238 */ addCmdFile(File cmdFile, List<String> extraArgs, Collection<String> includedFiles)239 public void addCmdFile(File cmdFile, List<String> extraArgs, Collection<String> includedFiles) { 240 List<File> includesAsFiles = new ArrayList<File>(includedFiles.size()); 241 for (String p : includedFiles) { 242 includesAsFiles.add(new File(p)); 243 } 244 addCmdFile(cmdFile, extraArgs, includesAsFiles); 245 } 246 247 /** 248 * Remove all files from the watched list 249 */ removeAllFiles()250 public void removeAllFiles() { 251 mCmdFileMap.clear(); 252 } 253 254 /** 255 * Retrieves the extra arguments associated with given file being watched. 256 * <p> 257 * TODO: extra args list should likely be stored elsewhere, and have this class just operate 258 * as a generic file watcher with dependencies 259 * </p> 260 * @return the list of extra arguments associated with command file. Returns empty list if 261 * command path is not recognized 262 */ getExtraArgsForFile(String cmdPath)263 public List<String> getExtraArgsForFile(String cmdPath) { 264 CommandFile cmdFile = mCmdFileMap.get(cmdPath); 265 if (cmdFile != null) { 266 return cmdFile.extraArgs; 267 } 268 CLog.w("Could not find cmdfile %s", cmdPath); 269 return Collections.<String>emptyList(); 270 } 271 } 272