Fix #56 Require Java 8+ instead of Java 11+ at runtime

This commit is contained in:
Peter Palaga
2020-08-14 17:48:54 +02:00
parent d780eb0f57
commit 6ac2a01c64
37 changed files with 260 additions and 157 deletions

84
common/pom.xml Normal file
View File

@@ -0,0 +1,84 @@
<!--
Copyright 2019 the original author or authors.
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.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jboss.fuse.mvnd</groupId>
<artifactId>mvnd</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>mvnd-common</artifactId>
<packaging>jar</packaging>
<name>Maven Daemon - Common</name>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline-terminal-jansi</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>io.takari.maven.plugins</groupId>
<artifactId>takari-lifecycle-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>sisu-index</goal>
</goals>
<phase>process-classes</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2018 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.nio.Buffer;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/base-services/src/main/java/org/gradle/internal/io/BufferCaster.java
*/
public class BufferCaster {
/**
* Without this cast, when the code compiled by Java 9+ is executed on Java 8, it will throw
* java.lang.NoSuchMethodError: Method flip()Ljava/nio/ByteBuffer; does not exist in class java.nio.ByteBuffer
*/
@SuppressWarnings("RedundantCast")
public static <T extends Buffer> Buffer cast(T byteBuffer) {
return (Buffer) byteBuffer;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2018 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class BuildProperties {
private static final BuildProperties INSTANCE = load();
public static BuildProperties getInstance() {
return INSTANCE;
}
public static BuildProperties load() {
final Properties buildProperties = new Properties();
try (InputStream is = BuildProperties.class.getResourceAsStream("build.properties")) {
buildProperties.load(is);
} catch (IOException e) {
throw new RuntimeException("Could not read build.properties");
}
return new BuildProperties(buildProperties.getProperty("version"));
}
private final String version;
public BuildProperties(String version) {
this.version = version;
}
public String getVersion() {
return version;
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2011 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/context/DaemonCompatibilitySpec.java
*/
public class DaemonCompatibilitySpec {
private final Path javaHome;
private final List<String> options;
/**
* @param javaHome make sure the Path is a result of {@link Path#toRealPath(java.nio.file.LinkOption...)}
* @param options the options
*/
public DaemonCompatibilitySpec(Path javaHome, List<String> options) {
this.javaHome = Objects.requireNonNull(javaHome, "javaHome");
this.options = Objects.requireNonNull(options, "options");
}
public Result isSatisfiedBy(DaemonInfo daemon) {
if (!javaHomeMatches(daemon)) {
return new Result(false, () -> "Java home is different.\n" + diff(daemon));
}
if (!daemonOptsMatch(daemon)) {
return new Result(false, () -> "At least one daemon option is different.\n" + diff(daemon));
}
return new Result(true, () -> {
throw new RuntimeException("No reason if DaemonCompatibilityResult.compatible == true");
});
}
private String diff(DaemonInfo context) {
final StringBuilder sb = new StringBuilder("Wanted: ");
appendFields(sb);
sb.append("\nActual: ");
context.appendNonKeyFields(sb).append("uid=").append(context.getUid()).append('\n');
return sb.toString();
}
private boolean daemonOptsMatch(DaemonInfo daemon) {
return daemon.getOptions().containsAll(options)
&& daemon.getOptions().size() == options.size();
}
private boolean javaHomeMatches(DaemonInfo daemon) {
return javaHome.equals(Paths.get(daemon.getJavaHome()));
}
StringBuilder appendFields(StringBuilder sb) {
return sb.append("javaHome=").append(javaHome)
.append(", options=").append(options);
}
@Override
public String toString() {
return appendFields(new StringBuilder("DaemonCompatibilitySpec{")).append('}').toString();
}
public static class Result {
private final boolean compatible;
private final Supplier<String> why;
Result(boolean compatible, Supplier<String> why) {
super();
this.compatible = compatible;
this.why = why;
}
public boolean isCompatible() {
return compatible;
}
public String getWhy() {
return why.get();
}
}
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2016 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/messaging/src/main/java/org/gradle/internal/remote/internal/inet/SocketConnection.java
*
*/
public class DaemonConnection<T> implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonConnection.class);
private final SocketChannel socket;
private final Serializer<T> serializer;
private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress;
private final DataInputStream instr;
private final DataOutputStream outstr;
public DaemonConnection(SocketChannel socket, Serializer<T> serializer) {
this.socket = socket;
this.serializer = serializer;
try {
// NOTE: we use non-blocking IO as there is no reliable way when using blocking IO to shutdown reads while
// keeping writes active. For example, Socket.shutdownInput() does not work on Windows.
socket.configureBlocking(false);
outstr = new DataOutputStream(new SocketOutputStream(socket));
instr = new DataInputStream(new SocketInputStream(socket));
} catch (IOException e) {
throw new DaemonException.InterruptedException(e);
}
localAddress = (InetSocketAddress) socket.socket().getLocalSocketAddress();
remoteAddress = (InetSocketAddress) socket.socket().getRemoteSocketAddress();
}
@Override
public String toString() {
return "socket connection from " + localAddress + " to " + remoteAddress;
}
public T receive() throws DaemonException.MessageIOException {
try {
return serializer.read(instr);
} catch (EOFException e) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Discarding EOFException: {}", e.toString());
}
return null;
} catch (ClassNotFoundException | IOException e) {
throw new DaemonException.RecoverableMessageIOException(
String.format("Could not read message from '%s'.", remoteAddress), e);
} catch (Throwable e) {
throw new DaemonException.MessageIOException(String.format("Could not read message from '%s'.", remoteAddress), e);
}
}
private static boolean isEndOfStream(Exception e) {
if (e instanceof EOFException) {
return true;
}
if (e instanceof IOException) {
if (Objects.equals(e.getMessage(), "An existing connection was forcibly closed by the remote host")) {
return true;
}
if (Objects.equals(e.getMessage(), "An established connection was aborted by the software in your host machine")) {
return true;
}
if (Objects.equals(e.getMessage(), "Connection reset by peer")) {
return true;
}
if (Objects.equals(e.getMessage(), "Connection reset")) {
return true;
}
}
return false;
}
public void dispatch(T message) throws DaemonException.MessageIOException {
try {
serializer.write(outstr, message);
outstr.flush();
} catch (ClassNotFoundException | IOException e) {
throw new DaemonException.RecoverableMessageIOException(
String.format("Could not write message %s to '%s'.", message, remoteAddress), e);
} catch (Throwable e) {
throw new DaemonException.MessageIOException(
String.format("Could not write message %s to '%s'.", message, remoteAddress), e);
}
}
public void flush() throws DaemonException.MessageIOException {
try {
outstr.flush();
} catch (Throwable e) {
throw new DaemonException.MessageIOException(String.format("Could not write '%s'.", remoteAddress), e);
}
}
public void close() {
Throwable failure = null;
List<Closeable> elements = Arrays.asList(this::flush, instr, outstr, socket);
for (Closeable element : elements) {
try {
element.close();
} catch (Throwable throwable) {
if (failure == null) {
failure = throwable;
} else if (!Thread.currentThread().isInterrupted()) {
LOGGER.error(String.format("Could not stop %s.", element), throwable);
}
}
}
if (failure != null) {
throw new DaemonException(failure);
}
}
private static class SocketInputStream extends InputStream {
private final Selector selector;
private final ByteBuffer buffer;
private final SocketChannel socket;
private final byte[] readBuffer = new byte[1];
public SocketInputStream(SocketChannel socket) throws IOException {
this.socket = socket;
selector = Selector.open();
socket.register(selector, SelectionKey.OP_READ);
buffer = ByteBuffer.allocateDirect(4096);
BufferCaster.cast(buffer).limit(0);
}
@Override
public int read() throws IOException {
int nread = read(readBuffer, 0, 1);
if (nread <= 0) {
return nread;
}
return readBuffer[0] & 0xFF;
}
@Override
public int read(byte[] dest, int offset, int max) throws IOException {
if (max == 0) {
return 0;
}
if (buffer.remaining() == 0) {
try {
selector.select();
} catch (ClosedSelectorException e) {
return -1;
}
if (!selector.isOpen()) {
return -1;
}
BufferCaster.cast(buffer).clear();
int nread;
try {
nread = socket.read(buffer);
} catch (IOException e) {
if (isEndOfStream(e)) {
BufferCaster.cast(buffer).position(0);
BufferCaster.cast(buffer).limit(0);
return -1;
}
throw e;
}
BufferCaster.cast(buffer).flip();
if (nread < 0) {
return -1;
}
}
int count = Math.min(buffer.remaining(), max);
buffer.get(dest, offset, count);
return count;
}
@Override
public void close() throws IOException {
selector.close();
}
}
private static class SocketOutputStream extends OutputStream {
private static final int RETRIES_WHEN_BUFFER_FULL = 2;
private Selector selector;
private final SocketChannel socket;
private final ByteBuffer buffer;
private final byte[] writeBuffer = new byte[1];
public SocketOutputStream(SocketChannel socket) throws IOException {
this.socket = socket;
buffer = ByteBuffer.allocateDirect(32 * 1024);
}
@Override
public void write(int b) throws IOException {
writeBuffer[0] = (byte) b;
write(writeBuffer);
}
@Override
public void write(byte[] src, int offset, int max) throws IOException {
int remaining = max;
int currentPos = offset;
while (remaining > 0) {
int count = Math.min(remaining, buffer.remaining());
if (count > 0) {
buffer.put(src, currentPos, count);
remaining -= count;
currentPos += count;
}
while (buffer.remaining() == 0) {
writeBufferToChannel();
}
}
}
@Override
public void flush() throws IOException {
while (buffer.position() > 0) {
writeBufferToChannel();
}
}
private void writeBufferToChannel() throws IOException {
BufferCaster.cast(buffer).flip();
int count = writeWithNonBlockingRetry();
if (count == 0) {
// buffer was still full after non-blocking retries, now block
waitForWriteBufferToDrain();
}
buffer.compact();
}
private int writeWithNonBlockingRetry() throws IOException {
int count = 0;
int retryCount = 0;
while (count == 0 && retryCount++ < RETRIES_WHEN_BUFFER_FULL) {
count = socket.write(buffer);
if (count < 0) {
throw new EOFException();
} else if (count == 0) {
// buffer was full, just call Thread.yield
Thread.yield();
}
}
return count;
}
private void waitForWriteBufferToDrain() throws IOException {
if (selector == null) {
selector = Selector.open();
}
SelectionKey key = socket.register(selector, SelectionKey.OP_WRITE);
// block until ready for write operations
selector.select();
// cancel OP_WRITE selection
key.cancel();
// complete cancelling key
selector.selectNow();
}
@Override
public void close() throws IOException {
if (selector != null) {
selector.close();
selector = null;
}
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2009 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.stream.Collector;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/diagnostics/DaemonDiagnostics.java
*/
public class DaemonDiagnostics {
private final static int TAIL_SIZE = 20;
private final String uid;
private final Path daemonLog;
public DaemonDiagnostics(String uid, Path daemonLog) {
this.uid = uid;
this.daemonLog = daemonLog;
}
@Override
public String toString() {
return "{"
+ "uid=" + uid
+ ", daemonLog=" + daemonLog
+ '}';
}
private String tailDaemonLog() {
try {
String tail = tail(daemonLog, TAIL_SIZE);
return formatTail(tail);
} catch (IOException e) {
return "Unable to read from the daemon log file: " + daemonLog + ", because of: " + e.getCause();
}
}
/**
* @param path to read from tail
* @param maxLines max lines to read
* @return tail content
* @throws IOException when reading failed
*/
static String tail(Path path, int maxLines) throws IOException {
try (BufferedReader r = Files.newBufferedReader(path)) {
return String.join("\n", r.lines().collect(lastN(maxLines)));
}
}
static <T> Collector<T, ?, List<T>> lastN(int n) {
return Collector.<T, Deque<T>, List<T>> of(ArrayDeque::new, (acc, t) -> {
if (acc.size() == n)
acc.pollFirst();
acc.add(t);
}, (acc1, acc2) -> {
while (acc2.size() < n && !acc1.isEmpty()) {
acc2.addFirst(acc1.pollLast());
}
return acc2;
}, ArrayList::new);
}
private String formatTail(String tail) {
return "----- Last " + TAIL_SIZE + " lines from daemon log file - " + daemonLog + " -----\n"
+ tail
+ "----- End of the daemon log -----\n";
}
public String describe() {
return "Daemon uid: " + uid + "\n"
+ " log file: " + daemonLog + "\n"
+ tailDaemonLog();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
public class DaemonException extends RuntimeException {
public DaemonException(String message) {
super(message);
}
public DaemonException(String message, Throwable cause) {
super(message, cause);
}
public DaemonException(Throwable cause) {
super(cause);
}
public static class InterruptedException extends DaemonException {
public InterruptedException(Throwable cause) {
super(cause);
}
}
public static class ConnectException extends DaemonException {
public ConnectException(String message) {
super(message);
}
public ConnectException(String message, Throwable cause) {
super(message, cause);
}
}
public static class StartException extends DaemonException {
public StartException(String message) {
super(message);
}
public StartException(String message, Throwable cause) {
super(message, cause);
}
}
public static class MessageIOException extends DaemonException {
public MessageIOException(String message) {
super(message);
}
public MessageIOException(String message, Throwable cause) {
super(message, cause);
}
}
public static class RecoverableMessageIOException extends MessageIOException {
public RecoverableMessageIOException(String message) {
super(message);
}
public RecoverableMessageIOException(String message, Throwable cause) {
super(message, cause);
}
}
public static class StaleAddressException extends DaemonException {
public StaleAddressException(String message) {
super(message);
}
public StaleAddressException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2016 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
/**
* Expiration status for daemon expiration check results.
* Note that order here is important, higher ordinal statuses
* take precedent over lower ordinal statuses when aggregating
* results.
*
* File origin:
* https://github.com/gradle/gradle/blob/v6.5.1/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/server/expiry/DaemonExpirationStatus.java
*/
public enum DaemonExpirationStatus {
DO_NOT_EXPIRE,
QUIET_EXPIRE,
GRACEFUL_EXPIRE,
IMMEDIATE_EXPIRE;
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2011 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.util.List;
import static org.jboss.fuse.mvnd.common.DaemonState.Busy;
import static org.jboss.fuse.mvnd.common.DaemonState.Idle;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/registry/DaemonInfo.java
*/
public class DaemonInfo {
private final String uid;
private final String javaHome;
private final String mavenHome;
private final int pid;
private final int address;
private final int idleTimeout;
private final String locale;
private final List<String> options;
private final DaemonState state;
private final long lastIdle;
private final long lastBusy;
public DaemonInfo(String uid, String javaHome, String mavenHome,
int pid, int address, int idleTimeout,
String locale, List<String> options,
DaemonState state, long lastIdle, long lastBusy) {
this.uid = uid;
this.javaHome = javaHome;
this.mavenHome = mavenHome;
this.pid = pid;
this.address = address;
this.idleTimeout = idleTimeout;
this.locale = locale;
this.options = options;
this.state = state;
this.lastIdle = lastIdle;
this.lastBusy = lastBusy;
}
public String getUid() {
return uid;
}
public String getJavaHome() {
return javaHome;
}
public String getMavenHome() {
return mavenHome;
}
public int getPid() {
return pid;
}
public int getAddress() {
return address;
}
public int getIdleTimeout() {
return idleTimeout;
}
public String getLocale() {
return locale;
}
public List<String> getOptions() {
return options;
}
public DaemonState getState() {
return state;
}
public long getLastIdle() {
return lastIdle;
}
public long getLastBusy() {
return lastBusy;
}
public DaemonInfo withState(DaemonState state) {
long lb, li;
if (this.state == Idle && state == Busy) {
li = lastIdle;
lb = System.currentTimeMillis();
} else if (this.state == Busy && state == Idle) {
li = System.currentTimeMillis();
lb = lastBusy;
} else {
li = lastIdle;
lb = lastBusy;
}
return new DaemonInfo(uid, javaHome, mavenHome, pid, address,
idleTimeout, locale, options, state, li, lb);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DaemonInfo{uid=").append(uid);
appendNonKeyFields(sb);
return sb.append('}').toString();
}
public StringBuilder appendNonKeyFields(StringBuilder sb) {
return sb.append("javaHome=").append(javaHome)
.append(", options=").append(options)
.append(", mavenHome=").append(mavenHome)
.append(", pid=").append(pid)
.append(", address=").append(address)
.append(", idleTimeout=").append(idleTimeout)
.append(", locale=").append(locale)
.append(", state=").append(state)
.append(", lastIdle=").append(lastIdle)
.append(", lastBusy=").append(lastBusy);
}
}

View File

@@ -0,0 +1,538 @@
/*
* Copyright 2011 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Unsafe;
import sun.nio.ch.DirectBuffer;
import static org.jboss.fuse.mvnd.common.DaemonState.Canceled;
import static org.jboss.fuse.mvnd.common.DaemonState.Idle;
/**
* Access to daemon registry files. Useful also for testing.
*
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/registry/DaemonRegistry.java
*/
public class DaemonRegistry implements AutoCloseable {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonRegistry.class);
private static final int MAX_LENGTH = 32768;
private final Path registryFile;
private final Lock lock = new ReentrantLock();
private final FileChannel channel;
private final MappedByteBuffer buffer;
private long seq;
private final Map<String, DaemonInfo> infosMap = new HashMap<>();
private final List<DaemonStopEvent> stopEvents = new ArrayList<>();
public DaemonRegistry(Path registryFile) {
this.registryFile = registryFile;
try {
if (!Files.isRegularFile(registryFile)) {
if (!Files.isDirectory(registryFile.getParent())) {
Files.createDirectories(registryFile.getParent());
}
}
channel = FileChannel.open(registryFile,
StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, MAX_LENGTH);
} catch (IOException e) {
throw new DaemonException(e);
}
}
public void close() {
try {
channel.close();
} catch (IOException e) {
throw new DaemonException("Error closing registry", e);
}
}
public Path getRegistryFile() {
return registryFile;
}
public DaemonInfo get(String uid) {
lock.lock();
try {
read();
return infosMap.get(uid);
} finally {
lock.unlock();
}
}
public List<DaemonInfo> getAll() {
lock.lock();
try {
read();
return new ArrayList<>(infosMap.values());
} finally {
lock.unlock();
}
}
public List<DaemonInfo> getIdle() {
lock.lock();
try {
read();
return infosMap.values().stream()
.filter(di -> di.getState() == Idle)
.collect(Collectors.toList());
} finally {
lock.unlock();
}
}
public List<DaemonInfo> getNotIdle() {
lock.lock();
try {
read();
return infosMap.values().stream()
.filter(di -> di.getState() != Idle)
.collect(Collectors.toList());
} finally {
lock.unlock();
}
}
public List<DaemonInfo> getCanceled() {
lock.lock();
try {
read();
return infosMap.values().stream()
.filter(di -> di.getState() == Canceled)
.collect(Collectors.toList());
} finally {
lock.unlock();
}
}
public void remove(final String uid) {
lock.lock();
LOGGER.debug("Removing daemon uid: {}", uid);
try {
update(() -> infosMap.remove(uid));
} finally {
lock.unlock();
}
}
public void markState(final String uid, final DaemonState state) {
lock.lock();
LOGGER.debug("Marking busy by uid: {}", uid);
try {
update(() -> infosMap.computeIfPresent(uid, (id, di) -> di.withState(state)));
} finally {
lock.unlock();
}
}
public void storeStopEvent(final DaemonStopEvent stopEvent) {
lock.lock();
LOGGER.debug("Storing daemon stop event with timestamp {}", stopEvent.getTimestamp());
try {
update(() -> stopEvents.add(stopEvent));
} finally {
lock.unlock();
}
}
public List<DaemonStopEvent> getStopEvents() {
lock.lock();
LOGGER.debug("Getting daemon stop events");
try {
read();
return new ArrayList<>(stopEvents);
} finally {
lock.unlock();
}
}
public void removeStopEvents(final Collection<DaemonStopEvent> events) {
lock.lock();
LOGGER.info("Removing {} daemon stop events from registry", events.size());
try {
update(() -> stopEvents.removeAll(events));
} finally {
lock.unlock();
}
}
public void store(final DaemonInfo info) {
lock.lock();
LOGGER.debug("Storing daemon {}", info);
try {
update(() -> infosMap.put(info.getUid(), info));
} finally {
lock.unlock();
}
}
private static final long OFFSET_LOCK = 0;
private static final long OFFSET_SEQ = OFFSET_LOCK + Long.BYTES;
private static final long OFFSET_DATA = OFFSET_SEQ + Long.BYTES;
private void read() {
doUpdate(null);
}
private void update(Runnable updater) {
doUpdate(updater);
}
private void doUpdate(Runnable updater) {
if (!Files.isReadable(getRegistryFile())) {
throw new DaemonException("Registry became unaccessible");
}
try {
busyLockLong(OFFSET_LOCK);
try {
long newSeq = readLong(OFFSET_SEQ);
if (newSeq != seq) {
seq = newSeq;
BufferCaster.cast(buffer).position((int) OFFSET_DATA);
infosMap.clear();
int nb = buffer.getInt();
for (int i = 0; i < nb; i++) {
String uid = readString();
String javaHome = readString();
String mavenHome = readString();
int pid = buffer.getInt();
int address = buffer.getInt();
int idle = buffer.getInt();
String locale = readString();
List<String> opts = new ArrayList<>();
int nbOpts = buffer.getInt();
for (int j = 0; j < nbOpts; j++) {
opts.add(readString());
}
DaemonState state = DaemonState.values()[buffer.get()];
long lastIdle = buffer.getLong();
long lastBusy = buffer.getLong();
DaemonInfo di = new DaemonInfo(uid, javaHome, mavenHome, pid, address, idle, locale, opts, state,
lastIdle, lastBusy);
infosMap.putIfAbsent(di.getUid(), di);
}
stopEvents.clear();
nb = buffer.getInt();
for (int i = 0; i < nb; i++) {
String uid = readString();
long date = buffer.getLong();
int ord = buffer.get();
DaemonExpirationStatus des = ord >= 0 ? DaemonExpirationStatus.values()[ord] : null;
String reason = readString();
DaemonStopEvent se = new DaemonStopEvent(uid, date, des, reason);
stopEvents.add(se);
}
}
if (updater != null) {
updater.run();
writeLong(OFFSET_SEQ, ++seq);
BufferCaster.cast(buffer).position((int) OFFSET_DATA);
buffer.putInt(infosMap.size());
for (DaemonInfo di : infosMap.values()) {
writeString(di.getUid());
writeString(di.getJavaHome());
writeString(di.getMavenHome());
buffer.putInt(di.getPid());
buffer.putInt(di.getAddress());
buffer.putInt(di.getIdleTimeout());
writeString(di.getLocale());
buffer.putInt(di.getOptions().size());
for (String opt : di.getOptions()) {
writeString(opt);
}
buffer.put((byte) di.getState().ordinal());
buffer.putLong(di.getLastIdle());
buffer.putLong(di.getLastBusy());
}
buffer.putInt(stopEvents.size());
for (DaemonStopEvent dse : stopEvents) {
writeString(dse.getUid());
buffer.putLong(dse.getTimestamp());
buffer.put((byte) (dse.getStatus() == null ? -1 : dse.getStatus().ordinal()));
writeString(dse.getReason());
}
}
} finally {
unlockLong(OFFSET_LOCK);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private static final int PROCESS_ID = getProcessId0();
private static int getProcessId0() {
String pid = null;
final File self = new File("/proc/self");
try {
if (self.exists()) {
pid = self.getCanonicalFile().getName();
}
} catch (IOException ignored) {
}
if (pid == null) {
pid = ManagementFactory.getRuntimeMXBean().getName().split("@", 0)[0];
}
if (pid == null) {
int rpid = new Random().nextInt(1 << 16);
LOGGER.warn("Unable to determine PID, picked a random number=" + rpid);
return rpid;
} else {
return Integer.parseInt(pid);
}
}
private static long uniqueTid() {
// Assume 48 bit for 16 to 24-bit process id and 16 million threads from the start.
return ((long) getProcessId() << 24) | currentThread().getId();
}
public static int getProcessId() {
return PROCESS_ID;
}
private static Thread currentThread() {
return Thread.currentThread();
}
static final int SLEEP_THRESHOLD = 20 * 1000 * 1000;
static final long BUSY_LOCK_LIMIT = 20L * 1000 * 1000 * 1000;
public void busyLockLong(long offset) throws InterruptedException, IllegalStateException {
boolean success = tryLockNanosLong(offset, BUSY_LOCK_LIMIT);
if (!success)
if (currentThread().isInterrupted())
throw new InterruptedException();
else
throw new IllegalStateException("Failed to acquire lock after " + BUSY_LOCK_LIMIT / 1e9 + " seconds.");
}
public void unlockLong(long offset) throws IllegalMonitorStateException {
long id = uniqueTid();
long firstValue = (1L << 48) | id;
if (compareAndSwapLong(offset, firstValue, 0))
return;
// try to check the lowId and the count.
unlockFailedLong(offset, id);
}
public void resetLockLong(long offset) {
writeOrderedLong(offset, 0L);
}
public boolean tryLockLong(long offset) {
long id = uniqueTid();
return tryLockNanos8a(offset, id);
}
public boolean tryLockNanosLong(long offset, long nanos) {
long id = uniqueTid();
int limit = nanos <= 10000 ? (int) nanos / 10 : 1000;
for (int i = 0; i < limit; i++)
if (tryLockNanos8a(offset, id))
return true;
if (nanos <= 10000)
return false;
return tryLockNanosLong0(offset, nanos, id);
}
private boolean tryLockNanosLong0(long offset, long nanos, long id) {
long nanos0 = Math.min(nanos, SLEEP_THRESHOLD);
long start = System.nanoTime();
long end0 = start + nanos0 - 10000;
do {
if (tryLockNanos8a(offset, id))
return true;
} while (end0 > System.nanoTime() && !currentThread().isInterrupted());
long end = start + nanos - SLEEP_THRESHOLD;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(currentThread().getName() + ", waiting for lock");
}
try {
do {
if (tryLockNanos8a(offset, id)) {
long millis = (System.nanoTime() - start) / 1000000;
if (millis > 200) {
LOGGER.warn(currentThread().getName() +
", to obtain a lock took " +
millis / 1e3 + " seconds");
}
return true;
}
Thread.sleep(1);
} while (end > System.nanoTime());
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
return false;
}
private boolean tryLockNanos8a(long offset, long id) {
long firstValue = (1L << 48) | id;
if (compareAndSwapLong(offset, 0, firstValue))
return true;
long currentValue = readLong(offset);
long lockedId = currentValue & ((1L << 48) - 1);
if (lockedId == 0) {
int count = (int) (currentValue >>> 48);
if (count != 0)
LOGGER.warn("Lock held by threadId 0 !?");
return compareAndSwapLong(offset, currentValue, firstValue);
}
if (lockedId == id) {
if (currentValue >>> 48 == 65535)
throw new IllegalStateException("Reentered 65535 times without an unlock");
currentValue += 1L << 48;
writeOrderedLong(offset, currentValue);
return true;
}
return false;
}
private void unlockFailedLong(long offset, long id) throws IllegalMonitorStateException {
long currentValue = readLong(offset);
long holderId = currentValue & (-1L >>> 16);
if (holderId == id) {
currentValue -= 1L << 48;
writeOrderedLong(offset, currentValue);
} else if (currentValue == 0) {
throw new IllegalMonitorStateException("No thread holds this lock");
} else {
throw new IllegalMonitorStateException("Process " + ((currentValue >>> 32) & 0xFFFF)
+ " thread " + (holderId & (-1L >>> 32))
+ " holds this lock, " + (currentValue >>> 48)
+ " times, unlock from " + getProcessId()
+ " thread " + currentThread().getId());
}
}
static final Unsafe UNSAFE;
static final int BYTES_OFFSET;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
BYTES_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public boolean compareAndSwapLong(long offset, long expected, long x) {
if (buffer instanceof DirectBuffer)
return UNSAFE.compareAndSwapLong(null, ((DirectBuffer) buffer).address() + offset, expected, x);
return UNSAFE.compareAndSwapLong(buffer.array(), BYTES_OFFSET + offset, expected, x);
}
public long readVolatileLong(int offset) {
readBarrier();
return readLong(offset);
}
public long readLong(long offset) {
return buffer.getLong((int) offset);
}
public void writeOrderedLong(long offset, long v) {
writeLong(offset, v);
writeBarrier();
}
public void writeLong(long offset, long v) {
buffer.putLong((int) offset, v);
}
private AtomicBoolean barrier;
private void readBarrier() {
if (barrier == null)
barrier = new AtomicBoolean();
barrier.get();
}
private void writeBarrier() {
if (barrier == null)
barrier = new AtomicBoolean();
barrier.lazySet(false);
}
private String readString() {
int sz = buffer.getShort();
if (sz == -1) {
return null;
}
if (sz < -1 || sz > 1024) {
throw new IllegalStateException("Bad string size: " + sz);
}
byte[] buf = new byte[sz];
buffer.get(buf);
return new String(buf, StandardCharsets.UTF_8);
}
private void writeString(String str) {
if (str == null) {
buffer.putShort((short) -1);
} else if (str.length() > 1024) {
throw new IllegalStateException("String too long: " + str);
} else {
byte[] buf = str.getBytes(StandardCharsets.UTF_8);
buffer.putShort((short) buf.length);
buffer.put(buf);
}
}
public String toString() {
return String.format("PersistentDaemonRegistry[file=%s]", registryFile);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
/**
* File origin
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/server/api/DaemonStateControl.java
*/
public enum DaemonState {
Idle,
Busy,
Canceled,
StopRequested,
Stopped,
Broken
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2016 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Date;
import java.util.Objects;
/**
* Information regarding when and why a daemon was stopped.
*
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/launcher/src/main/java/org/gradle/launcher/daemon/registry/DaemonStopEvent.java
*/
public class DaemonStopEvent implements Serializable {
private final String uid;
private final long timestamp;
private final DaemonExpirationStatus status;
private final String reason;
public DaemonStopEvent(String uid, long timestamp, DaemonExpirationStatus status, String reason) {
this.uid = uid;
this.timestamp = timestamp;
this.status = status;
this.reason = reason != null ? reason : "";
}
public String getUid() {
return uid;
}
public long getTimestamp() {
return timestamp;
}
public DaemonExpirationStatus getStatus() {
return status;
}
public String getReason() {
return reason;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
DaemonStopEvent that = (DaemonStopEvent) o;
return Objects.equals(uid, that.uid)
&& timestamp == that.timestamp
&& status == that.status
&& Objects.equals(reason, that.reason);
}
@Override
public int hashCode() {
return Objects.hash(timestamp, uid, status, reason);
}
@Override
public String toString() {
return "DaemonStopEvent{"
+ "uid=" + uid
+ ", timestamp=" + DateFormat.getDateTimeInstance().format(new Date(timestamp))
+ ", status=" + status
+ ", reason=" + reason
+ "}";
}
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Collects system properties and environment variables used by mvnd client or server.
*/
public enum Environment {
LOGBACK_CONFIGURATION_FILE("logback.configurationFile", null),
JAVA_HOME("java.home", "JAVA_HOME"),
MVND_HOME("mvnd.home", "MVND_HOME"),
MAVEN_REPO_LOCAL("maven.repo.local", null),
MAVEN_MULTIMODULE_PROJECT_DIRECTORY("maven.multiModuleProjectDirectory", null),
MVND_PROPERTIES_PATH("mvnd.properties.path", "MVND_PROPERTIES_PATH"),
MVND_DIST_URI("mvnd.dist.uri", "MVND_DIST_URI"),
DAEMON_DEBUG("daemon.debug", null),
DAEMON_IDLE_TIMEOUT("daemon.idleTimeout", null),
DAEMON_UID("daemon.uid", null);
static Properties properties = System.getProperties();
static Map<String, String> env = System.getenv();
private final String property;
private final String environmentVariable;
Environment(String property, String environmentVariable) {
this.property = property;
this.environmentVariable = environmentVariable;
}
public Environment.EnvValue systemProperty() {
return new EnvValue(this, systemPropertySource());
}
public Environment.EnvValue commandLineProperty(Supplier<Properties> commandLineProperties) {
return new EnvValue(this, new ValueSource(
description -> description.append("command line property ").append(property),
() -> commandLineProperties.get().getProperty(property)));
}
public Environment.EnvValue environmentVariable() {
return new EnvValue(this, environmentVariableSource());
}
public String asCommandLineProperty(String value) {
return "-D" + property + "=" + value;
}
public boolean hasCommandLineProperty(Collection<String> args) {
final String prefix = "-D" + property + "=";
return args.stream().anyMatch(s -> s.startsWith(prefix));
}
public static Path findJavaHome(Supplier<Properties> mvndProperties, Path mvndPropertiesPath) {
final Path result = JAVA_HOME
.environmentVariable()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orSystemProperty()
.orFail()
.asPath();
try {
return result
.toRealPath();
} catch (IOException e) {
throw new RuntimeException("Could not get a real path from path " + result);
}
}
public static Path findMvndPropertiesPath() {
return MVND_PROPERTIES_PATH
.environmentVariable()
.orSystemProperty()
.orDefault(() -> System.getProperty("user.home") + "/.m2/mvnd.properties")
.asPath()
.toAbsolutePath().normalize();
}
public static Path findMavenHome(Supplier<Properties> mvndProperties, Path mvndPropertiesPath) {
return findBasicMavenHome()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orFail()
.asPath()
.toAbsolutePath().normalize();
}
public static EnvValue findBasicMavenHome() {
return MVND_HOME
.environmentVariable()
.orSystemProperty();
}
public static Path findMultiModuleProjectDirectory(Path pwd) {
return MAVEN_MULTIMODULE_PROJECT_DIRECTORY
.systemProperty()
.orDefault(() -> findDefaultMultimoduleProjectDirectory(pwd))
.asPath()
.toAbsolutePath().normalize();
}
public static String findDefaultMultimoduleProjectDirectory(Path pwd) {
Path dir = pwd;
do {
if (Files.isDirectory(dir.resolve(".mvn"))) {
return dir.toString();
}
dir = dir.getParent();
} while (dir != null);
/*
* Return pwd if .mvn directory was not found in the hierarchy.
* Maven does the same thing in mvn shell script's find_maven_basedir()
* and find_file_argument_basedir() routines
*/
return pwd.toString();
}
public static Path findLogbackConfigurationPath(Supplier<Properties> mvndProperties, Path mvndPropertiesPath,
Path mvndHome) {
return LOGBACK_CONFIGURATION_FILE
.systemProperty()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orDefault(() -> mvndHome.resolve("conf/logging/logback.xml").toString())
.orFail()
.asPath();
}
public static boolean isNative() {
return "executable".equals(System.getProperty("org.graalvm.nativeimage.kind"));
}
private Environment.ValueSource systemPropertySource() {
if (property == null) {
throw new IllegalStateException("Cannot use " + Environment.class.getName() + " for getting a system property");
}
return new ValueSource(
description -> description.append("system property ").append(property),
() -> properties.getProperty(property));
}
private Environment.ValueSource environmentVariableSource() {
if (environmentVariable == null) {
throw new IllegalStateException(
"Cannot use " + Environment.class.getName() + "." + name() + " for getting an environment variable");
}
return new ValueSource(
description -> description.append("environment variable ").append(environmentVariable),
() -> env.get(environmentVariable));
}
/**
* A source of an environment value with a description capability.
*/
public static class ValueSource {
final Function<StringBuilder, StringBuilder> descriptionFunction;
final Supplier<String> valueSupplier;
public ValueSource(Function<StringBuilder, StringBuilder> descriptionFunction, Supplier<String> valueSupplier) {
this.descriptionFunction = descriptionFunction;
this.valueSupplier = valueSupplier;
}
/** Mostly for debugging */
@Override
public String toString() {
return descriptionFunction.apply(new StringBuilder()).toString();
}
}
/**
* A chained lazy environment value.
*/
public static class EnvValue {
private final Environment envKey;
private final Environment.ValueSource valueSource;
protected Environment.EnvValue previous;
public EnvValue(Environment envKey, Environment.ValueSource valueSource) {
this.previous = null;
this.envKey = envKey;
this.valueSource = valueSource;
}
public EnvValue(Environment.EnvValue previous, Environment envKey, Environment.ValueSource valueSource) {
this.previous = previous;
this.envKey = envKey;
this.valueSource = valueSource;
}
public Environment.EnvValue orSystemProperty() {
return new EnvValue(this, envKey, envKey.systemPropertySource());
}
public Environment.EnvValue orLocalProperty(Supplier<Properties> localProperties, Path localPropertiesPath) {
return new EnvValue(this, envKey, new ValueSource(
description -> description.append("property ").append(envKey.property).append(" in ")
.append(localPropertiesPath),
() -> localProperties.get().getProperty(envKey.property)));
}
public Environment.EnvValue orEnvironmentVariable() {
return new EnvValue(this, envKey, envKey.environmentVariableSource());
}
public EnvValue or(ValueSource source) {
return new EnvValue(this, envKey, source);
}
public Environment.EnvValue orDefault(Supplier<String> defaultSupplier) {
return new EnvValue(this, envKey,
new ValueSource(sb -> sb.append("default").append(defaultSupplier.get()), defaultSupplier));
}
public Environment.EnvValue orFail() {
return new EnvValue(this, envKey, new ValueSource(sb -> sb, () -> {
final StringBuilder sb = new StringBuilder("Could not get value for ")
.append(Environment.class.getSimpleName())
.append(".").append(envKey.name()).append(" from any of the following sources: ");
/*
* Compose the description functions to invert the order thus getting the resolution order in the
* message
*/
Function<StringBuilder, StringBuilder> description = (s -> s);
EnvValue val = this;
while (val != null) {
description = description.compose(val.valueSource.descriptionFunction);
val = val.previous;
if (val != null) {
description = description.compose(s -> s.append(", "));
}
}
description.apply(sb);
throw new IllegalStateException(sb.toString());
}));
}
String get() {
if (previous != null) {
final String result = previous.get();
if (result != null) {
return result;
}
}
return valueSource.valueSupplier.get();
}
public String asString() {
return get();
}
public Optional<String> asOptional() {
return Optional.ofNullable(get());
}
public Path asPath() {
final String result = get();
return result == null ? null : Paths.get(result);
}
public boolean asBoolean() {
return Boolean.parseBoolean(get());
}
public int asInt() {
return Integer.parseInt(get());
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import java.util.function.Supplier;
public class Layout {
private static Layout ENV_INSTANCE;
private final Path mavenHome;
private final Path userDir;
private final Path multiModuleProjectDirectory;
private final Path mvndPropertiesPath;
public Layout(Path mvndPropertiesPath, Path mavenHome, Path userDir, Path multiModuleProjectDirectory) {
super();
this.mvndPropertiesPath = mvndPropertiesPath;
this.mavenHome = mavenHome;
this.userDir = userDir;
this.multiModuleProjectDirectory = multiModuleProjectDirectory;
}
public Path mavenHome() {
return mavenHome;
}
public Path userDir() {
return userDir;
}
public Path registry() {
return mavenHome.resolve("daemon/registry.bin");
}
public Path daemonLog(String daemon) {
return mavenHome.resolve("daemon/daemon-" + daemon + ".log");
}
public Path multiModuleProjectDirectory() {
return multiModuleProjectDirectory;
}
public Path getMvndPropertiesPath() {
return mvndPropertiesPath;
}
public static Layout getEnvInstance() {
if (ENV_INSTANCE == null) {
final Path mvndPropertiesPath = Environment.findMvndPropertiesPath();
final Supplier<Properties> mvndProperties = lazyMvndProperties(mvndPropertiesPath);
final Path pwd = Paths.get(".").toAbsolutePath().normalize();
ENV_INSTANCE = new Layout(
mvndPropertiesPath,
Environment.findBasicMavenHome()
.orLocalProperty(mvndProperties, mvndPropertiesPath)
.orFail()
.asPath()
.toAbsolutePath().normalize(),
pwd,
Environment.findMultiModuleProjectDirectory(pwd));
}
return ENV_INSTANCE;
}
public static Supplier<Properties> lazyMvndProperties(Path mvndPropertiesPath) {
return new Supplier<Properties>() {
private volatile Properties properties;
@Override
public Properties get() {
Properties result = this.properties;
if (result == null) {
result = new Properties();
if (Files.exists(mvndPropertiesPath)) {
try (InputStream in = Files.newInputStream(mvndPropertiesPath)) {
result.load(in);
} catch (IOException e) {
throw new RuntimeException("Could not read " + mvndPropertiesPath);
}
}
this.properties = result;
}
return result;
}
};
}
@Override
public String toString() {
return "Layout [mavenHome=" + mavenHome + ", userDir=" + userDir + ", multiModuleProjectDirectory="
+ multiModuleProjectDirectory + "]";
}
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UTFDataFormatException;
import java.util.ArrayList;
import java.util.List;
public abstract class Message {
final long timestamp = System.nanoTime();
public long timestamp() {
return timestamp;
}
public static class BuildRequest extends Message {
final List<String> args;
final String workingDir;
final String projectDir;
public BuildRequest(List<String> args, String workingDir, String projectDir) {
this.args = args;
this.workingDir = workingDir;
this.projectDir = projectDir;
}
public List<String> getArgs() {
return args;
}
public String getWorkingDir() {
return workingDir;
}
public String getProjectDir() {
return projectDir;
}
@Override
public String toString() {
return "BuildRequest{" +
"args=" + args +
", workingDir='" + workingDir + '\'' +
", projectDir='" + projectDir + '\'' +
'}';
}
}
public static class BuildException extends Message {
final String message;
final String className;
final String stackTrace;
public BuildException(Throwable t) {
this(t.getMessage(), t.getClass().getName(), getStackTrace(t));
}
static String getStackTrace(Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw, true));
return sw.toString();
}
public BuildException(String message, String className, String stackTrace) {
this.message = message;
this.className = className;
this.stackTrace = stackTrace;
}
public String getMessage() {
return message;
}
public String getClassName() {
return className;
}
public String getStackTrace() {
return stackTrace;
}
@Override
public String toString() {
return "BuildException{" +
"message='" + message + '\'' +
", className='" + className + '\'' +
", stackTrace='" + stackTrace + '\'' +
'}';
}
}
public static class BuildEvent extends Message {
public enum Type {
BuildStarted, BuildStopped, ProjectStarted, ProjectStopped, MojoStarted, MojoStopped
}
final Type type;
final String projectId;
final String display;
public BuildEvent(Type type, String projectId, String display) {
this.type = type;
this.projectId = projectId;
this.display = display;
}
public Type getType() {
return type;
}
public String getProjectId() {
return projectId;
}
public String getDisplay() {
return display;
}
@Override
public String toString() {
return "BuildEvent{" +
"type=" + type +
", display='" + display + '\'' +
'}';
}
}
public static class BuildMessage extends Message {
final String message;
public BuildMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
@Override
public String toString() {
return "BuildMessage{" +
"message='" + message + '\'' +
'}';
}
}
public static class MessageSerializer implements Serializer<Message> {
final int BUILD_REQUEST = 0;
final int BUILD_EVENT = 1;
final int BUILD_MESSAGE = 2;
final int BUILD_EXCEPTION = 3;
@Override
public Message read(DataInputStream input) throws EOFException, Exception {
int type = input.read();
if (type == -1) {
return null;
}
switch (type) {
case BUILD_REQUEST:
return readBuildRequest(input);
case BUILD_EVENT:
return readBuildEvent(input);
case BUILD_MESSAGE:
return readBuildMessage(input);
case BUILD_EXCEPTION:
return readBuildException(input);
}
throw new IllegalStateException("Unexpected message type: " + type);
}
@Override
public void write(DataOutputStream output, Message value) throws Exception {
if (value instanceof BuildRequest) {
output.write(BUILD_REQUEST);
writeBuildRequest(output, (BuildRequest) value);
} else if (value instanceof BuildEvent) {
output.write(BUILD_EVENT);
writeBuildEvent(output, (BuildEvent) value);
} else if (value instanceof BuildMessage) {
output.write(BUILD_MESSAGE);
writeBuildMessage(output, (BuildMessage) value);
} else if (value instanceof BuildException) {
output.write(BUILD_EXCEPTION);
writeBuildException(output, (BuildException) value);
} else {
throw new IllegalStateException();
}
}
private BuildRequest readBuildRequest(DataInputStream input) throws IOException {
List<String> args = readStringList(input);
String workingDir = readUTF(input);
String projectDir = readUTF(input);
return new BuildRequest(args, workingDir, projectDir);
}
private void writeBuildRequest(DataOutputStream output, BuildRequest value) throws IOException {
writeStringList(output, value.args);
writeUTF(output, value.workingDir);
writeUTF(output, value.projectDir);
}
private BuildEvent readBuildEvent(DataInputStream input) throws IOException {
BuildEvent.Type type = BuildEvent.Type.values()[input.read()];
String projectId = readUTF(input);
String display = readUTF(input);
return new BuildEvent(type, projectId, display);
}
private void writeBuildEvent(DataOutputStream output, BuildEvent value) throws IOException {
output.write(value.type.ordinal());
writeUTF(output, value.projectId);
writeUTF(output, value.display);
}
private BuildMessage readBuildMessage(DataInputStream input) throws IOException {
String message = readUTF(input);
return new BuildMessage(message);
}
private void writeBuildMessage(DataOutputStream output, BuildMessage value) throws IOException {
writeUTF(output, value.message);
}
private BuildException readBuildException(DataInputStream input) throws IOException {
String message = readUTF(input);
String className = readUTF(input);
String stackTrace = readUTF(input);
return new BuildException(message, className, stackTrace);
}
private void writeBuildException(DataOutputStream output, BuildException value) throws IOException {
writeUTF(output, value.message);
writeUTF(output, value.className);
writeUTF(output, value.stackTrace);
}
private List<String> readStringList(DataInputStream input) throws IOException {
ArrayList<String> l = new ArrayList<>();
int nb = input.readInt();
for (int i = 0; i < nb; i++) {
l.add(readUTF(input));
}
return l;
}
private void writeStringList(DataOutputStream output, List<String> value) throws IOException {
output.writeInt(value.size());
for (String v : value) {
writeUTF(output, v);
}
}
private static final String INVALID_BYTE = "Invalid byte";
private static final int UTF_BUFS_CHAR_CNT = 256;
private static final int UTF_BUFS_BYTE_CNT = UTF_BUFS_CHAR_CNT * 3;
final byte[] byteBuf = new byte[UTF_BUFS_BYTE_CNT];
String readUTF(DataInputStream input) throws IOException {
int len = input.readInt();
final char[] chars = new char[len];
int i = 0, cnt = 0, charIdx = 0;
while (charIdx < len) {
if (i == cnt) {
cnt = input.read(byteBuf, 0, Math.min(UTF_BUFS_BYTE_CNT, len - charIdx));
if (cnt < 0) {
throw new EOFException();
}
i = 0;
}
final int a = byteBuf[i++] & 0xff;
if (a < 0x80) {
// low bit clear
chars[charIdx++] = (char) a;
} else if (a < 0xc0) {
throw new UTFDataFormatException(INVALID_BYTE);
} else if (a < 0xe0) {
if (i == cnt) {
cnt = input.read(byteBuf, 0, Math.min(UTF_BUFS_BYTE_CNT, len - charIdx));
if (cnt < 0) {
throw new EOFException();
}
i = 0;
}
final int b = byteBuf[i++] & 0xff;
if ((b & 0xc0) != 0x80) {
throw new UTFDataFormatException(INVALID_BYTE);
}
chars[charIdx++] = (char) ((a & 0x1f) << 6 | b & 0x3f);
} else if (a < 0xf0) {
if (i == cnt) {
cnt = input.read(byteBuf, 0, Math.min(UTF_BUFS_BYTE_CNT, len - charIdx));
if (cnt < 0) {
throw new EOFException();
}
i = 0;
}
final int b = byteBuf[i++] & 0xff;
if ((b & 0xc0) != 0x80) {
throw new UTFDataFormatException(INVALID_BYTE);
}
if (i == cnt) {
cnt = input.read(byteBuf, 0, Math.min(UTF_BUFS_BYTE_CNT, len - charIdx));
if (cnt < 0) {
throw new EOFException();
}
i = 0;
}
final int c = byteBuf[i++] & 0xff;
if ((c & 0xc0) != 0x80) {
throw new UTFDataFormatException(INVALID_BYTE);
}
chars[charIdx++] = (char) ((a & 0x0f) << 12 | (b & 0x3f) << 6 | c & 0x3f);
} else {
throw new UTFDataFormatException(INVALID_BYTE);
}
}
return String.valueOf(chars);
}
void writeUTF(DataOutputStream output, String s) throws IOException {
final int length = s.length();
output.writeInt(length);
int strIdx = 0;
int byteIdx = 0;
while (strIdx < length) {
final char c = s.charAt(strIdx++);
if (c > 0 && c <= 0x7f) {
byteBuf[byteIdx++] = (byte) c;
} else if (c <= 0x07ff) {
byteBuf[byteIdx++] = (byte) (0xc0 | 0x1f & c >> 6);
byteBuf[byteIdx++] = (byte) (0x80 | 0x3f & c);
} else {
byteBuf[byteIdx++] = (byte) (0xe0 | 0x0f & c >> 12);
byteBuf[byteIdx++] = (byte) (0x80 | 0x3f & c >> 6);
byteBuf[byteIdx++] = (byte) (0x80 | 0x3f & c);
}
if (byteIdx > UTF_BUFS_BYTE_CNT - 4) {
output.write(byteBuf, 0, byteIdx);
byteIdx = 0;
}
}
if (byteIdx > 0) {
output.write(byteBuf, 0, byteIdx);
}
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2009 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
/**
* File origin:
* https://github.com/gradle/gradle/blob/v5.6.2/subprojects/messaging/src/main/java/org/gradle/internal/serialize/Serializer.java
*/
public interface Serializer<T> {
/**
* Reads the next object from the given stream. The implementation must not perform any buffering, so that it reads only
* those bytes from the input stream that are
* required to deserialize the next object.
*
* @throws EOFException When the next object cannot be fully read due to reaching the end of stream.
*/
T read(DataInputStream input) throws EOFException, Exception;
/**
* Writes the given object to the given stream. The implementation must not perform any buffering.
*/
void write(DataOutputStream output, T value) throws Exception;
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
public class ServerMain {
public static void main(String[] args) throws Exception {
final String uidStr = Environment.DAEMON_UID.systemProperty().orFail().asString();
final Path mavenHome = Environment.MVND_HOME.systemProperty().orFail().asPath();
URL[] classpath = Stream.concat(
Stream.concat(Files.list(mavenHome.resolve("lib/ext")),
Files.list(mavenHome.resolve("lib")))
.filter(p -> p.getFileName().toString().endsWith(".jar"))
.filter(Files::isRegularFile),
Stream.of(mavenHome.resolve("conf"), mavenHome.resolve("conf/logging")))
.map(Path::normalize)
.map(Path::toUri)
.map(uri -> {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
ClassLoader loader = new URLClassLoader(classpath, null) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return ServerMain.class.getClassLoader().loadClass(name);
}
}
};
Thread.currentThread().setContextClassLoader(loader);
Class<?> clazz = loader.loadClass("org.jboss.fuse.mvnd.daemon.Server");
try (AutoCloseable server = (AutoCloseable) clazz.getConstructor(String.class).newInstance(uidStr)) {
((Runnable) server).run();
}
}
}

View File

@@ -0,0 +1,23 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
buildNumber=${buildNumber}
timestamp=${timestamp}
version=${project.version}
distributionId=${distributionId}
distributionShortName=${distributionShortName}
distributionName=${distributionName}

View File

@@ -0,0 +1,5 @@
# An absolute path to your Maven Daemon installation
mvnd.home = %s
# java.home is optional if you have JAVA_HOME environment variable set
%s

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class DaemonRegistryTest {
@Test
public void testReadWrite() throws IOException {
Path temp = File.createTempFile("reg", ".data").toPath();
DaemonRegistry reg1 = new DaemonRegistry(temp);
DaemonRegistry reg2 = new DaemonRegistry(temp);
assertNotNull(reg1.getAll());
assertEquals(0, reg1.getAll().size());
assertNotNull(reg2.getAll());
assertEquals(0, reg2.getAll().size());
byte[] token = new byte[16];
new Random().nextBytes(token);
reg1.store(new DaemonInfo("the-uid", "/java/home/",
"/data/reg/", 0x12345678, 7502, 65536,
Locale.getDefault().toLanguageTag(), Arrays.asList("-Xmx"),
DaemonState.Idle, System.currentTimeMillis(), System.currentTimeMillis()));
assertNotNull(reg1.getAll());
assertEquals(1, reg1.getAll().size());
assertNotNull(reg2.getAll());
assertEquals(1, reg2.getAll().size());
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2019 the original author or authors.
*
* 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 org.jboss.fuse.mvnd.common;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class EnvironmentTest {
@Test
void prop() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.props("mvnd.home", "/maven/home/prop");
Assertions.assertEquals("/maven/home/prop", Environment.MVND_HOME.systemProperty().asString());
}
}
@Test
void env() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.env("MVND_HOME", "/maven/home/env");
Assertions.assertEquals("/maven/home/env", Environment.MVND_HOME.environmentVariable().asString());
}
}
@Test
void localProps() {
try (EnvironmentResource env = new EnvironmentResource()) {
final Properties localProps = new Properties();
localProps.put("mvnd.home", "/maven/home/local");
Assertions.assertEquals(Paths.get("/maven/home/local"),
Environment.MVND_HOME
.environmentVariable()
.orSystemProperty()
.orLocalProperty(() -> localProps, Paths.get("/local/properties"))
.orFail()
.asPath());
}
}
@Test
void envBeforeProp() {
try (EnvironmentResource env = new EnvironmentResource()) {
env.props("mvnd.home", "/maven/home/prop");
env.env("MVND_HOME", "/maven/home/env");
Assertions.assertEquals("/maven/home/env",
Environment.MVND_HOME
.environmentVariable()
.orSystemProperty()
.asString());
}
}
@Test
void fail() {
try (EnvironmentResource env = new EnvironmentResource()) {
try {
Assertions.assertEquals("/maven/home/env",
Environment.MVND_HOME
.environmentVariable()
.orSystemProperty()
.orFail()
.asString());
Assertions.fail("IllegalStateException expected");
} catch (IllegalStateException e) {
Assertions.assertEquals(
"Could not get value for Environment.MVND_HOME from any of the following sources: environment variable MVND_HOME, system property mvnd.home",
e.getMessage());
}
}
}
static class EnvironmentResource implements AutoCloseable {
private final Properties props = new Properties();
private final Map<String, String> env = new HashMap<>();
public EnvironmentResource() {
Environment.env = env;
Environment.properties = props;
}
public void props(String... props) {
int i = 0;
while (i < props.length) {
this.props.setProperty(props[i++], props[i++]);
}
}
public void env(String... env) {
int i = 0;
while (i < env.length) {
this.env.put(env[i++], env[i++]);
}
}
@Override
public void close() {
Environment.env = System.getenv();
Environment.properties = System.getProperties();
}
}
}