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 File buildDir = createBuildDir(buildInfo, reportDir); 199 logReportDir = FileUtil.createTempDir("inv_", buildDir); 200 } catch (IOException e) { 201 CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead", 202 reportDir.getAbsolutePath()); 203 CLog.e(e); 204 // try to create one in a tmp location instead 205 logReportDir = createTempDir(); 206 } 207 208 boolean setPerms = FileUtil.chmodGroupRWX(logReportDir); 209 if (!setPerms) { 210 CLog.w(String.format("Failed to set dir %s to be group accessible.", logReportDir)); 211 } 212 213 if (logRetentionDays != null && logRetentionDays > 0) { 214 new RetentionFileSaver().writeRetentionFile(logReportDir, logRetentionDays); 215 } 216 CLog.d("Using log file directory %s", logReportDir.getAbsolutePath()); 217 return logReportDir; 218 } 219 220 /** 221 * A helper method to get or create a build directory based on the build info of the invocation. 222 * <p> 223 * Create a unique file system directory with the structure 224 * {@code report-dir/[branch/]build-id/test-tag} for saving logs. 225 * </p> 226 * 227 * @param buildInfo the {@link IBuildInfo} 228 * @param reportDir the {@link File} for the report directory. 229 * @return The directory where invocations for the same build should be saved. 230 * @throws IOException if the directory could not be created because a file with the same name 231 * exists or there are no permissions to write to it. 232 */ createBuildDir(IBuildInfo buildInfo, File reportDir)233 private File createBuildDir(IBuildInfo buildInfo, File reportDir) throws IOException { 234 List<String> pathSegments = new ArrayList<String>(); 235 if (buildInfo.getBuildBranch() != null) { 236 pathSegments.add(buildInfo.getBuildBranch()); 237 } 238 pathSegments.add(buildInfo.getBuildId()); 239 pathSegments.add(buildInfo.getTestTag()); 240 File buildReportDir = FileUtil.getFileForPath(reportDir, 241 pathSegments.toArray(new String[] {})); 242 243 // if buildReportDir already exists and is a directory - use it. 244 if (buildReportDir.exists()) { 245 if (buildReportDir.isDirectory()) { 246 return buildReportDir; 247 } else { 248 final String msg = String.format("Cannot create build-specific output dir %s. " + 249 "File already exists.", buildReportDir.getAbsolutePath()); 250 CLog.w(msg); 251 throw new IOException(msg); 252 } 253 } else { 254 if (FileUtil.mkdirsRWX(buildReportDir)) { 255 return buildReportDir; 256 } else { 257 final String msg = String.format("Cannot create build-specific output dir %s. " + 258 "Failed to create directory.", buildReportDir.getAbsolutePath()); 259 CLog.w(msg); 260 throw new IOException(msg); 261 } 262 } 263 } 264 265 /** 266 * A helper method to create a temp directory for an invocation. 267 */ createTempDir()268 private File createTempDir() { 269 try { 270 return FileUtil.createTempDir("inv_"); 271 } catch (IOException e) { 272 // Abort tradefed if a temp directory cannot be created 273 throw new FatalHostError("Cannot create tmp directory.", e); 274 } 275 } 276 277 /** 278 * A helper function that translates a string into something that can be used as a filename 279 */ sanitizeFilename(String name)280 private static String sanitizeFilename(String name) { 281 return name.replace(File.separatorChar, '_'); 282 } 283 284 /** 285 * A helper method that returns a URL for a given {@link File}. 286 * 287 * @param file the {@link File} of the log. 288 * @return The report directory path replaced with the report-url and path separators normalized 289 * (for Windows), or {@code null} if the report-url is not set, report-url ends with /, 290 * report-dir ends with {@link File#separator}, or the file is not in the report directory. 291 */ getUrl(File file)292 private String getUrl(File file) { 293 if (mReportUrl == null) { 294 return null; 295 } 296 297 final String filePath = file.getAbsolutePath(); 298 final String reportPath = mRootReportDir.getAbsolutePath(); 299 300 if (reportPath.endsWith(File.separator)) { 301 CLog.w("Cannot create URL. getAbsolutePath() returned %s which ends with %s", 302 reportPath, File.separator); 303 return null; 304 } 305 306 // Log file starts with the mReportDir path, so do a simple replacement. 307 if (filePath.startsWith(reportPath)) { 308 String relativePath = filePath.substring(reportPath.length()); 309 // relativePath should start with /, drop the / from the url if it exists. 310 String url = mReportUrl; 311 if (url.endsWith("/")) { 312 url = url.substring(0, url.length() - 1); 313 } 314 // FIXME: Sanitize the URL. 315 return String.format("%s%s", url, relativePath.replace(File.separator, "/")); 316 } 317 318 return null; 319 } 320 321 /** 322 * Set the report directory. Exposed for unit testing. 323 */ setReportDir(File reportDir)324 void setReportDir(File reportDir) { 325 mRootReportDir = reportDir; 326 } 327 328 /** 329 * Set the log retentionDays. Exposed for unit testing. 330 */ setLogRetentionDays(int logRetentionDays)331 void setLogRetentionDays(int logRetentionDays) { 332 mLogRetentionDays = logRetentionDays; 333 } 334 } 335