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