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