View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package de.softwareforge.testing.postgres.embedded;
15  
16  import static com.google.common.base.Preconditions.checkNotNull;
17  import static com.google.common.base.Preconditions.checkState;
18  import static java.lang.String.format;
19  import static java.nio.file.StandardOpenOption.CREATE;
20  import static java.nio.file.StandardOpenOption.WRITE;
21  
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.nio.ByteBuffer;
27  import java.nio.channels.AsynchronousFileChannel;
28  import java.nio.channels.Channel;
29  import java.nio.channels.CompletionHandler;
30  import java.nio.channels.FileLock;
31  import java.nio.file.FileSystems;
32  import java.nio.file.Files;
33  import java.nio.file.LinkOption;
34  import java.nio.file.Path;
35  import java.util.Map;
36  import java.util.concurrent.ConcurrentHashMap;
37  import java.util.concurrent.Phaser;
38  import java.util.concurrent.locks.Lock;
39  import java.util.concurrent.locks.ReentrantLock;
40  
41  import edu.umd.cs.findbugs.annotations.NonNull;
42  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
43  import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  import org.tukaani.xz.XZInputStream;
47  
48  /**
49   * Loads a native binary installation and returns the location of it.
50   */
51  public final class TarXzCompressedBinaryManager implements NativeBinaryManager {
52  
53      private static final Logger LOG = LoggerFactory.getLogger(TarXzCompressedBinaryManager.class);
54  
55      private static final Map<NativeBinaryLocator, File> KNOWN_INSTALLATIONS = new ConcurrentHashMap<>();
56  
57      private final Lock prepareBinariesLock = new ReentrantLock();
58  
59      private File installationBaseDirectory = EmbeddedUtil.getWorkingDirectory();
60      private String lockFileName = EmbeddedPostgres.LOCK_FILE_NAME;
61  
62      private final NativeBinaryLocator nativeBinaryLocator;
63  
64      /**
65       * Creates a new binary manager for tar-xz compressed archives.
66       * <p>
67       * The implementation of {@link NativeBinaryLocator} to locate the stream that gets unpacked must satisfy the following criteria:
68       * <ul>
69       *     <li>It must implement {@link #equals(Object)} and {@link #hashCode()}.</li>
70       *     <li>It should implement {@link #toString()} to return meaningful information about the locator.</li>
71       *     <li>It must allow multiple calls to {@link NativeBinaryLocator#getInputStream()} which all return the same, byte-identical contents.
72       *     The operation should be cheap as it may be called multiple times.</li>
73       * </ul>
74       *
75       * @param nativeBinaryLocator An implementation of {@link NativeBinaryLocator} that satisfies the conditions above. Must not be null.
76       */
77      public TarXzCompressedBinaryManager(@NonNull NativeBinaryLocator nativeBinaryLocator) {
78          this.nativeBinaryLocator = checkNotNull(nativeBinaryLocator, "nativeBinaryLocator is null");
79  
80          checkState(this.installationBaseDirectory.setWritable(true, false),
81                  "Could not make install base directory %s writable!", this.installationBaseDirectory);
82      }
83  
84      @Override
85      public void setInstallationBaseDirectory(File installationBaseDirectory) {
86          this.installationBaseDirectory = checkNotNull(installationBaseDirectory, "installationBaseDirectory is null");
87  
88          checkState(this.installationBaseDirectory.setWritable(true, false),
89                  "Could not make install base directory %s writable!", this.installationBaseDirectory);
90      }
91  
92      /**
93       * Sets the lock file name. This method must be called before the first call to {@link TarXzCompressedBinaryManager#getLocation()}.
94       *
95       * @param lockFileName Name of a file to use as file lock when unpacking the distribution.
96       */
97      public void setLockFileName(String lockFileName) {
98          this.lockFileName = checkNotNull(lockFileName, "lockFileName is null");
99      }
100 
101     @Override
102     @NonNull
103     public File getLocation() throws IOException {
104 
105         // the installation cache saves ~ 1% CPU according to the profiler
106         File installationDirectory = KNOWN_INSTALLATIONS.get(nativeBinaryLocator);
107         if (installationDirectory != null && installationDirectory.exists()) {
108             return installationDirectory;
109         }
110 
111         prepareBinariesLock.lock();
112         try {
113             String installationIdentifier = nativeBinaryLocator.getIdentifier();
114             installationDirectory = new File(installationBaseDirectory, installationIdentifier);
115             EmbeddedUtil.mkdirs(installationDirectory);
116 
117             final File unpackLockFile = new File(installationDirectory, lockFileName);
118             final File installationExistsFile = new File(installationDirectory, ".exists");
119 
120             if (!installationExistsFile.exists()) {
121                 try (FileOutputStream lockStream = new FileOutputStream(unpackLockFile);
122                         FileLock unpackLock = lockStream.getChannel().tryLock()) {
123                     if (unpackLock != null) {
124                         checkState(!installationExistsFile.exists(), "unpack lock acquired but .exists file is present " + installationExistsFile);
125                         LOG.info("extracting archive...");
126                         try (InputStream archiveStream = nativeBinaryLocator.getInputStream()) {
127                             extractTxz(archiveStream, installationDirectory.getPath());
128                             checkState(installationExistsFile.createNewFile(), "couldn't create %s file!", installationExistsFile);
129                         }
130                     } else {
131                         // the other guy is unpacking for us.
132                         int maxAttempts = 60;
133                         while (!installationExistsFile.exists() && --maxAttempts > 0) { // NOPMD
134                             Thread.sleep(1000L);
135                         }
136                         checkState(installationExistsFile.exists(), "Waited 60 seconds for archive to be unpacked but it never finished!");
137                     }
138                 } finally {
139                     if (unpackLockFile.exists() && !unpackLockFile.delete()) {
140                         LOG.error(format("could not remove lock file %s", unpackLockFile.getAbsolutePath()));
141                     }
142                 }
143             }
144 
145             KNOWN_INSTALLATIONS.putIfAbsent(nativeBinaryLocator, installationDirectory);
146             LOG.debug(format("Unpacked archive at %s", installationDirectory));
147             return installationDirectory;
148 
149         } catch (final InterruptedException e) {
150             Thread.currentThread().interrupt();
151             throw new IOException(e);
152         } finally {
153             prepareBinariesLock.lock();
154         }
155     }
156 
157     /**
158      * Unpack archive compressed by tar with xz compression.
159      *
160      * @param stream    A tar-xz compressed data stream.
161      * @param targetDir The directory to extract the content to.
162      */
163     private static void extractTxz(InputStream stream, String targetDir) throws IOException {
164         try (XZInputStream xzIn = new XZInputStream(stream);
165                 TarArchiveInputStream tarIn = new TarArchiveInputStream(xzIn)) {
166             final Phaser phaser = new Phaser(1);
167             TarArchiveEntry entry;
168 
169             while ((entry = tarIn.getNextTarEntry()) != null) { //NOPMD
170                 final String individualFile = entry.getName();
171                 final File fsObject = new File(targetDir, individualFile);
172                 final Path fsPath = fsObject.toPath();
173                 if (Files.exists(fsPath, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(fsPath, LinkOption.NOFOLLOW_LINKS)) {
174                     Files.delete(fsPath);
175                     LOG.debug(format("Deleting existing entry %s", fsPath));
176                 }
177 
178                 if (entry.isSymbolicLink() || entry.isLink()) {
179                     Path target = FileSystems.getDefault().getPath(entry.getLinkName());
180                     Files.createSymbolicLink(fsPath, target);
181                 } else if (entry.isFile()) {
182                     byte[] content = new byte[(int) entry.getSize()];
183                     int read = tarIn.read(content, 0, content.length);
184                     checkState(read != -1, "could not read %s", individualFile);
185                     EmbeddedUtil.mkdirs(fsObject.getParentFile());
186 
187                     final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(fsPath, CREATE, WRITE); //NOPMD
188                     final ByteBuffer buffer = ByteBuffer.wrap(content); //NOPMD
189 
190                     phaser.register();
191                     fileChannel.write(buffer, 0, fileChannel, new CompletionHandler<Integer, Channel>() {
192                         @Override
193                         public void completed(Integer written, Channel channel) {
194                             closeChannel(channel);
195                         }
196 
197                         @Override
198                         public void failed(Throwable error, Channel channel) {
199                             LOG.error(format("could not write file %s", fsObject.getAbsolutePath()), error);
200                             closeChannel(channel);
201                         }
202 
203                         private void closeChannel(Channel channel) {
204                             try {
205                                 channel.close();
206                             } catch (IOException e) {
207                                 LOG.error("While closing channel:", e);
208                             } finally {
209                                 phaser.arriveAndDeregister();
210                             }
211                         }
212                     });
213                 } else if (entry.isDirectory()) {
214                     EmbeddedUtil.mkdirs(fsObject);
215                 } else {
216                     throw new IOException(format("Unsupported entry in tar file found: %s", individualFile));
217                 }
218 
219                 if (individualFile.startsWith("bin/") || individualFile.startsWith("./bin/")) {
220                     if (!fsObject.setExecutable(true, false)) {
221                         throw new IOException(format("Could not make %s executable!", individualFile));
222                     }
223                 }
224             }
225 
226             phaser.arriveAndAwaitAdvance();
227         }
228     }
229 }