blob: d53f0c664aaaadb902ecc41bf3860d4b5839f87e [file] [log] [blame]
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.apache.winegrower.extension.agent;
import static java.util.Arrays.asList;
import static java.util.Collections.enumeration;
import static java.util.Collections.list;
import static java.util.Comparator.comparing;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.jar.JarFile;
import java.util.stream.Stream;
public class WinegrowerAgent {
private static final boolean DEBUG = Boolean.getBoolean("winegrower.agent.debug");
public static void premain(final String agentArgs, final Instrumentation instrumentation) {
agentmain(agentArgs, instrumentation);
}
public static void agentmain(final String agentArgs, final Instrumentation instrumentation) {
if (Boolean.getBoolean("winegrower.agent.started")) {
return;
}
if (DEBUG) {
print("agentargs: " + agentArgs);
}
ofNullable(extractConfig(agentArgs, "libs="))
.ifPresent(value -> toLibStream(value)
.forEach(lib -> {
try {
instrumentation.appendToSystemClassLoaderSearch(new JarFile(lib));
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}));
final Collection<URL> isolatedLibs = ofNullable(extractConfig(agentArgs, "isolatedlibs="))
.map(value -> toLibStream(value)
.map(lib -> {
try {
return lib.toURI().toURL();
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
}).collect(toList()))
.orElseGet(Collections::emptyList);
final URLClassLoader loader = new WinegrowerAgentClassLoader(
isolatedLibs,
toValues(extractConfig(agentArgs, "ignoredParentClasses=")).collect(toList()),
toValues(extractConfig(agentArgs, "ignoredParentResources=")).collect(toList()));
final Thread thread = Thread.currentThread();
final ClassLoader contextualLoader = thread.getContextClassLoader();
thread.setContextClassLoader(loader);
try {
doStart(agentArgs, instrumentation);
} catch (final Throwable e) {
final Integer exitStatus = Integer.getInteger("winegrower.agent.exitStatusOnError", -1);
if (exitStatus > 0) {
System.exit(exitStatus);
}
throw new IllegalStateException(e);
} finally {
thread.setContextClassLoader(contextualLoader);
}
}
private static Stream<File> toLibStream(final String paths) {
return toValues(paths)
.map(File::new)
.filter(File::exists)
.flatMap(it -> it.isDirectory() ?
ofNullable(it.listFiles()).map(Stream::of).orElseGet(Stream::empty) : Stream.of(it))
.sorted(comparing(File::getName))
.filter(it -> it.getName().endsWith(".zip") || it.getName().endsWith(".jar"));
}
private static Stream<String> toValues(final String csv) {
return ofNullable(csv)
.map(v -> v.split(","))
.map(v -> Stream.of(v).map(String::trim).filter(it -> !it.isEmpty()))
.orElseGet(Stream::empty);
}
private static void doStart(final String agentArgs, final Instrumentation instrumentation) throws Throwable {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
final Class<?> bundleClass = loader.loadClass("org.osgi.framework.Bundle");
final Class<?> ripenerImplClass = loader.loadClass("org.apache.winegrower.Ripener$Impl");
final Class<?> ripenerConfigurationClass = loader.loadClass("org.apache.winegrower.Ripener$Configuration");
final Object ripener = ripenerImplClass.getConstructor(ripenerConfigurationClass)
.newInstance(createConfiguration(ripenerConfigurationClass, agentArgs));
// before start - avoids reload/refresh
final Object services = ripenerImplClass.getMethod("getServices").invoke(ripener);
final Object bundleRegistry = ripenerImplClass.getMethod("getRegistry").invoke(ripener);
final Object bundleLifecycle0 = Map.class.cast(bundleRegistry.getClass().getMethod("getBundles").invoke(bundleRegistry)).get(0L);
final Object bundle0 = bundleLifecycle0.getClass().getMethod("getBundle").invoke(bundleLifecycle0);
services.getClass().getMethod("registerService", String[].class, Object.class, Dictionary.class, bundleClass)
.invoke(services, new String[]{Instrumentation.class.getName()}, instrumentation, new Hashtable<>(), bundle0);
doCall(ripener, "start", new Class<?>[0], new Object[0]);
System.setProperty("winegrower.agent.started", "true");
final Thread stopThread = new Thread(() -> {
try {
doCall(ripener, "stop", new Class<?>[0], new Object[0]);
} catch (final Throwable throwable) {
if (DEBUG) {
throwable.printStackTrace();
} // else: not that important
} finally {
if (WinegrowerAgentClassLoader.class.isInstance(loader)) {
try {
WinegrowerAgentClassLoader.class.cast(loader).close();
} catch (final IOException e) {
// no-op
}
}
}
}, WinegrowerAgent.class.getName() + "-shutdown");
stopThread.setContextClassLoader(loader);
Runtime.getRuntime().addShutdownHook(stopThread);
}
private static void doCall(final Object instance, final String mtd, final Class<?>[] paramTypes, final Object[] args) {
try {
instance.getClass().getMethod(mtd, paramTypes).invoke(instance, args);
} catch (final InvocationTargetException ite) {
final Throwable targetException = ite.getTargetException();
if (RuntimeException.class.isInstance(targetException)) {
throw RuntimeException.class.cast(targetException);
}
throw new IllegalStateException(targetException);
} catch (final Exception ex) {
if (RuntimeException.class.isInstance(ex)) {
throw RuntimeException.class.cast(ex);
}
throw new IllegalStateException(ex);
}
}
private static Object createConfiguration(final Class<?> configType, final String agentArgs) throws Throwable {
final Object configuration = configType.getConstructor().newInstance();
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
ofNullable(extractConfig(agentArgs,"workDir="))
.map(String::valueOf)
.map(File::new)
.ifPresent(value -> doCall(configuration, "setWorkDir", new Class<?>[]{File.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"useLifecycleCallbacks="))
.map(String::valueOf)
.map(Boolean::parseBoolean)
.ifPresent(value -> doCall(configuration, "setUseLifecycleCallbacks", new Class<?>[]{boolean.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"lifecycleCallbacks="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.map(callbacks -> {
try {
final Class<?> type = contextClassLoader.loadClass("org.apache.winegrower.api.LifecycleCallbacks");
return callbacks.stream()
.map(clazz -> {
try {
return contextClassLoader
.loadClass(clazz)
.getConstructor()
.newInstance();
} catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(
e.getTargetException());
}
})
.map(type::cast)
.collect(toList());
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
})
.ifPresent(value -> doCall(configuration, "setLifecycleCallbacks", new Class<?>[]{List.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"prioritizedBundles="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(value -> doCall(configuration, "setPrioritizedBundles", new Class<?>[]{List.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"ignoredBundles="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(value -> doCall(configuration, "setIgnoredBundles", new Class<?>[]{Collection.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"scanningIncludes="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(value -> doCall(configuration, "setScanningIncludes", new Class<?>[]{Collection.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"scanningExcludes="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(value -> doCall(configuration, "setScanningExcludes", new Class<?>[]{Collection.class}, new Object[]{value}));
ofNullable(extractConfig(agentArgs,"manifestContributors="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(contributors -> {
try {
final Class<?> type = contextClassLoader.loadClass(
"org.apache.winegrower.scanner.manifest.ManifestContributor");
final Collection<?> value = contributors.stream()
.map(clazz -> {
try {
return contextClassLoader
.loadClass(clazz)
.getConstructor()
.newInstance();
} catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(
e.getTargetException());
}
})
.map(type::cast)
.collect(toList());
doCall(configuration, "setManifestContributors", new Class<?>[]{Collection.class}, new Object[]{value});
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
});
ofNullable(extractConfig(agentArgs,"jarFilter="))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.ifPresent(filter -> {
try {
final Predicate<String> predicate = (Predicate<String>) contextClassLoader.loadClass(filter).getConstructor().newInstance();
doCall(configuration, "setJarFilter", new Class<?>[]{Predicate.class}, new Object[]{predicate});
} catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(e.getTargetException());
}
});
return configuration;
}
private static String extractConfig(final String agentArgs, final String startStr) {
if (agentArgs != null && agentArgs.contains(startStr)) {
final int start = agentArgs.indexOf(startStr) + startStr.length();
final int separator = agentArgs.indexOf('|', start);
final int endIdx;
if (separator > 0) {
endIdx = separator;
} else {
endIdx = agentArgs.length();
}
final String value = agentArgs.substring(start, endIdx);
if (DEBUG) {
print(startStr + value);
}
return value;
}
if (DEBUG) {
print("No configuration for " + startStr.substring(0, startStr.length() - 1));
}
return null;
}
private static void print(final String string) {
System.out.println("[winegrower-agent:debug] " + string);
}
private static class WinegrowerAgentClassLoader extends URLClassLoader {
private final Collection<String> ignoredParentClasses;
private final Collection<String> ignoredParentResources;
private WinegrowerAgentClassLoader(final Collection<URL> urls,
final Collection<String> ignoredParentClasses,
final Collection<String> ignoredParentResources) {
super(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
this.ignoredParentClasses = ignoredParentClasses;
this.ignoredParentResources = ignoredParentResources;
}
@Override
protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
if (name != null && ignoredParentClasses.stream().anyMatch(name::startsWith)) {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
clazz = findClass(name);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
return super.loadClass(name, resolve);
}
@Override
public URL getResource(final String name) {
if (name != null && ignoredParentResources.stream().anyMatch(name::startsWith)) {
return findResource(name);
}
return super.getResource(name);
}
@Override
public Enumeration<URL> getResources(final String name) throws IOException {
final Enumeration<URL> resources = super.getResources(name);
if (name != null && ignoredParentResources.stream().anyMatch(name::startsWith)) {
final List<URL> list = new ArrayList<>(list(resources));
list.removeAll(list(getParent().getResources(name)));
return enumeration(list);
}
return resources;
}
}
}