| /** |
| * 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; |
| } |
| } |
| } |