1
2
3
4
5
6
7
8
9
10
11
12
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
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
66
67
68
69
70
71
72
73
74
75
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
94
95
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
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
132 int maxAttempts = 60;
133 while (!installationExistsFile.exists() && --maxAttempts > 0) {
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
159
160
161
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) {
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);
188 final ByteBuffer buffer = ByteBuffer.wrap(content);
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 }