2828import groovy .lang .Binding ;
2929import groovy .lang .GroovyClassLoader ;
3030import groovy .lang .GroovyShell ;
31+ import groovy .lang .Script ;
3132import hudson .Extension ;
3233import hudson .PluginManager ;
3334import hudson .model .AbstractDescribableImpl ;
3637import hudson .util .FormValidation ;
3738
3839import java .beans .Introspector ;
40+ import java .io .IOException ;
41+ import java .io .Closeable ;
3942import java .lang .ref .Reference ;
4043import java .lang .ref .WeakReference ;
4144import java .lang .reflect .Field ;
6871import org .jenkinsci .plugins .scriptsecurity .scripts .UnapprovedClasspathException ;
6972import org .jenkinsci .plugins .scriptsecurity .scripts .UnapprovedUsageException ;
7073import org .jenkinsci .plugins .scriptsecurity .scripts .languages .GroovyLanguage ;
74+ import org .kohsuke .groovy .sandbox .GroovyInterceptor ;
7175import org .kohsuke .stapler .DataBoundConstructor ;
7276import org .kohsuke .stapler .QueryParameter ;
7377import org .kohsuke .stapler .Stapler ;
@@ -277,6 +281,17 @@ private static void cleanUpObjectStreamClassCaches(@Nonnull Class<?> clazz) thro
277281 }
278282 }
279283
284+
285+ /**
286+ * Prepares the Groovy script to be executed.
287+ * @param loader see parameter explanation under {@link #evaluate(ClassLoader, Binding)}
288+ * @param binding see parameter explanation under {@link #evaluate(ClassLoader, Binding)}
289+ * @return the prepared script
290+ */
291+ public PreparedScript prepare (ClassLoader loader , Binding binding ) throws IllegalAccessException , IOException {
292+ return new PreparedScript (loader , binding );
293+ }
294+
280295 /**
281296 * Runs the Groovy script, using the sandbox if so configured.
282297 * @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
287302 * @throws UnapprovedUsageException in case of a non-sandbox issue
288303 * @throws UnapprovedClasspathException in case some unapproved classpath entries were requested
289304 */
290- @ SuppressFBWarnings (value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED" , justification = "Managed by GroovyShell." )
291305 public Object evaluate (ClassLoader loader , Binding binding ) throws Exception {
292- if (!calledConfiguring ) {
293- throw new IllegalStateException ("you need to call configuring or a related method before using GroovyScript" );
294- }
295- URLClassLoader urlcl = null ;
296- ClassLoader memoryProtectedLoader = null ;
297- List <ClasspathEntry > cp = getClasspath ();
298- if (!cp .isEmpty ()) {
299- List <URL > urlList = new ArrayList <URL >(cp .size ());
300-
301- for (ClasspathEntry entry : cp ) {
302- ScriptApproval .get ().using (entry );
303- urlList .add (entry .getURL ());
304- }
305-
306- loader = urlcl = new URLClassLoader (urlList .toArray (new URL [urlList .size ()]), loader );
306+ try (PreparedScript scriptInstance = prepare (loader , binding )) {
307+ return scriptInstance .run ();
307308 }
308- boolean canDoCleanup = false ;
309+ }
309310
310- try {
311- loader = GroovySandbox .createSecureClassLoader (loader );
311+ /**
312+ * Allows access to execute a script multiple times without preparation and clean-up overhead.
313+ * While the intricate logic to do this continues to be hidden from users.
314+ */
315+ public final class PreparedScript implements Closeable {
316+
317+ private final GroovyShell sh ;
318+ private final Script preparedScript ;
319+ private ClassLoader memoryProtectedLoader = null ;
320+ private GroovyInterceptor scriptSandbox = null ;
321+ private URLClassLoader urlcl = null ;
322+ private boolean canDoCleanup = false ;
323+
324+ /**
325+ * @see @see SecureGroovyScript#evaluate()
326+ */
327+ @ SuppressFBWarnings (value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED" , justification = "Managed by GroovyShell." )
328+ private PreparedScript (ClassLoader loader , Binding binding ) throws IllegalAccessException , IOException {
329+ List <ClasspathEntry > cp = getClasspath ();
330+ if (!cp .isEmpty ()) {
331+ List <URL > urlList = new ArrayList <URL >(cp .size ());
332+
333+ for (ClasspathEntry entry : cp ) {
334+ ScriptApproval .get ().using (entry );
335+ urlList .add (entry .getURL ());
336+ }
312337
313- Field loaderF = null ;
314- try {
315- loaderF = GroovyShell .class .getDeclaredField ("loader" );
316- loaderF .setAccessible (true );
317- canDoCleanup = true ;
318- } catch (NoSuchFieldException nsme ) {
319- LOGGER .log (Level .FINE , "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work" );
338+ loader = urlcl = new URLClassLoader (urlList .toArray (new URL [urlList .size ()]), loader );
320339 }
321340
322- GroovyShell sh ;
323- if (sandbox ) {
324- CompilerConfiguration cc = GroovySandbox .createSecureCompilerConfiguration ();
325- sh = new GroovyShell (loader , binding , cc );
341+ try {
342+ loader = GroovySandbox .createSecureClassLoader (loader );
343+ Field loaderF = null ;
344+ try {
345+ loaderF = GroovyShell .class .getDeclaredField ("loader" );
346+ loaderF .setAccessible (true );
347+ canDoCleanup = true ;
348+ } catch (NoSuchFieldException nsme ) {
349+ LOGGER .log (Level .FINE , "GroovyShell fields have changed, field loader no longer exists -- memory leak fixes won't work" );
350+ }
326351
327- if (canDoCleanup ) {
328- memoryProtectedLoader = new CleanGroovyClassLoader (loader , cc );
329- loaderF .set (sh , memoryProtectedLoader );
352+ if (sandbox ) {
353+ CompilerConfiguration cc = GroovySandbox .createSecureCompilerConfiguration ();
354+ sh = new GroovyShell (loader , binding , cc );
355+
356+ if (canDoCleanup ) {
357+ memoryProtectedLoader = new CleanGroovyClassLoader (loader , cc );
358+ loaderF .set (sh , memoryProtectedLoader );
359+ }
360+
361+ preparedScript = sh .parse (script );
362+ scriptSandbox = GroovySandbox .createSandbox (preparedScript , Whitelist .all ());
363+ } else {
364+ sh = new GroovyShell (loader , binding );
365+ if (canDoCleanup ) {
366+ memoryProtectedLoader = new CleanGroovyClassLoader (loader );
367+ loaderF .set (sh , memoryProtectedLoader );
368+ }
369+
370+ preparedScript = sh .parse (ScriptApproval .get ().using (script , GroovyLanguage .get ()));
330371 }
372+ } catch (Exception e ) {
373+ close ();
374+ throw e ;
375+ }
376+ }
331377
378+ public Object run () throws Exception {
379+ if (sandbox ) {
380+ scriptSandbox .register ();
332381 try {
333- return GroovySandbox .run (sh . parse ( script ), Whitelist . all () );
382+ return preparedScript .run ();
334383 } catch (RejectedAccessException x ) {
335384 throw ScriptApproval .get ().accessRejected (x , ApprovalContext .create ());
385+ } finally {
386+ scriptSandbox .unregister ();
336387 }
337388 } else {
338- sh = new GroovyShell (loader , binding );
339- if (canDoCleanup ) {
340- memoryProtectedLoader = new CleanGroovyClassLoader (loader );
341- loaderF .set (sh , memoryProtectedLoader );
342- }
343- return sh .evaluate (ScriptApproval .get ().using (script , GroovyLanguage .get ()));
389+ return preparedScript .run ();
344390 }
391+ }
345392
346- } finally {
393+ @ Override
394+ public void close () throws IOException {
347395 try {
348396 if (canDoCleanup ) {
349397 cleanUpLoader (memoryProtectedLoader , new HashSet <ClassLoader >(), new HashSet <Class <?>>());
0 commit comments