1 /*
2  * Copyright (C) 2010 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.log.LogUtil.CLog;
21 import com.android.tradefed.result.error.InfraErrorIdentifier;
22 import com.android.tradefed.util.FileUtil;
23 import com.android.tradefed.util.StreamUtil;
24 
25 import java.io.BufferedInputStream;
26 import java.io.BufferedOutputStream;
27 import java.io.File;
28 import java.io.FileInputStream;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.zip.GZIPOutputStream;
36 
37 
38 /**
39  * A helper for {@link ITestInvocationListener}'s that will save log data to a file
40  */
41 public class LogFileSaver {
42 
43     private static final int BUFFER_SIZE = 64 * 1024;
44     private File mInvLogDir;
45     private List<String> mInvLogPathSegments;
46 
47     /**
48      * Creates a {@link LogFileSaver}.
49      * <p/>
50      * Construct a unique file system directory in rootDir/branch/build_id/testTag/uniqueDir
51      * <p/>
52      * If directory creation fails, will use a temp directory.
53      *
54      * @param buildInfo the {@link IBuildInfo}
55      * @param rootDir the root file system path
56      * @param logRetentionDays If provided a '.retention' file will be written to log directory
57      *            containing a timestamp equal to current time + logRetentionDays. External cleanup
58      *            scripts can use this file to determine when to delete log directories.
59      */
LogFileSaver(IBuildInfo buildInfo, File rootDir, Integer logRetentionDays)60     public LogFileSaver(IBuildInfo buildInfo, File rootDir, Integer logRetentionDays) {
61         List<String> testArtifactPathSegments = generateTestArtifactPath(buildInfo);
62         File buildDir = createBuildDir(testArtifactPathSegments, rootDir);
63         mInvLogDir = createInvLogDir(buildDir, logRetentionDays);
64         String invLogDirName = mInvLogDir.getName();
65         mInvLogPathSegments = new ArrayList<>(testArtifactPathSegments);
66         mInvLogPathSegments.add(invLogDirName);
67     }
68 
69     /**
70      * Creates a {@link LogFileSaver}.
71      * <p/>
72      * Construct a unique file system directory in rootDir/branch/build_id/uniqueDir
73      *
74      * @param buildInfo the {@link IBuildInfo}
75      * @param rootDir the root file system path
76      */
LogFileSaver(IBuildInfo buildInfo, File rootDir)77     public LogFileSaver(IBuildInfo buildInfo, File rootDir) {
78         this(buildInfo, rootDir, null);
79     }
80 
81     /**
82      * An alternate {@link LogFileSaver} constructor that will just use given directory as the
83      * log storage directory.
84      *
85      * @param rootDir
86      */
LogFileSaver(File rootDir)87     public LogFileSaver(File rootDir) {
88         this(null, rootDir, null);
89     }
90 
createTempDir()91     private File createTempDir() {
92         try {
93             return FileUtil.createTempDir("inv_");
94         } catch (IOException e) {
95             // uh oh, this can't be good, abort tradefed
96             throw new FatalHostError(
97                     "Cannot create tmp directory.",
98                     e,
99                     InfraErrorIdentifier.LAB_HOST_FILESYSTEM_ERROR);
100         }
101     }
102 
103     /**
104      * Get the directory used to store files.
105      *
106      * @return the {@link File} directory
107      */
getFileDir()108     public File getFileDir() {
109         return mInvLogDir;
110     }
111 
112     /**
113      * Create unique invocation log directory.
114      * @param buildDir the build directory
115      * @param logRetentionDays
116      * @return the create invocation directory
117      */
createInvLogDir(File buildDir, Integer logRetentionDays)118     File createInvLogDir(File buildDir, Integer logRetentionDays) {
119         // now create unique directory within the buildDir
120         File invocationDir = null;
121         try {
122             invocationDir = FileUtil.createTempDir("inv_", buildDir);
123             if (logRetentionDays != null && logRetentionDays > 0) {
124                 new RetentionFileSaver().writeRetentionFile(invocationDir, logRetentionDays);
125             }
126         } catch (IOException e) {
127             CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead",
128                     buildDir.getAbsolutePath());
129             CLog.e(e);
130             // try to create one in a tmp location instead
131             invocationDir = createTempDir();
132         }
133         CLog.i("Using log file directory %s", invocationDir.getAbsolutePath());
134         return invocationDir;
135     }
136 
137     /**
138      * Attempt to create a folder to store log's for given build info.
139      *
140      * @param buildPathSegments build path segments
141      * @param rootDir the root file system path to create directory from
142      * @return a {@link File} pointing to the directory to store log files in
143      */
createBuildDir(List<String> buildPathSegments, File rootDir)144     File createBuildDir(List<String> buildPathSegments, File rootDir) {
145         File buildReportDir;
146         buildReportDir = FileUtil.getFileForPath(rootDir,
147                 buildPathSegments.toArray(new String[] {}));
148 
149         // if buildReportDir already exists and is a directory - use it.
150         if (buildReportDir.exists()) {
151             if (buildReportDir.isDirectory()) {
152                 return buildReportDir;
153             } else {
154                 CLog.w("Cannot create build-specific output dir %s. File already exists.",
155                         buildReportDir.getAbsolutePath());
156             }
157         } else {
158             if (FileUtil.mkdirsRWX(buildReportDir)) {
159                 return buildReportDir;
160             } else {
161                 CLog.w("Cannot create build-specific output dir %s. Failed to create directory.",
162                         buildReportDir.getAbsolutePath());
163             }
164         }
165         return buildReportDir;
166     }
167 
168     /**
169      * A helper to create test artifact path segments based on the build info.
170      * <p />
171      * {@code [branch/]build-id/test-tag}
172      */
generateTestArtifactPath(IBuildInfo buildInfo)173     List<String> generateTestArtifactPath(IBuildInfo buildInfo) {
174         final List<String> pathSegments = new ArrayList<String>();
175         if (buildInfo == null) {
176             return pathSegments;
177         }
178         if (buildInfo.getBuildBranch() != null) {
179             pathSegments.add(buildInfo.getBuildBranch());
180         }
181         pathSegments.add(buildInfo.getBuildId());
182         pathSegments.add(buildInfo.getTestTag());
183         return pathSegments;
184     }
185 
186     /**
187      * A helper function that translates a string into something that can be used as a filename
188      */
sanitizeFilename(String name)189     private static String sanitizeFilename(String name) {
190         return name.replace(File.separatorChar, '_');
191     }
192 
193     /**
194      * Save the log data to a file
195      *
196      * @param dataName a {@link String} descriptive name of the data.
197      * @param dataType the {@link LogDataType} of the file.
198      * @param dataStream the {@link InputStream} of the data.
199      * @return the file of the generated data
200      * @throws IOException if log file could not be generated
201      */
saveLogData(String dataName, LogDataType dataType, InputStream dataStream)202     public File saveLogData(String dataName, LogDataType dataType, InputStream dataStream)
203             throws IOException {
204         return saveLogDataRaw(dataName, dataType.getFileExt(), dataStream);
205     }
206 
207     /**
208      * Save a given log file
209      *
210      * @param dataName a {@link String} descriptive name of the data.
211      * @param dataType the {@link LogDataType} of the file.
212      * @param fileToLog the {@link File} to be logged
213      * @return the file of the generated data
214      * @throws IOException if log file could not be generated
215      */
saveLogFile(String dataName, LogDataType dataType, File fileToLog)216     public File saveLogFile(String dataName, LogDataType dataType, File fileToLog)
217             throws IOException {
218         long startTime = System.currentTimeMillis();
219         final String saneDataName = sanitizeFilename(dataName);
220         if (mInvLogDir != null && !mInvLogDir.exists()) {
221             mInvLogDir.mkdirs();
222         }
223         // add underscore to end of data name to make generated name more readable
224         File logFile =
225                 FileUtil.createTempFile(
226                         saneDataName + "_", "." + dataType.getFileExt(), mInvLogDir);
227         // Delete to avoid hardlink collision
228         logFile.delete();
229         // Hardlink fallback to copy if needed
230         FileUtil.hardlinkFile(fileToLog, logFile);
231         CLog.i(
232                 "Saved log file %s. [size=%s, elapsed=%sms]",
233                 logFile.getAbsolutePath(),
234                 logFile.length(),
235                 System.currentTimeMillis() - startTime);
236         return logFile;
237     }
238 
239     /**
240      * Save raw data to a file
241      * @param dataName a {@link String} descriptive name of the data.
242      * @param ext the extension of the date
243      * @param dataStream the {@link InputStream} of the data.
244      * @return the file of the generated data
245      * @throws IOException if log file could not be generated
246      */
saveLogDataRaw(String dataName, String ext, InputStream dataStream)247     public File saveLogDataRaw(String dataName, String ext, InputStream dataStream)
248             throws IOException {
249         long startTime = System.currentTimeMillis();
250         final String saneDataName = sanitizeFilename(dataName);
251         if (mInvLogDir != null && !mInvLogDir.exists()) {
252             mInvLogDir.mkdirs();
253         }
254         // add underscore to end of data name to make generated name more readable
255         File logFile = FileUtil.createTempFile(saneDataName + "_", "." + ext, mInvLogDir);
256         FileUtil.writeToFile(dataStream, logFile);
257         CLog.i(
258                 "Saved log file %s. [size=%s, elapsed=%sms]",
259                 logFile.getAbsolutePath(),
260                 logFile.length(),
261                 System.currentTimeMillis() - startTime);
262         return logFile;
263     }
264 
265     /**
266      * Save and compress, if necessary, the log data to a gzip file
267      *
268      * @param dataName a {@link String} descriptive name of the data.
269      * @param dataType the {@link LogDataType} of the file. Log data which is a (ie
270      *            {@link LogDataType#isCompressed()} is <code>true</code>)
271      * @param dataStream the {@link InputStream} of the data.
272      * @return the file of the generated data
273      * @throws IOException if log file could not be generated
274      */
saveAndGZipLogData(String dataName, LogDataType dataType, InputStream dataStream)275     public File saveAndGZipLogData(String dataName, LogDataType dataType, InputStream dataStream)
276             throws IOException {
277         if (dataType.isCompressed()) {
278             CLog.d("Log data for %s is already compressed, skipping compression", dataName);
279             return saveLogData(dataName, dataType, dataStream);
280         }
281         long startTime = System.currentTimeMillis();
282         BufferedInputStream bufInput = null;
283         OutputStream outStream = null;
284         try {
285             final String saneDataName = sanitizeFilename(dataName);
286             File logFile = createCompressedLogFile(saneDataName, dataType);
287             bufInput = new BufferedInputStream(dataStream);
288             outStream = createGZipLogStream(logFile);
289             StreamUtil.copyStreams(bufInput, outStream);
290             CLog.i(
291                     "Saved gzip log file %s. [size=%s, elapsed=%sms]",
292                     logFile.getAbsolutePath(),
293                     logFile.length(),
294                     System.currentTimeMillis() - startTime);
295             return logFile;
296         } finally {
297             StreamUtil.close(bufInput);
298             StreamUtil.close(outStream);
299         }
300     }
301 
302     /**
303      * Save and compress, if necessary, the log data to a gzip file
304      *
305      * @param dataName a {@link String} descriptive name of the data.
306      * @param dataType the {@link LogDataType} of the file. Log data which is a (ie {@link
307      *     LogDataType#isCompressed()} is <code>true</code>)
308      * @param fileToLog the {@link File} to save
309      * @return the file of the generated data
310      * @throws IOException if log file could not be generated
311      */
saveAndGZipLogFile(String dataName, LogDataType dataType, File fileToLog)312     public File saveAndGZipLogFile(String dataName, LogDataType dataType, File fileToLog)
313             throws IOException {
314         if (dataType.isCompressed() || fileToLog.getName().endsWith(".gz")) {
315             CLog.d("Log data for %s is already compressed, skipping compression", dataName);
316             return saveLogFile(dataName, dataType, fileToLog);
317         }
318         long startTime = System.currentTimeMillis();
319         BufferedInputStream bufInput = null;
320         OutputStream outStream = null;
321         try {
322             final String saneDataName = sanitizeFilename(dataName);
323             File logFile = createCompressedLogFile(saneDataName, dataType);
324             // TODO: Optimize gzip of existing log file
325             bufInput = new BufferedInputStream(new FileInputStream(fileToLog));
326             outStream = createGZipLogStream(logFile);
327             StreamUtil.copyStreams(bufInput, outStream);
328             CLog.i(
329                     "Saved gzip log file %s. [size=%s, elapsed=%sms]",
330                     logFile.getAbsolutePath(),
331                     logFile.length(),
332                     System.currentTimeMillis() - startTime);
333             return logFile;
334         } finally {
335             StreamUtil.close(bufInput);
336             StreamUtil.close(outStream);
337         }
338     }
339 
340     /**
341      * Creates an empty file for storing compressed log data.
342      *
343      * @param dataName a {@link String} descriptive name of the data to be stored.
344      * @param origDataType the type of {@link LogDataType} to be stored
345      * @return a {@link File}
346      * @throws IOException if log file could not be created
347      */
createCompressedLogFile(String dataName, LogDataType origDataType)348     public File createCompressedLogFile(String dataName, LogDataType origDataType)
349             throws IOException {
350         if (mInvLogDir != null && !mInvLogDir.exists()) {
351             mInvLogDir.mkdirs();
352         }
353         // add underscore to end of data name to make generated name more readable
354         return FileUtil.createTempFile(dataName + "_",
355                 String.format(".%s.%s", origDataType.getFileExt(), LogDataType.GZIP.getFileExt()),
356                 mInvLogDir);
357     }
358 
359     /**
360      * Creates a output stream to write GZIP-compressed data to a file
361      *
362      * @param logFile the {@link File} to write to
363      * @return the {@link OutputStream} to compress and write data to the file.
364      *         this stream when complete
365      * @throws IOException if stream could not be generated
366      */
createGZipLogStream(File logFile)367     public OutputStream createGZipLogStream(File logFile) throws IOException {
368         return new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(
369                 logFile)), BUFFER_SIZE);
370     }
371 
372     /**
373      * Helper method to create an input stream to read contents of given log fi
374      * <p/>
375      * TODO: consider moving this method elsewhere. Placed here for now so it e
376      * users of this class to mock.
377      *
378      * @param logFile the {@link File} to read from
379      * @return a buffered {@link InputStream} to read file data. Callers must call
380      *         this stream when complete
381      * @throws IOException if stream could not be generated
382      */
createInputStreamFromFile(File logFile)383     public InputStream createInputStreamFromFile(File logFile) throws IOException {
384         return new BufferedInputStream(new FileInputStream(logFile), BUFFER_SIZE);
385     }
386 
387     /**
388      *
389      * @return the unique invocation log path segments.
390      */
getInvocationLogPathSegments()391     public List<String> getInvocationLogPathSegments() {
392         return new ArrayList<>(mInvLogPathSegments);
393     }
394 }
395