Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:Spoon agent #2645

Merged
merged 27 commits into from
Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0d7c009
refactor(MavenLauncher)
nharrand Sep 3, 2018
8c4037d
merge
nharrand Sep 3, 2018
c09c060
vanity
nharrand Sep 4, 2018
7a2fd36
removes parameter m2repository as it has become useless
nharrand Sep 4, 2018
2e88419
override last modified date when classpath is evaluated but has not c…
nharrand Sep 4, 2018
ee3fd53
doc(MavenLauncher)
nharrand Sep 4, 2018
0932e53
untrack .tmp files and cosmetic
nharrand Sep 4, 2018
11ca5a0
fixes a bug when compiler plugin exist but has no configuration child…
nharrand Sep 4, 2018
1a43b2d
fixes a bug which made InheritanceModel incorectly read properties co…
nharrand Sep 4, 2018
c974268
re add deprecated MavenLauncher constructor and add a forceRefresh op…
nharrand Sep 4, 2018
178bb9d
style
nharrand Sep 5, 2018
1567cf5
Merge branch 'master' of https://github.com/INRIA/spoon
nharrand Sep 7, 2018
a10d66e
Merge branch 'master' of https://github.com/INRIA/spoon
nharrand Sep 27, 2018
2ec40a2
test PR template
nharrand Sep 27, 2018
6bebb2c
Merge branch 'master' of https://github.com/INRIA/spoon
nharrand Oct 1, 2018
1fe7901
PoC SpoonClassFileTransformer
nharrand Oct 9, 2018
e75cdfc
Merge branch 'master' of https://github.com/INRIA/spoon into spoon-agent
nharrand Oct 9, 2018
7a3133a
fix header
nharrand Oct 9, 2018
85735de
remove leftover template
nharrand Oct 9, 2018
9030114
fix IncrementalLauncher uses, and add a test
nharrand Oct 11, 2018
a2a90f1
Experimental I guess
nharrand Oct 11, 2018
a35fa53
adding agent doc to menu
nharrand Oct 12, 2018
d2070c5
Implements suggested changes in SpoonClassFileTransformer's API
nharrand Oct 15, 2018
a6e9438
update doc
nharrand Oct 15, 2018
1fb1eb9
fix Licenses
nharrand Oct 15, 2018
529fa59
Update agent.md
monperrus Oct 15, 2018
df48bfb
Update agent.md
nharrand Oct 15, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions doc/agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: Agent
tags: [usage]
keywords: agent, usage, java, loadtime
---

# Spoon Agent

Spoon can also be used to transform classes at load time in the JVM. FOr this, `SpoonClassFileTransformer` provide an abstraction of `ClassFileTransformer`
where the user can define Spoon transformation.
Bytecode of classes will be decompiled on-the-fly when loaded, and the Spoon AST will be updated in consequence, and the code is recompiled on-the-fly.

The following example shows the definition of a basic JVM agent for inserting a tracing method call a the end of every method called `foo`.

Here is the agent:
```java
public class Agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println( "Hello Agent" );

//Create a SpoonClassFileTransformer, that
// * excludes any classes not in our package from decompilation
// * adds the statement System.out.println("Hello <className>"); to the (first) method named "foo" of every classes
SpoonClassFileTransformer transformer = new SpoonClassFileTransformer(
cl -> cl.startsWith("org/my/package"),
new InsertPrintTransformer()
);
inst.addTransformer(transformer);

System.out.println( "Agent Done." );
}
}
```

```java
public class InsertPrintTransformer implements TypeTransformer {

@Override
public boolean accept(CtType type) {
if ((type instanceof CtClass) &&
type.getMethodsByName("foo").size() > 0) {
return true;
} else {
return false;
}
}

@Override
public void transform(CtType type) {
System.err.println("Transforming " + type.getQualifiedName());
CtMethod main = (CtMethod) type.getMethodsByName("foo").get(0);
main.getBody().addStatement(type.getFactory().createCodeSnippetStatement("System.out.println(\"Hello " + type.getQualifiedName() + "\");"));
System.err.println("Done transforming " + type.getQualifiedName());
}
}
```
58 changes: 58 additions & 0 deletions src/main/java/spoon/decompiler/MultiTypeTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import spoon.reflect.declaration.CtType;

import java.util.Collection;
import java.util.LinkedHashSet;

public class MultiTypeTransformer implements TypeTransformer {

protected LinkedHashSet<TypeTransformer> transformers;

public MultiTypeTransformer() {
transformers = new LinkedHashSet<>();
}

public void addTransformer(TypeTransformer transformer) {
transformers.add(transformer);
}

public void addTransformers(Collection<TypeTransformer> transformers) {
this.transformers.addAll(transformers);
}

@Override
public void transform(CtType type) {
for (TypeTransformer transformer: transformers) {
if (transformer.accept(type)) {
transformer.transform(type);
}
}
}

@Override
public boolean accept(CtType type) {
for (TypeTransformer transformer: transformers) {
if (transformer.accept(type)) {
return true;
}
}
return false;
}
}
185 changes: 185 additions & 0 deletions src/main/java/spoon/decompiler/SpoonClassFileTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import org.benf.cfr.reader.Main;
import spoon.IncrementalLauncher;
import spoon.SpoonModelBuilder;
import spoon.reflect.CtModel;
import spoon.reflect.declaration.CtType;
import spoon.support.Experimental;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.nio.file.Files;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

