package net.sourceforge.cruisecontrol.distributed.util; import org.apache.log4j.Logger; import net.jini.core.lookup.ServiceItem; import net.jini.core.lookup.ServiceID; import net.jini.core.lookup.ServiceRegistrar; import net.sourceforge.cruisecontrol.distributed.BuildAgentService; import net.sourceforge.cruisecontrol.distributed.BuildAgentEntryOverrideUI; import net.sourceforge.cruisecontrol.distributed.core.MulticastDiscovery; import net.sourceforge.cruisecontrol.distributed.core.CCDistVersion; import net.sourceforge.cruisecontrol.distributed.core.PreferencesHelper; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JButton; import javax.swing.JTextArea; import javax.swing.JScrollPane; import javax.swing.JComboBox; import javax.swing.JCheckBox; import javax.swing.ComboBoxModel; import javax.swing.DefaultComboBoxModel; import javax.swing.text.BadLocationException; import javax.swing.SwingUtilities; import javax.swing.Action; import javax.swing.AbstractAction; import javax.swing.JOptionPane; import java.rmi.RemoteException; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; import java.awt.Window; import java.awt.Image; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.prefs.Preferences; /** * @author Dan Rollo * Date: Aug 1, 2005 * Time: 4:00:38 PM */ public final class BuildAgentUtility { private static final Logger LOG = Logger.getLogger(BuildAgentUtility.class); // helps make UI class testable in a headless environment static interface UISetInfo { void setInfo(final String infoText); } // @todo make BuidAgentService implement/extend jini ServiceUI? static class UI extends JFrame implements PreferencesHelper.UIPreferences, UISetInfo { private static final int CONSOLE_LINE_BUFFER_SIZE = 1000; private final String origTitle; private final BuildAgentUtility buildAgentUtility; private final JButton btnRefresh = new JButton("Refresh"); private final JComboBox cmbAgents = new JComboBox(); private final Action atnInvoke; private final Action atnEditEntries; private final Action atnLiveOutput; private final Action atnListLookupServices; private static final String METH_RESTART = "restart"; private static final String METH_KILL = "kill"; private final JComboBox cmbRestartOrKill = new JComboBox(new String[] {METH_RESTART, METH_KILL}); private final JCheckBox chkAfterBuildFinished = new JCheckBox("Wait for build to finish.", true); private final JButton btnInvokeOnAll = new JButton("Invoke on All"); private final JPanel pnlEdit = new JPanel(new BorderLayout()); private final JButton btnClose = new JButton("Close"); private final JTextArea txaConsole = new JTextArea(); private final JScrollPane scrConsole = new JScrollPane(); private static final Preferences PREFS_BASE = Preferences.userNodeForPackage(UI.class); static Preferences getPrefsRoot() { return PREFS_BASE; } /** * No-arg constructor for use in unit tests, to be overridden by MockUI. */ UI() { origTitle = null; buildAgentUtility = null; atnInvoke = null; atnEditEntries = null; atnLiveOutput = null; atnListLookupServices = null; } private UI(final BuildAgentUtility buildAgentUtil) { super("CruiseControl - Agent Utility " + CCDistVersion.getVersion()); origTitle = getTitle(); buildAgentUtility = buildAgentUtil; btnClose.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { exitForm(); } }); addWindowListener(new WindowAdapter() { public void windowClosing(final WindowEvent evt) { exitForm(); } }); btnRefresh.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { refreshAgentList(); } }); cmbAgents.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { atnInvoke.setEnabled(true); atnEditEntries.setEnabled(true); atnLiveOutput.setEnabled(true); } }); atnInvoke = new AbstractAction("Invoke") { public void actionPerformed(final ActionEvent e) { try { invokeOnAgent( ((ComboItemWrapper) cmbAgents.getSelectedItem()).getAgent() ); } catch (RemoteException e1) { checkAndShowRestartRequiresWebStart(e1); LOG.info(e1.getMessage()); appendInfo(e1.getMessage()); throw new RuntimeException(e1); } } }; atnInvoke.setEnabled(false); atnEditEntries = new AbstractAction("Entries") { public void actionPerformed(final ActionEvent e) { try { final ComboItemWrapper agentWrapper = ((ComboItemWrapper) cmbAgents.getSelectedItem()); final BuildAgentService agentService = agentWrapper.getAgent(); new BuildAgentEntryOverrideUI(BuildAgentUtility.UI.this, agentService, agentService.getMachineName() + ": " + agentWrapper.getServiceID()); } catch (RemoteException e1) { final String msg = "An error occurred while editing entry overrides: "; LOG.error(msg, e1); appendInfo(msg + e1.getMessage()); JOptionPane.showMessageDialog(BuildAgentUtility.UI.this, msg + "\n\n" + e1.getMessage(), "Error Editing Entry Overrides", JOptionPane.ERROR_MESSAGE); throw new RuntimeException(e1); } } }; atnEditEntries.setEnabled(false); atnLiveOutput = new AbstractAction("Live Output") { public void actionPerformed(final ActionEvent e) { final ComboItemWrapper agentWrapper = ((ComboItemWrapper) cmbAgents.getSelectedItem()); final BuildAgentService agentService = agentWrapper.getAgent(); final String agentMachineName; try { agentMachineName = agentService.getMachineName(); } catch (RemoteException e1) { throw new RuntimeException(e1); } final LiveOutputUI liveOutputUI = new LiveOutputUI(BuildAgentUtility.UI.this, agentService, agentMachineName + ": " + agentWrapper.getServiceID(), atnLiveOutput); liveOutputUI.setVisible(true); liveOutputUI.pack(); atnLiveOutput.setEnabled(false); } }; atnLiveOutput.setEnabled(false); atnListLookupServices = new AbstractAction("Lookup Services") { public void actionPerformed(final ActionEvent e) { final LookupServiceUI lookupServiceUI = new LookupServiceUI(BuildAgentUtility.UI.this, buildAgentUtil, atnListLookupServices); lookupServiceUI.setVisible(true); lookupServiceUI.pack(); atnListLookupServices.setEnabled(false); } }; btnInvokeOnAll.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { for (int i = 0; i < cmbAgents.getItemCount(); i++) { try { invokeOnAgent(((ComboItemWrapper) cmbAgents.getItemAt(i)).getAgent()); } catch (RemoteException e1) { checkAndShowRestartRequiresWebStart(e1); LOG.info(e1.getMessage()); appendInfo(e1.getMessage()); //throw new RuntimeException(e1); // allow remaining items to be invoked } } } }); txaConsole.setFont(new Font("Courier New", 0, 12)); scrConsole.setViewportView(txaConsole); scrConsole.setPreferredSize(new Dimension(626, 300)); getContentPane().setLayout(new BorderLayout()); final JPanel pnlNN = new JPanel(new BorderLayout()); pnlNN.add(btnRefresh, BorderLayout.WEST); pnlNN.add(cmbAgents, BorderLayout.CENTER); final JPanel pnlButtonsTop = new JPanel(new GridLayout(1, 0)); final JButton btnInvoke = new JButton(atnInvoke); btnInvoke.setPreferredSize(new Dimension(76, -1)); pnlButtonsTop.add(btnInvoke); pnlButtonsTop.add(new JButton(atnEditEntries)); pnlNN.add(pnlButtonsTop, BorderLayout.EAST); final JPanel pnlNS = new JPanel(new BorderLayout()); pnlNS.add(new JButton(atnListLookupServices), BorderLayout.WEST); pnlNS.add(btnClose, BorderLayout.EAST); pnlEdit.add(cmbRestartOrKill, BorderLayout.WEST); final JPanel pnlMiddleCenter = new JPanel(new BorderLayout()); pnlMiddleCenter.add(chkAfterBuildFinished, BorderLayout.WEST); pnlMiddleCenter.add(new JButton(atnLiveOutput), BorderLayout.EAST); pnlEdit.add(pnlMiddleCenter, BorderLayout.CENTER); pnlEdit.add(btnInvokeOnAll, BorderLayout.EAST); final JPanel northPanel = new JPanel(new BorderLayout()); northPanel.add(pnlNN, BorderLayout.NORTH); northPanel.add(pnlEdit, BorderLayout.CENTER); northPanel.add(pnlNS, BorderLayout.SOUTH); getContentPane().add(northPanel, BorderLayout.NORTH); getContentPane().add(scrConsole, BorderLayout.CENTER); final Image imgIcon = PreferencesHelper.getCCImageIcon(); if (imgIcon != null) { setIconImage(imgIcon); } pack(); // set screen info from last run PreferencesHelper.applyWindowInfo(this); setVisible(true); } private void checkAndShowRestartRequiresWebStart(RemoteException e1) { final String msg = checkRestartRequiresWebStart(e1); if (msg != null) { LOG.info(msg); appendInfo(msg); } } private void invokeOnAgent(final BuildAgentService agent) throws RemoteException { if (METH_RESTART.equals(cmbRestartOrKill.getSelectedItem())) { agent.restart(chkAfterBuildFinished.isSelected()); } else { agent.kill(chkAfterBuildFinished.isSelected()); } } // begin implementation of UIPreferences public Preferences getPrefsBase() { return UI.getPrefsRoot(); } public Window getWindow() { return this; } // end implementation of UIPreferences private static final class ComboItemWrapper { private static ComboItemWrapper[] wrapArray(final ServiceItem[] serviceItems) { final ComboItemWrapper[] result = new ComboItemWrapper[serviceItems.length]; for (int i = 0; i < serviceItems.length; i++) { result[i] = new ComboItemWrapper(serviceItems[i]); } return result; } private final ServiceItem serviceItem; private ComboItemWrapper(final ServiceItem serviceItemToWrap) { this.serviceItem = serviceItemToWrap; } public BuildAgentService getAgent() { return (BuildAgentService) serviceItem.service; } public ServiceID getServiceID() { return serviceItem.serviceID; } private String errToString; public String toString() { // don't make remote calls if agent has errored out already, otherwise may appear to hang ui if (errToString != null) { return errToString; } try { return getAgent().getMachineName() + ": " + serviceItem.serviceID; } catch (RemoteException e) { errToString = "Remote Error: " + e.getMessage(); } catch (Exception e) { errToString = "Error: " + e.getMessage(); } return errToString; } } private void refreshAgentList() { //"BuildAgentUtility btn.disable Thread" SwingUtilities.invokeLater(new Runnable() { public void run() { btnRefresh.setEnabled(false); atnInvoke.setEnabled(false); atnLiveOutput.setEnabled(false); atnEditEntries.setEnabled(false); btnInvokeOnAll.setEnabled(false); cmbAgents.setEnabled(false); } }); new Thread("BuildAgentUtility refreshAgentList Thread") { public void run() { doRefreshAgentList(); } } .start(); } private void doRefreshAgentList() { try { final List<ServiceItem> tmpList = new ArrayList<ServiceItem>(); final String agentInfoAll = buildAgentUtility.getAgentInfoAll(tmpList); final ServiceItem[] serviceItems = tmpList.toArray(new ServiceItem[tmpList.size()]); final ComboBoxModel comboBoxModel = new DefaultComboBoxModel( ComboItemWrapper.wrapArray(serviceItems)); //"BuildAgentUtility setcomboBoxModel Thread" SwingUtilities.invokeLater(new Runnable() { public void run() { updateLUSCountUI(buildAgentUtility.lastLUSCount); cmbAgents.setModel(comboBoxModel); } }); setInfo(agentInfoAll); } finally { //"BuildAgentUtility btn.enable Thread" SwingUtilities.invokeLater(new Runnable() { public void run() { btnRefresh.setEnabled(true); btnInvokeOnAll.setEnabled(true); cmbAgents.setEnabled(true); } }); } } void updateLUSCountUI(final int lusCount) { // make sure lastLUSCount matches the on given, could be different when called from LookupServiceUI buildAgentUtility.lastLUSCount = lusCount; UI.this.setTitle(origTitle + ", LUS's: " + buildAgentUtility.lastLUSCount); } private void exitForm() { // save screen info PreferencesHelper.saveWindowInfo(this); System.exit(0); } // Only public to allow testing of UI in headless environs public void setInfo(final String infoText) { LOG.debug(infoText); //"BuildAgentUtility txaConsole.setInfo Thread" SwingUtilities.invokeLater(new Runnable() { public void run() { txaConsole.setText(infoText); } }); } private void appendInfo(final String infoText) { //"BuildAgentUtility txaConsole.appendInfo Thread" SwingUtilities.invokeLater(new Runnable() { public void run() { txaConsole.append(infoText + "\n"); if (txaConsole.getLineCount() > CONSOLE_LINE_BUFFER_SIZE) { // remove old lines try { txaConsole.replaceRange("", 0, txaConsole.getLineEndOffset( txaConsole.getLineCount() - CONSOLE_LINE_BUFFER_SIZE )); } catch (BadLocationException e) { //ignore } } // Make sure the last line is always visible txaConsole.setCaretPosition(txaConsole.getDocument().getLength()); } }); } } private final UISetInfo ui; private int lastLUSCount; private boolean isFailFast; private boolean isInited; public static BuildAgentUtility createForJMX() { return new BuildAgentUtility(true); } static final String SYS_PROP_IS_FAIL_FAST = "agentUtilFailFast"; private BuildAgentUtility(final boolean isHeadlessUtil) { if (!isHeadlessUtil) { throw new IllegalStateException("this contructor must be passed a param value of true"); } ui = new UISetInfo() { public void setInfo(String infoText) { LOG.info(infoText); } }; isFailFast = Boolean.getBoolean(SYS_PROP_IS_FAIL_FAST); } private BuildAgentUtility() { this(null); } BuildAgentUtility(final UISetInfo mockUI) { CCDistVersion.printCCDistVersion(); if (mockUI == null) { ui = new UI(this); ((UI) ui).btnRefresh.doClick(); } else { ui = mockUI; isFailFast = true; } } public int getLastLUSCount() { return lastLUSCount; } /** Number of seconds to wait for Lookup Services to report in. */ public static final int LUS_WAIT_SECONDS = 5; public String getAgentInfoAll(final List<ServiceItem> lstServiceItems) { final StringBuilder result = new StringBuilder(); try { if (!isInited && !isFailFast) { try { MulticastDiscovery.begin(); final String msgWaitLUS = "Waiting " + LUS_WAIT_SECONDS + " seconds for registrars to report in..."; ui.setInfo(msgWaitLUS); LOG.info(msgWaitLUS); Thread.sleep(LUS_WAIT_SECONDS * 1000); isInited = true; } catch (InterruptedException e1) { LOG.warn("Sleep interrupted", e1); } } final String waitMessage = "Waiting for Build Agents to report in..."; ui.setInfo(waitMessage); LOG.info(waitMessage); final ServiceItem[] serviceItems = MulticastDiscovery.findBuildAgentServices(null, (isFailFast ? 0 : MulticastDiscovery.DEFAULT_FIND_WAIT_DUR_MILLIS)); // update LUS count lastLUSCount = MulticastDiscovery.getLUSCount(); // clear and rebuild list lstServiceItems.clear(); lstServiceItems.addAll(Arrays.asList(serviceItems)); LOG.info("Agents found: " + serviceItems.length); result.append("Found: ").append(serviceItems.length).append(" agent") .append(serviceItems.length != 1 ? "s" : "") .append(".\n"); BuildAgentService agent; String agentInfo; for (ServiceItem serviceItem : serviceItems) { agent = (BuildAgentService) serviceItem.service; agentInfo = "Build Agent: " + serviceItem.serviceID + "\n" + agent.asString() + MulticastDiscovery.toStringEntries(serviceItem.attributeSets) + "\n"; LOG.debug(agentInfo); result.append(agentInfo); } } catch (RemoteException e) { final String message = "Search failed due to an unexpected error"; LOG.error(message, e); throw new RuntimeException(message, e); } return result.toString(); } public ServiceRegistrar[] getValidRegistrars() { return MulticastDiscovery.getValidRegistrars(); } public void destroyLookupService(final ServiceRegistrar registrar) throws RemoteException { MulticastDiscovery.destroyLookupService(registrar, MulticastDiscovery.DEFAULT_FIND_WAIT_DUR_MILLIS); } public static String checkRestartRequiresWebStart(final RemoteException e) { if (e.getCause() != null && e.getCause() instanceof ClassNotFoundException && "javax.jnlp.UnavailableServiceException".equals(e.getCause().getMessage())) { return "\nNOTE: Restart feature is only available on Agents launched via WebStart.\n" + e.getCause().getMessage(); } return null; } public static void main(final String[] args) { new BuildAgentUtility(); } }