• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.result;
17 
18 import com.android.tradefed.build.IBuildInfo;
19 import com.android.tradefed.command.FatalHostError;
20 import com.android.tradefed.config.Option;
21 import com.android.tradefed.config.OptionClass;
22 import com.android.tradefed.invoker.IInvocationContext;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.util.FileUtil;
25 import com.android.tradefed.util.StreamUtil;
26 
27 import java.io.BufferedInputStream;
28 import java.io.BufferedOutputStream;
29 import java.io.File;
30 import java.io.FileOutputStream;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipOutputStream;
37 
38 /**
39  * Save logs to a file system.
40  */
41 @OptionClass(alias = "file-system-log-saver")
42 public class FileSystemLogSaver implements ILogSaver {
43 
44     private static final int BUFFER_SIZE = 64 * 1024;
45 
46     @Option(name = "log-file-path", description = "root file system path to store log files.")
47     private File mRootReportDir = new File(System.getProperty("java.io.tmpdir"));
48 
49     @Option(name = "log-file-url", description =
50             "root http url of log files. Assumes files placed in log-file-path are visible via " +
51             "this url.")
52     private String mReportUrl = null;
53 
54     @Option(name = "log-retention-days", description =
55             "the number of days to keep saved log files.")
56     private Integer mLogRetentionDays = null;
57 
58     @Option(name = "compress-files", description =
59             "whether to compress files which are not already compressed")
60     private boolean mCompressFiles = true;
61 
62     private File mLogReportDir = null;
63 
64     /**
65      * A counter to control access to methods which modify this class's directories. Acting as a
66      * non-blocking reentrant lock, this int blocks access to sharded child invocations from
67      * attempting to create or delete directories.
68      */
69     private int mShardingLock = 0;
70 
71     /**
72      * {@inheritDoc}
73      *
74      * <p>Also, create a unique file system directory under {@code
75      * report-dir/[branch/]build-id/test-tag/unique_dir} for saving logs. If the creation of the
76      * directory fails, will write logs to a temporary directory on the local file system.
77      */
78     @Override
invocationStarted(IInvocationContext context)79     public void invocationStarted(IInvocationContext context) {
80         // Create log directory on first build info
81         IBuildInfo info = context.getBuildInfos().get(0);
82         synchronized (this) {
83             if (mShardingLock == 0) {
84                 mLogReportDir = createLogReportDir(info, mRootReportDir, mLogRetentionDays);
85             }
86             mShardingLock++;
87         }
88     }
89 
90     /**
91      * {@inheritDoc}
92      */
93     @Override
invocationEnded(long elapsedTime)94     public void invocationEnded(long elapsedTime) {
95         // no clean up needed.
96         synchronized (this) {
97             --mShardingLock;
98             if (mShardingLock < 0) {
99                 CLog.w(
100                         "Sharding lock exited more times than entered, possible "
101                                 + "unbalanced invocationStarted/Ended calls");
102             }
103         }
104     }
105 
106     /**
107      * {@inheritDoc}
108      * <p>
109      * Will zip and save the log file if {@link LogDataType#isCompressed()} returns false for
110      * {@code dataType} and {@code compressed-files} is set, otherwise, the stream will be saved
111      * uncompressed.
112      * </p>
113      */
114     @Override
saveLogData(String dataName, LogDataType dataType, InputStream dataStream)115     public LogFile saveLogData(String dataName, LogDataType dataType, InputStream dataStream)
116             throws IOException {
117         if (!mCompressFiles || dataType.isCompressed()) {
118             File log = saveLogDataInternal(dataName, dataType.getFileExt(), dataStream);
119             return new LogFile(log.getAbsolutePath(), getUrl(log), dataType);
120         }
121         BufferedInputStream bufferedDataStream = null;
122         ZipOutputStream outputStream = null;
123         // add underscore to end of data name to make generated name more readable
124         final String saneDataName = sanitizeFilename(dataName);
125         File log = FileUtil.createTempFile(saneDataName + "_", "." + LogDataType.ZIP.getFileExt(),
126                 mLogReportDir);
127 
128         boolean setPerms = FileUtil.chmodGroupRWX(log);
129         if (!setPerms) {
130             CLog.w(String.format("Failed to set dir %s to be group accessible.", log));
131         }
132 
133         try {
134             bufferedDataStream = new BufferedInputStream(dataStream);
135             outputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(log),
136                     BUFFER_SIZE));
137             outputStream.putNextEntry(new ZipEntry(saneDataName + "." + dataType.getFileExt()));
138             StreamUtil.copyStreams(bufferedDataStream, outputStream);
139             CLog.d("Saved log file %s", log.getAbsolutePath());
140             return new LogFile(log.getAbsolutePath(), getUrl(log), true, dataType, log.length());
141         } finally {
142             StreamUtil.close(bufferedDataStream);
143             StreamUtil.close(outputStream);
144         }
145     }
146 
147     /** {@inheritDoc} */
148     @Override
saveLogDataRaw(String dataName, LogDataType dataType, InputStream dataStream)149     public LogFile saveLogDataRaw(String dataName, LogDataType dataType, InputStream dataStream)
150             throws IOException {
151         File log = saveLogDataInternal(dataName, dataType.getFileExt(), dataStream);
152         return new LogFile(log.getAbsolutePath(), getUrl(log), dataType);
153     }
154 
saveLogDataInternal(String dataName, String ext, InputStream dataStream)155     private File saveLogDataInternal(String dataName, String ext, InputStream dataStream)
156             throws IOException {
157         final String saneDataName = sanitizeFilename(dataName);
158         // add underscore to end of data name to make generated name more readable
159         File log = FileUtil.createTempFile(saneDataName + "_", "." + ext, mLogReportDir);
160 
161         boolean setPerms = FileUtil.chmodGroupRWX(log);
162         if (!setPerms) {
163             CLog.w(String.format("Failed to set dir %s to be group accessible.", log));
164         }
165 
166         FileUtil.writeToFile(dataStream, log);
167         CLog.d("Saved raw log file %s", log.getAbsolutePath());
168         return log;
169     }
170 
171     /**
172      * {@inheritDoc}
173      */
174     @Override
getLogReportDir()175     public LogFile getLogReportDir() {
176         return new LogFile(mLogReportDir.getAbsolutePath(), getUrl(mLogReportDir), LogDataType.DIR);
177     }
178 
179     /**
180      * A helper method to create an invocation directory unique for saving logs.
181      * <p>
182      * Create a unique file system directory with the structure
183      * {@code report-dir/[branch/]build-id/test-tag/unique_dir} for saving logs.  If the creation
184      * of the directory fails, will write logs to a temporary directory on the local file system.
185      * </p>
186      *
187      * @param buildInfo the {@link IBuildInfo}
188      * @param reportDir the {@link File} for the report directory.
189      * @param logRetentionDays how many days logs should be kept for. If {@code null}, then no log
190      * retention file is writen.
191      * @return The directory created.
192      */
createLogReportDir(IBuildInfo buildInfo, File reportDir, Integer logRetentionDays)193     private File createLogReportDir(IBuildInfo buildInfo, File reportDir,
194             Integer logRetentionDays) {
195         File logReportDir;
196         // now create unique directory within the buildDir
197         try {
198             File buildDir = createBuildDir(buildInfo, reportDir);
199             logReportDir = FileUtil.createTempDir("inv_", buildDir);
200         } catch (IOException e) {
201             CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead",
202                     reportDir.getAbsolutePath());
203             CLog.e(e);
204             // try to create one in a tmp location instead
205             logReportDir = createTempDir();
206         }
207 
208         boolean setPerms = FileUtil.chmodGroupRWX(logReportDir);
209         if (!setPerms) {
210             CLog.w(String.format("Failed to set dir %s to be group accessible.", logReportDir));
211         }
212 
213         if (logRetentionDays != null && logRetentionDays > 0) {
214             new RetentionFileSaver().writeRetentionFile(logReportDir, logRetentionDays);
215         }
216         CLog.d("Using log file directory %s", logReportDir.getAbsolutePath());
217         return logReportDir;
218     }
219 
220     /**
221      * A helper method to get or create a build directory based on the build info of the invocation.
222      * <p>
223      * Create a unique file system directory with the structure
224      * {@code report-dir/[branch/]build-id/test-tag} for saving logs.
225      * </p>
226      *
227      * @param buildInfo the {@link IBuildInfo}
228      * @param reportDir the {@link File} for the report directory.
229      * @return The directory where invocations for the same build should be saved.
230      * @throws IOException if the directory could not be created because a file with the same name
231      * exists or there are no permissions to write to it.
232      */
createBuildDir(IBuildInfo buildInfo, File reportDir)233     private File createBuildDir(IBuildInfo buildInfo, File reportDir) throws IOException {
234         List<String> pathSegments = new ArrayList<String>();
235         if (buildInfo.getBuildBranch() != null) {
236             pathSegments.add(buildInfo.getBuildBranch());
237         }
238         pathSegments.add(buildInfo.getBuildId());
239         pathSegments.add(buildInfo.getTestTag());
240         File buildReportDir = FileUtil.getFileForPath(reportDir,
241                 pathSegments.toArray(new String[] {}));
242 
243         // if buildReportDir already exists and is a directory - use it.
244         if (buildReportDir.exists()) {
245             if (buildReportDir.isDirectory()) {
246                 return buildReportDir;
247             } else {
248                 final String msg = String.format("Cannot create build-specific output dir %s. " +
249                         "File already exists.", buildReportDir.getAbsolutePath());
250                 CLog.w(msg);
251                 throw new IOException(msg);
252             }
253         } else {
254             if (FileUtil.mkdirsRWX(buildReportDir)) {
255                 return buildReportDir;
256             } else {
257                 final String msg = String.format("Cannot create build-specific output dir %s. " +
258                         "Failed to create directory.", buildReportDir.getAbsolutePath());
259                 CLog.w(msg);
260                 throw new IOException(msg);
261             }
262         }
263     }
264 
265     /**
266      * A helper method to create a temp directory for an invocation.
267      */
createTempDir()268     private File createTempDir() {
269         try {
270             return FileUtil.createTempDir("inv_");
271         } catch (IOException e) {
272             // Abort tradefed if a temp directory cannot be created
273             throw new FatalHostError("Cannot create tmp directory.", e);
274         }
275     }
276 
277     /**
278      * A helper function that translates a string into something that can be used as a filename
279      */
sanitizeFilename(String name)280     private static String sanitizeFilename(String name) {
281         return name.replace(File.separatorChar, '_');
282     }
283 
284     /**
285      * A helper method that returns a URL for a given {@link File}.
286      *
287      * @param file the {@link File} of the log.
288      * @return The report directory path replaced with the report-url and path separators normalized
289      * (for Windows), or {@code null} if the report-url is not set, report-url ends with /,
290      * report-dir ends with {@link File#separator}, or the file is not in the report directory.
291      */
getUrl(File file)292     private String getUrl(File file) {
293         if (mReportUrl == null) {
294             return null;
295         }
296 
297         final String filePath = file.getAbsolutePath();
298         final String reportPath = mRootReportDir.getAbsolutePath();
299 
300         if (reportPath.endsWith(File.separator)) {
301             CLog.w("Cannot create URL. getAbsolutePath() returned %s which ends with %s",
302                     reportPath, File.separator);
303             return null;
304         }
305 
306         // Log file starts with the mReportDir path, so do a simple replacement.
307         if (filePath.startsWith(reportPath)) {
308             String relativePath = filePath.substring(reportPath.length());
309             // relativePath should start with /, drop the / from the url if it exists.
310             String url = mReportUrl;
311             if (url.endsWith("/")) {
312                 url =  url.substring(0, url.length() - 1);
313             }
314             // FIXME: Sanitize the URL.
315             return String.format("%s%s", url, relativePath.replace(File.separator, "/"));
316         }
317 
318         return null;
319     }
320 
321     /**
322      * Set the report directory. Exposed for unit testing.
323      */
setReportDir(File reportDir)324     void setReportDir(File reportDir) {
325         mRootReportDir = reportDir;
326     }
327 
328     /**
329      * Set the log retentionDays. Exposed for unit testing.
330      */
setLogRetentionDays(int logRetentionDays)331     void setLogRetentionDays(int logRetentionDays) {
332         mLogRetentionDays = logRetentionDays;
333     }
334 }
335