@Experimental
public class SpoonClassFileTransformer implements ClassFileTransformer {

protected String pathToDecompiled;
//protected String pathToRecompile;
/*protected String pathToCache;*/

//Field filled by Constructor
protected File cache;
protected File recompileDir;
protected Set<String> classPath;
protected Set<File> inputSources;

protected TypeTransformer transformer;

protected Decompiler decompiler;

//Classes to exclude from decompilation
protected Predicate<String> classNameFilter;
//Exclude jvm classes
public static final Predicate<String> defaultFilter = s -> !(s.startsWith("java") || s.startsWith("sun"));


/**
* Default Constructor for SpoonClassFileTransformer
*
* @param typeTransformer Transformation to apply on loaded types.
*/
public SpoonClassFileTransformer(TypeTransformer typeTransformer) {
this(defaultFilter, typeTransformer, "spoon-decompiled", "spoon-cache", "spoon-recompiled", null);
}


/**
* Default Constructor for SpoonClassFileTransformer
*
* @param classNameFilter Filter for classname. If classeNameFilter.test(className) returns false,
* the class will be loaded without decompilation nor transformation.
* If null, a default filter will filter out typical jvm classes (starting with java* or sun*)
* Note @{SpoonClassFileTransformer.defaultFilter} may be used in conjunction of custom filter
* with `defaultFilter.and(classNameFilter)`.
* @param typeTransformer Transformation to apply on loaded types.
*/
public SpoonClassFileTransformer(Predicate<String> classNameFilter, TypeTransformer typeTransformer) {
this(classNameFilter, typeTransformer, "spoon-decompiled", "spoon-cache", "spoon-recompiled", null);
}

/**
* Default Constructor for SpoonClassFileTransformer
*
* @param classNameFilter Filter for classname. If classeNameFilter.test(className) returns false,
* the class will be loaded without decompilation nor transformation.
* If null, a default filter will filter out typical jvm classes (starting with java* or sun*)
* Note @{SpoonClassFileTransformer.defaultFilter} may be used in conjunction of custom filter
* with `defaultFilter.and(classNameFilter)`.
* @param typeTransformer Transformation to apply on loaded types.
* @param pathToDecompiled path to directory in which to put decompiled sources.
* @param pathToCache path to cache directory for IncrementalLauncher
* @param pathToRecompile path to recompiled classes
* @param decompiler Decompiler to use on classFile before building Spoon model. If null, default compiler (cfr) will be used.
*/
public SpoonClassFileTransformer(Predicate<String> classNameFilter,
TypeTransformer typeTransformer,
String pathToDecompiled,
String pathToCache,
String pathToRecompile,
Decompiler decompiler) {
if (classNameFilter == null) {
this.classNameFilter = defaultFilter;
} else {
this.classNameFilter = classNameFilter;
}

String classPathAr[] = System.getProperty("java.class.path").split(":");
classPath = new HashSet<>(Arrays.asList(classPathAr));
this.pathToDecompiled = pathToDecompiled;
recompileDir = new File(pathToRecompile);
cache = new File(pathToCache);

inputSources = new HashSet<>();
inputSources.add(new File(pathToDecompiled));

this.transformer = typeTransformer;

if (decompiler == null) {
this.decompiler = s -> Main.main(new String[]{s, "--outputdir", pathToDecompiled});
} else {
this.decompiler = decompiler;
}
}

@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) throws IllegalClassFormatException {
try {

//If the class is not matched by user's filter, resume unmodified loading
if (!classNameFilter.test(className)) {
return classfileBuffer;
}

//Decompile classfile
String pathToClassFile = loader.getResource(className + ".class").getPath();
decompiler.decompile(pathToClassFile);

IncrementalLauncher launcher = new IncrementalLauncher(inputSources, classPath, cache);
launcher.addInputResource(pathToDecompiled);

//Get updated model
CtModel model = launcher.buildModel();
launcher.saveCache();

//Get class model
CtType toBeTransformed = model.getAllTypes().stream().filter(t -> t.getQualifiedName().equals(className.replace("/", "."))).findAny().get();

//If the class model is not modified by user, resume unmodified loading
if (!transformer.accept(toBeTransformed)) {
return classfileBuffer;
}
launcher.getEnvironment().debugMessage("[Agent] transforming " + className);
transformer.transform(toBeTransformed);

//Compile new class model
SpoonModelBuilder compiler = launcher.createCompiler();
compiler.setBinaryOutputDirectory(recompileDir);
compiler.compile(SpoonModelBuilder.InputType.CTTYPES);


File transformedClass = new File(compiler.getBinaryOutputDirectory(), className + ".class");
try {
//Load Modified classFile
byte[] fileContent = Files.readAllBytes(transformedClass.toPath());
launcher.getEnvironment().debugMessage("[Agent] loading transformed " + className);
return fileContent;
} catch (IOException e) {
launcher.getEnvironment().debugMessage("[ERROR][Agent] while loading transformed " + className);
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
36 changes: 36 additions & 0 deletions src/main/java/spoon/decompiler/TypeTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (C) 2006-2018 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.decompiler;

import spoon.reflect.declaration.CtType;

public interface TypeTransformer {

/**
* User's implementation of transformation to apply on type.
* @param type type to be transformed
*/
void transform(CtType type);

/**
* User defined filter to discard type that will not be transformed by the SpoonClassFileTransformer.
* @param type type considered for transformation
*/
default boolean accept(CtType type) {
return true;
}
}