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