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