├── .gitignore ├── Jenkinsfile ├── src ├── main │ ├── webapp │ │ └── icons │ │ │ └── 48x48 │ │ │ └── attribute.png │ ├── groovy │ │ └── org │ │ │ └── jenkinsci │ │ │ └── plugins │ │ │ └── adaptivedisonnector │ │ │ └── Updater.groovy │ └── java │ │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── adaptivedisonnector │ │ └── FailureListener.java └── test │ └── java │ └── org │ └── jenkinsci │ └── plugins │ └── adaptivedisonnector │ └── FailureListenerTest.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !.gitignore 3 | 4 | /work 5 | /target 6 | 7 | *.iml 8 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | /* `buildPlugin` step provided by: https://github.com/jenkins-infra/pipeline-library */ 2 | buildPlugin() 3 | -------------------------------------------------------------------------------- /src/main/webapp/icons/48x48/attribute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/adaptive-disconnector-plugin/master/src/main/webapp/icons/48x48/attribute.png -------------------------------------------------------------------------------- /src/main/groovy/org/jenkinsci/plugins/adaptivedisonnector/Updater.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2014 Red Hat, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package org.jenkinsci.plugins.adaptivedisonnector; 25 | 26 | import hudson.model.Computer; 27 | import hudson.node_monitors.AbstractNodeMonitorDescriptor; 28 | import hudson.node_monitors.NodeMonitor; 29 | 30 | /*package*/ class Updater { 31 | 32 | /*package*/ static void trigger(AbstractNodeMonitorDescriptor descriptor, NodeMonitor monitor, Computer computer) { 33 | 34 | def recordField = AbstractNodeMonitorDescriptor.class.getDeclaredField("record"); 35 | recordField.accessible = true; 36 | 37 | def record = recordField.get(descriptor); 38 | if (record == null) return; 39 | 40 | if(computer.getChannel() == null) { 41 | record.data.put(computer, null); 42 | return; 43 | } 44 | 45 | def monitorMethod = AbstractNodeMonitorDescriptor.class.getDeclaredMethod("monitor", Computer.class); 46 | monitorMethod.accessible = true; 47 | record.data.put(computer, monitorMethod.invoke(descriptor, computer)); 48 | 49 | monitor.data(computer); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 2.19 7 | 8 | 9 | org.jenkins-ci.plugins 10 | adaptive-disconnector 11 | 0.3-SNAPSHOT 12 | hpi 13 | Adaptive Disconnector Plugin 14 | Run node monitors after build failure to detect infrastructure problems early 15 | http://wiki.jenkins-ci.org/display/JENKINS/Adaptive+Disconnector+Plugin 16 | 17 | 18 | scm:git:git://github.com/jenkinsci/adaptive-disconnector-plugin.git 19 | scm:git:git@github.com:jenkinsci/adaptive-disconnector-plugin.git 20 | https://github.com/jenkinsci/adaptive-disconnector-plugin 21 | HEAD 22 | 23 | 24 | 25 | 26 | org.hamcrest 27 | hamcrest-core 28 | 1.2.1 29 | test 30 | 31 | 32 | 33 | 34 | org.jenkins-ci.main 35 | jenkins-core 36 | ${jenkins.version} 37 | 38 | 39 | org.sonatype.sisu 40 | sisu-guice 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | repo.jenkins-ci.org 49 | http://repo.jenkins-ci.org/public/ 50 | 51 | 52 | 53 | 54 | 55 | repo.jenkins-ci.org 56 | http://repo.jenkins-ci.org/public/ 57 | 58 | 59 | 60 | 61 | UTF-8 62 | false 63 | 1.609 64 | 2.17 65 | 6 66 | true 67 | 68 | 69 | 70 | 71 | 72 | org.codehaus.gmaven 73 | gmaven-plugin 74 | 75 | 76 | 77 | generateStubs 78 | compile 79 | generateTestStubs 80 | testCompile 81 | 82 | 83 | 84 | 85 | 87 | 1.7 88 | 89 | 90 | 91 | org.codehaus.gmaven.runtime 92 | gmaven-runtime-1.7 93 | 1.3 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/adaptivedisonnector/FailureListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2014 Red Hat, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package org.jenkinsci.plugins.adaptivedisonnector; 25 | 26 | import hudson.Extension; 27 | import hudson.model.Result; 28 | import hudson.model.Computer; 29 | import hudson.model.ComputerSet; 30 | import hudson.model.Descriptor; 31 | import hudson.model.Run; 32 | import hudson.model.listeners.RunListener; 33 | import hudson.node_monitors.AbstractNodeMonitorDescriptor; 34 | import hudson.node_monitors.NodeMonitor; 35 | import hudson.remoting.Callable; 36 | import hudson.remoting.VirtualChannel; 37 | import jenkins.security.MasterToSlaveCallable; 38 | 39 | import java.io.IOException; 40 | import java.util.Map.Entry; 41 | import java.util.logging.Level; 42 | import java.util.logging.Logger; 43 | 44 | @Extension 45 | public class FailureListener extends RunListener> { 46 | 47 | private static final Logger LOGGER = Logger.getLogger(FailureListener.class.getName()); 48 | 49 | @Override 50 | public void onFinalized(Run r) { 51 | if (!Result.FAILURE.equals(r.getResult())) return; 52 | 53 | Computer computer = Computer.currentComputer(); 54 | if (computer == null) { 55 | LOGGER.info("Unable to detect computer for " + r); 56 | return; 57 | } 58 | 59 | LOGGER.log(Level.INFO, "Monitoring {0} after failed run of {1}", 60 | new String[] {computer.getName(), r.getFullDisplayName()} 61 | ); 62 | 63 | for (Entry, NodeMonitor> pair: ComputerSet.getNonIgnoredMonitors().entrySet()) { 64 | AbstractNodeMonitorDescriptor descriptor = (AbstractNodeMonitorDescriptor) pair.getKey(); 65 | Updater.trigger(descriptor, pair.getValue(), computer); 66 | } 67 | 68 | if (computer.isOffline()) { 69 | final String message = String.format( 70 | "%s taken offline after failed run of %s", 71 | computer.getName(), r.getFullDisplayName() 72 | ); 73 | 74 | LOGGER.log(Level.WARNING, message); 75 | 76 | try { 77 | final VirtualChannel channel = computer.getChannel(); 78 | // Channel might not be available if slave crashed causing build to fail 79 | if (channel == null) return; 80 | 81 | channel.call(new LogToSlave(message)); 82 | } catch (IOException ex) { 83 | ex.printStackTrace(); 84 | } catch (InterruptedException ex) { 85 | ex.printStackTrace(); 86 | } 87 | } 88 | } 89 | 90 | private static class LogToSlave extends MasterToSlaveCallable { 91 | private static final long serialVersionUID = 1L; 92 | private String message; 93 | public LogToSlave(String message) { 94 | this.message = message; 95 | } 96 | public Void call() throws IOException { 97 | LOGGER.log(Level.WARNING, message); 98 | return null; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/plugins/adaptivedisonnector/FailureListenerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright (c) 2014 Red Hat, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | package org.jenkinsci.plugins.adaptivedisonnector; 25 | 26 | import static org.junit.Assert.assertTrue; 27 | import hudson.Extension; 28 | import hudson.model.BuildListener; 29 | import hudson.model.FreeStyleBuild; 30 | import hudson.model.Result; 31 | import hudson.model.AbstractBuild; 32 | import hudson.model.Computer; 33 | import hudson.model.Executor; 34 | import hudson.model.FreeStyleProject; 35 | import hudson.node_monitors.AbstractNodeMonitorDescriptor; 36 | import hudson.node_monitors.NodeMonitor; 37 | import hudson.slaves.DumbSlave; 38 | import hudson.slaves.OfflineCause; 39 | import hudson.tasks.Builder; 40 | 41 | import java.io.IOException; 42 | 43 | import net.sf.json.JSONObject; 44 | 45 | import org.junit.Rule; 46 | import org.junit.Test; 47 | import org.jvnet.hudson.test.JenkinsRule; 48 | import org.jvnet.hudson.test.FailureBuilder; 49 | import org.kohsuke.stapler.StaplerRequest; 50 | 51 | public class FailureListenerTest { 52 | 53 | @Rule public final JenkinsRule j = new JenkinsRule(); 54 | 55 | @Test 56 | public void doNothingIfSuccessfull() throws Exception { 57 | DumbSlave slave = j.createOnlineSlave(); 58 | FreeStyleProject project = j.createFreeStyleProject(); 59 | project.setAssignedNode(slave); 60 | 61 | j.buildAndAssertSuccess(project); 62 | 63 | Thread.sleep(1000); 64 | 65 | assertTrue(slave.getComputer().isOnline()); 66 | } 67 | 68 | @Test 69 | public void disconnectSlave() throws Exception { 70 | DumbSlave slave = j.createOnlineSlave(); 71 | FreeStyleProject project = j.createFreeStyleProject(); 72 | project.getBuildersList().add(new FailureBuilder()); 73 | project.setAssignedNode(slave); 74 | 75 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 76 | j.assertBuildStatus(Result.FAILURE, build); 77 | 78 | Thread.sleep(1000); 79 | 80 | assertTrue(slave.getComputer().isOffline()); 81 | } 82 | 83 | @Test 84 | public void avoidNpeWhenDisconnected() throws Exception { 85 | DumbSlave slave = j.createOnlineSlave(); 86 | FreeStyleProject project = j.createFreeStyleProject(); 87 | project.getBuildersList().add(new SlaveKiller()); 88 | project.setAssignedNode(slave); 89 | 90 | FreeStyleBuild build = project.scheduleBuild2(0).get(); 91 | j.assertBuildStatus(Result.FAILURE, build); 92 | 93 | assertTrue(slave.getComputer().isOffline()); 94 | } 95 | 96 | private static final OfflineCause OFFLINE_CAUSE = new OfflineCause() {}; 97 | 98 | private static final class SlaveKiller extends Builder { 99 | @Override 100 | public boolean prebuild(AbstractBuild build, BuildListener listener) { 101 | build.getBuiltOn().toComputer().disconnect(null); 102 | return true; 103 | } 104 | } 105 | 106 | public static final class TestNodeMonitor extends NodeMonitor { 107 | 108 | @Override 109 | public Object data(Computer c) { 110 | if (!(Thread.currentThread() instanceof Executor)) return null; 111 | 112 | c.setTemporarilyOffline(true, OFFLINE_CAUSE); 113 | return this; 114 | } 115 | 116 | @Extension(ordinal = Double.MIN_VALUE) 117 | public static final class Descriptor extends AbstractNodeMonitorDescriptor { 118 | 119 | @Override 120 | protected Void monitor(Computer c) throws IOException, InterruptedException { 121 | return null; 122 | } 123 | 124 | @Override 125 | public String getDisplayName() { 126 | return "test-node-monitor"; 127 | } 128 | 129 | @Override 130 | public NodeMonitor newInstance(StaplerRequest req, JSONObject formData) throws FormException { 131 | return new TestNodeMonitor(); 132 | } 133 | } 134 | } 135 | } 136 | --------------------------------------------------------------------------------