Home | History | Annotate | Download | only in result
      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