Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Implemented the bootstrap logic and necessary packaging change to load
the core from a separate classloader.
  • Loading branch information
kohsuke committed Feb 17, 2017
1 parent 3cd1c36 commit d13004c
Show file tree
Hide file tree
Showing 13 changed files with 700 additions and 220 deletions.
21 changes: 21 additions & 0 deletions bootstrap/src/main/java/jenkins/bootstrap/BootLogic.java
@@ -0,0 +1,21 @@
package jenkins.bootstrap;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.ServiceLoader;

/**
* Interface for code that boots up Jenkins.
*
* <p>
* Bootstrap code uses {@link ServiceLoader} to look for subtypes of this marker interface
* and invokes {@link #contextInitialized(ServletContextEvent)} and {@link #contextDestroyed(ServletContextEvent)}
*
* @author Kohsuke Kawaguchi
*/
public interface BootLogic extends ServletContextListener {
/**
* Higher the number, the earlier it gets to run
*/
float ordinal();
}
304 changes: 304 additions & 0 deletions bootstrap/src/main/java/jenkins/bootstrap/Bootstrap.java
@@ -0,0 +1,304 @@
/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Jean-Baptiste Quenot, Tom Huybrechts
*
* 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 jenkins.bootstrap;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.Logger;

import static java.util.logging.Level.*;

/**
* Entry point when Jenkins is used as a webapp.
*
* @author Kohsuke Kawaguchi
*/
public class Bootstrap implements ServletContextListener {

private List<BootLogic> bootLogics = Collections.emptyList();
private ServletContext context;
private ClassLoader coreClassLoader;
/**
* JENKINS_HOME
*/
private File home;

/**
* Creates the sole instance of {@link jenkins.model.Jenkins} and register it to the {@link ServletContext}.
*/
public void contextInitialized(ServletContextEvent event) {
context = event.getServletContext();
context.setAttribute(Bootstrap.class.getName(),this); // allow discovery of this
try {

final FileAndDescription describedHomeDir = getHomeDir(event);
home = describedHomeDir.file.getAbsoluteFile();
home.mkdirs();
System.out.println("Jenkins home directory: "+ home +" found at: "+describedHomeDir.description);

// check that home exists (as mkdirs could have failed silently), otherwise throw a meaningful error
if (!home.exists())
throw new Error("Invalid JENKINS_HOME: "+ home);

recordBootAttempt(home);

coreClassLoader = buildCoreClassLoader();
bootLogics = loadBootLogics(coreClassLoader);

for (BootLogic b : bootLogics) {
b.contextInitialized(event);
}
} catch (Error | RuntimeException e) {
LOGGER.log(SEVERE, "Failed to initialize Jenkins",e);
throw e;
} catch (Exception e) {
LOGGER.log(SEVERE, "Failed to initialize Jenkins",e);
throw new Error(e);
}
}

public void contextDestroyed(ServletContextEvent event) {
for (BootLogic b : bootLogics) {
b.contextDestroyed(event);
}
}

/**
* {@link BootLogic}s that were found, in the order of preference.
*/
public List<BootLogic> getBootLogics() {
return Collections.unmodifiableList(bootLogics);
}

/**
* ClassLoader that loads Jenkins core.
*/
public ClassLoader getCoreClassLoader() {
return coreClassLoader;
}

/**
* Location of JENKINS_HOME.
*/
public File getHome() {
return home;
}

private ClassLoader buildCoreClassLoader() throws IOException {
List<URL> urls = new ArrayList<>();
List<DependenciesTxt> overrides = new ArrayList<>();

DependenciesTxt core = new DependenciesTxt(getClass().getClassLoader().getResourceAsStream("dependencies.txt"));

File[] plugins = new File(home, "plugins").listFiles();
if (plugins!=null) {
for (File p : plugins) {
String n = p.getName();
if (p.isFile() && (n.endsWith(".jpi") || n.endsWith(".hpi"))) {
try {
new Plugin(p).process(core, urls, overrides);
} catch (IOException e) {
LOGGER.log(WARNING, "Skipping "+p, e);
}
}
}
}

Set<String> jars = context.getResourcePaths("/WEB-INF/jars");
if (jars==null || jars.isEmpty()) {
throw new IllegalStateException("No WEB-INF/jars");
}

OUTER:
for (String jar : jars) {
if (!jar.endsWith(".jar"))
continue; // not a jar file

String justName = jar.substring(jar.lastIndexOf('/')+1);

Dependency d = core.fromFileName(justName);
for (DependenciesTxt o : overrides) {
if (o.contains(d.ga))
continue OUTER; // this jar got overriden
}

urls.add(context.getResource(jar));
}

return new URLClassLoader(urls.toArray(new URL[urls.size()]),Thread.currentThread().getContextClassLoader());
}

private List<BootLogic> loadBootLogics(ClassLoader cl) {
List<BootLogic> r = new ArrayList<>();
for (BootLogic b : ServiceLoader.load(BootLogic.class,cl)) {
r.add(b);
}

Collections.sort(r, new Comparator<BootLogic>() {
@Override
public int compare(BootLogic o1, BootLogic o2) {
return -Float.compare(o1.ordinal(),o2.ordinal());
}
});

if (r.isEmpty()) {
throw new IllegalStateException("No @BootLogic found. Aborting");
}

return r;
}

/**
* To assist boot failure script, record the number of boot attempts.
* This file gets deleted in case of successful boot.
*
* See {@code BootFailure} in core.
*/
private void recordBootAttempt(File home) {
try (FileOutputStream o=new FileOutputStream(getBootFailureFile(home), true)) {
o.write((new Date().toString() + System.getProperty("line.separator", "\n")).getBytes());
} catch (IOException e) {
LOGGER.log(WARNING, "Failed to record boot attempts",e);
}
}

/** Add some metadata to a File, allowing to trace setup issues */
public static class FileAndDescription {
public final File file;
public final String description;
public FileAndDescription(File file,String description) {
this.file = file;
this.description = description;
}
}

/**
* Determines the home directory for Jenkins.
*
* <p>
* We look for a setting that affects the smallest scope first, then bigger ones later.
*
* <p>
* People makes configuration mistakes, so we are trying to be nice
* with those by doing {@link String#trim()}.
*
* <p>
* @return the File alongside with some description to help the user troubleshoot issues
*/
public FileAndDescription getHomeDir(ServletContextEvent event) {
// check JNDI for the home directory first
for (String name : HOME_NAMES) {
try {
InitialContext iniCtxt = new InitialContext();
Context env = (Context) iniCtxt.lookup("java:comp/env");
String value = (String) env.lookup(name);
if(value!=null && value.trim().length()>0)
return new FileAndDescription(new File(value.trim()),"JNDI/java:comp/env/"+name);
// look at one more place. See issue #1314
value = (String) iniCtxt.lookup(name);
if(value!=null && value.trim().length()>0)
return new FileAndDescription(new File(value.trim()),"JNDI/"+name);
} catch (NamingException e) {
// ignore
}
}

// next the system property
for (String name : HOME_NAMES) {
String sysProp = getString(name);
if(sysProp!=null)
return new FileAndDescription(new File(sysProp.trim()),"SystemProperties.getProperty(\""+name+"\")");
}

// look at the env var next
for (String name : HOME_NAMES) {
String env = System.getenv(name);
if(env!=null)
return new FileAndDescription(new File(env.trim()).getAbsoluteFile(),"EnvVars.masterEnvVars.get(\""+name+"\")");
}

// otherwise pick a place by ourselves

String root = event.getServletContext().getRealPath("/WEB-INF/workspace");
if(root!=null) {
File ws = new File(root.trim());
if(ws.exists())
// Hudson <1.42 used to prefer this before ~/.hudson, so
// check the existence and if it's there, use it.
// otherwise if this is a new installation, prefer ~/.hudson
return new FileAndDescription(ws,"getServletContext().getRealPath(\"/WEB-INF/workspace\")");
}

File legacyHome = new File(new File(System.getProperty("user.home")),".hudson");
if (legacyHome.exists()) {
return new FileAndDescription(legacyHome,"$user.home/.hudson"); // before rename, this is where it was stored
}

File newHome = new File(new File(System.getProperty("user.home")),".jenkins");
return new FileAndDescription(newHome,"$user.home/.jenkins");
}

private String getString(String key) {
String value = System.getProperty(key); // keep passing on any exceptions
if (value != null) {
return value;
}

value = context.getInitParameter(key);
if (value != null) {
return value;
}

return null;
}

/**
* This file captures failed boot attempts.
* Every time we try to boot, we add the timestamp to this file,
* then when we boot, the file gets deleted.
*/
public static File getBootFailureFile(File home) {
return new File(home, "failed-boot-attempts.txt");
}

private static final Logger LOGGER = Logger.getLogger(Bootstrap.class.getName());

private static final String[] HOME_NAMES = {"JENKINS_HOME","HUDSON_HOME"};
}
72 changes: 72 additions & 0 deletions bootstrap/src/main/java/jenkins/bootstrap/DependenciesTxt.java
@@ -0,0 +1,72 @@
package jenkins.bootstrap;

