From d50997c548cafa6fea0228d21a7e5ade91409daf Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 5 Jan 2021 15:25:02 +0100 Subject: [PATCH] Use an agent to support processes launched with redirected io, fixes #241 --- agent/pom.xml | 59 +++++++++++++++++ .../java/org/mvndaemon/mvnd/agent/Agent.java | 63 +++++++++++++++++++ .../mvnd/client/DaemonConnector.java | 32 ++++++++-- dist/pom.xml | 10 ++- dist/src/main/provisio/maven-distro.xml | 4 +- pom.xml | 21 ++++++- pump/pom.xml | 33 ++++++++++ .../org/mvndaemon/mvnd/pump/PumpThread.java | 56 +++++++++++++++++ 8 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 agent/pom.xml create mode 100644 agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java create mode 100644 pump/pom.xml create mode 100644 pump/src/main/java/org/mvndaemon/mvnd/pump/PumpThread.java diff --git a/agent/pom.xml b/agent/pom.xml new file mode 100644 index 000000000..0449f80ad --- /dev/null +++ b/agent/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + org.mvndaemon.mvnd + mvnd + 0.2.1-SNAPSHOT + + + mvnd-agent + + jar + Maven Daemon - Agent + + + + + org.javassist + javassist + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.mvndaemon.mvnd.agent.Agent + mvnd-pump-${version}.jar + + + + + + + + diff --git a/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java b/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java new file mode 100644 index 000000000..9d8054ffe --- /dev/null +++ b/agent/src/main/java/org/mvndaemon/mvnd/agent/Agent.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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.mvndaemon.mvnd.agent; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import javassist.ClassPool; +import javassist.CtClass; + +public class Agent { + + public static final String START_WITH_PIPES = "if (redirects != null\n" + + " && redirects[1] == ProcessBuilder$Redirect.INHERIT\n" + + " && redirects[2] == ProcessBuilder$Redirect.INHERIT) {\n" + + " redirects[1] = redirects[2] = ProcessBuilder$Redirect.PIPE;" + + " Process p = start(redirects);\n" + + " PumpThread.start(p.getInputStream(), System.out);\n" + + " PumpThread.start(p.getErrorStream(), System.err);\n" + + " return p;\n" + + "}"; + + public static void premain(String args, Instrumentation instrumentation) throws Exception { + instrumentation.addTransformer(new ClassFileTransformer() { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + if ("java/lang/ProcessBuilder".equals(className)) { + try { + ClassPool pool = ClassPool.getDefault(); + CtClass clazz = pool.get("java.lang.ProcessBuilder"); + pool.importPackage("org.mvndaemon.mvnd.pump"); + clazz.getDeclaredMethod("start", + new CtClass[] { clazz.getClassPool().get("java.lang.ProcessBuilder$Redirect[]") }) + .insertBefore(START_WITH_PIPES); + byte[] data = clazz.toBytecode(); + clazz.detach(); + return data; + } catch (Throwable e) { + throw new IllegalClassFormatException(e.toString()); + } + } else { + return classfileBuffer; + } + } + }); + } + +} diff --git a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonConnector.java b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonConnector.java index 74963bb14..933eca641 100644 --- a/client/src/main/java/org/mvndaemon/mvnd/client/DaemonConnector.java +++ b/client/src/main/java/org/mvndaemon/mvnd/client/DaemonConnector.java @@ -15,10 +15,12 @@ */ package org.mvndaemon.mvnd.client; +import java.io.File; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.SocketChannel; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -32,7 +34,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import org.mvndaemon.mvnd.common.BuildProperties; import org.mvndaemon.mvnd.common.DaemonCompatibilitySpec; import org.mvndaemon.mvnd.common.DaemonCompatibilitySpec.Result; import org.mvndaemon.mvnd.common.DaemonConnection; @@ -300,10 +301,33 @@ private Process startDaemon(String uid) { final String java = Os.current().isUnixLike() ? "bin/java" : "bin\\java.exe"; args.add(parameters.javaHome().resolve(java).toString()); // classpath + String mvndCommonPath = null; + String mvndAgentPath = null; + String javassistPath = null; + for (Path jar : Files.newDirectoryStream(mvndHome.resolve("mvn/lib/ext"))) { + String s = jar.getFileName().toString(); + if (s.endsWith(".jar")) { + if (s.startsWith("mvnd-common-")) { + mvndCommonPath = jar.toString(); + } else if (s.startsWith("mvnd-agent-")) { + mvndAgentPath = jar.toString(); + } else if (s.startsWith("javassist-")) { + javassistPath = jar.toString(); + } + } + } + if (mvndCommonPath == null) { + throw new IllegalStateException("Could not find mvnd-common jar in mvn/lib/ext/"); + } + if (mvndAgentPath == null) { + throw new IllegalStateException("Could not find mvnd-agent jar in mvn/lib/ext/"); + } + if (javassistPath == null) { + throw new IllegalStateException("Could not find javassist jar in mvn/lib/ext/"); + } args.add("-classpath"); - final String mvndCommonPath = "mvn/lib/ext/mvnd-common-" + BuildProperties.getInstance().getVersion() + ".jar"; - final String classpath = mvndHome.resolve(mvndCommonPath).toString(); - args.add(classpath); + args.add(mvndCommonPath + File.pathSeparator + mvndAgentPath + File.pathSeparator + javassistPath); + args.add("-javaagent:" + mvndAgentPath); // debug options if (parameters.property(Environment.MVND_DEBUG).asBoolean()) { args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"); diff --git a/dist/pom.xml b/dist/pom.xml index a68d64620..661a41971 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -1,6 +1,6 @@ + + + 4.0.0 + + org.mvndaemon.mvnd + mvnd + 0.2.1-SNAPSHOT + + + mvnd-pump + + jar + Maven Daemon - Pump + + diff --git a/pump/src/main/java/org/mvndaemon/mvnd/pump/PumpThread.java b/pump/src/main/java/org/mvndaemon/mvnd/pump/PumpThread.java new file mode 100644 index 000000000..9206f33df --- /dev/null +++ b/pump/src/main/java/org/mvndaemon/mvnd/pump/PumpThread.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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.mvndaemon.mvnd.pump; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.UncheckedIOException; + +public class PumpThread extends Thread { + + private final InputStream stream; + private final PrintStream out; + + public static void start(InputStream stream, PrintStream out) { + new PumpThread(stream, out).start(); + } + + public PumpThread(InputStream stream, PrintStream out) { + this.stream = stream; + this.out = out; + } + + @Override + public void run() { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + try { + String line; + while ((line = reader.readLine()) != null) { + out.println(line); + } + } finally { + reader.close(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +}