-
Notifications
You must be signed in to change notification settings - Fork 165
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
performant multi-run support #169
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# download the plugin for your IDE from http://editorconfig.org/#download | ||
root = true | ||
|
||
## default style | ||
[*] | ||
indent_style = space | ||
indent_size = 4 | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true | ||
end_of_line = lf | ||
charset = utf-8 | ||
|
||
# Docstrings and comments use max_line_length = 79 | ||
# Use 2 spaces for ruby/cucumber | ||
[*.rb] | ||
indent_size = 2 | ||
|
||
[*.go] | ||
indent_size = tab | ||
indent_style = tab | ||
tab_width = 2 | ||
|
||
[*.js] | ||
indent_size = 2 | ||
|
||
# gherkins | ||
[*.feature] | ||
indent_size = 2 | ||
|
||
[*.py] | ||
max_line_length = 119 | ||
indent_size = 4 | ||
|
||
# Use 2 spaces for the HTML files | ||
[*.html] | ||
indent_size = 2 | ||
|
||
# The JSON files contain newlines inconsistently | ||
[*.json] | ||
indent_size = 2 | ||
insert_final_newline = ignore | ||
|
||
[*.xyaml] | ||
indent_size = 2 | ||
|
||
[*.yaml] | ||
indent_size = 2 | ||
|
||
# Tab indentation (no size specified) | ||
[Makefile] | ||
indent_style = tab |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
import groovy.lang.Binding; | ||
import groovy.lang.GroovyClassLoader; | ||
import groovy.lang.GroovyShell; | ||
import groovy.lang.Script; | ||
import hudson.Extension; | ||
import hudson.PluginManager; | ||
import hudson.model.AbstractDescribableImpl; | ||
|
@@ -36,6 +37,8 @@ | |
import hudson.util.FormValidation; | ||
|
||
import java.beans.Introspector; | ||
import java.io.IOException; | ||
import java.io.Closeable; | ||
import java.lang.ref.Reference; | ||
import java.lang.ref.WeakReference; | ||
import java.lang.reflect.Field; | ||
|
@@ -68,6 +71,7 @@ | |
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedClasspathException; | ||
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException; | ||
import org.jenkinsci.plugins.scriptsecurity.scripts.languages.GroovyLanguage; | ||
import org.kohsuke.groovy.sandbox.GroovyInterceptor; | ||
import org.kohsuke.stapler.DataBoundConstructor; | ||
import org.kohsuke.stapler.QueryParameter; | ||
import org.kohsuke.stapler.Stapler; | ||
|
@@ -277,6 +281,17 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro | |
} | ||
} | ||
|
||
|
||
/** | ||
* Prepares the Groovy script to be executed. | ||
* @param loader see parameter explanation under {@link #evaluate(ClassLoader, Binding)} | ||
* @param binding see parameter explanation under {@link #evaluate(ClassLoader, Binding)} | ||
* @return the prepared script | ||
*/ | ||
public PreparedScript prepare(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deserves Javadoc at least for the |
||
return new PreparedScript(loader, binding); | ||
} | ||
|
||
/** | ||
* Runs the Groovy script, using the sandbox if so configured. | ||
* @param loader a class loader for constructing the shell, such as {@link PluginManager#uberClassLoader} (will be augmented by {@link #getClasspath} if nonempty) | ||
|
@@ -287,63 +302,96 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro | |
* @throws UnapprovedUsageException in case of a non-sandbox issue | ||
* @throws UnapprovedClasspathException in case some unapproved classpath entries were requested | ||
*/ | ||
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.") | ||
public Object evaluate(ClassLoader loader, Binding binding) throws Exception { | ||
if (!calledConfiguring) { | ||
throw new IllegalStateException("you need to call configuring or a related method before using GroovyScript"); | ||
} | ||
URLClassLoader urlcl = null; | ||
ClassLoader memoryProtectedLoader = null; | ||
List<ClasspathEntry> cp = getClasspath(); | ||
if (!cp.isEmpty()) { | ||
List<URL> urlList = new ArrayList<URL>(cp.size()); | ||
|
||
for (ClasspathEntry entry : cp) { | ||
ScriptApproval.get().using(entry); | ||
urlList.add(entry.getURL()); | ||
} | ||
|
||
loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader); | ||
try (PreparedScript scriptInstance = prepare(loader, binding)) { | ||
return scriptInstance.run(); | ||
} | ||
boolean canDoCleanup = false; | ||
} | ||
|
||
try { | ||
loader = GroovySandbox.createSecureClassLoader(loader); | ||
/** | ||
* Allows access to execute a script multiple times without preparation and clean-up overhead. | ||
* While the intricate logic to do this continues to be hidden from users. | ||
*/ | ||
public final class PreparedScript implements Closeable { | ||
|
||
private final GroovyShell sh; | ||
private final Script preparedScript; | ||
private ClassLoader memoryProtectedLoader = null; | ||
private GroovyInterceptor scriptSandbox = null; | ||
private URLClassLoader urlcl = null; | ||
private boolean canDoCleanup = false; | ||
|
||
/** | ||
* @see @see SecureGroovyScript#evaluate() | ||
*/ | ||
@SuppressFBWarnings(value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Managed by GroovyShell.") | ||
private PreparedScript(ClassLoader loader, Binding binding) throws IllegalAccessException, IOException { | ||
List<ClasspathEntry> cp = getClasspath(); | ||
if (!cp.isEmpty()) { | ||
List<URL> urlList = new ArrayList<URL>(cp.size()); | ||
|
||
for (ClasspathEntry entry : cp) { | ||
ScriptApproval.get().using(entry); | ||
urlList.add(entry.getURL()); | ||
} | ||
|
||
Field loaderF = null; | ||
try { | ||
loaderF = GroovyShell.class.getDeclaredField("loader"); | ||
loaderF.setAccessible(true); | ||
canDoCleanup = true; | ||
} catch (NoSuchFieldException nsme) { | ||
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work"); | ||
loader = urlcl = new URLClassLoader(urlList.toArray(new URL[urlList.size()]), loader); | ||
} | ||
|
||
GroovyShell sh; | ||
if (sandbox) { | ||
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration(); | ||
sh = new GroovyShell(loader, binding, cc); | ||
try { | ||
loader = GroovySandbox.createSecureClassLoader(loader); | ||
Field loaderF = null; | ||
try { | ||
loaderF = GroovyShell.class.getDeclaredField("loader"); | ||
loaderF.setAccessible(true); | ||
canDoCleanup = true; | ||
} catch (NoSuchFieldException nsme) { | ||
LOGGER.log(Level.FINE, "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work"); | ||
} | ||
|
||
if (canDoCleanup) { | ||
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc); | ||
loaderF.set(sh, memoryProtectedLoader); | ||
if (sandbox) { | ||
CompilerConfiguration cc = GroovySandbox.createSecureCompilerConfiguration(); | ||
sh = new GroovyShell(loader, binding, cc); | ||
|
||
if (canDoCleanup) { | ||
memoryProtectedLoader = new CleanGroovyClassLoader(loader, cc); | ||
loaderF.set(sh, memoryProtectedLoader); | ||
} | ||
|
||
preparedScript = sh.parse(script); | ||
scriptSandbox = GroovySandbox.createSandbox(preparedScript, Whitelist.all()); | ||
} else { | ||
sh = new GroovyShell(loader, binding); | ||
if (canDoCleanup) { | ||
memoryProtectedLoader = new CleanGroovyClassLoader(loader); | ||
loaderF.set(sh, memoryProtectedLoader); | ||
} | ||
|
||
preparedScript = sh.parse(ScriptApproval.get().using(script, GroovyLanguage.get())); | ||
} | ||
} catch (Exception e) { | ||
close(); | ||
throw e; | ||
} | ||
} | ||
|
||
public Object run() throws Exception { | ||
if (sandbox) { | ||
scriptSandbox.register(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self: |
||
try { | ||
return GroovySandbox.run(sh.parse(script), Whitelist.all()); | ||
return preparedScript.run(); | ||
} catch (RejectedAccessException x) { | ||
throw ScriptApproval.get().accessRejected(x, ApprovalContext.create()); | ||
} finally { | ||
scriptSandbox.unregister(); | ||
} | ||
} else { | ||
sh = new GroovyShell(loader, binding); | ||
if (canDoCleanup) { | ||
memoryProtectedLoader = new CleanGroovyClassLoader(loader); | ||
loaderF.set(sh, memoryProtectedLoader); | ||
} | ||
return sh.evaluate(ScriptApproval.get().using(script, GroovyLanguage.get())); | ||
return preparedScript.run(); | ||
} | ||
} | ||
|
||
} finally { | ||
@Override | ||
public void close() throws IOException { | ||
try { | ||
if (canDoCleanup) { | ||
cleanUpLoader(memoryProtectedLoader, new HashSet<ClassLoader>(), new HashSet<Class<?>>()); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be helpful to
@link
toregister
andunregister
so callers can see how to use it.