/*******************************************************************************
*
* Copyright (c) 2004-2009 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Seiji Sogabe, CloudBees, Inc.
*
*
*******************************************************************************/
package hudson.lifecycle;
import hudson.model.ManagementLink;
import hudson.model.Hudson;
import hudson.AbortException;
import hudson.Extension;
import hudson.Functions;
import hudson.util.StreamTaskListener;
import org.eclipse.hudson.jna.NativeAccessException;
import org.eclipse.hudson.jna.NativeUtils;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.tools.ant.taskdefs.Move;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.types.FileSet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.net.URL;
/**
* {@link ManagementLink} that allows the installation as a Windows service.
*
* @author Kohsuke Kawaguchi
*/
public class WindowsInstallerLink extends ManagementLink {
/**
* Location of the hudson.war. In general case, we can't determine this
* value, yet having this is a requirement for the installer.
*/
private final File hudsonWar;
/**
* If the installation is completed, this value holds the installation
* directory.
*/
private volatile File installationDir;
private WindowsInstallerLink(File hudsonWar) {
this.hudsonWar = hudsonWar;
}
public String getIconFileName() {
return "installer.png";
}
public String getUrlName() {
return "install";
}
public String getDisplayName() {
return Messages.WindowsInstallerLink_DisplayName();
}
public String getDescription() {
return Messages.WindowsInstallerLink_Description();
}
/**
* Is the installation successful?
*/
public boolean isInstalled() {
return installationDir != null;
}
/**
* Performs installation.
*/
public void doDoInstall(StaplerRequest req, StaplerResponse rsp, @QueryParameter("dir") String _dir) throws IOException, ServletException {
NativeUtils nativeUtils = NativeUtils.getInstance();
if (installationDir != null) {
// installation already complete
sendError("Installation is already complete", req, rsp);
return;
}
try {
if (!nativeUtils.isDotNetInstalled(2, 0)) {
sendError(".NET Framework 2.0 or later is required for this feature", req, rsp);
}
} catch (NativeAccessException exc) {
if (exc.getMessage().contains("Native Windows Support plugin not installed")) {
sendError("Java Native Access support plugin is not installed. It is required to create Windows Service", req, rsp);
} else {
StringWriter stWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stWriter);
exc.printStackTrace(writer);
sendError("Native Windows function isDotNetInstalled() failed. " + stWriter.toString(), req, rsp);
}
}
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
File dir = new File(_dir).getAbsoluteFile();
dir.mkdirs();
if (!dir.exists()) {
sendError("Failed to create installation directory: " + dir, req, rsp);
return;
}
try {
// copy files over there
copy(req, rsp, dir, getClass().getResource("/windows-service/hudson.exe"), "hudson.exe");
copy(req, rsp, dir, getClass().getResource("/windows-service/hudson.xml"), "hudson.xml");
if (!hudsonWar.getCanonicalFile().equals(new File(dir, "hudson.war").getCanonicalFile())) {
copy(req, rsp, dir, hudsonWar.toURI().toURL(), "hudson.war");
}
// install as a service
ByteArrayOutputStream baos = new ByteArrayOutputStream();
StreamTaskListener task = new StreamTaskListener(baos);
task.getLogger().println("Installing a service");
int r = WindowsSlaveInstaller.runElevated(
new File(dir, "hudson.exe"), "install", task, dir, nativeUtils);
if (r != 0) {
sendError(baos.toString(), req, rsp);
return;
}
// installation was successful
installationDir = dir;
rsp.sendRedirect(".");
} catch (AbortException e) {
// this exception is used as a signal to terminate processing. the error should have been already reported
} catch (InterruptedException e) {
throw new ServletException(e);
}
}
/**
* Copies a single resource into the target folder, by the given name, and
* handle errors gracefully.
*/
private void copy(StaplerRequest req, StaplerResponse rsp, File dir, URL src, String name) throws ServletException, IOException {
try {
FileUtils.copyURLToFile(src, new File(dir, name));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to copy " + name, e);
sendError("Failed to copy " + name + ": " + e.getMessage(), req, rsp);
throw new AbortException();
}
}
public void doRestart(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
if (installationDir == null) {
// if the user reloads the page after Hudson has restarted,
// it comes back here. In such a case, don't let this restart Hudson.
// so just send them back to the top page
rsp.sendRedirect(Functions.getRequestRootPath(req) + "/");
return;
}
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
rsp.forward(this, "_restart", req);
final File oldRoot = Hudson.getInstance().getRootDir();
final NativeUtils nativeUtils = NativeUtils.getInstance();
// initiate an orderly shutdown after we finished serving this request
new Thread("terminator") {
@Override
public void run() {
try {
Thread.sleep(1000);
// let the service start after we close our sockets, to avoid conflicts
Runtime.getRuntime().addShutdownHook(new Thread("service starter") {
@Override
public void run() {
try {
if (!oldRoot.equals(installationDir)) {
LOGGER.info("Moving data");
Move mv = new Move();
Project p = new Project();
p.addBuildListener(createLogger());
mv.setProject(p);
FileSet fs = new FileSet();
fs.setDir(oldRoot);
fs.setExcludes("war/**"); // we can't really move the exploded war.
mv.addFileset(fs);
mv.setTodir(installationDir);
mv.setFailOnError(false); // plugins can also fail to move
mv.execute();
}
LOGGER.info("Starting a Windows service");
StreamTaskListener task = StreamTaskListener.fromStdout();
int r = WindowsSlaveInstaller.runElevated(
new File(installationDir, "hudson.exe"), "start", task, installationDir, nativeUtils);
task.getLogger().println(r == 0 ? "Successfully started" : "start service failed. Exit code=" + r);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private DefaultLogger createLogger() {
DefaultLogger logger = new DefaultLogger();
logger.setOutputPrintStream(System.out);
logger.setErrorPrintStream(System.err);
return logger;
}
});
System.exit(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
/**
* Displays the error in a page.
*/
protected final void sendError(Exception e, StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
sendError(e.getMessage(), req, rsp);
}
protected final void sendError(String message, StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
req.setAttribute("message", message);
req.setAttribute("pre", true);
rsp.forward(Hudson.getInstance(), "error", req);
}
/**
* Decide if {@link WindowsInstallerLink} should show up in UI, and if so,
* register it.
*/
@Extension
public static ManagementLink registerIfApplicable() {
if (!Functions.isWindows()) {
return null; // this is a Windows only feature
}
if (Lifecycle.get() instanceof WindowsServiceLifecycle) {
return null; // already installed as Windows service
}
// this system property is set by the launcher when we run "java -jar hudson.war"
// and this is how we know where is hudson.war.
String war = System.getProperty("executable-war");
if (war != null && new File(war).exists()) {
WindowsInstallerLink link = new WindowsInstallerLink(new File(war));
// in certain situations where we know the user is just trying Hudson (like when Hudson is launched
// from JNLP from https://hudson.java.net/), also put this link on the navigation bar to increase
// visibility
if (System.getProperty(WindowsInstallerLink.class.getName() + ".prominent") != null) {
Hudson.getInstance().getActions().add(link);
}
return link;
}
return null;
}
private static final Logger LOGGER = Logger.getLogger(WindowsInstallerLink.class.getName());
}