import hudson.util.VersionNumber;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Parses {@code dependencies.txt} produced by Maven dependency:list-dependencies mojo.
*
* @author Kohsuke Kawaguchi
*/
class DependenciesTxt {
final List<Dependency> dependencies;
/**
* 'groupId:artifactId' to version number
*/
private final Map<String,VersionNumber> jars = new HashMap<>();

public DependenciesTxt(InputStream i) throws IOException {
List<Dependency> list = new ArrayList<>();

try (Reader r = new InputStreamReader(i,"UTF-8");
BufferedReader br = new BufferedReader(r)) {

// line that we care about has 5 components: groupId:artifactId:packaging:version:scope
String line;
while ((line=br.readLine())!=null) {
String[] tokens = line.trim().split(":");
if (tokens.length!=5) continue;

Dependency d = new Dependency(tokens[0], tokens[1], tokens[3]);
list.add(d);
jars.put(d.ga,d.vv);
}
}

dependencies = Collections.unmodifiableList(list);
}

/**
* Attempts to reverse map a jar file name back to {@link Dependency}
*/
public Dependency fromFileName(String jarFileName) {
for (Dependency d : dependencies) {
if (jarFileName.equals(d.a+"-"+d.v+".jar"))
return d;
}
return null;
}

/**
* Does the core have a jar file newer than the specified version?
*/
public boolean hasNewerThan(Dependency d) {
VersionNumber x = jars.get(d.ga);
return x != null && x.isNewerThan(d.vv);
}

public boolean contains(String ga) {
return jars.containsKey(ga);
}
}

0 comments on commit d13004c

Please sign in to comment.