├── 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 |
--------------------------------------------------------------------------------