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             logReportDir = generateLogReportDir(buildInfo, reportDir);
199         } catch (IOException e) {
200             CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead",
201                     reportDir.getAbsolutePath());
202             CLog.e(e);
203             // try to create one in a tmp location instead
204             logReportDir = createTempDir();
205         }
206 
207         boolean setPerms = FileUtil.chmodGroupRWX(logReportDir);
208         if (!setPerms) {
209             CLog.w(String.format("Failed to set dir %s to be group accessible.", logReportDir));
210         }
211 
212         if (logRetentionDays != null && logRetentionDays > 0) {
213             new RetentionFileSaver().writeRetentionFile(logReportDir, logRetentionDays);
214         }
215         CLog.d("Using log file directory %s", logReportDir.getAbsolutePath());
216         return logReportDir;
217     }
218 
219     /**
220      * An exposed method that allow subclass to customize generating path logic.
221      *
222      * @param buildInfo the {@link IBuildInfo}
223      * @param reportDir the {@link File} for the report directory.
224      * @return The directory created.
225      */
generateLogReportDir(IBuildInfo buildInfo, File reportDir)226     protected File generateLogReportDir(IBuildInfo buildInfo, File reportDir) throws IOException {
227         File buildDir = createBuildDir(buildInfo, reportDir);
228         return FileUtil.createTempDir("inv_", buildDir);
229     }
230 
231     /**
232      * A helper method to get or create a build directory based on the build info of the invocation.
233      * <p>
234      * Create a unique file system directory with the structure
235      * {@code report-dir/[branch/]build-id/test-tag} for saving logs.
236      * </p>
237      *
238      * @param buildInfo the {@link IBuildInfo}
239      * @param reportDir the {@link File} for the report directory.
240      * @return The directory where invocations for the same build should be saved.
241      * @throws IOException if the directory could not be created because a file with the same name
242      * exists or there are no permissions to write to it.
243      */
createBuildDir(IBuildInfo buildInfo, File reportDir)244     private File createBuildDir(IBuildInfo buildInfo, File reportDir) throws IOException {
245         List<String> pathSegments = new ArrayList<String>();
246         if (buildInfo.getBuildBranch() != null) {
247             pathSegments.add(buildInfo.getBuildBranch());
248         }
249         pathSegments.add(buildInfo.getBuildId());
250         pathSegments.add(buildInfo.getTestTag());
251         File buildReportDir = FileUtil.getFileForPath(reportDir,
252                 pathSegments.toArray(new String[] {}));
253 
254         // if buildReportDir already exists and is a directory - use it.
255         if (buildReportDir.exists()) {
256             if (buildReportDir.isDirectory()) {
257                 return buildReportDir;
258             } else {
259                 final String msg = String.format("Cannot create build-specific output dir %s. " +
260                         "File already exists.", buildReportDir.getAbsolutePath());
261                 CLog.w(msg);
262                 throw new IOException(msg);
263             }
264         } else {
265             if (FileUtil.mkdirsRWX(buildReportDir)) {
266                 return buildReportDir;
267             } else {
268                 final String msg = String.format("Cannot create build-specific output dir %s. " +
269                         "Failed to create directory.", buildReportDir.getAbsolutePath());
270                 CLog.w(msg);
271                 throw new IOException(msg);
272             }
273         }
274     }
275 
276     /**
277      * A helper method to create a temp directory for an invocation.
278      */
createTempDir()279     private File createTempDir() {
280         try {
281             return FileUtil.createTempDir("inv_");
282         } catch (IOException e) {
283             // Abort tradefed if a temp directory cannot be created
284             throw new FatalHostError("Cannot create tmp directory.", e);
285         }
286     }
287 
288     /**
289      * A helper function that translates a string into something that can be used as a filename
290      */
sanitizeFilename(String name)291     private static String sanitizeFilename(String name) {
292         return name.replace(File.separatorChar, '_');
293     }
294 
295     /**
296      * A helper method that returns a URL for a given {@link File}.
297      *
298      * @param file the {@link File} of the log.
299      * @return The report directory path replaced with the report-url and path separators normalized
300      * (for Windows), or {@code null} if the report-url is not set, report-url ends with /,
301      * report-dir ends with {@link File#separator}, or the file is not in the report directory.
302      */
getUrl(File file)303     private String getUrl(File file) {
304         if (mReportUrl == null) {
305             return null;
306         }
307 
308         final String filePath = file.getAbsolutePath();
309         final String reportPath = mRootReportDir.getAbsolutePath();
310 
311         if (reportPath.endsWith(File.separator)) {
312             CLog.w("Cannot create URL. getAbsolutePath() returned %s which ends with %s",
313                     reportPath, File.separator);
314             return null;
315         }
316 
317         // Log file starts with the mReportDir path, so do a simple replacement.
318         if (filePath.startsWith(reportPath)) {
319             String relativePath = filePath.substring(reportPath.length());
320             // relativePath should start with /, drop the / from the url if it exists.
321             String url = mReportUrl;
322             if (url.endsWith("/")) {
323                 url =  url.substring(0, url.length() - 1);
324             }
325             // FIXME: Sanitize the URL.
326             return String.format("%s%s", url, relativePath.replace(File.separator, "/"));
327         }
328 
329         return null;
330     }
331 
332     /**
333      * Set the report directory. Exposed for unit testing.
334      */
setReportDir(File reportDir)335     void setReportDir(File reportDir) {
336         mRootReportDir = reportDir;
337     }
338 
339     /**
340      * Set the log retentionDays. Exposed for unit testing.
341      */
setLogRetentionDays(int logRetentionDays)342     void setLogRetentionDays(int logRetentionDays) {
343         mLogRetentionDays = logRetentionDays;
344     }
345 
setCompressFiles(boolean compress)346     public void setCompressFiles(boolean compress) {
347         mCompressFiles = compress;
348     }
349 }
350