| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tradefed.result; |
| |
| import com.android.tradefed.build.IBuildInfo; |
| import com.android.tradefed.command.FatalHostError; |
| import com.android.tradefed.config.Option; |
| import com.android.tradefed.config.OptionClass; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.util.FileUtil; |
| import com.android.tradefed.util.StreamUtil; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipOutputStream; |
| |
| /** |
| * Save logs to a file system. |
| */ |
| @OptionClass(alias = "file-system-log-saver") |
| public class FileSystemLogSaver implements ILogSaver { |
| |
| private static final int BUFFER_SIZE = 64 * 1024; |
| |
| @Option(name = "log-file-path", description = "root file system path to store log files.") |
| private File mRootReportDir = new File(System.getProperty("java.io.tmpdir")); |
| |
| @Option(name = "log-file-url", description = |
| "root http url of log files. Assumes files placed in log-file-path are visible via " + |
| "this url.") |
| private String mReportUrl = null; |
| |
| @Option(name = "log-retention-days", description = |
| "the number of days to keep saved log files.") |
| private Integer mLogRetentionDays = null; |
| |
| @Option(name = "compress-files", description = |
| "whether to compress files which are not already compressed") |
| private boolean mCompressFiles = true; |
| |
| private File mLogReportDir = null; |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Also, create a unique file system directory under |
| * {@code report-dir/[branch/]build-id/test-tag/unique_dir} for saving logs. If the creation |
| * of the directory fails, will write logs to a temporary directory on the local file system. |
| * </p> |
| */ |
| @Override |
| public void invocationStarted(IBuildInfo buildInfo) { |
| mLogReportDir = createLogReportDir(buildInfo, mRootReportDir, mLogRetentionDays); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void invocationEnded(long elapsedTime) { |
| // Ignore, no clean up needed. |
| } |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * Will zip and save the log file if {@link LogDataType#isCompressed()} returns false for |
| * {@code dataType} and {@code compressed-files} is set, otherwise, the stream will be saved |
| * uncompressed. |
| * </p> |
| */ |
| @Override |
| public LogFile saveLogData(String dataName, LogDataType dataType, InputStream dataStream) |
| throws IOException { |
| if (!mCompressFiles || dataType.isCompressed()) { |
| return saveLogDataRaw(dataName, dataType.getFileExt(), dataStream); |
| } |
| BufferedInputStream bufferedDataStream = null; |
| ZipOutputStream outputStream = null; |
| // add underscore to end of data name to make generated name more readable |
| final String saneDataName = sanitizeFilename(dataName); |
| File log = FileUtil.createTempFile(saneDataName + "_", "." + LogDataType.ZIP.getFileExt(), |
| mLogReportDir); |
| |
| boolean setPerms = FileUtil.chmodGroupRWX(log); |
| if (!setPerms) { |
| CLog.w(String.format("Failed to set dir %s to be group accessible.", log)); |
| } |
| |
| try { |
| bufferedDataStream = new BufferedInputStream(dataStream); |
| outputStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(log), |
| BUFFER_SIZE)); |
| outputStream.putNextEntry(new ZipEntry(saneDataName + "." + dataType.getFileExt())); |
| StreamUtil.copyStreams(bufferedDataStream, outputStream); |
| CLog.i("Saved log file %s", log.getAbsolutePath()); |
| return new LogFile(log.getAbsolutePath(), getUrl(log)); |
| } finally { |
| StreamUtil.close(bufferedDataStream); |
| StreamUtil.close(outputStream); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public LogFile saveLogDataRaw(String dataName, String ext, InputStream dataStream) |
| throws IOException { |
| final String saneDataName = sanitizeFilename(dataName); |
| // add underscore to end of data name to make generated name more readable |
| File log = FileUtil.createTempFile(saneDataName + "_", "." + ext, mLogReportDir); |
| |
| boolean setPerms = FileUtil.chmodGroupRWX(log); |
| if (!setPerms) { |
| CLog.w(String.format("Failed to set dir %s to be group accessible.", log)); |
| } |
| |
| FileUtil.writeToFile(dataStream, log); |
| CLog.i("Saved log file %s", log.getAbsolutePath()); |
| return new LogFile(log.getAbsolutePath(), getUrl(log)); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public LogFile getLogReportDir() { |
| return new LogFile(mLogReportDir.getAbsolutePath(), getUrl(mLogReportDir)); |
| } |
| |
| /** |
| * A helper method to create an invocation directory unique for saving logs. |
| * <p> |
| * Create a unique file system directory with the structure |
| * {@code report-dir/[branch/]build-id/test-tag/unique_dir} for saving logs. If the creation |
| * of the directory fails, will write logs to a temporary directory on the local file system. |
| * </p> |
| * |
| * @param buildInfo the {@link IBuildInfo} |
| * @param reportDir the {@link File} for the report directory. |
| * @param logRetentionDays how many days logs should be kept for. If {@code null}, then no log |
| * retention file is writen. |
| * @return The directory created. |
| */ |
| private File createLogReportDir(IBuildInfo buildInfo, File reportDir, |
| Integer logRetentionDays) { |
| File logReportDir; |
| // now create unique directory within the buildDir |
| try { |
| File buildDir = createBuildDir(buildInfo, reportDir); |
| logReportDir = FileUtil.createTempDir("inv_", buildDir); |
| } catch (IOException e) { |
| CLog.e("Unable to create unique directory in %s. Attempting to use tmp dir instead", |
| reportDir.getAbsolutePath()); |
| CLog.e(e); |
| // try to create one in a tmp location instead |
| logReportDir = createTempDir(); |
| } |
| |
| boolean setPerms = FileUtil.chmodGroupRWX(logReportDir); |
| if (!setPerms) { |
| CLog.w(String.format("Failed to set dir %s to be group accessible.", logReportDir)); |
| } |
| |
| if (logRetentionDays != null && logRetentionDays > 0) { |
| new RetentionFileSaver().writeRetentionFile(logReportDir, logRetentionDays); |
| } |
| CLog.i("Using log file directory %s", logReportDir.getAbsolutePath()); |
| return logReportDir; |
| } |
| |
| /** |
| * A helper method to get or create a build directory based on the build info of the invocation. |
| * <p> |
| * Create a unique file system directory with the structure |
| * {@code report-dir/[branch/]build-id/test-tag} for saving logs. |
| * </p> |
| * |
| * @param buildInfo the {@link IBuildInfo} |
| * @param reportDir the {@link File} for the report directory. |
| * @return The directory where invocations for the same build should be saved. |
| * @throws IOException if the directory could not be created because a file with the same name |
| * exists or there are no permissions to write to it. |
| */ |
| private File createBuildDir(IBuildInfo buildInfo, File reportDir) throws IOException { |
| List<String> pathSegments = new ArrayList<String>(); |
| if (buildInfo.getBuildBranch() != null) { |
| pathSegments.add(buildInfo.getBuildBranch()); |
| } |
| pathSegments.add(buildInfo.getBuildId()); |
| pathSegments.add(buildInfo.getTestTag()); |
| File buildReportDir = FileUtil.getFileForPath(reportDir, |
| pathSegments.toArray(new String[] {})); |
| |
| // if buildReportDir already exists and is a directory - use it. |
| if (buildReportDir.exists()) { |
| if (buildReportDir.isDirectory()) { |
| return buildReportDir; |
| } else { |
| final String msg = String.format("Cannot create build-specific output dir %s. " + |
| "File already exists.", buildReportDir.getAbsolutePath()); |
| CLog.w(msg); |
| throw new IOException(msg); |
| } |
| } else { |
| if (FileUtil.mkdirsRWX(buildReportDir)) { |
| return buildReportDir; |
| } else { |
| final String msg = String.format("Cannot create build-specific output dir %s. " + |
| "Failed to create directory.", buildReportDir.getAbsolutePath()); |
| CLog.w(msg); |
| throw new IOException(msg); |
| } |
| } |
| } |
| |
| /** |
| * A helper method to create a temp directory for an invocation. |
| */ |
| private File createTempDir() { |
| try { |
| return FileUtil.createTempDir("inv_"); |
| } catch (IOException e) { |
| // Abort tradefed if a temp directory cannot be created |
| throw new FatalHostError("Cannot create tmp directory.", e); |
| } |
| } |
| |
| /** |
| * A helper function that translates a string into something that can be used as a filename |
| */ |
| private static String sanitizeFilename(String name) { |
| return name.replace(File.separatorChar, '_'); |
| } |
| |
| /** |
| * A helper method that returns a URL for a given {@link File}. |
| * |
| * @param file the {@link File} of the log. |
| * @return The report directory path replaced with the report-url and path separators normalized |
| * (for Windows), or {@code null} if the report-url is not set, report-url ends with /, |
| * report-dir ends with {@link File#separator}, or the file is not in the report directory. |
| */ |
| private String getUrl(File file) { |
| if (mReportUrl == null) { |
| return null; |
| } |
| |
| final String filePath = file.getAbsolutePath(); |
| final String reportPath = mRootReportDir.getAbsolutePath(); |
| |
| if (reportPath.endsWith(File.separator)) { |
| CLog.w("Cannot create URL. getAbsolutePath() returned %s which ends with %s", |
| reportPath, File.separator); |
| return null; |
| } |
| |
| // Log file starts with the mReportDir path, so do a simple replacement. |
| if (filePath.startsWith(reportPath)) { |
| String relativePath = filePath.substring(reportPath.length()); |
| // relativePath should start with /, drop the / from the url if it exists. |
| String url = mReportUrl; |
| if (url.endsWith("/")) { |
| url = url.substring(0, url.length() - 1); |
| } |
| // FIXME: Sanitize the URL. |
| return String.format("%s%s", url, relativePath.replace(File.separator, "/")); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Set the report directory. Exposed for unit testing. |
| */ |
| void setReportDir(File reportDir) { |
| mRootReportDir = reportDir; |
| } |
| |
| /** |
| * Set the log retentionDays. Exposed for unit testing. |
| */ |
| void setLogRetentionDays(int logRetentionDays) { |
| mLogRetentionDays = logRetentionDays; |
| } |
| } |