forked from jenkinsci/jenkins
-
Notifications
You must be signed in to change notification settings - Fork 0
/
WebAppMain.java
404 lines (357 loc) · 16.4 KB
/
WebAppMain.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
/*
* 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 hudson;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
import com.thoughtworks.xstream.core.JVM;
import hudson.model.Hudson;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.util.AWTProblem;
import hudson.util.BootFailure;
import hudson.util.ChartUtil;
import hudson.util.HudsonFailedToLoad;
import hudson.util.HudsonIsLoading;
import hudson.util.IncompatibleAntVersionDetected;
import hudson.util.IncompatibleServletVersionDetected;
import hudson.util.IncompatibleVMDetected;
import hudson.util.InsufficientPermissionDetected;
import hudson.util.NoHomeDir;
import hudson.util.NoTempDir;
import hudson.util.RingBufferLogHandler;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.StandardOpenOption;
import java.security.Security;
import java.util.Date;
import java.util.EnumSet;
import java.util.Locale;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletResponse;
import javax.servlet.SessionTrackingMode;
import jenkins.model.Jenkins;
import jenkins.util.JenkinsJVM;
import jenkins.util.SystemProperties;
import org.apache.tools.ant.types.FileSet;
import org.jvnet.localizer.LocaleProvider;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.jelly.JellyFacet;
/**
* Entry point when Hudson is used as a webapp.
*
* @author Kohsuke Kawaguchi
*/
public class WebAppMain implements ServletContextListener {
/**
* System property name to force the session tracking by cookie.
* This prevents Tomcat to use the URL tracking in addition to the cookie by default.
* This could be useful for instances that requires to have
* the {@link jenkins.security.SuspiciousRequestFilter#allowSemicolonsInPath} turned off.
* <p>
* If you allow semicolon in URL and the session to be tracked by URL and you have
* a SecurityRealm that does not invalidate session after authentication,
* your instance is vulnerable to session hijacking.
* <p>
* The SecurityRealm should be corrected but this is a hardening in Jenkins core.
* <p>
* As this property is read during startup, you will not be able to change it at runtime
* depending on your application server (not possible with Jetty nor Tomcat)
* <p>
* When running hpi:run, the default tracking is COOKIE+URL.
* When running java -jar with Winstone/Jetty, the default setting is set to COOKIE only.
* When running inside Tomcat, the default setting is COOKIE+URL.
*/
@Restricted(NoExternalUse.class)
public static final String FORCE_SESSION_TRACKING_BY_COOKIE_PROP = WebAppMain.class.getName() + ".forceSessionTrackingByCookie";
private final RingBufferLogHandler handler = new RingBufferLogHandler(WebAppMain.getDefaultRingBufferSize()) {
@Override public synchronized void publish(LogRecord record) {
if (record.getLevel().intValue() >= Level.INFO.intValue()) {
super.publish(record);
}
}
};
/**
* This getter returns the int DEFAULT_RING_BUFFER_SIZE from the class RingBufferLogHandler from a static context.
* Exposes access from RingBufferLogHandler.DEFAULT_RING_BUFFER_SIZE to WebAppMain.
* Written for the requirements of JENKINS-50669
* @return int This returns DEFAULT_RING_BUFFER_SIZE
* @see <a href="https://issues.jenkins.io/browse/JENKINS-50669">JENKINS-50669</a>
* @since 2.259
*/
public static int getDefaultRingBufferSize() {
return RingBufferLogHandler.getDefaultRingBufferSize();
}
private static final String APP = "app";
private boolean terminated;
private Thread initThread;
/**
* Creates the sole instance of {@link jenkins.model.Jenkins} and register it to the {@link ServletContext}.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
// Nicer console log formatting when using mvn jetty:run.
if (Main.isDevelopmentMode && System.getProperty("java.util.logging.config.file") == null) {
try {
Formatter formatter = (Formatter) Class.forName("io.jenkins.lib.support_log_formatter.SupportLogFormatter").getDeclaredConstructor().newInstance();
for (Handler h : Logger.getLogger("").getHandlers()) {
if (h instanceof ConsoleHandler) {
((ConsoleHandler) h).setFormatter(formatter);
}
}
} catch (ClassNotFoundException x) {
// ignore
} catch (Exception x) {
LOGGER.log(Level.WARNING, null, x);
}
}
JenkinsJVMAccess._setJenkinsJVM(true);
final ServletContext context = event.getServletContext();
File home = null;
try {
// use the current request to determine the language
LocaleProvider.setProvider(new LocaleProvider() {
@Override
public Locale get() {
return Functions.getCurrentLocale();
}
});
// quick check to see if we (seem to) have enough permissions to run. (see JENKINS-719)
JVM jvm;
try {
jvm = new JVM();
new URLClassLoader(new URL[0], getClass().getClassLoader());
} catch (SecurityException e) {
throw new InsufficientPermissionDetected(e);
}
try { // remove Sun PKCS11 provider if present. See http://wiki.jenkins-ci.org/display/JENKINS/Solaris+Issue+6276483
Security.removeProvider("SunPKCS11-Solaris");
} catch (SecurityException e) {
// ignore this error.
}
installLogger();
final FileAndDescription describedHomeDir = getHomeDir(event);
home = describedHomeDir.file.getAbsoluteFile();
try {
Util.createDirectories(home.toPath());
} catch (IOException | InvalidPathException e) {
throw (NoHomeDir) new NoHomeDir(home).initCause(e);
}
LOGGER.info("Jenkins home directory: " + home + " found at: " + describedHomeDir.description);
recordBootAttempt(home);
// make sure that we are using XStream in the "enhanced" (JVM-specific) mode
if (jvm.bestReflectionProvider().getClass() == PureJavaReflectionProvider.class) {
throw new IncompatibleVMDetected(); // nope
}
// make sure this is servlet 2.4 container or above
try {
ServletResponse.class.getMethod("setCharacterEncoding", String.class);
} catch (NoSuchMethodException e) {
throw (IncompatibleServletVersionDetected) new IncompatibleServletVersionDetected(ServletResponse.class).initCause(e);
}
// make sure that we see Ant 1.7
try {
FileSet.class.getMethod("getDirectoryScanner");
} catch (NoSuchMethodException e) {
throw (IncompatibleAntVersionDetected) new IncompatibleAntVersionDetected(FileSet.class).initCause(e);
}
// make sure AWT is functioning, or else JFreeChart won't even load.
if (ChartUtil.awtProblemCause != null) {
throw new AWTProblem(ChartUtil.awtProblemCause);
}
// some containers (in particular Tomcat) doesn't abort a launch
// even if the temp directory doesn't exist.
// check that and report an error
try {
File f = File.createTempFile("test", "test");
boolean result = f.delete();
if (!result) {
LOGGER.log(FINE, "Temp file test.test could not be deleted.");
}
} catch (IOException e) {
throw new NoTempDir(e);
}
installExpressionFactory(event);
context.setAttribute(APP, new HudsonIsLoading());
if (SystemProperties.getBoolean(FORCE_SESSION_TRACKING_BY_COOKIE_PROP, true)) {
context.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));
}
final File _home = home;
initThread = new Thread("Jenkins initialization thread") {
@Override
public void run() {
boolean success = false;
try {
Jenkins instance = new Hudson(_home, context);
// one last check to make sure everything is in order before we go live
if (Thread.interrupted())
throw new InterruptedException();
context.setAttribute(APP, instance);
Files.deleteIfExists(BootFailure.getBootFailureFile(_home).toPath());
// at this point we are open for business and serving requests normally
Jenkins.get().getLifecycle().onReady();
success = true;
} catch (Error e) {
new HudsonFailedToLoad(e).publish(context, _home);
throw e;
} catch (Exception e) {
new HudsonFailedToLoad(e).publish(context, _home);
} finally {
Jenkins instance = Jenkins.getInstanceOrNull();
if (!success && instance != null)
instance.cleanUp();
}
}
};
initThread.start();
} catch (BootFailure e) {
e.publish(context, home);
} catch (Error | RuntimeException e) {
LOGGER.log(SEVERE, "Failed to initialize Jenkins", e);
throw e;
}
}
public void joinInit() throws InterruptedException {
initThread.join();
}
/**
* To assist boot failure script, record the number of boot attempts.
* This file gets deleted in case of successful boot.
*
* @see BootFailure
*/
private void recordBootAttempt(File home) {
try (OutputStream o = Files.newOutputStream(BootFailure.getBootFailureFile(home).toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
o.write((new Date() + System.getProperty("line.separator", "\n")).getBytes(Charset.defaultCharset()));
} catch (IOException | InvalidPathException e) {
LOGGER.log(WARNING, "Failed to record boot attempts", e);
}
}
public static void installExpressionFactory(ServletContextEvent event) {
JellyFacet.setExpressionFactory(event, new ExpressionFactory2());
}
/**
* Installs log handler to monitor all Hudson logs.
*/
private void installLogger() {
Jenkins.logRecords = handler.getView();
Logger.getLogger("").addHandler(handler);
}
/** 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 make configuration mistakes, so we are trying to be nice
* with those by doing {@link String#trim()}.
*
* @return the File alongside with some description to help the user troubleshoot issues
*/
public FileAndDescription getHomeDir(ServletContextEvent event) {
// check the system property for the home directory first
for (String name : HOME_NAMES) {
String sysProp = SystemProperties.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 = EnvVars.masterEnvVars.get(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");
}
@Override
public void contextDestroyed(ServletContextEvent event) {
try (ACLContext old = ACL.as2(ACL.SYSTEM2)) {
Jenkins instance = Jenkins.getInstanceOrNull();
try {
if (instance != null) {
instance.cleanUp();
}
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, "Failed to clean up. Restart will continue.", e);
}
terminated = true;
Thread t = initThread;
if (t != null && t.isAlive()) {
LOGGER.log(Level.INFO, "Shutting down a Jenkins instance that was still starting up", new Throwable("reason"));
t.interrupt();
}
// Logger is in the system classloader, so if we don't do this
// the whole web app will never be undeployed.
Logger.getLogger("").removeHandler(handler);
} finally {
JenkinsJVMAccess._setJenkinsJVM(false);
}
}
private static final Logger LOGGER = Logger.getLogger(WebAppMain.class.getName());
private static final class JenkinsJVMAccess extends JenkinsJVM {
private static void _setJenkinsJVM(boolean jenkinsJVM) {
JenkinsJVM.setJenkinsJVM(jenkinsJVM);
}
}
private static final String[] HOME_NAMES = {"JENKINS_HOME", "HUDSON_HOME"};
}