mirror of
https://github.com/apache/maven-mvnd.git
synced 2025-09-10 04:59:54 +00:00
Move client classes into a separate module
This commit is contained in:
62
client/pom.xml
Normal file
62
client/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<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-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>mvnd-client</artifactId>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
<name>Maven Daemon - Client</name>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</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.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>
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.nio.Buffer;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
242
client/src/main/java/org/jboss/fuse/mvnd/client/Client.java
Normal file
242
client/src/main/java/org/jboss/fuse/mvnd/client/Client.java
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.fusesource.jansi.Ansi;
|
||||
import org.jboss.fuse.mvnd.client.ClientOutput.TerminalOutput;
|
||||
import org.jboss.fuse.mvnd.client.Message.BuildEvent;
|
||||
import org.jboss.fuse.mvnd.client.Message.BuildException;
|
||||
import org.jboss.fuse.mvnd.client.Message.BuildMessage;
|
||||
import org.jboss.fuse.mvnd.client.Message.MessageSerializer;
|
||||
import org.jboss.fuse.mvnd.jpm.Process;
|
||||
import org.jboss.fuse.mvnd.jpm.ProcessImpl;
|
||||
import org.jboss.fuse.mvnd.jpm.ScriptUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Client {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);
|
||||
public static final String DAEMON_DEBUG = "daemon.debug";
|
||||
public static final String DAEMON_IDLE_TIMEOUT = "daemon.idleTimeout";
|
||||
public static final int DEFAULT_IDLE_TIMEOUT = (int) TimeUnit.HOURS.toMillis(3);
|
||||
public static final int DEFAULT_PERIODIC_CHECK_INTERVAL_MILLIS = 10 * 1000;
|
||||
public static final int CANCEL_TIMEOUT = 10 * 1000;
|
||||
private final Layout layout;
|
||||
private final Optional<ClientLayout> clientLayout;
|
||||
|
||||
public static void main(String[] argv) throws Exception {
|
||||
final List<String> args = new ArrayList<>(Arrays.asList(argv));
|
||||
|
||||
Path logFile = null;
|
||||
for (int i = 0; i < args.size() - 2; i++) {
|
||||
String arg = args.get(i);
|
||||
if ("-l".equals(arg) || "--log-file".equals(arg)) {
|
||||
logFile = Paths.get(args.get(i + 1));
|
||||
args.remove(i);
|
||||
args.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try (TerminalOutput output = new TerminalOutput(logFile)) {
|
||||
new Client(Layout.getEnvInstance(), Optional.empty()).execute(output, args);
|
||||
}
|
||||
}
|
||||
|
||||
public Client(Layout layout, Optional<ClientLayout> clientLayout) {
|
||||
this.layout = layout;
|
||||
this.clientLayout = clientLayout;
|
||||
}
|
||||
|
||||
public <O extends ClientOutput> ClientResult<O> execute(O output, String... argv) throws IOException {
|
||||
return execute(output, Arrays.asList(argv));
|
||||
}
|
||||
|
||||
public <O extends ClientOutput> ClientResult<O> execute(O output, List<String> argv) throws IOException {
|
||||
output.debug("Starting client");
|
||||
|
||||
final List<String> args = new ArrayList<>(argv);
|
||||
|
||||
// Print version if needed
|
||||
boolean version = args.contains("-v") || args.contains("-version") || args.contains("--version");
|
||||
boolean showVersion = args.contains("-V") || args.contains("--show-version");
|
||||
boolean debug = args.contains("-X") || args.contains("--debug");
|
||||
if (version || showVersion || debug) {
|
||||
Properties props = new Properties();
|
||||
try (InputStream is = Client.class.getResourceAsStream("build.properties")) {
|
||||
props.load(is);
|
||||
}
|
||||
String v = Ansi.ansi().bold().a("Maven Daemon " + props.getProperty("version")).reset().toString();
|
||||
output.log(v);
|
||||
/* Do not return, rather pass -v to the server so that the client module does not need to depend on any Maven artifacts */
|
||||
}
|
||||
|
||||
final Path javaHome = layout.javaHome();
|
||||
try (DaemonRegistry registry = new DaemonRegistry(layout.registry())) {
|
||||
boolean status = args.remove("--status");
|
||||
if (status) {
|
||||
output.log(String.format(" %36s %5s %5s %7s %s",
|
||||
"UUID", "PID", "Port", "Status", "Timestamp"));
|
||||
registry.getAll().forEach(d -> output.log(String.format(" %36s %5s %5s %7s %s",
|
||||
d.getUid(), d.getPid(), d.getAddress(), d.getState(),
|
||||
new Date(Math.max(d.getLastIdle(), d.getLastBusy())).toString())));
|
||||
return new ClientResult<O>(argv, true, output);
|
||||
}
|
||||
boolean stop = args.remove("--stop");
|
||||
if (stop) {
|
||||
DaemonInfo[] dis = registry.getAll().toArray(new DaemonInfo[0]);
|
||||
if (dis.length > 0) {
|
||||
output.log("Stopping " + dis.length + " running daemons");
|
||||
for (DaemonInfo di : dis) {
|
||||
try {
|
||||
new ProcessImpl(di.getPid()).destroy();
|
||||
} catch (IOException t) {
|
||||
System.out.println("Daemon " + di.getUid() + ": " + t.getMessage());
|
||||
} catch (Exception t) {
|
||||
System.out.println("Daemon " + di.getUid() + ": " + t);
|
||||
} finally {
|
||||
registry.remove(di.getUid());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ClientResult<O>(argv, true, output);
|
||||
}
|
||||
|
||||
setDefaultArgs(args);
|
||||
clientLayout.ifPresent(cl -> clientLayout(cl, args));
|
||||
|
||||
DaemonConnector connector = new DaemonConnector(layout, registry, this::startDaemon, new MessageSerializer());
|
||||
List<String> opts = new ArrayList<>();
|
||||
DaemonClientConnection daemon = connector.connect(new DaemonCompatibilitySpec(javaHome.toString(), opts));
|
||||
|
||||
daemon.dispatch(new Message.BuildRequest(
|
||||
args,
|
||||
layout.userDir().toString(),
|
||||
layout.multiModuleProjectDirectory().toString()));
|
||||
|
||||
while (true) {
|
||||
Message m = daemon.receive();
|
||||
if (m instanceof BuildException) {
|
||||
output.error((BuildException) m);
|
||||
return new ClientResult<O>(argv, false, output);
|
||||
} else if (m instanceof BuildEvent) {
|
||||
BuildEvent be = (BuildEvent) m;
|
||||
switch (be.getType()) {
|
||||
case BuildStarted:
|
||||
break;
|
||||
case BuildStopped:
|
||||
return new ClientResult<O>(argv, true, output);
|
||||
case ProjectStarted:
|
||||
case MojoStarted:
|
||||
case MojoStopped:
|
||||
output.projectStateChanged(be.projectId, be.display);
|
||||
break;
|
||||
case ProjectStopped:
|
||||
output.projectFinished(be.projectId);
|
||||
}
|
||||
} else if (m instanceof BuildMessage) {
|
||||
BuildMessage bm = (BuildMessage) m;
|
||||
output.log(bm.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void clientLayout(ClientLayout cl, List<String> args) {
|
||||
if (!args.stream().anyMatch(arg -> arg.equals("-s") || arg.equals("--settings"))) {
|
||||
args.add("-s");
|
||||
args.add(cl.getSettings().toString());
|
||||
}
|
||||
if (!args.stream().anyMatch(arg -> arg.startsWith("-Dmaven.repo.local"))) {
|
||||
args.add("-Dmaven.repo.local=" + cl.getLocalMavenRepository().toString());
|
||||
}
|
||||
}
|
||||
|
||||
static void setDefaultArgs(List<String> args) {
|
||||
if (!args.stream().anyMatch(arg -> arg.startsWith("-T") || arg.equals("--threads"))) {
|
||||
args.add("-T1C");
|
||||
}
|
||||
if (!args.stream().anyMatch(arg -> arg.startsWith("-b") || arg.equals("--builder"))) {
|
||||
args.add("-bsmart");
|
||||
}
|
||||
}
|
||||
|
||||
String startDaemon() {
|
||||
// DaemonParameters parms = new DaemonParameters();
|
||||
// for (String arg : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
|
||||
//
|
||||
// }
|
||||
// List<String> args = new ArrayList<>();
|
||||
// args.add(javaHome.resolve(java).toString());
|
||||
// args.addAll(parms.getEffectiveJvmArgs());
|
||||
// args.add("-cp");
|
||||
// args.add(classpath);
|
||||
|
||||
String uid = UUID.randomUUID().toString();
|
||||
Path mavenHome = layout.mavenHome();
|
||||
Path javaHome = layout.javaHome();
|
||||
Path workingDir = layout.userDir();
|
||||
String command = "";
|
||||
try {
|
||||
String url = Client.class.getClassLoader().getResource(Client.class.getName().replace('.', '/') + ".class").toString();
|
||||
String classpath = url.substring("file:jar:".length(), url.indexOf('!'));
|
||||
String java = ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java";
|
||||
List<String> args = new ArrayList<>();
|
||||
args.add("\"" + javaHome.resolve(java) + "\"");
|
||||
args.add("-classpath");
|
||||
args.add("\"" + classpath + "\"");
|
||||
if (Boolean.getBoolean(DAEMON_DEBUG)) {
|
||||
args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000");
|
||||
}
|
||||
args.add("-Dmaven.home=\"" + mavenHome + "\"");
|
||||
args.add("-Dlogback.configurationFile=logback.xml");
|
||||
args.add("-Ddaemon.uid=" + uid);
|
||||
args.add("-Xmx4g");
|
||||
final String timeout = System.getProperty(DAEMON_IDLE_TIMEOUT);
|
||||
if (timeout != null) {
|
||||
args.add("-D" + DAEMON_IDLE_TIMEOUT + "=" + timeout);
|
||||
}
|
||||
args.add("\"-Dmaven.multiModuleProjectDirectory=" + layout.multiModuleProjectDirectory().toString() + "\"");
|
||||
|
||||
args.add(ServerMain.class.getName());
|
||||
command = String.join(" ", args);
|
||||
|
||||
LOGGER.debug("Starting daemon process: uid = {}, workingDir = {}, daemonArgs: {}", uid, workingDir, command);
|
||||
Process.create(workingDir.toFile(), command);
|
||||
return uid;
|
||||
} catch (Exception e) {
|
||||
throw new DaemonException.StartException(
|
||||
String.format("Error starting daemon: uid = %s, workingDir = %s, daemonArgs: %s",
|
||||
uid, workingDir, command), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package org.jboss.fuse.mvnd.client;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Local paths relevant for the {@link Client}.
|
||||
*/
|
||||
public class ClientLayout {
|
||||
|
||||
private final Path localMavenRepository;
|
||||
private final Path settings;
|
||||
|
||||
public ClientLayout(Path localMavenRepository, Path settings) {
|
||||
super();
|
||||
this.localMavenRepository = Objects.requireNonNull(localMavenRepository, "localMavenRepository");
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
}
|
||||
|
||||
public Path getLocalMavenRepository() {
|
||||
return localMavenRepository;
|
||||
}
|
||||
|
||||
public Path getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,191 @@
|
||||
package org.jboss.fuse.mvnd.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jboss.fuse.mvnd.client.Message.BuildException;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.TerminalBuilder;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.AttributedStyle;
|
||||
import org.jline.utils.Display;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A sink for various kinds of events sent by the daemon.
|
||||
*/
|
||||
public interface ClientOutput extends AutoCloseable {
|
||||
|
||||
public void projectStateChanged(String projectId, String display);
|
||||
|
||||
public void projectFinished(String projectId);
|
||||
|
||||
public void log(String message);
|
||||
|
||||
public void error(BuildException m);
|
||||
|
||||
public void debug(String string);
|
||||
|
||||
/**
|
||||
* A terminal {@link ClientOutput} based on JLine.
|
||||
*/
|
||||
static class TerminalOutput implements ClientOutput {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TerminalOutput.class);
|
||||
|
||||
private final Terminal terminal;
|
||||
private final Display display;
|
||||
private final LinkedHashMap<String, String> projects = new LinkedHashMap<>();
|
||||
private long lastUpdate = 0;
|
||||
private final Log log;
|
||||
|
||||
public TerminalOutput(Path logFile) throws IOException {
|
||||
this.terminal = TerminalBuilder.terminal();
|
||||
this.display = new Display(terminal, false);
|
||||
this.log = logFile == null ? new ClientOutput.Log.MessageCollector(terminal)
|
||||
: new ClientOutput.Log.FileLog(logFile);
|
||||
}
|
||||
|
||||
public void projectStateChanged(String projectId, String task) {
|
||||
projects.put(projectId, task);
|
||||
update();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
// no need to refresh the display at every single step
|
||||
long curTime = System.currentTimeMillis();
|
||||
if (curTime - lastUpdate >= 10) {
|
||||
Size size = terminal.getSize();
|
||||
display.resize(size.getRows(), size.getColumns());
|
||||
List<AttributedString> lines = new ArrayList<>();
|
||||
projects.values().stream()
|
||||
.map(AttributedString::fromAnsi)
|
||||
.map(s -> s.columnSubSequence(0, size.getColumns() - 1))
|
||||
.forEachOrdered(lines::add);
|
||||
// Make sure we don't try to display more lines than the terminal height
|
||||
int rem = 0;
|
||||
while (lines.size() >= terminal.getHeight()) {
|
||||
lines.remove(0);
|
||||
rem++;
|
||||
}
|
||||
lines.add(0, new AttributedString("Building..." + (rem > 0 ? " (" + rem + " more)" : "")));
|
||||
display.update(lines, -1);
|
||||
lastUpdate = curTime;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void projectFinished(String projectId) {
|
||||
projects.remove(projectId);
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
log.accept(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
display.update(Collections.emptyList(), 0);
|
||||
LOGGER.debug("Done receiving, printing log");
|
||||
log.close();
|
||||
LOGGER.debug("Done !");
|
||||
terminal.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(BuildException error) {
|
||||
display.update(Collections.emptyList(), 0);
|
||||
final AttributedStyle s = new AttributedStyle().bold().foreground(AttributedStyle.RED);
|
||||
final String msg;
|
||||
if ("org.apache.commons.cli.UnrecognizedOptionException".equals(error.getClassName())) {
|
||||
msg = "Unable to parse command line options: " + error.getMessage();
|
||||
} else {
|
||||
msg = error.getClassName() + ": " + error.getMessage();
|
||||
}
|
||||
terminal.writer().println(new AttributedString(msg, s).toAnsi());
|
||||
terminal.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void debug(String msg) {
|
||||
LOGGER.debug(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A closeable string message consumer.
|
||||
*/
|
||||
interface Log extends Consumer<String>, AutoCloseable {
|
||||
|
||||
/**
|
||||
* A {@link Log} backed by a file.
|
||||
*/
|
||||
public static class FileLog implements Log {
|
||||
|
||||
private final Writer out;
|
||||
private Path logFile;
|
||||
|
||||
public FileLog(Path logFile) throws IOException {
|
||||
super();
|
||||
this.out = Files.newBufferedWriter(logFile, StandardCharsets.UTF_8);
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(String message) {
|
||||
try {
|
||||
out.write(message);
|
||||
out.write('\n');
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not write to " + logFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Log} that first collects all incoming messages in a {@link List} and outputs them to a JLine
|
||||
* {@link Terminal} upon {@link #close()}.
|
||||
*/
|
||||
public static class MessageCollector implements Log {
|
||||
|
||||
private final List<String> messages = new ArrayList<>();
|
||||
private final Terminal terminal;
|
||||
|
||||
public MessageCollector(Terminal terminal) {
|
||||
super();
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(String message) {
|
||||
messages.add(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
messages.forEach(terminal.writer()::println);
|
||||
terminal.flush();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package org.jboss.fuse.mvnd.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A result of a {@code mvnd} build.
|
||||
*
|
||||
* @param <O> the type of the {@link ClientOutput}.
|
||||
*/
|
||||
public class ClientResult<O extends ClientOutput> {
|
||||
|
||||
private final boolean success;
|
||||
private final O clientOutput;
|
||||
private final List<String> args;
|
||||
|
||||
public ClientResult(List<String> args, boolean success, O clientOutput) {
|
||||
super();
|
||||
this.args = new ArrayList<>(args);
|
||||
this.success = success;
|
||||
this.clientOutput = clientOutput;
|
||||
}
|
||||
|
||||
public ClientResult<O> assertSuccess() {
|
||||
if (!this.success) {
|
||||
throw new AssertionError(appendCommand(new StringBuilder("Build failed: ")));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientResult<O> assertFailure() {
|
||||
if (this.success) {
|
||||
throw new AssertionError(appendCommand(new StringBuilder("Build did not fail: ")));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public O getClientOutput() {
|
||||
return clientOutput;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
StringBuilder appendCommand(StringBuilder sb) {
|
||||
sb.append("mvnd");
|
||||
for (String arg : args) {
|
||||
sb.append(" \"").append(arg).append('"');
|
||||
}
|
||||
return sb;
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DaemonClientConnection {
|
||||
|
||||
private final static Logger LOG = LoggerFactory.getLogger(DaemonClientConnection.class);
|
||||
|
||||
private final DaemonConnection<Message> connection;
|
||||
private final DaemonInfo daemon;
|
||||
private final StaleAddressDetector staleAddressDetector;
|
||||
private boolean hasReceived;
|
||||
private final Lock dispatchLock = new ReentrantLock();
|
||||
|
||||
public DaemonClientConnection(DaemonConnection<Message> connection, DaemonInfo daemon, StaleAddressDetector staleAddressDetector) {
|
||||
this.connection = connection;
|
||||
this.daemon = daemon;
|
||||
this.staleAddressDetector = staleAddressDetector;
|
||||
}
|
||||
|
||||
public DaemonInfo getDaemon() {
|
||||
return daemon;
|
||||
}
|
||||
|
||||
public void dispatch(Message message) throws DaemonException.ConnectException {
|
||||
LOG.debug("thread {}: dispatching {}", Thread.currentThread().getId(), message.getClass());
|
||||
try {
|
||||
dispatchLock.lock();
|
||||
try {
|
||||
connection.dispatch(message);
|
||||
connection.flush();
|
||||
} finally {
|
||||
dispatchLock.unlock();
|
||||
}
|
||||
} catch (DaemonException.MessageIOException e) {
|
||||
LOG.debug("Problem dispatching message to the daemon. Performing 'on failure' operation...");
|
||||
if (!hasReceived && staleAddressDetector.maybeStaleAddress(e)) {
|
||||
throw new DaemonException.StaleAddressException("Could not dispatch a message to the daemon.", e);
|
||||
}
|
||||
throw new DaemonException.ConnectException("Could not dispatch a message to the daemon.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Message receive() throws DaemonException.ConnectException {
|
||||
try {
|
||||
return connection.receive();
|
||||
} catch (DaemonException.MessageIOException e) {
|
||||
LOG.debug("Problem receiving message to the daemon. Performing 'on failure' operation...");
|
||||
if (!hasReceived && staleAddressDetector.maybeStaleAddress(e)) {
|
||||
throw new DaemonException.StaleAddressException("Could not receive a message from the daemon.", e);
|
||||
}
|
||||
throw new DaemonException.ConnectException("Could not receive a message from the daemon.", e);
|
||||
} finally {
|
||||
hasReceived = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
LOG.debug("thread {}: connection stop", Thread.currentThread().getId());
|
||||
connection.close();
|
||||
}
|
||||
|
||||
interface StaleAddressDetector {
|
||||
/**
|
||||
* @return true if the failure should be considered due to a stale address.
|
||||
*/
|
||||
boolean maybeStaleAddress(Exception failure);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DaemonCompatibilitySpec {
|
||||
|
||||
private final String javaHome;
|
||||
private final List<String> options;
|
||||
|
||||
public DaemonCompatibilitySpec(String javaHome, List<String> options) {
|
||||
this.javaHome = javaHome;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public boolean isSatisfiedBy(DaemonInfo daemon) {
|
||||
return whyUnsatisfied(daemon) == null;
|
||||
}
|
||||
|
||||
public String whyUnsatisfied(DaemonInfo daemon) {
|
||||
if (!javaHomeMatches(daemon)) {
|
||||
return "Java home is different.\n" + description(daemon);
|
||||
} else if (!daemonOptsMatch(daemon)) {
|
||||
return "At least one daemon option is different.\n" + description(daemon);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String description(DaemonInfo context) {
|
||||
return "Wanted: " + this + "\n"
|
||||
+ "Actual: " + context + "\n";
|
||||
}
|
||||
|
||||
private boolean daemonOptsMatch(DaemonInfo daemon) {
|
||||
return daemon.getOptions().containsAll(options)
|
||||
&& daemon.getOptions().size() == options.size();
|
||||
}
|
||||
|
||||
private boolean javaHomeMatches(DaemonInfo daemon) {
|
||||
return Objects.equals(
|
||||
Paths.get(daemon.getJavaHome()).normalize(),
|
||||
Paths.get(javaHome).normalize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DaemonCompatibilitySpec{" +
|
||||
"javaHome='" + javaHome + '\'' +
|
||||
", daemonOpts=" + options +
|
||||
'}';
|
||||
}
|
||||
}
|
302
client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnection.java
Executable file
302
client/src/main/java/org/jboss/fuse/mvnd/client/DaemonConnection.java
Executable file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static java.lang.Thread.sleep;
|
||||
import static org.jboss.fuse.mvnd.client.DaemonState.Canceled;
|
||||
|
||||
public class DaemonConnector {
|
||||
|
||||
public static final int DEFAULT_CONNECT_TIMEOUT = 30000;
|
||||
public static final int CANCELED_WAIT_TIMEOUT = 3000;
|
||||
private static final int CONNECT_TIMEOUT = 10000;
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonConnector.class);
|
||||
|
||||
private final DaemonRegistry registry;
|
||||
private final Layout layout;
|
||||
private final DaemonStarter daemonStarter;
|
||||
private final Serializer<Message> serializer;
|
||||
|
||||
public DaemonConnector(Layout layout, DaemonRegistry registry, DaemonStarter daemonStarter, Serializer<Message> serializer) {
|
||||
this.layout = layout;
|
||||
this.registry = registry;
|
||||
this.daemonStarter = daemonStarter;
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
public DaemonClientConnection maybeConnect(DaemonCompatibilitySpec constraint) {
|
||||
return findConnection(getCompatibleDaemons(registry.getAll(), constraint));
|
||||
}
|
||||
|
||||
public DaemonClientConnection maybeConnect(DaemonInfo daemon) {
|
||||
try {
|
||||
return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon, true));
|
||||
} catch (DaemonException.ConnectException e) {
|
||||
LOGGER.debug("Cannot connect to daemon {} due to {}. Ignoring.", daemon, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DaemonClientConnection connect(DaemonCompatibilitySpec constraint) {
|
||||
Map<Boolean, List<DaemonInfo>> idleBusy = registry.getAll().stream()
|
||||
.collect(Collectors.groupingBy(di -> di.getState() == DaemonState.Idle));
|
||||
|
||||
final Collection<DaemonInfo> idleDaemons = idleBusy.getOrDefault(true, Collections.emptyList());
|
||||
final Collection<DaemonInfo> busyDaemons = idleBusy.getOrDefault(false, Collections.emptyList());
|
||||
|
||||
// Check to see if there are any compatible idle daemons
|
||||
DaemonClientConnection connection = connectToIdleDaemon(idleDaemons, constraint);
|
||||
if (connection != null) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Check to see if there are any compatible canceled daemons and wait to see if one becomes idle
|
||||
connection = connectToCanceledDaemon(busyDaemons, constraint);
|
||||
if (connection != null) {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// No compatible daemons available - start a new daemon
|
||||
handleStopEvents(idleDaemons, busyDaemons);
|
||||
return startDaemon(constraint);
|
||||
}
|
||||
|
||||
private void handleStopEvents(Collection<DaemonInfo> idleDaemons, Collection<DaemonInfo> busyDaemons) {
|
||||
final List<DaemonStopEvent> stopEvents = registry.getStopEvents();
|
||||
|
||||
// Clean up old stop events
|
||||
long time = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
|
||||
|
||||
List<DaemonStopEvent> oldStopEvents = stopEvents.stream()
|
||||
.filter(e -> e.getTimestamp() < time)
|
||||
.collect(Collectors.toList());
|
||||
registry.removeStopEvents(oldStopEvents);
|
||||
|
||||
final List<DaemonStopEvent> recentStopEvents = stopEvents.stream()
|
||||
.filter(e -> e.getTimestamp() >= time)
|
||||
.collect(Collectors.groupingBy(DaemonStopEvent::getUid,
|
||||
Collectors.minBy(this::compare)))
|
||||
.values()
|
||||
.stream()
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toList());
|
||||
for (DaemonStopEvent stopEvent : recentStopEvents) {
|
||||
LOGGER.info("Previous Daemon ({}) stopped at {} {}",
|
||||
stopEvent.getUid(), stopEvent.getTimestamp(), stopEvent.getReason());
|
||||
}
|
||||
|
||||
LOGGER.info(generate(busyDaemons.size(), idleDaemons.size(), recentStopEvents.size()));
|
||||
}
|
||||
|
||||
public static String generate(final int numBusy, final int numIncompatible, final int numStopped) {
|
||||
final int totalUnavailableDaemons = numBusy + numIncompatible + numStopped;
|
||||
if (totalUnavailableDaemons > 0) {
|
||||
final List<String> reasons = new ArrayList<>();
|
||||
if (numBusy > 0) {
|
||||
reasons.add(numBusy + " busy");
|
||||
}
|
||||
if (numIncompatible > 0) {
|
||||
reasons.add(numIncompatible + " incompatible");
|
||||
}
|
||||
if (numStopped > 0) {
|
||||
reasons.add(numStopped + " stopped");
|
||||
}
|
||||
return "Starting a Maven Daemon, "
|
||||
+ String.join(" and ", reasons) + " Daemon" + (totalUnavailableDaemons > 1 ? "s" : "")
|
||||
+ " could not be reused, use --status for details";
|
||||
} else {
|
||||
return "Starting a Maven Daemon (subsequent builds will be faster)";
|
||||
}
|
||||
}
|
||||
|
||||
private int compare(DaemonStopEvent event1, DaemonStopEvent event2) {
|
||||
if (event1.getStatus() != null && event2.getStatus() == null) {
|
||||
return -1;
|
||||
} else if (event1.getStatus() == null && event2.getStatus() != null) {
|
||||
return 1;
|
||||
} else if (event1.getStatus() != null && event2.getStatus() != null) {
|
||||
return event2.getStatus().compareTo(event1.getStatus());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private DaemonClientConnection connectToIdleDaemon(Collection<DaemonInfo> idleDaemons, DaemonCompatibilitySpec constraint) {
|
||||
final List<DaemonInfo> compatibleIdleDaemons = getCompatibleDaemons(idleDaemons, constraint);
|
||||
return findConnection(compatibleIdleDaemons);
|
||||
}
|
||||
|
||||
private DaemonClientConnection connectToCanceledDaemon(Collection<DaemonInfo> busyDaemons, DaemonCompatibilitySpec constraint) {
|
||||
DaemonClientConnection connection = null;
|
||||
Map<Boolean, List<DaemonInfo>> canceledBusy = busyDaemons.stream()
|
||||
.collect(Collectors.groupingBy(di -> di.getState() == Canceled));
|
||||
final Collection<DaemonInfo> compatibleCanceledDaemons = getCompatibleDaemons(
|
||||
canceledBusy.getOrDefault(true, Collections.emptyList()), constraint);
|
||||
if (!compatibleCanceledDaemons.isEmpty()) {
|
||||
LOGGER.info("Waiting for daemons with canceled builds to become available");
|
||||
long start = System.currentTimeMillis();
|
||||
while (connection == null && System.currentTimeMillis() - start < CANCELED_WAIT_TIMEOUT) {
|
||||
try {
|
||||
sleep(200);
|
||||
connection = connectToIdleDaemon(registry.getIdle(), constraint);
|
||||
} catch (InterruptedException e) {
|
||||
throw new DaemonException.InterruptedException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
private List<DaemonInfo> getCompatibleDaemons(Iterable<DaemonInfo> daemons, DaemonCompatibilitySpec constraint) {
|
||||
List<DaemonInfo> compatibleDaemons = new LinkedList<>();
|
||||
for (DaemonInfo daemon : daemons) {
|
||||
if (constraint.isSatisfiedBy(daemon)) {
|
||||
compatibleDaemons.add(daemon);
|
||||
} else {
|
||||
LOGGER.info("Found daemon {} however it does not match the desired criteria.\n"
|
||||
+ constraint.whyUnsatisfied(daemon) + "\n"
|
||||
+ " Looking for a different daemon...", daemon);
|
||||
}
|
||||
}
|
||||
return compatibleDaemons;
|
||||
}
|
||||
|
||||
private DaemonClientConnection findConnection(List<DaemonInfo> compatibleDaemons) {
|
||||
for (DaemonInfo daemon : compatibleDaemons) {
|
||||
try {
|
||||
return connectToDaemon(daemon, new CleanupOnStaleAddress(daemon, true));
|
||||
} catch (DaemonException.ConnectException e) {
|
||||
LOGGER.debug("Cannot connect to daemon {} due to {}. Trying a different daemon...", daemon, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DaemonClientConnection startDaemon(DaemonCompatibilitySpec constraint) {
|
||||
final String daemon = daemonStarter.startDaemon();
|
||||
LOGGER.debug("Started Maven daemon {}", daemon);
|
||||
long start = System.currentTimeMillis();
|
||||
do {
|
||||
DaemonClientConnection daemonConnection = connectToDaemonWithId(daemon);
|
||||
if (daemonConnection != null) {
|
||||
return daemonConnection;
|
||||
}
|
||||
try {
|
||||
sleep(200L);
|
||||
} catch (InterruptedException e) {
|
||||
throw new DaemonException.InterruptedException(e);
|
||||
}
|
||||
} while (System.currentTimeMillis() - start < DEFAULT_CONNECT_TIMEOUT);
|
||||
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout.daemonLog(daemon));
|
||||
throw new DaemonException.ConnectException("Timeout waiting to connect to the Maven daemon.\n" + diag.describe());
|
||||
}
|
||||
|
||||
private DaemonClientConnection connectToDaemonWithId(String daemon) throws DaemonException.ConnectException {
|
||||
// Look for 'our' daemon among the busy daemons - a daemon will start in busy state so that nobody else will grab it.
|
||||
DaemonInfo daemonInfo = registry.get(daemon);
|
||||
if (daemonInfo != null) {
|
||||
try {
|
||||
return connectToDaemon(daemonInfo, new CleanupOnStaleAddress(daemonInfo, false));
|
||||
} catch (DaemonException.ConnectException e) {
|
||||
DaemonDiagnostics diag = new DaemonDiagnostics(daemon, layout.daemonLog(daemon));
|
||||
throw new DaemonException.ConnectException("Could not connect to the Maven daemon.\n" + diag.describe(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DaemonClientConnection connectToDaemon(DaemonInfo daemon, DaemonClientConnection.StaleAddressDetector staleAddressDetector) throws DaemonException.ConnectException {
|
||||
LOGGER.debug("Connecting to Daemon");
|
||||
try {
|
||||
DaemonConnection<Message> connection = connect(daemon.getAddress());
|
||||
return new DaemonClientConnection(connection, daemon, staleAddressDetector);
|
||||
} catch (DaemonException.ConnectException e) {
|
||||
staleAddressDetector.maybeStaleAddress(e);
|
||||
throw e;
|
||||
} finally {
|
||||
LOGGER.debug("Connected");
|
||||
}
|
||||
}
|
||||
|
||||
private class CleanupOnStaleAddress implements DaemonClientConnection.StaleAddressDetector {
|
||||
private final DaemonInfo daemon;
|
||||
private final boolean exposeAsStale;
|
||||
|
||||
public CleanupOnStaleAddress(DaemonInfo daemon, boolean exposeAsStale) {
|
||||
this.daemon = daemon;
|
||||
this.exposeAsStale = exposeAsStale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean maybeStaleAddress(Exception failure) {
|
||||
LOGGER.info("Removing daemon from the registry due to communication failure. Daemon information: {}", daemon);
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final DaemonStopEvent stopEvent = new DaemonStopEvent(daemon.getUid(), timestamp, null, "by user or operating system");
|
||||
registry.storeStopEvent(stopEvent);
|
||||
registry.remove(daemon.getUid());
|
||||
return exposeAsStale;
|
||||
}
|
||||
}
|
||||
|
||||
public DaemonConnection<Message> connect(int port) throws DaemonException.ConnectException {
|
||||
InetSocketAddress address = new InetSocketAddress(port);
|
||||
try {
|
||||
LOGGER.debug("Trying to connect to address {}.", address);
|
||||
SocketChannel socketChannel = SocketChannel.open();
|
||||
Socket socket = socketChannel.socket();
|
||||
socket.connect(address, CONNECT_TIMEOUT);
|
||||
if (socket.getLocalSocketAddress().equals(socket.getRemoteSocketAddress())) {
|
||||
socketChannel.close();
|
||||
throw new DaemonException.ConnectException(String.format("Socket connected to itself on %s.", address));
|
||||
}
|
||||
LOGGER.debug("Connected to address {}.", socket.getRemoteSocketAddress());
|
||||
return new DaemonConnection<>(socketChannel, serializer);
|
||||
} catch (DaemonException.ConnectException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new DaemonException.ConnectException(String.format("Could not connect to server %s.", address), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@@ -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.client;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package org.jboss.fuse.mvnd.client;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public enum DaemonExpirationStatus {
|
||||
DO_NOT_EXPIRE,
|
||||
QUIET_EXPIRE,
|
||||
GRACEFUL_EXPIRE,
|
||||
IMMEDIATE_EXPIRE;
|
||||
}
|
130
client/src/main/java/org/jboss/fuse/mvnd/client/DaemonInfo.java
Normal file
130
client/src/main/java/org/jboss/fuse/mvnd/client/DaemonInfo.java
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import static org.jboss.fuse.mvnd.client.DaemonState.Busy;
|
||||
import static org.jboss.fuse.mvnd.client.DaemonState.Idle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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() {
|
||||
return "DaemonInfo{" +
|
||||
"uid='" + uid + '\'' +
|
||||
", javaHome='" + javaHome + '\'' +
|
||||
", mavenHome='" + mavenHome + '\'' +
|
||||
", pid=" + pid +
|
||||
", address=" + address +
|
||||
", idleTimeout=" + idleTimeout +
|
||||
", locale='" + locale + '\'' +
|
||||
", options=" + options +
|
||||
", state=" + state +
|
||||
", lastIdle=" + lastIdle +
|
||||
", lastBusy=" + lastBusy +
|
||||
'}';
|
||||
}
|
||||
}
|
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import static org.jboss.fuse.mvnd.client.DaemonState.Canceled;
|
||||
import static org.jboss.fuse.mvnd.client.DaemonState.Idle;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Access to daemon registry files. Useful also for testing.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
public interface DaemonStarter {
|
||||
|
||||
String startDaemon();
|
||||
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
public enum DaemonState {
|
||||
|
||||
Idle,
|
||||
Busy,
|
||||
Canceled,
|
||||
StopRequested,
|
||||
Stopped,
|
||||
Broken
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
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.
|
||||
*/
|
||||
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
|
||||
+ "}";
|
||||
}
|
||||
|
||||
}
|
77
client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java
Normal file
77
client/src/main/java/org/jboss/fuse/mvnd/client/Layout.java
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Layout {
|
||||
|
||||
private static Layout ENV_INSTANCE;
|
||||
|
||||
private final Path javaHome;
|
||||
private final Path mavenHome;
|
||||
private final Path userDir;
|
||||
private final Path multiModuleProjectDirectory;
|
||||
|
||||
public Layout(Path javaHome, Path mavenHome, Path userDir, Path multiModuleProjectDirectory) {
|
||||
super();
|
||||
this.javaHome = javaHome;
|
||||
this.mavenHome = mavenHome;
|
||||
this.userDir = userDir;
|
||||
this.multiModuleProjectDirectory = multiModuleProjectDirectory;
|
||||
}
|
||||
|
||||
public Path javaHome() {
|
||||
return javaHome;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static String getProperty(String key) {
|
||||
return Objects.requireNonNull(System.getProperty(key), "Undefined system property: " + key);
|
||||
}
|
||||
|
||||
public static Layout getEnvInstance() {
|
||||
if (ENV_INSTANCE == null) {
|
||||
ENV_INSTANCE = new Layout(Paths.get(getProperty("java.home")).toAbsolutePath().normalize(),
|
||||
Paths.get(getProperty("maven.home")).toAbsolutePath().normalize(),
|
||||
Paths.get(getProperty("user.dir")).toAbsolutePath().normalize(),
|
||||
Paths.get(getProperty("maven.multiModuleProjectDirectory")).toAbsolutePath().normalize());
|
||||
}
|
||||
return ENV_INSTANCE;
|
||||
}
|
||||
|
||||
}
|
370
client/src/main/java/org/jboss/fuse/mvnd/client/Message.java
Normal file
370
client/src/main/java/org/jboss/fuse/mvnd/client/Message.java
Normal file
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.EOFException;
|
||||
|
||||
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;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ServerMain {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String uidStr = System.getProperty("daemon.uid");
|
||||
String mavenHomeStr = System.getProperty("maven.home");
|
||||
if (uidStr == null || mavenHomeStr == null) {
|
||||
throw new IllegalStateException("The system properties 'daemon.uid' and 'maven.home' must be valid");
|
||||
}
|
||||
|
||||
Path mavenHome = Paths.get(mavenHomeStr);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
56
client/src/main/java/org/jboss/fuse/mvnd/jpm/Process.java
Normal file
56
client/src/main/java/org/jboss/fuse/mvnd/jpm/Process.java
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package org.jboss.fuse.mvnd.jpm;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Interface representing a process
|
||||
*/
|
||||
public interface Process extends Serializable {
|
||||
|
||||
/**
|
||||
* Retrieves the PID of the process
|
||||
* @return the pid
|
||||
*/
|
||||
int getPid();
|
||||
|
||||
/**
|
||||
* Check if this process is still running
|
||||
* @return <code>true</code> if the process is running
|
||||
* @throws IOException if an error occurs
|
||||
*/
|
||||
boolean isRunning() throws IOException;
|
||||
|
||||
/**
|
||||
* Destroy the process.
|
||||
*
|
||||
* @throws IOException If an error occurs.
|
||||
*/
|
||||
void destroy() throws IOException;
|
||||
|
||||
static Process create(File dir, String command) throws IOException {
|
||||
return ProcessImpl.create(dir, command);
|
||||
}
|
||||
|
||||
static Process attach(int pid) throws IOException {
|
||||
return ProcessImpl.attach(pid);
|
||||
}
|
||||
|
||||
}
|
148
client/src/main/java/org/jboss/fuse/mvnd/jpm/ProcessImpl.java
Normal file
148
client/src/main/java/org/jboss/fuse/mvnd/jpm/ProcessImpl.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package org.jboss.fuse.mvnd.jpm;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProcessImpl implements Process {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -8140632422386086507L;
|
||||
|
||||
private int pid;
|
||||
//private File input;
|
||||
//private File output;
|
||||
//private File error;
|
||||
|
||||
public ProcessImpl(int pid/*, File input, File output, File error*/) {
|
||||
this.pid = pid;
|
||||
//this.input = input;
|
||||
//this.output = output;
|
||||
//this.error = error;
|
||||
}
|
||||
|
||||
public int getPid() {
|
||||
return pid;
|
||||
}
|
||||
|
||||
public boolean isRunning() throws IOException {
|
||||
if (ScriptUtils.isWindows()) {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
props.put("${pid}", Integer.toString(pid));
|
||||
int ret = ScriptUtils.execute("running", props);
|
||||
return ret == 0;
|
||||
} else {
|
||||
try {
|
||||
java.lang.Process process = new java.lang.ProcessBuilder("ps", "-p", Integer.toString(pid)).start();
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
r.readLine(); // skip headers
|
||||
String s = r.readLine();
|
||||
boolean running = s != null && s.length() > 0;
|
||||
process.waitFor();
|
||||
return running;
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() throws IOException {
|
||||
int ret;
|
||||
if (ScriptUtils.isWindows()) {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
props.put("${pid}", Integer.toString(pid));
|
||||
ret = ScriptUtils.execute("destroy", props);
|
||||
} else {
|
||||
ret = ScriptUtils.executeProcess(new java.lang.ProcessBuilder("kill", "-9", Integer.toString(pid)));
|
||||
}
|
||||
if (ret != 0) {
|
||||
throw new IOException("Unable to destroy process, it may already be terminated");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public OutputStream getInputStream() throws FileNotFoundException {
|
||||
return new FileOutputStream(input);
|
||||
}
|
||||
|
||||
public InputStream getOutputStream() throws FileNotFoundException {
|
||||
return new FileInputStream(output);
|
||||
}
|
||||
|
||||
public InputStream getErrorStream() throws FileNotFoundException {
|
||||
return new FileInputStream(error);
|
||||
}
|
||||
*/
|
||||
|
||||
public int waitFor() throws InterruptedException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int exitValue() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static Process create(File dir, String command) throws IOException {
|
||||
//File input = File.createTempFile("jpm.", ".input");
|
||||
//File output = File.createTempFile("jpm.", ".output");
|
||||
//File error = File.createTempFile("jpm.", ".error");
|
||||
File pidFile = File.createTempFile("jpm.", ".pid");
|
||||
try {
|
||||
Map<String, String> props = new HashMap<>();
|
||||
//props.put("${in.file}", input.getCanonicalPath());
|
||||
//props.put("${out.file}", output.getCanonicalPath());
|
||||
//props.put("${err.file}", error.getCanonicalPath());
|
||||
props.put("${pid.file}", pidFile.getCanonicalPath());
|
||||
props.put("${dir}", dir != null ? dir.getCanonicalPath() : "");
|
||||
if (ScriptUtils.isWindows()) {
|
||||
command = command.replaceAll("\"", "\"\"");
|
||||
}
|
||||
props.put("${command}", command);
|
||||
int ret = ScriptUtils.execute("start", props);
|
||||
if (ret != 0) {
|
||||
throw new IOException("Unable to create process (error code: " + ret + ")");
|
||||
}
|
||||
int pid = readPid(pidFile);
|
||||
return new ProcessImpl(pid/*, input, output, error*/);
|
||||
} finally {
|
||||
pidFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static Process attach(int pid) throws IOException {
|
||||
return new ProcessImpl(pid);
|
||||
}
|
||||
|
||||
private static int readPid(File pidFile) throws IOException {
|
||||
try (InputStream is = new FileInputStream(pidFile)) {
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(is));
|
||||
String pidString = r.readLine();
|
||||
return Integer.parseInt(pidString);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
129
client/src/main/java/org/jboss/fuse/mvnd/jpm/ScriptUtils.java
Normal file
129
client/src/main/java/org/jboss/fuse/mvnd/jpm/ScriptUtils.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package org.jboss.fuse.mvnd.jpm;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class ScriptUtils {
|
||||
|
||||
public static int execute(String name, Map<String, String> props) throws IOException {
|
||||
File script = File.createTempFile("jpm.", ".script");
|
||||
try {
|
||||
if (isWindows()) {
|
||||
String res = "windows/" + name + ".vbs";
|
||||
ScriptUtils.copyFilteredResource(res, script, props);
|
||||
return executeProcess(new java.lang.ProcessBuilder("cscript",
|
||||
"/NOLOGO",
|
||||
"//E:vbs",
|
||||
script.getCanonicalPath()));
|
||||
} else {
|
||||
String res = "unix/" + name + ".sh";
|
||||
ScriptUtils.copyFilteredResource(res, script, props);
|
||||
return executeProcess(new java.lang.ProcessBuilder("/bin/sh",
|
||||
script.getCanonicalPath()));
|
||||
}
|
||||
} finally {
|
||||
script.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static int executeProcess(java.lang.ProcessBuilder builder) throws IOException {
|
||||
try {
|
||||
java.lang.Process process = builder.start();
|
||||
return process.waitFor();
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFilteredResource(String resource, File outFile, Map<String, String> props) throws IOException {
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = ScriptUtils.class.getResourceAsStream(resource);
|
||||
// Read it line at a time so that we can use the platform line ending when we write it out.
|
||||
PrintStream out = new PrintStream(new FileOutputStream(outFile));
|
||||
try {
|
||||
Scanner scanner = new Scanner(is);
|
||||
while (scanner.hasNextLine() ) {
|
||||
String line = scanner.nextLine();
|
||||
line = filter(line, props);
|
||||
out.println(line);
|
||||
}
|
||||
scanner.close();
|
||||
} finally {
|
||||
safeClose(out);
|
||||
}
|
||||
} finally {
|
||||
safeClose(is);
|
||||
}
|
||||
}
|
||||
|
||||
private static void safeClose(InputStream is) throws IOException {
|
||||
if (is == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
is.close();
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void safeClose(OutputStream is) throws IOException {
|
||||
if (is == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
is.close();
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private static String filter(String line, Map<String, String> props) {
|
||||
for (Map.Entry<String, String> i : props.entrySet()) {
|
||||
int p1 = line.indexOf(i.getKey());
|
||||
if( p1 >= 0 ) {
|
||||
String l1 = line.substring(0, p1);
|
||||
String l2 = line.substring(p1+i.getKey().length());
|
||||
line = l1+i.getValue()+l2;
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private static final boolean windows;
|
||||
|
||||
static {
|
||||
windows = System.getProperty("os.name").toLowerCase().contains("windows");
|
||||
}
|
||||
|
||||
public static boolean isWindows() {
|
||||
return windows;
|
||||
}
|
||||
|
||||
public static String getJavaCommandPath() throws IOException {
|
||||
return new File(System.getProperty("java.home"), isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath();
|
||||
}
|
||||
|
||||
}
|
@@ -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}
|
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
################################################################################
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
#exec 1>${out.file}
|
||||
#exec 2>${err.file}
|
||||
exec 1>/dev/null
|
||||
exec 2>/dev/null
|
||||
if [ "x${dir}" != "x" ]; then
|
||||
cd "${dir}"
|
||||
fi
|
||||
nohup ${command} &
|
||||
echo $! > "${pid.file}"
|
@@ -0,0 +1,27 @@
|
||||
'===============================================================================
|
||||
'
|
||||
' 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.
|
||||
'
|
||||
'===============================================================================
|
||||
|
||||
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
|
||||
Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process Where ProcessId = ${pid}")
|
||||
intRetVal = 1
|
||||
For Each objProcess in colProcessList
|
||||
objProcess.Terminate()
|
||||
intRetVal = 0
|
||||
Next
|
||||
WScript.Quit(intRetVal)
|
@@ -0,0 +1,26 @@
|
||||
'===============================================================================
|
||||
'
|
||||
' 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.
|
||||
'
|
||||
'===============================================================================
|
||||
|
||||
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
|
||||
Set colProcessList = objWMIService.ExecQuery("Select * from Win32_Process Where ProcessId = ${pid}")
|
||||
intRetVal = 1
|
||||
For Each objProcess in colProcessList
|
||||
intRetVal = 0
|
||||
Next
|
||||
WScript.Quit(intRetVal)
|
@@ -0,0 +1,34 @@
|
||||
'===============================================================================
|
||||
'
|
||||
' 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.
|
||||
'
|
||||
'===============================================================================
|
||||
|
||||
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
|
||||
Set objConfig = objWMIService.Get("Win32_ProcessStartup").SpawnInstance_
|
||||
objConfig.ShowWindow = SW_HIDE
|
||||
objConfig.CreateFlags = 8
|
||||
If Len("${dir}") > 0 Then
|
||||
intReturn = objWMIService.Get("Win32_Process").Create("${command}", "${dir}", objConfig, intProcessID)
|
||||
Else
|
||||
intReturn = objWMIService.Get("Win32_Process").Create("${command}", Null, objConfig, intProcessID)
|
||||
End If
|
||||
If intReturn = 0 Then
|
||||
Set objOutputFile = CreateObject("Scripting.fileSystemObject").CreateTextFile("${pid.file}", TRUE)
|
||||
objOutputFile.WriteLine(intProcessID)
|
||||
objOutputFile.Close
|
||||
End If
|
||||
WScript.Quit(intReturn)
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.daemon;
|
||||
|
||||
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.jboss.fuse.mvnd.client.DaemonInfo;
|
||||
import org.jboss.fuse.mvnd.client.DaemonRegistry;
|
||||
import org.jboss.fuse.mvnd.client.DaemonState;
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user