1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 
  25 package org.graalvm.compiler.debug;
  26 
  27 import java.io.File;
  28 import java.io.FileOutputStream;
  29 import java.io.IOException;
  30 import java.nio.file.FileVisitResult;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.nio.file.Paths;
  34 import java.nio.file.SimpleFileVisitor;
  35 import java.nio.file.attribute.BasicFileAttributes;
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.List;
  39 import java.util.zip.Deflater;
  40 import java.util.zip.ZipEntry;
  41 import java.util.zip.ZipOutputStream;
  42 
  43 import org.graalvm.compiler.options.OptionValues;
  44 import org.graalvm.compiler.serviceprovider.GraalServices;
  45 
  46 /**
  47  * Manages a directory into which diagnostics such crash reports and dumps should be written. The
  48  * directory is archived and deleted when {@link #close()} is called.
  49  */
  50 public class DiagnosticsOutputDirectory {
  51 
  52     /**
  53      * Use an illegal file name to denote that {@link #close()} has been called.
  54      */
  55     private static final String CLOSED = "\u0000";
  56 
  57     public DiagnosticsOutputDirectory(OptionValues options) {
  58         this.options = options;
  59     }
  60 
  61     private final OptionValues options;
  62 
  63     private String path;
  64 
  65     /**
  66      * Gets the path to the output directory managed by this object, creating if it doesn't exist
  67      * and has not been deleted.
  68      *
  69      * @returns the directory or {@code null} if the could not be created or has been deleted
  70      */
  71     public String getPath() {
  72         return getPath(true);
  73     }
  74 
  75     private synchronized String getPath(boolean createIfNull) {
  76         if (path == null && createIfNull) {
  77             path = createPath();
  78             File dir = new File(path).getAbsoluteFile();
  79             if (!dir.exists()) {
  80                 dir.mkdirs();
  81                 if (!dir.exists()) {
  82                     TTY.println("Warning: could not create Graal diagnostic directory " + dir);
  83                     return null;
  84                 }
  85             }
  86         }
  87         if (CLOSED.equals(path)) {
  88             TTY.println("Warning: Graal diagnostic directory already closed");
  89         }
  90         return path;
  91     }
  92 
  93     /**
  94      * Gets the path of the directory to be created.
  95      *
  96      * Subclasses can override this to determine how the path name is created.
  97      *
  98      * @return the path to be created
  99      */
 100     protected String createPath() {
 101         Path baseDir;
 102         try {
 103             baseDir = DebugOptions.getDumpDirectory(options);
 104         } catch (IOException e) {
 105             // Default to current directory if there was a problem creating the
 106             // directory specified by the DumpPath option.
 107             baseDir = Paths.get(".");
 108         }
 109         return baseDir.resolve("graal_diagnostics_" + GraalServices.getExecutionID()).toAbsolutePath().toString();
 110     }
 111 
 112     /**
 113      * Archives and deletes this directory if it exists.
 114      */
 115     public void close() {
 116         archiveAndDelete();
 117     }
 118 
 119     /**
 120      * Archives and deletes the {@linkplain #getPath() output directory} if it exists.
 121      */
 122     private synchronized void archiveAndDelete() {
 123         String outDir = getPath(false);
 124         if (outDir != null) {
 125             // Notify other threads calling getPath() that the directory is deleted.
 126             // This attempts to mitigate other threads writing to the directory
 127             // while it is being archived and deleted.
 128             path = CLOSED;
 129 
 130             Path dir = Paths.get(outDir);
 131             if (dir.toFile().exists()) {
 132                 File zip = new File(outDir + ".zip").getAbsoluteFile();
 133                 List<Path> toDelete = new ArrayList<>();
 134                 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) {
 135                     zos.setLevel(Deflater.BEST_COMPRESSION);
 136                     Files.walkFileTree(dir, Collections.emptySet(), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
 137                         @Override
 138                         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
 139                             if (attrs.isRegularFile()) {
 140                                 String name = dir.relativize(file).toString();
 141                                 ZipEntry ze = new ZipEntry(name);
 142                                 zos.putNextEntry(ze);
 143                                 Files.copy(file, zos);
 144                                 zos.closeEntry();
 145                             }
 146                             toDelete.add(file);
 147                             return FileVisitResult.CONTINUE;
 148                         }
 149 
 150                         @Override
 151                         public FileVisitResult postVisitDirectory(Path d, IOException exc) throws IOException {
 152                             toDelete.add(d);
 153                             return FileVisitResult.CONTINUE;
 154                         }
 155                     });
 156                     // Keep this in sync with the catch_files in common.hocon
 157                     TTY.println("Graal diagnostic output saved in %s", zip);
 158                 } catch (IOException e) {
 159                     TTY.printf("IO error archiving %s:%n%s. The directory will not be deleted and must be " +
 160                                     "manually removed once the VM exits.%n", dir, e);
 161                     toDelete.clear();
 162                 }
 163                 if (!toDelete.isEmpty()) {
 164                     IOException lastDeletionError = null;
 165                     for (Path p : toDelete) {
 166                         try {
 167                             Files.delete(p);
 168                         } catch (IOException e) {
 169                             lastDeletionError = e;
 170                         }
 171                     }
 172                     if (lastDeletionError != null) {
 173                         TTY.printf("IO error deleting %s:%n%s. This is most likely due to a compilation on " +
 174                                         "another thread holding a handle to a file within this directory. " +
 175                                         "Please delete the directory manually once the VM exits.%n", dir, lastDeletionError);
 176                     }
 177                 }
 178             }
 179         }
 180     }
 181 }