From 5aeb3823d20ceb890e722d843865df8788814ab5 Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Thu, 23 May 2024 10:33:19 -0700 Subject: [PATCH] Forward compatibility with EE 9 cores --- pom.xml | 51 ++++ .../benchmark/jmh/JmhBenchmarkState.java | 72 ++++- .../hudson/test/ComputerConnectorTester.java | 13 +- .../org/jvnet/hudson/test/HudsonTestCase.java | 151 ++++++++-- .../hudson/test/JavaNetReverseProxy.java | 2 + .../hudson/test/JavaNetReverseProxy2.java | 98 +++++++ .../test/JenkinsComputerConnectorTester.java | 13 +- .../org/jvnet/hudson/test/JenkinsRule.java | 263 +++++++++++++++--- .../org/jvnet/hudson/test/MockFolder.java | 11 +- .../hudson/test/NoListenerConfiguration.java | 2 + .../hudson/test/NoListenerConfiguration2.java | 56 ++++ .../jvnet/hudson/test/RealJenkinsRule.java | 2 +- .../test/UnitTestSupportingPluginManager.java | 3 +- src/spotbugs/excludesFilter.xml | 3 + 14 files changed, 663 insertions(+), 77 deletions(-) create mode 100644 src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy2.java create mode 100644 src/main/java/org/jvnet/hudson/test/NoListenerConfiguration2.java diff --git a/pom.xml b/pom.xml index f0541b525..dfce2c4b4 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,13 @@ THE SOFTWARE. pom import + + org.eclipse.jetty.ee9 + jetty-ee9-bom + ${jetty.version} + pom + import + org.jenkins-ci annotation-indexer @@ -112,6 +119,12 @@ THE SOFTWARE. support-log-formatter 1.2 + + io.jenkins.servlet + javax-servlet-api + + 4.0.7-rc18.81a_78f068132 + junit junit @@ -131,6 +144,11 @@ THE SOFTWARE. org.eclipse.jetty.ee8 jetty-ee8-webapp + + + org.eclipse.jetty.toolchain + jetty-servlet-api + org.slf4j @@ -142,6 +160,39 @@ THE SOFTWARE. org.eclipse.jetty.ee8.websocket jetty-ee8-websocket-jetty-server + + + + jakarta.annotation + jakarta.annotation-api + + + + org.eclipse.jetty.toolchain + jetty-servlet-api + + + + org.slf4j + slf4j-api + + + + + org.eclipse.jetty.ee9 + jetty-ee9-webapp + + + + org.slf4j + slf4j-api + + + + + org.eclipse.jetty.ee9.websocket + jetty-ee9-websocket-jetty-server + diff --git a/src/main/java/jenkins/benchmark/jmh/JmhBenchmarkState.java b/src/main/java/jenkins/benchmark/jmh/JmhBenchmarkState.java index 8ba0f8173..78278b8b6 100644 --- a/src/main/java/jenkins/benchmark/jmh/JmhBenchmarkState.java +++ b/src/main/java/jenkins/benchmark/jmh/JmhBenchmarkState.java @@ -1,22 +1,27 @@ package jenkins.benchmark.jmh; import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.PluginManager; import hudson.model.Hudson; import hudson.model.RootAction; import hudson.security.ACL; +import jakarta.servlet.ServletContext; +import java.io.File; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import javax.servlet.ServletContext; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; -import org.eclipse.jetty.ee8.webapp.WebAppContext; +import org.eclipse.jetty.ee9.webapp.WebAppContext; import org.eclipse.jetty.server.Server; +import org.junit.internal.AssumptionViolatedException; import org.jvnet.hudson.test.JavaNetReverseProxy; +import org.jvnet.hudson.test.JavaNetReverseProxy2; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.TemporaryDirectoryAllocator; import org.jvnet.hudson.test.TestPluginManager; @@ -79,7 +84,11 @@ public final void terminateJenkins() { } finally { JenkinsRule._stopJenkins(server, null, jenkins); try { - JavaNetReverseProxy.getInstance().stop(); + if (_isEE9Plus()) { + JavaNetReverseProxy2.getInstance().stop(); + } else { + JavaNetReverseProxy.getInstance().stop(); + } } catch (Exception e) { LOGGER.log(Level.WARNING, "Unable to stop JavaNetReverseProxy server", e); } @@ -93,17 +102,45 @@ public final void terminateJenkins() { } private void launchInstance() throws Exception { - WebAppContext context = JenkinsRule._createWebAppContext( - contextPath, - localPort::set, - getClass().getClassLoader(), - localPort.get(), - JenkinsRule::_configureUserRealm); - server = context.getServer(); - - ServletContext webServer = context.getServletContext(); + if (_isEE9Plus()) { + WebAppContext context = JenkinsRule._createWebAppContext2( + contextPath, + localPort::set, + getClass().getClassLoader(), + localPort.get(), + JenkinsRule::_configureUserRealm); + server = context.getServer(); + ServletContext webServer = context.getServletContext(); + try { + jenkins = Hudson.class + .getDeclaredConstructor(File.class, ServletContext.class, PluginManager.class) + .newInstance(temporaryDirectoryAllocator.allocate(), webServer, TestPluginManager.INSTANCE); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof InterruptedException) { + throw new AssumptionViolatedException("Jenkins startup interrupted", e.getCause()); + } else if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + } else { + org.eclipse.jetty.ee8.webapp.WebAppContext context = JenkinsRule._createWebAppContext( + contextPath, + localPort::set, + getClass().getClassLoader(), + localPort.get(), + JenkinsRule::_configureUserRealm); + server = context.getServer(); + javax.servlet.ServletContext webServer = context.getServletContext(); + jenkins = new Hudson(temporaryDirectoryAllocator.allocate(), webServer, TestPluginManager.INSTANCE); + } - jenkins = new Hudson(temporaryDirectoryAllocator.allocate(), webServer, TestPluginManager.INSTANCE); JenkinsRule._configureJenkinsForTest(jenkins); JenkinsRule._configureUpdateCenter(jenkins); jenkins.getActions().add(this); @@ -113,6 +150,15 @@ private void launchInstance() throws Exception { LOGGER.log(Level.INFO, "Running on {0}", url); } + private static boolean _isEE9Plus() { + try { + Jenkins.class.getDeclaredMethod("getServletContext"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + private URL getJenkinsURL() throws MalformedURLException { return new URL("http://localhost:" + localPort.get() + contextPath + "/"); } diff --git a/src/main/java/org/jvnet/hudson/test/ComputerConnectorTester.java b/src/main/java/org/jvnet/hudson/test/ComputerConnectorTester.java index 8eb9db97a..0bb6e73f1 100644 --- a/src/main/java/org/jvnet/hudson/test/ComputerConnectorTester.java +++ b/src/main/java/org/jvnet/hudson/test/ComputerConnectorTester.java @@ -28,9 +28,8 @@ import hudson.model.Descriptor; import hudson.slaves.ComputerConnector; import hudson.slaves.ComputerConnectorDescriptor; -import java.io.IOException; import java.util.List; -import javax.servlet.ServletException; +import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; /** @@ -47,8 +46,14 @@ public ComputerConnectorTester(HudsonTestCase testCase) { this.testCase = testCase; } - public void doConfigSubmit(StaplerRequest req) throws IOException, ServletException { - connector = req.bindJSON(ComputerConnector.class, req.getSubmittedForm().getJSONObject("connector")); + public void doConfigSubmit(StaplerRequest req) { + JSONObject form; + try { + form = req.getSubmittedForm(); + } catch (Exception e) { + throw new RuntimeException(e); + } + connector = req.bindJSON(ComputerConnector.class, form.getJSONObject("connector")); } public List getConnectorDescriptors() { diff --git a/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java b/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java index ff5145725..a69127a9c 100644 --- a/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java +++ b/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java @@ -90,6 +90,8 @@ import hudson.util.ReflectionUtils; import hudson.util.StreamTaskListener; import hudson.util.jna.GNUCLibrary; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; import java.beans.PropertyDescriptor; import java.io.BufferedReader; import java.io.File; @@ -100,6 +102,7 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -126,8 +129,6 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; import jenkins.model.Jenkins; import jenkins.model.JenkinsAdaptor; import jenkins.model.JenkinsLocationConfiguration; @@ -139,10 +140,10 @@ import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.beanutils.PropertyUtils; -import org.eclipse.jetty.ee8.webapp.Configuration; -import org.eclipse.jetty.ee8.webapp.WebAppContext; -import org.eclipse.jetty.ee8.webapp.WebXmlConfiguration; -import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.ee9.webapp.Configuration; +import org.eclipse.jetty.ee9.webapp.WebAppContext; +import org.eclipse.jetty.ee9.webapp.WebXmlConfiguration; +import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.security.HashLoginService; @@ -354,9 +355,44 @@ protected void setUp() throws Exception { jenkins.setCrumbIssuer(new TestCrumbIssuer()); - jenkins.servletContext.setAttribute("app", jenkins); - jenkins.servletContext.setAttribute("version","?"); - WebAppMain.installExpressionFactory(new ServletContextEvent(jenkins.servletContext)); + if (_isEE9Plus()) { + ServletContext servletContext; + try { + servletContext = (ServletContext) Jenkins.class.getDeclaredMethod("getServletContext").invoke(jenkins); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + servletContext.setAttribute("app", jenkins); + servletContext.setAttribute("version", "?"); + try { + WebAppMain.class.getDeclaredMethod("installExpressionFactory", ServletContextEvent.class).invoke(null, new ServletContextEvent(servletContext)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + } else { + javax.servlet.ServletContext servletContext = jenkins.servletContext; + servletContext.setAttribute("app", jenkins); + servletContext.setAttribute("version", "?"); + WebAppMain.installExpressionFactory(new javax.servlet.ServletContextEvent(servletContext)); + } JenkinsLocationConfiguration.get().setUrl(getURL().toString()); // set a default JDK to be the one that the harness is using. @@ -380,6 +416,15 @@ protected void setUp() throws Exception { setUpTimeout(); } + private static boolean _isEE9Plus() { + try { + Jenkins.class.getDeclaredMethod("getServletContext"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + protected void setUpTimeout() { if (timeout <= 0) { // no timeout @@ -405,7 +450,8 @@ public void run() { * By default, we load updates from local proxy to avoid network traffic as much as possible. */ protected void configureUpdateCenter() throws Exception { - final String updateCenterUrl = "http://localhost:"+ JavaNetReverseProxy.getInstance().localPort+"/update-center.json"; + int localPort = _isEE9Plus() ? JavaNetReverseProxy2.getInstance().localPort : JavaNetReverseProxy.getInstance().localPort; + final String updateCenterUrl = "http://localhost:" + localPort + "/update-center.json"; // don't waste bandwidth talking to the update center DownloadService.neverUpdate = true; @@ -519,11 +565,34 @@ public String getUrlName() { * you can override it. */ protected Hudson newHudson() throws Exception { - File home = homeLoader.allocate(); - for (Runner r : recipes) { - r.decorateHome(this,home); + if (_isEE9Plus()) { + File home = homeLoader.allocate(); + for (Runner r : recipes) { + r.decorateHome(this,home); + } + try { + return Hudson.class + .getDeclaredConstructor(File.class, ServletContext.class, PluginManager.class) + .newInstance(home, createWebServer2(), useLocalPluginManager ? null : pluginManager); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + } else { + File home = homeLoader.allocate(); + for (Runner r : recipes) { + r.decorateHome(this,home); + } + return new Hudson(home, createWebServer(), useLocalPluginManager ? null : pluginManager); } - return new Hudson(home, createWebServer(), useLocalPluginManager ? null : pluginManager); } /** @@ -544,7 +613,7 @@ public void setPluginManager(PluginManager pluginManager) { * Prepares a webapp hosting environment to get {@link ServletContext} implementation * that we need for testing. */ - protected ServletContext createWebServer() throws Exception { + protected ServletContext createWebServer2() throws Exception { QueuedThreadPool qtp = new QueuedThreadPool(); qtp.setName("Jetty (HudsonTestCase)"); server = new Server(qtp); @@ -560,7 +629,7 @@ protected ClassLoader configureClassLoader(ClassLoader loader) { context.setResourceBase(explodedWarDir.getPath()); context.setClassLoader(getClass().getClassLoader()); context.setConfigurations(new Configuration[]{new WebXmlConfiguration()}); - context.addBean(new NoListenerConfiguration(context)); + context.addBean(new NoListenerConfiguration2(context)); context.setServer(server); server.setHandler(context); JettyWebSocketServletContainerInitializer.configure(context, null); @@ -583,6 +652,52 @@ protected ClassLoader configureClassLoader(ClassLoader loader) { return context.getServletContext(); } + /** + * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation + * that we need for testing. + * + * @deprecated use {@link #createWebServer2()} + */ + @Deprecated + protected javax.servlet.ServletContext createWebServer() throws Exception { + QueuedThreadPool qtp = new QueuedThreadPool(); + qtp.setName("Jetty (HudsonTestCase)"); + server = new Server(qtp); + + explodedWarDir = WarExploder.getExplodedDir(); + org.eclipse.jetty.ee8.webapp.WebAppContext context = new org.eclipse.jetty.ee8.webapp.WebAppContext(explodedWarDir.getPath(), contextPath) { + @Override + protected ClassLoader configureClassLoader(ClassLoader loader) { + // Use flat classpath in tests + return loader; + } + }; + context.setResourceBase(explodedWarDir.getPath()); + context.setClassLoader(getClass().getClassLoader()); + context.setConfigurations(new org.eclipse.jetty.ee8.webapp.Configuration[]{new org.eclipse.jetty.ee8.webapp.WebXmlConfiguration()}); + context.addBean(new NoListenerConfiguration(context)); + context.setServer(server); + server.setHandler(context); + org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer.configure(context, null); + context.getSecurityHandler().setLoginService(configureUserRealm()); + + ServerConnector connector = new ServerConnector(server); + + HttpConfiguration config = connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration(); + // use a bigger buffer as Stapler traces can get pretty large on deeply nested URL + config.setRequestHeaderSize(12 * 1024); + config.setHttpCompliance(HttpCompliance.RFC7230); + config.setUriCompliance(UriCompliance.LEGACY); + connector.setHost("localhost"); + + server.addConnector(connector); + server.start(); + + localPort = connector.getLocalPort(); + + return context.getServletContext(); + } + /** * Configures a security realm for a test. */ @@ -1763,7 +1878,7 @@ public String getContextPath() throws IOException { public WebRequest addCrumb(WebRequest req) { NameValuePair crumb = new NameValuePair( jenkins.getCrumbIssuer().getDescriptor().getCrumbRequestField(), - jenkins.getCrumbIssuer().getCrumb( null )); + jenkins.getCrumbIssuer().getCrumb((javax.servlet.ServletRequest) null)); req.setRequestParameters(List.of(crumb)); return req; } @@ -1774,7 +1889,7 @@ public WebRequest addCrumb(WebRequest req) { public URL createCrumbedUrl(String relativePath) throws IOException { CrumbIssuer issuer = jenkins.getCrumbIssuer(); String crumbName = issuer.getDescriptor().getCrumbRequestField(); - String crumb = issuer.getCrumb(null); + String crumb = issuer.getCrumb((javax.servlet.ServletRequest) null); return new URL(getContextPath()+relativePath+"?"+crumbName+"="+crumb); } diff --git a/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy.java b/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy.java index ceab85e34..34b18f353 100644 --- a/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy.java +++ b/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy.java @@ -24,7 +24,9 @@ * The contents are cached locally. * * @author Kohsuke Kawaguchi + * @deprecated use {@link JavaNetReverseProxy2} */ +@Deprecated public class JavaNetReverseProxy extends HttpServlet { private final Server server; public final int localPort; diff --git a/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy2.java b/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy2.java new file mode 100644 index 000000000..471167d64 --- /dev/null +++ b/src/main/java/org/jvnet/hudson/test/JavaNetReverseProxy2.java @@ -0,0 +1,98 @@ +package org.jvnet.hudson.test; + +import hudson.Util; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import org.apache.commons.io.FileUtils; +import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + * Acts as a reverse proxy, so that during a test we can avoid hitting updates.jenkins.io. + * + *

+ * The contents are cached locally. + * + * @author Kohsuke Kawaguchi + */ +public class JavaNetReverseProxy2 extends HttpServlet { + private final Server server; + public final int localPort; + + private final File cacheFolder; + + public JavaNetReverseProxy2(File cacheFolder) throws Exception { + this.cacheFolder = cacheFolder; + cacheFolder.mkdirs(); + QueuedThreadPool qtp = new QueuedThreadPool(); + qtp.setName("Jetty (JavaNetReverseProxy)"); + server = new Server(qtp); + + ContextHandlerCollection contexts = new ContextHandlerCollection(); + server.setHandler(contexts); + + ServletContextHandler root = new ServletContextHandler(contexts, "/", ServletContextHandler.SESSIONS); + root.addServlet(new ServletHolder(this), "/"); + + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + server.start(); + + localPort = connector.getLocalPort(); + } + + public void stop() throws Exception { + server.stop(); + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String path = req.getServletPath(); + String d = Util.getDigestOf(path); + + File cache = new File(cacheFolder, d); + synchronized(this) { + if (!cache.exists()) { + URL url = new URL("https://updates.jenkins.io/" + path); + FileUtils.copyURLToFile(url,cache); + } + } + + resp.setContentType(getMimeType(path)); + Files.copy(cache.toPath(), resp.getOutputStream()); + } + + private String getMimeType(String path) { + int idx = path.indexOf('?'); + if (idx >= 0) { + path = path.substring(0,idx); + } + if (path.endsWith(".json")) { + return "text/javascript"; + } + return getServletContext().getMimeType(path); + } + + private static volatile JavaNetReverseProxy2 INSTANCE; + + /** + * Gets the default instance. + */ + public static synchronized JavaNetReverseProxy2 getInstance() throws Exception { + if (INSTANCE == null) { + // TODO: think of a better location --- ideally inside the target/ dir so that clean would wipe them out + INSTANCE = new JavaNetReverseProxy2(new File(new File(System.getProperty("java.io.tmpdir")),"jenkins.io-cache2")); + } + return INSTANCE; + } +} diff --git a/src/main/java/org/jvnet/hudson/test/JenkinsComputerConnectorTester.java b/src/main/java/org/jvnet/hudson/test/JenkinsComputerConnectorTester.java index 54c3604f8..efce5f309 100644 --- a/src/main/java/org/jvnet/hudson/test/JenkinsComputerConnectorTester.java +++ b/src/main/java/org/jvnet/hudson/test/JenkinsComputerConnectorTester.java @@ -28,9 +28,8 @@ import hudson.model.Descriptor; import hudson.slaves.ComputerConnector; import hudson.slaves.ComputerConnectorDescriptor; -import java.io.IOException; import java.util.List; -import javax.servlet.ServletException; +import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; /** @@ -48,8 +47,14 @@ public JenkinsComputerConnectorTester(JenkinsRule testCase) { this.jenkinsRule = testCase; } - public void doConfigSubmit(StaplerRequest req) throws IOException, ServletException { - connector = req.bindJSON(ComputerConnector.class, req.getSubmittedForm().getJSONObject("connector")); + public void doConfigSubmit(StaplerRequest req) { + JSONObject form; + try { + form = req.getSubmittedForm(); + } catch (Exception e) { + throw new RuntimeException(e); + } + connector = req.bindJSON(ComputerConnector.class, form.getJSONObject("connector")); } public List getConnectorDescriptors() { diff --git a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java b/src/main/java/org/jvnet/hudson/test/JenkinsRule.java index 50b587ca8..d732d22af 100644 --- a/src/main/java/org/jvnet/hudson/test/JenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/JenkinsRule.java @@ -100,6 +100,8 @@ import hudson.util.ReflectionUtils; import hudson.util.StreamTaskListener; import hudson.util.jna.GNUCLibrary; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.io.BufferedReader; @@ -162,8 +164,6 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; import jenkins.model.Jenkins; import jenkins.model.JenkinsAdaptor; import jenkins.model.JenkinsLocationConfiguration; @@ -174,10 +174,10 @@ import net.sf.json.JSONObject; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.io.FileUtils; -import org.eclipse.jetty.ee8.webapp.Configuration; -import org.eclipse.jetty.ee8.webapp.WebAppContext; -import org.eclipse.jetty.ee8.webapp.WebXmlConfiguration; -import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.ee9.webapp.Configuration; +import org.eclipse.jetty.ee9.webapp.WebAppContext; +import org.eclipse.jetty.ee9.webapp.WebXmlConfiguration; +import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.security.HashLoginService; @@ -432,6 +432,15 @@ public void before() throws Throwable { JenkinsLocationConfiguration.get().setUrl(getURL().toString()); } + private static boolean _isEE9Plus() { + try { + Jenkins.class.getDeclaredMethod("getServletContext"); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + /** * Configures a Jenkins instance for test. * @@ -441,9 +450,44 @@ public void before() throws Throwable { */ public static void _configureJenkinsForTest(Jenkins jenkins) throws Exception { jenkins.setNoUsageStatistics(true); // collecting usage stats from tests is pointless. - jenkins.servletContext.setAttribute("app", jenkins); - jenkins.servletContext.setAttribute("version", "?"); - WebAppMain.installExpressionFactory(new ServletContextEvent(jenkins.servletContext)); + if (_isEE9Plus()) { + ServletContext servletContext; + try { + servletContext = (ServletContext) Jenkins.class.getDeclaredMethod("getServletContext").invoke(jenkins); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + servletContext.setAttribute("app", jenkins); + servletContext.setAttribute("version", "?"); + try { + WebAppMain.class.getDeclaredMethod("installExpressionFactory", ServletContextEvent.class).invoke(null, new ServletContextEvent(servletContext)); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } + } else { + javax.servlet.ServletContext servletContext = jenkins.servletContext; + servletContext.setAttribute("app", jenkins); + servletContext.setAttribute("version", "?"); + WebAppMain.installExpressionFactory(new javax.servlet.ServletContextEvent(servletContext)); + } // set a default JDK to be the one that the harness is using. jenkins.getJDKs().add(new JDK("default", System.getProperty("java.home"))); @@ -474,7 +518,8 @@ public static void _configureUpdateCenter(Jenkins jenkins) throws Exception { final String updateCenterUrl; jettyLevel(Level.WARNING); try { - updateCenterUrl = "http://localhost:"+ JavaNetReverseProxy.getInstance().localPort+"/update-center.json"; + int localPort = _isEE9Plus() ? JavaNetReverseProxy2.getInstance().localPort : JavaNetReverseProxy.getInstance().localPort; + updateCenterUrl = "http://localhost:" + localPort + "/update-center.json"; } finally { jettyLevel(Level.INFO); } @@ -737,17 +782,45 @@ public String getUrlName() { */ protected Hudson newHudson() throws Exception { jettyLevel(Level.WARNING); - ServletContext webServer = createWebServer(); - File home = homeLoader.allocate(); - for (JenkinsRecipe.Runner r : recipes) { - r.decorateHome(this,home); - } - try { - return new Hudson(home, webServer, getPluginManager()); - } catch (InterruptedException x) { - throw new AssumptionViolatedException("Jenkins startup interrupted", x); - } finally { - jettyLevel(Level.INFO); + if (_isEE9Plus()) { + ServletContext webServer = createWebServer2(); + File home = homeLoader.allocate(); + for (JenkinsRecipe.Runner r : recipes) { + r.decorateHome(this, home); + } + try { + return Hudson.class + .getDeclaredConstructor(File.class, ServletContext.class, PluginManager.class) + .newInstance(home, webServer, getPluginManager()); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof InterruptedException) { + throw new AssumptionViolatedException("Jenkins startup interrupted", e.getCause()); + } else if (t instanceof Exception) { + throw (Exception) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw e; + } + } finally { + jettyLevel(Level.INFO); + } + } else { + javax.servlet.ServletContext webServer = createWebServer(); + File home = homeLoader.allocate(); + for (JenkinsRecipe.Runner r : recipes) { + r.decorateHome(this, home); + } + try { + return new Hudson(home, webServer, getPluginManager()); + } catch (InterruptedException e) { + throw new AssumptionViolatedException("Jenkins startup interrupted", e); + } finally { + jettyLevel(Level.INFO); + } } } @@ -783,23 +856,23 @@ public File getWebAppRoot() throws Exception { } /** - * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation + * Prepares a webapp hosting environment to get {@link jakarta.servlet.ServletContext} implementation * that we need for testing. */ - protected ServletContext createWebServer() throws Exception { - return createWebServer(null); + protected ServletContext createWebServer2() throws Exception { + return createWebServer2(null); } /** - * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation + * Prepares a webapp hosting environment to get {@link jakarta.servlet.ServletContext} implementation * that we need for testing. * * @param contextAndServerConsumer configures the {@link WebAppContext} and the {@link Server} for the instance, before they are started * @since 2.63 */ - protected ServletContext createWebServer(@CheckForNull BiConsumer contextAndServerConsumer) + protected ServletContext createWebServer2(@CheckForNull BiConsumer contextAndServerConsumer) throws Exception { - WebAppContext context = _createWebAppContext( + WebAppContext context = _createWebAppContext2( contextPath, (x) -> localPort = x, getClass().getClassLoader(), @@ -822,14 +895,14 @@ protected ServletContext createWebServer(@CheckForNull BiConsumer portSetter, ClassLoader classLoader, int localPort, Supplier loginServiceSupplier) throws Exception { - return _createWebAppContext(contextPath, portSetter, classLoader, localPort, loginServiceSupplier, null); + return _createWebAppContext2(contextPath, portSetter, classLoader, localPort, loginServiceSupplier, null); } /** * Creates a web server on which Jenkins can run @@ -843,7 +916,7 @@ public static WebAppContext _createWebAppContext( * @return the {@link Server} * @since 2.50 */ - public static WebAppContext _createWebAppContext( + public static WebAppContext _createWebAppContext2( String contextPath, Consumer portSetter, ClassLoader classLoader, @@ -864,7 +937,7 @@ protected ClassLoader configureClassLoader(ClassLoader loader) { }; context.setClassLoader(classLoader); context.setConfigurations(new Configuration[]{new WebXmlConfiguration()}); - context.addBean(new NoListenerConfiguration(context)); + context.addBean(new NoListenerConfiguration2(context)); context.setServer(server); server.setHandler(context); JettyWebSocketServletContainerInitializer.configure(context, null); @@ -895,6 +968,130 @@ protected ClassLoader configureClassLoader(ClassLoader loader) { return context; } + /** + * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation + * that we need for testing. + * + * @deprecated {use {@link #createWebServer2()}} + */ + @Deprecated + protected javax.servlet.ServletContext createWebServer() throws Exception { + return createWebServer(null); + } + + /** + * Prepares a webapp hosting environment to get {@link javax.servlet.ServletContext} implementation + * that we need for testing. + * + * @deprecated use {@link #createWebServer2(BiConsumer)} + * @param contextAndServerConsumer configures the {@link org.eclipse.jetty.ee8.webapp.WebAppContext} and the {@link Server} for the instance, before they are started + * @since 2.63 + */ + @Deprecated + protected javax.servlet.ServletContext createWebServer( + @CheckForNull BiConsumer contextAndServerConsumer) + throws Exception { + org.eclipse.jetty.ee8.webapp.WebAppContext context = _createWebAppContext( + contextPath, + (x) -> localPort = x, + getClass().getClassLoader(), + localPort, + this::configureUserRealm, + contextAndServerConsumer); + server = context.getServer(); + LOGGER.log(Level.INFO, "Running on {0}", getURL()); + return context.getServletContext(); + } + + /** + * Creates a web server on which Jenkins can run + * + * @param contextPath the context path at which to put Jenkins + * @param portSetter the port on which the server runs will be set using this function + * @param classLoader the class loader for the {@link org.eclipse.jetty.ee8.webapp.WebAppContext} + * @param localPort port on which the server runs + * @param loginServiceSupplier configures the {@link LoginService} for the instance + * @return the {@link Server} + * @deprecated use {@link #_createWebAppContext2(String, Consumer, ClassLoader, int, Supplier)} + * @since 2.50 + */ + @Deprecated + public static org.eclipse.jetty.ee8.webapp.WebAppContext _createWebAppContext( + String contextPath, + Consumer portSetter, + ClassLoader classLoader, + int localPort, + Supplier loginServiceSupplier) + throws Exception { + return _createWebAppContext(contextPath, portSetter, classLoader, localPort, loginServiceSupplier, null); + } + + /** + * Creates a web server on which Jenkins can run + * + * @param contextPath the context path at which to put Jenkins + * @param portSetter the port on which the server runs will be set using this function + * @param classLoader the class loader for the {@link org.eclipse.jetty.ee8.webapp.WebAppContext} + * @param localPort port on which the server runs + * @param loginServiceSupplier configures the {@link LoginService} for the instance + * @param contextAndServerConsumer configures the {@link org.eclipse.jetty.ee8.webapp.WebAppContext} and the {@link Server} for the instance, before they are started + * @return the {@link Server} + * @deprecated use {@link #_createWebAppContext2(String, Consumer, ClassLoader, int, Supplier, BiConsumer)} + * @since 2.50 + */ + @Deprecated + public static org.eclipse.jetty.ee8.webapp.WebAppContext _createWebAppContext( + String contextPath, + Consumer portSetter, + ClassLoader classLoader, + int localPort, + Supplier loginServiceSupplier, + @CheckForNull BiConsumer contextAndServerConsumer) + throws Exception { + QueuedThreadPool qtp = new QueuedThreadPool(); + qtp.setName("Jetty (JenkinsRule)"); + Server server = new Server(qtp); + + org.eclipse.jetty.ee8.webapp.WebAppContext context = new org.eclipse.jetty.ee8.webapp.WebAppContext(WarExploder.getExplodedDir().getPath(), contextPath) { + @Override + protected ClassLoader configureClassLoader(ClassLoader loader) { + // Use flat classpath in tests + return loader; + } + }; + context.setClassLoader(classLoader); + context.setConfigurations(new org.eclipse.jetty.ee8.webapp.Configuration[]{new org.eclipse.jetty.ee8.webapp.WebXmlConfiguration()}); + context.addBean(new NoListenerConfiguration(context)); + context.setServer(server); + server.setHandler(context); + org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer.configure(context, null); + context.getSecurityHandler().setLoginService(loginServiceSupplier.get()); + context.setResourceBase(WarExploder.getExplodedDir().getPath()); + + ServerConnector connector = new ServerConnector(server); + HttpConfiguration config = connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration(); + // use a bigger buffer as Stapler traces can get pretty large on deeply nested URL + config.setRequestHeaderSize(12 * 1024); + config.setHttpCompliance(HttpCompliance.RFC7230); + config.setUriCompliance(UriCompliance.LEGACY); + connector.setHost("localhost"); + if (System.getProperty("port") != null) { + connector.setPort(Integer.parseInt(System.getProperty("port"))); + } else if (localPort != 0) { + connector.setPort(localPort); + } + + server.addConnector(connector); + if (contextAndServerConsumer != null) { + contextAndServerConsumer.accept(context, server); + } + server.start(); + + portSetter.accept(connector.getLocalPort()); + + return context; + } + /** * Configures a security realm for a test. */ @@ -2827,7 +3024,7 @@ public WebRequest addCrumb(WebRequest req) { public URL createCrumbedUrl(String relativePath) throws IOException { CrumbIssuer issuer = jenkins.getCrumbIssuer(); String crumbName = issuer.getDescriptor().getCrumbRequestField(); - String crumb = issuer.getCrumb(null); + String crumb = issuer.getCrumb((javax.servlet.ServletRequest) null); if (relativePath.indexOf('?') == -1) { return new URL(getContextPath()+relativePath+"?"+crumbName+"="+crumb); } @@ -3062,6 +3259,6 @@ public Description getTestDescription() { private NameValuePair getCrumbHeaderNVP() { return new NameValuePair(jenkins.getCrumbIssuer().getDescriptor().getCrumbRequestField(), - jenkins.getCrumbIssuer().getCrumb( null )); + jenkins.getCrumbIssuer().getCrumb((javax.servlet.ServletRequest) null)); } } diff --git a/src/main/java/org/jvnet/hudson/test/MockFolder.java b/src/main/java/org/jvnet/hudson/test/MockFolder.java index 33bc9c3b3..86affdd0b 100644 --- a/src/main/java/org/jvnet/hudson/test/MockFolder.java +++ b/src/main/java/org/jvnet/hudson/test/MockFolder.java @@ -53,7 +53,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import javax.servlet.ServletException; import jenkins.model.DirectlyModifiableTopLevelItemGroup; import jenkins.model.Jenkins; import org.kohsuke.stapler.StaplerFallback; @@ -162,8 +161,14 @@ public T createProject(@NonNull Class type, @NonNull return type.cast(createProject((TopLevelItemDescriptor) Jenkins.get().getDescriptorOrDie(type), name, true)); } - @Override public TopLevelItem doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - return mixin().createTopLevelItem(req, rsp); + @Override public TopLevelItem doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException { + try { + return mixin().createTopLevelItem(req, rsp); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override public String getUrlChildPrefix() { diff --git a/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration.java b/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration.java index 2b6f7c9d3..0a58b21f2 100644 --- a/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration.java +++ b/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration.java @@ -37,7 +37,9 @@ * with the home directory of our choice. * * @author Kohsuke Kawaguchi + * @deprecated use {@link NoListenerConfiguration2} */ +@Deprecated public class NoListenerConfiguration extends AbstractLifeCycle { private final WebAppContext context; diff --git a/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration2.java b/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration2.java new file mode 100644 index 000000000..366176d16 --- /dev/null +++ b/src/main/java/org/jvnet/hudson/test/NoListenerConfiguration2.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jvnet.hudson.test; + +import hudson.WebAppMain; +import jakarta.servlet.ServletContextListener; +import java.util.EventListener; +import org.eclipse.jetty.ee9.webapp.WebAppContext; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +/** + * Kills off the {@link WebAppMain} {@link ServletContextListener}. + * + *

+ * This is so that the harness can create the {@link jenkins.model.Jenkins} object. + * with the home directory of our choice. + * + * @author Kohsuke Kawaguchi + */ +public class NoListenerConfiguration2 extends AbstractLifeCycle { + private final WebAppContext context; + + public NoListenerConfiguration2(WebAppContext context) { + this.context = context; + } + + @Override + protected void doStart() { + for (EventListener eventListener : context.getEventListeners()) { + if (eventListener instanceof WebAppMain) { + context.removeEventListener(eventListener); + } + } + } +} diff --git a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java index 65aa6278f..2ffa24e11 100644 --- a/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RealJenkinsRule.java @@ -1278,7 +1278,7 @@ public void doStep(StaplerRequest req, StaplerResponse rsp) throws Throwable { public HttpResponse doExit(@QueryParameter String token) throws IOException { checkToken(token); try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) { - return Jenkins.get().doSafeExit(null); + return Jenkins.get().doSafeExit((StaplerRequest) null); } } public void doTimeout(@QueryParameter String token) { diff --git a/src/main/java/org/jvnet/hudson/test/UnitTestSupportingPluginManager.java b/src/main/java/org/jvnet/hudson/test/UnitTestSupportingPluginManager.java index 517c44016..78fbec888 100644 --- a/src/main/java/org/jvnet/hudson/test/UnitTestSupportingPluginManager.java +++ b/src/main/java/org/jvnet/hudson/test/UnitTestSupportingPluginManager.java @@ -42,6 +42,7 @@ import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; +import javax.servlet.ServletContext; import org.apache.commons.io.FileUtils; import org.junit.Assert; @@ -62,7 +63,7 @@ public class UnitTestSupportingPluginManager extends PluginManager { public UnitTestSupportingPluginManager(File rootDir) { - super(null, new File(rootDir, "plugins")); + super((ServletContext) null, new File(rootDir, "plugins")); } /** @see LocalPluginManager#loadBundledPlugins */ diff --git a/src/spotbugs/excludesFilter.xml b/src/spotbugs/excludesFilter.xml index 9e97602fd..831b3e6d4 100644 --- a/src/spotbugs/excludesFilter.xml +++ b/src/spotbugs/excludesFilter.xml @@ -183,6 +183,7 @@ + @@ -213,6 +214,7 @@ + @@ -231,6 +233,7 @@ +