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 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 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 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.isCompressed(), 120 dataType.isText()); 121 } 122 BufferedInputStream bufferedDataStream = null; 123 ZipOutputStream outputStream = null; 124 // add underscore to end of data name to make generated name more readable 125 final String saneDataName = sanitizeFilename(dataName); 126 File log = FileUtil.createTempFile(saneDataName + "_", "." + LogDataType.ZIP.getFileExt(), 127 mLogReportDir); 128 129 boolean setPerms = FileUtil.chmodGroupRWX(log); 130 if (!setPerms) { 131 CLog.w(String.format("Failed to set dir %s to be group accessible.", log)); 132 } 133 134 try { 135 bufferedDataStream = new BufferedInputStream(dataStream); 136 outputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(log), 137 BUFFER_SIZE)); 138 outputStream.putNextEntry(new ZipEntry(saneDataName + "." + dataType.getFileExt())); 139 StreamUtil.copyStreams(bufferedDataStream, outputStream); 140 CLog.d("Saved log file %s", log.getAbsolutePath()); 141 return new LogFile(log.getAbsolutePath(), getUrl(log), true, dataType.isText()); 142 } finally { 143 StreamUtil.close(bufferedDataStream); 144 StreamUtil.close(outputStream); 145 } 146 } 147 148 /** 149 * {@inheritDoc} 150 */ 151 @Override 152 public LogFile saveLogDataRaw(String dataName, String ext, InputStream dataStream) 153 throws IOException { 154 File log = saveLogDataInternal(dataName, ext, dataStream); 155 return new LogFile(log.getAbsolutePath(), getUrl(log), false, false); 156 } 157 158 private File saveLogDataInternal(String dataName, String ext, InputStream dataStream) 159 throws IOException { 160 final String saneDataName = sanitizeFilename(dataName); 161 // add underscore to end of data name to make generated name more readable 162 File log = FileUtil.createTempFile(saneDataName + "_", "." + ext, mLogReportDir); 163 164 boolean setPerms = FileUtil.chmodGroupRWX(log); 165 if (!setPerms) { 166 CLog.w(String.format("Failed to set dir %s to be group accessible.", log)); 167 } 168 169 FileUtil.writeToFile(dataStream, log); 170 CLog.d("Saved raw log file %s", log.getAbsolutePath()); 171 return log; 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override 178 public LogFile getLogReportDir() { 179 return new LogFile(mLogReportDir.getAbsolutePath(), getUrl(mLogReportDir), false, false); 180 } 181 182 /** 183 * A helper method to create an invocation directory unique for saving logs. 184 * <p> 185 * Create a unique file system directory with the structure 186 * {@code report-dir/[branch/]build-id/test-tag/unique_dir} for saving logs. If the creation 187 * of the directory fails, will write logs to a temporary directory on the local file system. 188 * </p> 189 * 190 * @param buildInfo the {@link IBuildInfo} 191 * @param reportDir the {@link File} for the report directory. 192 * @param logRetentionDays how many days logs should be kept for. If {@code null}, then no log 193 * retention file is writen. 194 * @return The directory created. 195 */ 196 private File createLogReportDir(IBuildInfo buildInfo, File reportDir, 197 Integer logRetentionDays) { 198 File logReportDir; 199 // now create unique directory within the buildDir 200 try { 201 File buildDir = createBuildDir(buildInfo, reportDir); 202 logReportDir = FileUtil.createTempDir("inv_", buildDir); 203 } catch (IOException e) { 204 CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead", 205 reportDir.getAbsolutePath()); 206 CLog.e(e); 207 // try to create one in a tmp location instead 208 logReportDir = createTempDir(); 209 } 210 211 boolean setPerms = FileUtil.chmodGroupRWX(logReportDir); 212 if (!setPerms) { 213 CLog.w(String.format("Failed to set dir %s to be group accessible.", logReportDir)); 214 } 215 216 if (logRetentionDays != null && logRetentionDays > 0) { 217 new RetentionFileSaver().writeRetentionFile(logReportDir, logRetentionDays); 218 } 219 CLog.d("Using log file directory %s", logReportDir.getAbsolutePath()); 220 return logReportDir; 221 } 222 223 /** 224 * A helper method to get or create a build directory based on the build info of the invocation. 225 * <p> 226 * Create a unique file system directory with the structure 227 * {@code report-dir/[branch/]build-id/test-tag} for saving logs. 228 * </p> 229 * 230 * @param buildInfo the {@link IBuildInfo} 231 * @param reportDir the {@link File} for the report directory. 232 * @return The directory where invocations for the same build should be saved. 233 * @throws IOException if the directory could not be created because a file with the same name 234 * exists or there are no permissions to write to it. 235 */ 236 private File createBuildDir(IBuildInfo buildInfo, File reportDir) throws IOException { 237 List<String> pathSegments = new ArrayList<String>(); 238 if (buildInfo.getBuildBranch() != null) { 239 pathSegments.add(buildInfo.getBuildBranch()); 240 } 241 pathSegments.add(buildInfo.getBuildId()); 242 pathSegments.add(buildInfo.getTestTag()); 243 File buildReportDir = FileUtil.getFileForPath(reportDir, 244 pathSegments.toArray(new String[] {})); 245 246 // if buildReportDir already exists and is a directory - use it. 247 if (buildReportDir.exists()) { 248 if (buildReportDir.isDirectory()) { 249 return buildReportDir; 250 } else { 251 final String msg = String.format("Cannot create build-specific output dir %s. " + 252 "File already exists.", buildReportDir.getAbsolutePath()); 253 CLog.w(msg); 254 throw new IOException(msg); 255 } 256 } else { 257 if (FileUtil.mkdirsRWX(buildReportDir)) { 258 return buildReportDir; 259 } else { 260 final String msg = String.format("Cannot create build-specific output dir %s. " + 261 "Failed to create directory.", buildReportDir.getAbsolutePath()); 262 CLog.w(msg); 263 throw new IOException(msg); 264 } 265 } 266 } 267 268 /** 269 * A helper method to create a temp directory for an invocation. 270 */ 271 private File createTempDir() { 272 try { 273 return FileUtil.createTempDir("inv_"); 274 } catch (IOException e) { 275 // Abort tradefed if a temp directory cannot be created 276 throw new FatalHostError("Cannot create tmp directory.", e); 277 } 278 } 279 280 /** 281 * A helper function that translates a string into something that can be used as a filename 282 */ 283 private static String sanitizeFilename(String name) { 284 return name.replace(File.separatorChar, '_'); 285 } 286 287 /** 288 * A helper method that returns a URL for a given {@link File}. 289 * 290 * @param file the {@link File} of the log. 291 * @return The report directory path replaced with the report-url and path separators normalized 292 * (for Windows), or {@code null} if the report-url is not set, report-url ends with /, 293 * report-dir ends with {@link File#separator}, or the file is not in the report directory. 294 */ 295 private String getUrl(File file) { 296 if (mReportUrl == null) { 297 return null; 298 } 299 300 final String filePath = file.getAbsolutePath(); 301 final String reportPath = mRootReportDir.getAbsolutePath(); 302 303 if (reportPath.endsWith(File.separator)) { 304 CLog.w("Cannot create URL. getAbsolutePath() returned %s which ends with %s", 305 reportPath, File.separator); 306 return null; 307 } 308 309 // Log file starts with the mReportDir path, so do a simple replacement. 310 if (filePath.startsWith(reportPath)) { 311 String relativePath = filePath.substring(reportPath.length()); 312 // relativePath should start with /, drop the / from the url if it exists. 313 String url = mReportUrl; 314 if (url.endsWith("/")) { 315 url = url.substring(0, url.length() - 1); 316 } 317 // FIXME: Sanitize the URL. 318 return String.format("%s%s", url, relativePath.replace(File.separator, "/")); 319 } 320 321 return null; 322 } 323 324 /** 325 * Set the report directory. Exposed for unit testing. 326 */ 327 void setReportDir(File reportDir) { 328 mRootReportDir = reportDir; 329 } 330 331 /** 332 * Set the log retentionDays. Exposed for unit testing. 333 */ 334 void setLogRetentionDays(int logRetentionDays) { 335 mLogRetentionDays = logRetentionDays; 336 } 337 } 338