├── README.md ├── img ├── demo-video.jpg └── launcher.jpg └── retaliation.py /README.md: -------------------------------------------------------------------------------- 1 | ## RETALIATION - A Jenkins "Extreme Feedback" Contraption 2 | 3 | *Status boards are for ‘project managers’! Retaliate to a broken build with a barrage of foam missiles.* 4 | 5 | ### Summary 6 | 7 | Retaliation is a Jenkins CI build monitor that 8 | automatically coordinates a foam missile counter-attack against the developer who "breaks 9 | the build". It does this by playing a pre-programmed control sequence to a *USB Foam 10 | Missile Launcher* to target the offending code monkey. 11 | 12 |     13 | 14 | ### In Detail 15 | 16 | At a deeper level Retaliation is more than just a "simple python script". 17 | It's a radical rethink into how to manage software development teams and the software 18 | development life cycle. It works on a deep psychological level to offer productivity 19 | improvements unseen in all those other "extreme programming" things external consultants 20 | speak about. The primal threat of mutually assured destruction lurking in every coder's 21 | psyche ensures that even your sloppiest developers will never forget to "checkin that 22 | missing file" again! 23 | 24 | ### Testimonials 25 | 26 | *** 27 | > Retaliation brought us the productivity improvement pair-programming promised but 28 | > could never deliver! We've seen a 13.37% decrease in build breakage since its 29 | > implementation. 30 | > 31 | > **Will, Chief Code Hacker** 32 | *** 33 | > Honestly, would you work in a dev team with a Lava Lamp build notifier? What next? 34 | > Nyan Cat mouse mats? Real coders work under the threat of Retaliation! 35 | > 36 | > **Matt, Coding Machine** 37 | *** 38 | > Does what it says on the box. I've seen improvements in my team and we haven't even 39 | > installed it yet! Just the sheer threat has kicked my team's coding into line. 40 | > 41 | > **Tom, Head Code Captain** 42 | *** 43 | 44 | You can see *Retaliation* in action 45 | in this video. 46 | 47 | ### How to Use 48 | 49 | 1. Mount your Dream Cheeky Thunder USB Missile Launcher 50 | in a central and fixed location. 51 | 52 | 2. Download the retaliation.py 53 | script onto the system connected to your missile launcher. 54 | 55 | 3. Modify your `COMMAND_SETS` in the `retaliation.py` script to define your targeting 56 | commands for each one of your build-braking coders (their user ID as listed 57 | in Jenkins). A command set is an array of move and fire commands. It is recommend 58 | to start each command set with a "zero" command. This parks the launcher in a known 59 | position (bottom-left). You can then use "up" and "right" followed by a time (in 60 | milliseconds) to position your fire. 61 | 62 | You can test a set by calling retaliation.py with the target name. e.g.: 63 | 64 | python retaliation.py "[developer's user name]" 65 | 66 | Trial and error is the best approach. Consider doing this secretly after hours for 67 | best results! 68 | 69 | 4. Setup the Jenkins notification plugin. 70 | Define a `UDP` endpoint on port `22222` pointing to the system hosting 71 | `retaliation.py`. *Tip:* Make sure your firewall is not blocking UDP on this port. 72 | 73 | 5. Start listening for failed build events by running the command: 74 | 75 | python retaliation.py stalk 76 | 77 | (Consider setting this up as a boot/startup script. On Windows start with `pythonw.exe` 78 | to keep it running hidden in the background.) 79 | 80 | 6. Wait for DEFCON 1 - Let the war games begin! 81 | 82 | #### Requirements: 83 | 84 | * A Dream Cheeky Thunder USB Missile Launcher. 85 | It may work with other models but I've only tested with this one. 86 | * Python 2.6+ 87 | * Python PyUSB Support (on Mac use brew to "brew install libusb") 88 | * Should work on Windows, Mac and Linux 89 | 90 | Thanks to the dev team at PaperCut (working on print 91 | management software) for "coping a few in the head" during testing! 92 | 93 | ### Tips 94 | 95 | * Carefully select the mounting location. Pick a central location in your office space. 96 | Endeavor to maximize angular separation between targets. This will reduce the likelihood 97 | of friendly fire incidents... but then again this is comes with the territory and is all 98 | part of the fun! 99 | 100 | * Consider sticking down the launcher using double-sided tape to lock its position. This 101 | reduces the chance of someone using a "physical hack" to disrupt the coordinate 102 | targeting system. 103 | 104 | 105 | * If your build breaking perpetrator is at point-blank range, for health and safety 106 | reasons we suggest targeting their keyboard or monitor rather than their head. 107 | 108 | * If you have a wide area to cover, consider multiple missile launches (e.g. cluster 109 | support!). Set the script up on multiple machines and configure multiple endpoint 110 | notifications in Jenkins. 111 | 112 | * To get this working on Windows, you'll need to install 113 | PyUSB and 114 | libusb-win32. 115 | This can be a little tricky but if you've mastered CI build scripts then this 116 | should be easy! 117 | 118 | * If your dev team is Down Under and you're finding Retaliation is loosing its 119 | effect, try dipping each missile in some Vegemite 120 | for some added punch :-) 121 | 122 | ### News 123 | 124 | * Great to see Retaliation mashed up with the 125 | Raspberry Pi. 126 | It's also got a metion in the 127 | Guardian as the 4th best thing to do with the Pi! 128 | 129 | ### Future 130 | 131 | * Should we also make a version compatible with Hudson? :-) 132 | 133 | ### Other Uses 134 | 135 | `retaliation.py` also doubles as a command-line scripting API for the *Dream Cheeky 136 | USB Missile Launcher*. You can invoke it to control the device from a script or 137 | command-line as follows: 138 | 139 | retaliation.py reset 140 | retaliation.py right 3000 141 | retaliation.py up 700 142 | retaliation.py fire 1 143 | 144 | If you do come up with some other cool uses or ideas for retaliation, please share 145 | your story! 146 | 147 | -------------------------------------------------------------------------------- /img/demo-video.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codedance/Retaliation/d2f33b4abea0565860072b44ecac57de3a3a6599/img/demo-video.jpg -------------------------------------------------------------------------------- /img/launcher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codedance/Retaliation/d2f33b4abea0565860072b44ecac57de3a3a6599/img/launcher.jpg -------------------------------------------------------------------------------- /retaliation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2011 PaperCut Software Int. Pty. Ltd. http://www.papercut.com/ 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | ############################################################################ 19 | # 20 | # RETALIATION - A Jenkins "Extreme Feedback" Contraption 21 | # 22 | # Lava Lamps are for pussies! Retaliate to a broken build with a barrage 23 | # of foam missiles. 24 | # 25 | # Steps to use: 26 | # 27 | # 1. Mount your Dream Cheeky Thunder USB missile launcher in a central and 28 | # fixed location. 29 | # 30 | # 2. Copy this script onto the system connected to your missile lanucher. 31 | # 32 | # 3. Modify your `COMMAND_SETS` in the `retaliation.py` script to define 33 | # your targeting commands for each one of your build-braking coders 34 | # (their user ID as listed in Jenkins). A command set is an array of 35 | # move and fire commands. It is recommend to start each command set 36 | # with a "zero" command. This parks the launcher in a known position 37 | # (bottom-left). You can then use "up" and "right" followed by a 38 | # time (in milliseconds) to position your fire. 39 | # 40 | # You can test a set by calling retaliation.py with the target name. 41 | # e.g.: 42 | # 43 | # retaliation.py "[developer's user name]" 44 | # 45 | # Trial and error is the best approch. Consider doing this secretly 46 | # after hours for best results! 47 | # 48 | # 4. Setup the Jenkins "notification" plugin. Define a UDP endpoint 49 | # on port 22222 pointing to the system hosting this script. 50 | # Tip: Make sure your firewall is not blocking UDP on this port. 51 | # 52 | # 5. Start listening for failed build events by running the command: 53 | # retaliation.py stalk 54 | # (Consider setting this up as a boot/startup script. On Windows 55 | # start with pythonw.exe to keep it running hidden in the 56 | # background.) 57 | # 58 | # 6. Wait for DEFCON 1 - Let the war games begin! 59 | # 60 | # 61 | # Requirements: 62 | # * A Dream Cheeky Thunder USB Missile Launcher 63 | # * Python 2.6+ 64 | # * Python PyUSB Support and its dependencies 65 | # http://sourceforge.net/apps/trac/pyusb/ 66 | # (on Mac use brew to "brew install libusb") 67 | # * Should work on Windows, Mac and Linux 68 | # 69 | # Author: Chris Dance 70 | # Version: 1.0 : 2011-08-15 71 | # 72 | ############################################################################ 73 | 74 | import sys 75 | import platform 76 | import time 77 | import socket 78 | import re 79 | import json 80 | import urllib2 81 | import base64 82 | 83 | import usb.core 84 | import usb.util 85 | 86 | ########################## CONFIG ######################### 87 | 88 | # 89 | # Define a dictionary of "command sets" that map usernames to a sequence 90 | # of commands to target the user (e.g their desk/workstation). It's 91 | # suggested that each set start and end with a "zero" command so it's 92 | # always parked in a known reference location. The timing on move commands 93 | # is milli-seconds. The number after "fire" denotes the number of rockets 94 | # to shoot. 95 | # 96 | COMMAND_SETS = { 97 | "will" : ( 98 | ("zero", 0), # Zero/Park to know point (bottom-left) 99 | ("led", 1), # Turn the LED on 100 | ("right", 3250), 101 | ("up", 540), 102 | ("fire", 4), # Fire a full barrage of 4 missiles 103 | ("led", 0), # Turn the LED back off 104 | ("zero", 0), # Park after use for next time 105 | ), 106 | "tom" : ( 107 | ("zero", 0), 108 | ("right", 4400), 109 | ("up", 200), 110 | ("fire", 4), 111 | ("zero", 0), 112 | ), 113 | "chris" : ( # That's me - just dance around and missfire! 114 | ("zero", 0), 115 | ("right", 5200), 116 | ("up", 500), 117 | ("pause", 5000), 118 | ("left", 2200), 119 | ("down", 500), 120 | ("fire", 1), 121 | ("zero", 0), 122 | ), 123 | } 124 | 125 | # 126 | # The UDP port to listen to Jenkins events on (events are generated/supplied 127 | # by Jenkins "notification" plugin) 128 | # 129 | JENKINS_NOTIFICATION_UDP_PORT = 22222 130 | 131 | # 132 | # The URL of your Jenkins server - used to callback to determine who broke 133 | # the build. 134 | # 135 | JENKINS_SERVER = "http://192.168.1.100:23456" 136 | 137 | # 138 | # If you're Jenkins server is secured by HTTP basic auth, sent the 139 | # username and password here. Else leave this blank. 140 | HTTPAUTH_USER = "" 141 | HTTPAUTH_PASS = "" 142 | 143 | ########################## ENG CONFIG ######################### 144 | 145 | # The code... 146 | 147 | # Protocol command bytes 148 | DOWN = 0x01 149 | UP = 0x02 150 | LEFT = 0x04 151 | RIGHT = 0x08 152 | FIRE = 0x10 153 | STOP = 0x20 154 | 155 | DEVICE = None 156 | DEVICE_TYPE = None 157 | 158 | def usage(): 159 | print "Usage: retaliation.py [command] [value]" 160 | print "" 161 | print " commands:" 162 | print " stalk - sit around waiting for a Jenkins CI failed build" 163 | print " notification, then attack the perpetrator!" 164 | print "" 165 | print " up - move up milliseconds" 166 | print " down - move down milliseconds" 167 | print " right - move right milliseconds" 168 | print " left - move left milliseconds" 169 | print " fire - fire times (between 1-4)" 170 | print " zero - park at zero position (bottom-left)" 171 | print " pause - pause milliseconds" 172 | print " led - turn the led on or of (1 or 0)" 173 | print "" 174 | print " - run/test a defined COMMAND_SET" 175 | print " e.g. run:" 176 | print " retaliation.py 'chris'" 177 | print " to test targeting of chris as defined in your command set." 178 | print "" 179 | 180 | 181 | def setup_usb(): 182 | # Tested only with the Cheeky Dream Thunder 183 | # and original USB Launcher 184 | global DEVICE 185 | global DEVICE_TYPE 186 | 187 | DEVICE = usb.core.find(idVendor=0x2123, idProduct=0x1010) 188 | 189 | if DEVICE is None: 190 | DEVICE = usb.core.find(idVendor=0x0a81, idProduct=0x0701) 191 | if DEVICE is None: 192 | raise ValueError('Missile device not found') 193 | else: 194 | DEVICE_TYPE = "Original" 195 | else: 196 | DEVICE_TYPE = "Thunder" 197 | 198 | 199 | 200 | # On Linux we need to detach usb HID first 201 | if "Linux" == platform.system(): 202 | try: 203 | DEVICE.detach_kernel_driver(0) 204 | except Exception, e: 205 | pass # already unregistered 206 | 207 | DEVICE.set_configuration() 208 | 209 | 210 | def send_cmd(cmd): 211 | if "Thunder" == DEVICE_TYPE: 212 | DEVICE.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, cmd, 0x00,0x00,0x00,0x00,0x00,0x00]) 213 | elif "Original" == DEVICE_TYPE: 214 | DEVICE.ctrl_transfer(0x21, 0x09, 0x0200, 0, [cmd]) 215 | 216 | def led(cmd): 217 | if "Thunder" == DEVICE_TYPE: 218 | DEVICE.ctrl_transfer(0x21, 0x09, 0, 0, [0x03, cmd, 0x00,0x00,0x00,0x00,0x00,0x00]) 219 | elif "Original" == DEVICE_TYPE: 220 | print("There is no LED on this device") 221 | 222 | def send_move(cmd, duration_ms): 223 | send_cmd(cmd) 224 | time.sleep(duration_ms / 1000.0) 225 | send_cmd(STOP) 226 | 227 | 228 | def run_command(command, value): 229 | command = command.lower() 230 | if command == "right": 231 | send_move(RIGHT, value) 232 | elif command == "left": 233 | send_move(LEFT, value) 234 | elif command == "up": 235 | send_move(UP, value) 236 | elif command == "down": 237 | send_move(DOWN, value) 238 | elif command == "zero" or command == "park" or command == "reset": 239 | # Move to bottom-left 240 | send_move(DOWN, 2000) 241 | send_move(LEFT, 8000) 242 | elif command == "pause" or command == "sleep": 243 | time.sleep(value / 1000.0) 244 | elif command == "led": 245 | if value == 0: 246 | led(0x00) 247 | else: 248 | led(0x01) 249 | elif command == "fire" or command == "shoot": 250 | if value < 1 or value > 4: 251 | value = 1 252 | # Stabilize prior to the shot, then allow for reload time after. 253 | time.sleep(0.5) 254 | for i in range(value): 255 | send_cmd(FIRE) 256 | time.sleep(4.5) 257 | else: 258 | print "Error: Unknown command: '%s'" % command 259 | 260 | 261 | def run_command_set(commands): 262 | for cmd, value in commands: 263 | run_command(cmd, value) 264 | 265 | 266 | def jenkins_target_user(user): 267 | match = False 268 | # Not efficient but our user list is probably less than 1k. 269 | # Do a case insenstive search for convenience. 270 | for key in COMMAND_SETS: 271 | if key.lower() == user.lower(): 272 | # We have a command set that targets our user so got for it! 273 | run_command_set(COMMAND_SETS[key]) 274 | match = True 275 | break 276 | if not match: 277 | print "WARNING: No target command set defined for user %s" % user 278 | 279 | 280 | def read_url(url): 281 | request = urllib2.Request(url) 282 | 283 | if HTTPAUTH_USER and HTTPAUTH_PASS: 284 | authstring = base64.encodestring('%s:%s' % (HTTPAUTH_USER, HTTPAUTH_PASS)) 285 | authstring = authstring.replace('\n', '') 286 | request.add_header("Authorization", "Basic %s" % authstring) 287 | 288 | return urllib2.urlopen(request).read() 289 | 290 | 291 | def jenkins_get_responsible_user(job_name): 292 | # Call back to Jenkins and determin who broke the build. (Hacky) 293 | # We do this by crudly parsing the changes on the last failed build 294 | 295 | changes_url = JENKINS_SERVER + "/job/" + job_name + "/lastFailedBuild/changes" 296 | changedata = read_url(changes_url) 297 | 298 | # Look for the /user/[name] link 299 | m = re.compile('/user/([^/"]+)').search(changedata) 300 | if m: 301 | return m.group(1) 302 | else: 303 | return None 304 | 305 | 306 | def jenkins_wait_for_event(): 307 | 308 | # Data in the format: 309 | # {"name":"Project", "url":"JobUrl", "build":{"number":1, "phase":"STARTED", "status":"FAILURE" }} 310 | 311 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 312 | sock.bind(('', JENKINS_NOTIFICATION_UDP_PORT)) 313 | 314 | while True: 315 | data, addr = sock.recvfrom(8 * 1024) 316 | try: 317 | notification_data = json.loads(data) 318 | status = notification_data["build"]["status"].upper() 319 | phase = notification_data["build"]["phase"].upper() 320 | if phase == "FINISHED" and status.startswith("FAIL"): 321 | target = jenkins_get_responsible_user(notification_data["name"]) 322 | if target == None: 323 | print "WARNING: Could not identify the user who broke the build!" 324 | continue 325 | 326 | print "Build Failed! Targeting user: " + target 327 | jenkins_target_user(target) 328 | except: 329 | pass 330 | 331 | 332 | def main(args): 333 | 334 | if len(args) < 2: 335 | usage() 336 | sys.exit(1) 337 | 338 | setup_usb() 339 | 340 | if args[1] == "stalk": 341 | print "Listening and waiting for Jenkins failed build events..." 342 | jenkins_wait_for_event() 343 | # Will never return 344 | return 345 | 346 | # Process any passed commands or command_sets 347 | command = args[1] 348 | value = 0 349 | if len(args) > 2: 350 | value = int(args[2]) 351 | 352 | if command in COMMAND_SETS: 353 | run_command_set(COMMAND_SETS[command]) 354 | else: 355 | run_command(command, value) 356 | 357 | 358 | if __name__ == '__main__': 359 | main(sys.argv) 360 | --------------------------------------------------------------------------------