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