• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  * A helper for {@link ITestInvocationListener}'s that will save log data to a file
39  */
40 public class LogFileSaver {
41 
42     private static final int BUFFER_SIZE = 64 * 1024;
43     private File mInvLogDir;
44     private List<String> mInvLogPathSegments;
45 
46     /**
47      * Creates a {@link LogFileSaver}.
48      * <p/>
49      * Construct a unique file system directory in rootDir/branch/build_id/testTag/uniqueDir
50      * <p/>
51      * If directory creation fails, will use a temp directory.
52      *
53      * @param buildInfo the {@link IBuildInfo}
54      * @param rootDir the root file system path
55      * @param logRetentionDays If provided a '.retention' file will be written to log directory
56      *            containing a timestamp equal to current time + logRetentionDays. External cleanup
57      *            scripts can use this file to determine when to delete log directories.
58      */
LogFileSaver(IBuildInfo buildInfo, File rootDir, Integer logRetentionDays)59     public LogFileSaver(IBuildInfo buildInfo, File rootDir, Integer logRetentionDays) {
60         List<String> testArtifactPathSegments = generateTestArtifactPath(buildInfo);
61         File buildDir = createBuildDir(testArtifactPathSegments, rootDir);
62         mInvLogDir = createInvLogDir(buildDir, logRetentionDays);
63         String invLogDirName = mInvLogDir.getName();
64         mInvLogPathSegments = new ArrayList<>(testArtifactPathSegments);
65         mInvLogPathSegments.add(invLogDirName);
66     }
67 
68     /**
69      * Creates a {@link LogFileSaver}.
70      * <p/>
71      * Construct a unique file system directory in rootDir/branch/build_id/uniqueDir
72      *
73      * @param buildInfo the {@link IBuildInfo}
74      * @param rootDir the root file system path
75      */
LogFileSaver(IBuildInfo buildInfo, File rootDir)76     public LogFileSaver(IBuildInfo buildInfo, File rootDir) {
77         this(buildInfo, rootDir, null);
78     }
79 
80     /**
81      * An alternate {@link LogFileSaver} constructor that will just use given directory as the
82      * log storage directory.
83      *
84      * @param rootDir
85      */
LogFileSaver(File rootDir)86     public LogFileSaver(File rootDir) {
87         this(null, rootDir, null);
88     }
89 
createTempDir()90     private File createTempDir() {
91         try {
92             return FileUtil.createTempDir("inv_");
93         } catch (IOException e) {
94             // uh oh, this can't be good, abort tradefed
95             throw new FatalHostError(
96                     "Cannot create tmp directory.",
97                     e,
98                     InfraErrorIdentifier.LAB_HOST_FILESYSTEM_ERROR);
99         }
100     }
101 
102     /**
103      * Get the directory used to store files.
104      *
105      * @return the {@link File} directory
106      */
getFileDir()107     public File getFileDir() {
108         return mInvLogDir;
109     }
110 
111     /**
112      * Create unique invocation log directory.
113      * @param buildDir the build directory
114      * @param logRetentionDays
115      * @return the create invocation directory
116      */
createInvLogDir(File buildDir, Integer logRetentionDays)117     File createInvLogDir(File buildDir, Integer logRetentionDays) {
118         // now create unique directory within the buildDir
119         File invocationDir = null;
120         try {
121             invocationDir = FileUtil.createTempDir("inv_", buildDir);
122             if (logRetentionDays != null && logRetentionDays > 0) {
123                 new RetentionFileSaver().writeRetentionFile(invocationDir, logRetentionDays);
124             }
125         } catch (IOException e) {
126             CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead",
127                     buildDir.getAbsolutePath());
128             CLog.e(e);
129             // try to create one in a tmp location instead
130             invocationDir = createTempDir();
131         }
132         CLog.i("Using log file directory %s", invocationDir.getAbsolutePath());
133         return invocationDir;
134     }
135 
136     /**
137      * Attempt to create a folder to store log's for given build info.
138      *
139      * @param buildPathSegments build path segments
140      * @param rootDir the root file system path to create directory from
141      * @return a {@link File} pointing to the directory to store log files in
142      */
createBuildDir(List<String> buildPathSegments, File rootDir)143     File createBuildDir(List<String> buildPathSegments, File rootDir) {
144         File buildReportDir;
145         buildReportDir = FileUtil.getFileForPath(rootDir,
146                 buildPathSegments.toArray(new String[] {}));
147 
148         // if buildReportDir already exists and is a directory - use it.
149         if (buildReportDir.exists()) {
150             if (buildReportDir.isDirectory()) {
151                 return buildReportDir;
152             } else {
153                 CLog.w("Cannot create build-specific output dir %s. File already exists.",
154                         buildReportDir.getAbsolutePath());
155             }
156         } else {
157             if (FileUtil.mkdirsRWX(buildReportDir)) {
158                 return buildReportDir;
159             } else {
160                 CLog.w("Cannot create build-specific output dir %s. Failed to create directory.",
161                         buildReportDir.getAbsolutePath());
162             }
163         }
164         return buildReportDir;
165     }
166 
167     /**
168      * A helper to create test artifact path segments based on the build info.
169      * <p />
170      * {@code [branch/]build-id/test-tag}
171      */
generateTestArtifactPath(IBuildInfo buildInfo)172     List<String> generateTestArtifactPath(IBuildInfo buildInfo) {
173         final List<String> pathSegments = new ArrayList<String>();
174         if (buildInfo == null) {
175             return pathSegments;
176         }
177         if (buildInfo.getBuildBranch() != null) {
178             pathSegments.add(buildInfo.getBuildBranch());
179         }
180         pathSegments.add(buildInfo.getBuildId());
181         pathSegments.add(buildInfo.getTestTag());
182         return pathSegments;
183     }
184 
185     /**
186      * A helper function that translates a string into something that can be used as a filename
187      */
sanitizeFilename(String name)188     private static String sanitizeFilename(String name) {
189         return name.replace(File.separatorChar, '_');
190     }
191 
192     /**
193      * Save the log data to a file
194      *
195      * @param dataName a {@link String} descriptive name of the data.
196      * @param dataType the {@link LogDataType} of the file.
197      * @param dataStream the {@link InputStream} of the data.
198      * @return the file of the generated data
199      * @throws IOException if log file could not be generated
200      */
saveLogData(String dataName, LogDataType dataType, InputStream dataStream)201     public File saveLogData(String dataName, LogDataType dataType, InputStream dataStream)
202             throws IOException {
203         return saveLogDataRaw(dataName, dataType.getFileExt(), dataStream);
204     }
205 
206     /**
207      * Save a given log file
208      *
209      * @param dataName a {@link String} descriptive name of the data.
210      * @param dataType the {@link LogDataType} of the file.
211      * @param fileToLog the {@link File} to be logged
212      * @return the file of the generated data
213      * @throws IOException if log file could not be generated
214      */
saveLogFile(String dataName, LogDataType dataType, File fileToLog)215     public File saveLogFile(String dataName, LogDataType dataType, File fileToLog)
216             throws IOException {
217         long startTime = System.currentTimeMillis();
218         final String saneDataName = sanitizeFilename(dataName);
219         if (mInvLogDir != null && !mInvLogDir.exists()) {
220             mInvLogDir.mkdirs();
221         }
222         // add underscore to end of data name to make generated name more readable
223         File logFile =
224                 FileUtil.createTempFile(
225                         saneDataName + "_", "." + dataType.getFileExt(), mInvLogDir);
226         // Delete to avoid hardlink collision
227         logFile.delete();
228         // Hardlink fallback to copy if needed
229         FileUtil.hardlinkFile(fileToLog, logFile);
230         CLog.i(
231                 "Saved log file %s (type:%s). [size=%s, elapsed=%sms]",
232                 logFile.getAbsolutePath(),
233                 dataType,
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