/******************************************************************************* * * Copyright (c) 2004-2011 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: * * Inc., Kohsuke Kawaguchi, Winston Prakash * * *******************************************************************************/ package hudson.lifecycle; import hudson.Launcher.LocalLauncher; import hudson.Util; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.remoting.Engine; import hudson.remoting.jnlp.MainDialog; import hudson.remoting.jnlp.MainMenu; import hudson.util.StreamTaskListener; import org.eclipse.hudson.jna.NativeAccessException; import org.eclipse.hudson.jna.NativeUtils; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.ByteArrayOutputStream; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URL; import java.util.logging.Level; import static javax.swing.JOptionPane.*; /** * @author Kohsuke Kawaguchi */ public class WindowsSlaveInstaller implements Callable<Void, RuntimeException>, ActionListener { /** * Root directory of this slave. String, not File because the platform can * be different. */ private final String rootDir; private transient Engine engine; private transient MainDialog dialog; private NativeUtils nativeUtils = NativeUtils.getInstance(); public WindowsSlaveInstaller(String rootDir, NativeUtils nativeUtils) { this.rootDir = rootDir; } public WindowsSlaveInstaller(String rootDir) { this.rootDir = rootDir; } public Void call() { if (File.separatorChar == '/') { return null; // not Windows } if (System.getProperty("hudson.showWindowsServiceInstallLink") == null) { return null; // only show this when it makes sense, which is when we run from JNLP } dialog = MainDialog.get(); if (dialog == null) { return null; // can't find the main window. Maybe not running with GUI } // capture the engine engine = Engine.current(); SwingUtilities.invokeLater(new Runnable() { public void run() { MainMenu mainMenu = dialog.getMainMenu(); JMenu m = mainMenu.getFileMenu(); JMenuItem menu = new JMenuItem(Messages.WindowsInstallerLink_DisplayName(), KeyEvent.VK_W); menu.addActionListener(WindowsSlaveInstaller.this); m.add(menu); mainMenu.commit(); } }); return null; } /** * Invokes slave.exe with a SCM management command. * * <p> If it fails in a way that indicates the presence of UAC, retry in an * UAC compatible manner. */ static int runElevated(File slaveExe, String command, TaskListener out, File pwd, NativeUtils nativeUtils) throws IOException, InterruptedException { try { return new LocalLauncher(out).launch().cmds(slaveExe, command).stdout(out).pwd(pwd).join(); } catch (IOException e) { if (e.getMessage().contains("CreateProcess") && e.getMessage().contains("=740")) { // fall through } else { throw e; } } String logFile = "redirect.log"; try { return nativeUtils.windowsExec(slaveExe, command, logFile, pwd); } catch (NativeAccessException ex) { Logger.getLogger(WindowsSlaveInstaller.class.getName()).log(Level.SEVERE, null, ex); return -1; } finally { FileInputStream fin = null; try { fin = new FileInputStream(new File(pwd, "redirect.log")); IOUtils.copy(fin, out.getLogger()); } finally { IOUtils.closeQuietly(fin); } } } /** * Called when the install menu is selected */ public void actionPerformed(ActionEvent e) { try { int r = JOptionPane.showConfirmDialog(dialog, Messages.WindowsSlaveInstaller_ConfirmInstallation(), Messages.WindowsInstallerLink_DisplayName(), OK_CANCEL_OPTION); if (r != JOptionPane.OK_OPTION) { return; } if (!nativeUtils.isDotNetInstalled(2, 0)) { JOptionPane.showMessageDialog(dialog, Messages.WindowsSlaveInstaller_DotNetRequired(), Messages.WindowsInstallerLink_DisplayName(), ERROR_MESSAGE); return; } final File dir = new File(rootDir); if (!dir.exists()) { JOptionPane.showMessageDialog(dialog, Messages.WindowsSlaveInstaller_RootFsDoesntExist(rootDir), Messages.WindowsInstallerLink_DisplayName(), ERROR_MESSAGE); return; } final File slaveExe = new File(dir, "hudson-slave.exe"); FileUtils.copyURLToFile(getClass().getResource("/windows-service/hudson.exe"), slaveExe); // write out the descriptor URL jnlp = new URL(engine.getHudsonUrl(), "computer/" + Util.rawEncode(engine.slaveName) + "/slave-agent.jnlp"); String xml = generateSlaveXml( generateServiceId(rootDir), System.getProperty("java.home") + "\\bin\\java.exe", "-jnlpUrl " + jnlp.toExternalForm()); FileUtils.writeStringToFile(new File(dir, "hudson-slave.xml"), xml, "UTF-8"); // copy slave.jar URL slaveJar = new URL(engine.getHudsonUrl(), "jnlpJars/remoting.jar"); File dstSlaveJar = new File(dir, "slave.jar").getCanonicalFile(); if (!dstSlaveJar.exists()) // perhaps slave.jar is already there? { FileUtils.copyURLToFile(slaveJar, dstSlaveJar); } // install as a service ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamTaskListener task = new StreamTaskListener(baos); r = runElevated(slaveExe, "install", task, dir, nativeUtils); if (r != 0) { JOptionPane.showMessageDialog( dialog, baos.toString(), "Error", ERROR_MESSAGE); return; } r = JOptionPane.showConfirmDialog(dialog, Messages.WindowsSlaveInstaller_InstallationSuccessful(), Messages.WindowsInstallerLink_DisplayName(), OK_CANCEL_OPTION); if (r != JOptionPane.OK_OPTION) { return; } // let the service start after we close our connection, to avoid conflicts Runtime.getRuntime().addShutdownHook(new Thread("service starter") { public void run() { try { StreamTaskListener task = StreamTaskListener.fromStdout(); int r = runElevated(slaveExe, "start", task, dir, nativeUtils); task.getLogger().println(r == 0 ? "Successfully started" : "start service failed. Exit code=" + r); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }); System.exit(0); } catch (Exception t) {// this runs as a JNLP app, so if we let an exeption go, we'll never find out why it failed StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); JOptionPane.showMessageDialog(dialog, sw.toString(), "Error", ERROR_MESSAGE); } } public static String generateServiceId(String slaveRoot) throws IOException { return "hudsonslave-" + slaveRoot.replace(':', '_').replace('\\', '_').replace('/', '_'); } public static String generateSlaveXml(String id, String java, String args) throws IOException { String xml = IOUtils.toString(WindowsSlaveInstaller.class.getResourceAsStream("/windows-service/hudson-slave.xml"), "UTF-8"); xml = xml.replace("@ID@", id); xml = xml.replace("@JAVA@", java); xml = xml.replace("@ARGS@", args); return xml; } private static final long serialVersionUID = 1L; }