process-roulette 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e169b9806e4eeefe32ec16341caa1a05acf79c38
4
+ data.tar.gz: 3d92f020ff0e571ee1497c8d82754b078f05ed36
5
+ SHA512:
6
+ metadata.gz: 1c470fb7760097d8648fb262474c2415daa1c8ace730b2ed96c72a93e068c781eba850711ba7542035c12e7bea0207dfe70bc532ae9559fdcf12c9629e860743
7
+ data.tar.gz: ebf0276cdd0365ee54345a9a35fe43232c994dc9ff97ee014a342b18090f2ce2b303b70a886153839f9b04e2fb9da0c3953e6d10e805fbd86f825fe50fddbf7f
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2016 Jamis Buck <jamis@jamisbuck.org>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ # Process Roulette
2
+
3
+ This started as a throw-away tweet:
4
+ https://twitter.com/jamis/status/808779302468665344 . Only I couldn't stop
5
+ thinking about it...and then I needed a project to start experimenting with
6
+ [atom](https://atom.io/) and [Rubocop](http://rubocop.readthedocs.io/en/latest/)...
7
+ and the next thing I knew, I was working on this.
8
+
9
+ It's still a work-in-progress, and is not guaranteed to work, and is most
10
+ _definitely_ not guaranteed to be safe. (I mean, heck, the whole _point_ of
11
+ this is to randomly kill processes on your machine. Use with extreme
12
+ caution!)
13
+
14
+
15
+ ## Overview
16
+
17
+ There are three components to process roulette:
18
+
19
+ * The _croupier_. This is a supervisor that oversees the game. Start it running
20
+ on some machine (preferably one that will not be used during the roulette
21
+ game). The players and controllers will connect to the croupier, which will
22
+ referee the game.
23
+ * The _player_. Each player should be running in a virtual machine (or, at the
24
+ very least, on a box that you absolutely despise). Do _not_ run this process
25
+ on any machine that you care about! It's job is to connect to the croupier
26
+ service, and then (when the croupier gives the "go" signal) proceed to whack
27
+ random processes until the machine crashes. _You have been warned._
28
+ * The _controller_. Each controller should be running somewhere far away from
29
+ the players. They connect to the croupier service, and are used to control
30
+ the game. The controller says "go", and "exit", and the controller is told
31
+ the results of the game. If you begin a controller without a password, it is
32
+ considered a _spectator_, allowed to watch the bout and see the results, but
33
+ not to control it in any way.
34
+
35
+
36
+ ## Running a game
37
+
38
+ First, install process-roulette:
39
+
40
+ $ gem install process-roulette
41
+
42
+ Then, start the croupier service.
43
+
44
+ $ croupier -p <port> <password>
45
+
46
+ Then, start players, controllers and spectators.
47
+
48
+ $ roulette-ctl host:port password
49
+ $ roulette-ctl host:port
50
+ $ sudo roulette-player username@host:port
51
+
52
+ Note that the player must be run as the superuser, otherwise it won't be able
53
+ to whack the really important processes!
54
+
55
+ When everyone is joined, one of the controllers issues the "GO" command, and
56
+ the rest happens automatically!
57
+
58
+
59
+ ## License
60
+
61
+ This software is made available under the terms of the MIT license. (See the
62
+ LICENSE file for full details.)
63
+
64
+
65
+ ## Author
66
+
67
+ This software is written by Jamis Buck (<jamis@jamisbuck.org>).
@@ -0,0 +1,26 @@
1
+ #!/bin/sh ruby
2
+
3
+ require 'optparse'
4
+ require 'process/roulette/croupier'
5
+
6
+ port = 11_234
7
+
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options] password"
10
+
11
+ opts.on '-h', '--help', 'Display this text' do
12
+ puts opts
13
+ exit
14
+ end
15
+
16
+ opts.on '-p', '--port N', Integer,
17
+ 'The port to accept connections on' do |p|
18
+ port = p
19
+ end
20
+ end.parse!
21
+
22
+ password = ARGV.shift || abort('you must supply a password')
23
+
24
+ puts "Starting croupier process on port #{port}"
25
+ croupier = Process::Roulette::Croupier.new(port, password)
26
+ croupier.run
@@ -0,0 +1,24 @@
1
+ #!/bin/sh ruby
2
+
3
+ require 'optparse'
4
+ require 'process/roulette/controller'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options] host:port [password]"
8
+
9
+ opts.on '-h', '--help', 'Display this text' do
10
+ puts opts
11
+ exit
12
+ end
13
+ end.parse!
14
+
15
+ host_port = ARGV.shift || abort('you must specify the destination host:port')
16
+ password = ARGV.shift
17
+
18
+ host, port = host_port.split(/:/, 2)
19
+
20
+ type = password ? 'controller' : 'spectator'
21
+ puts "Starting #{type} process..."
22
+
23
+ controller = Process::Roulette::Controller.new(host, port.to_i, password)
24
+ controller.run
@@ -0,0 +1,26 @@
1
+ #!/bin/sh ruby
2
+
3
+ require 'optparse'
4
+ require 'process/roulette/player'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options] user@host:port"
8
+
9
+ opts.on '-h', '--help', 'Display this text' do
10
+ puts opts
11
+ exit
12
+ end
13
+ end.parse!
14
+
15
+ dest = ARGV.shift || abort('you must specify user@host:port')
16
+ matches = dest.match(/^(?<user>.*)@(?<host>.*):(?<port>\d+)$/)
17
+ if matches
18
+ user = matches[:user]
19
+ host = matches[:host]
20
+ port = matches[:port].to_i
21
+ else
22
+ abort 'destination must be formatted like user@host:port'
23
+ end
24
+
25
+ player = Process::Roulette::Player.new(host, port, user)
26
+ player.play
@@ -0,0 +1,16 @@
1
+ require 'process/roulette/controller/driver'
2
+
3
+ module Process
4
+ module Roulette
5
+
6
+ # Delegates to Controller::Driver
7
+ module Controller
8
+
9
+ def self.new(host, port, password = nil)
10
+ Driver.new(host, port, password)
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,123 @@
1
+ require 'process/roulette/controller/game_handler'
2
+ require 'process/roulette/controller/finish_handler'
3
+
4
+ module Process
5
+ module Roulette
6
+ module Controller
7
+
8
+ # Handles the COMMAND state of the controller state machine. Listens to
9
+ # both STDIN (unless we're a spectator) and the socket and acts
10
+ # accordingly.
11
+ class CommandHandler
12
+ def initialize(driver)
13
+ @driver = driver
14
+ @next_handler = nil
15
+ end
16
+
17
+ def run
18
+ STDOUT.sync = true
19
+ _say 'waiting for bout to begin'
20
+
21
+ while @next_handler.nil?
22
+ ready = _wait_for_input
23
+ @driver.socket.send_packet('PING')
24
+
25
+ ready.each do |io|
26
+ _process_ready_socket(io)
27
+ end
28
+ end
29
+
30
+ @next_handler == :quit ? nil : @next_handler
31
+ end
32
+
33
+ def _say(message, update_prompt = true)
34
+ puts unless @driver.spectator?
35
+ puts message if message
36
+ print 'controller> ' if update_prompt && !@driver.spectator?
37
+ end
38
+
39
+ def _wait_for_input
40
+ ios = [ @driver.socket ]
41
+ ios << STDIN unless @driver.spectator?
42
+
43
+ ready, = IO.select(ios, [], [], 0.2)
44
+ ready || []
45
+ end
46
+
47
+ def _process_ready_socket(io)
48
+ if io == STDIN
49
+ _process_user_input
50
+ elsif io == @driver.socket
51
+ _process_croupier_input
52
+ end
53
+ end
54
+
55
+ def _process_user_input
56
+ command = (STDIN.gets || '').strip.upcase
57
+
58
+ case command
59
+ when '', 'QUIT' then @next_handler = :quit
60
+ when 'EXIT' then _invoke_exit
61
+ when 'GO' then _invoke_go
62
+ when 'HELP', '?' then _invoke_help
63
+ else _invoke_error(command)
64
+ end
65
+ end
66
+
67
+ def _invoke_exit
68
+ _say 'telling croupier to terminate'
69
+ @driver.socket.send_packet('EXIT')
70
+ end
71
+
72
+ def _invoke_go
73
+ _say 'telling croupier to start game'
74
+ @driver.socket.send_packet('GO')
75
+ end
76
+
77
+ def _invoke_help
78
+ puts 'Ok. I understand these commands:'
79
+ puts ' - EXIT (terminates the croupier)'
80
+ puts ' - QUIT (terminates just this controller)'
81
+ puts ' - GO (starts the bout)'
82
+ puts ' - HELP (this page)'
83
+ _say nil
84
+ end
85
+
86
+ def _process_croupier_input
87
+ packet = @driver.socket.read_packet
88
+
89
+ case packet
90
+ when nil, 'EXIT' then _handle_exit
91
+ when 'GO' then _handle_go
92
+ when /^UPDATE:(.*)/ then _handle_update(Regexp.last_match(1))
93
+ else _handle_unexpected(packet)
94
+ end
95
+ end
96
+
97
+ def _invoke_error(text)
98
+ _say "#{text.inspect} is not understood", false
99
+ _invoke_help
100
+ end
101
+
102
+ def _handle_exit
103
+ _say 'croupier is terminating. bye!', false
104
+ @next_handler = Controller::FinishHandler
105
+ end
106
+
107
+ def _handle_go
108
+ _say nil, false
109
+ @next_handler = Controller::GameHandler
110
+ end
111
+
112
+ def _handle_update(msg)
113
+ _say msg
114
+ end
115
+
116
+ def _handle_unexpected(packet)
117
+ _say "unexpected message from croupier: #{packet.inspect}"
118
+ end
119
+ end
120
+
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,39 @@
1
+ require 'socket'
2
+ require 'process/roulette/enhance_socket'
3
+ require 'process/roulette/controller/command_handler'
4
+
5
+ module Process
6
+ module Roulette
7
+ module Controller
8
+
9
+ # Handles the CONNECT state of the controller state machine. Connects
10
+ # to the croupier, performs the handshake, and advances to COMMAND state.
11
+ class ConnectHandler
12
+ def initialize(driver)
13
+ @driver = driver
14
+ end
15
+
16
+ def run
17
+ puts 'connecting...'
18
+
19
+ socket = TCPSocket.new(@driver.host, @driver.port)
20
+ Roulette::EnhanceSocket(socket)
21
+
22
+ _handshake(socket)
23
+ @driver.socket = socket
24
+
25
+ Controller::CommandHandler
26
+ end
27
+
28
+ def _handshake(socket)
29
+ socket.send_packet(@driver.password || 'OK')
30
+
31
+ packet = socket.wait_with_ping
32
+ abort 'lost connection' unless packet
33
+ abort "unexpected packet #{packet.inspect}" unless packet == 'OK'
34
+ end
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ require 'process/roulette/controller/connect_handler'
2
+
3
+ module Process
4
+ module Roulette
5
+ module Controller
6
+
7
+ # Encapsulates both the controller and spectator behavior. If the password
8
+ # is nil, the controller is considered a spectator, allowed to watch the
9
+ # bout, but not to control it.
10
+ #
11
+ # It's state machine works as follows:
12
+ #
13
+ # CONNECT
14
+ # - connects to croupier, performs handshake
15
+ # - advances to COMMAND
16
+ # COMMAND
17
+ # - waits for input from terminal (unless spectator)
18
+ # * "GO" => sends "GO" to croupier
19
+ # * "EXIT" => sends "EXIT" to croupier
20
+ # - listens for commands from croupier
21
+ # * "GO" => advances to GAME
22
+ # * "EXIT" => advances to FINISH
23
+ # GAME
24
+ # - listens for updates from croupier
25
+ # # "GO" => begins next round
26
+ # * "UPDATE" => print update from croupier
27
+ # * "DONE" => advances to DONE
28
+ # DONE
29
+ # - diplays final scoreboard
30
+ # - advances to COMMAND
31
+ # FINISH
32
+ # - closes sockets, terminates
33
+ #
34
+ class Driver
35
+ attr_reader :host, :port, :password
36
+ attr_accessor :socket
37
+
38
+ def initialize(host, port, password = nil)
39
+ @host = host
40
+ @port = port
41
+ @password = password
42
+ end
43
+
44
+ def spectator?
45
+ password.nil?
46
+ end
47
+
48
+ def run
49
+ handler = Controller::ConnectHandler
50
+ handler = handler.new(self).run while handler
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ module Process
2
+ module Roulette
3
+ module Controller
4
+
5
+ # Handles the FINISH state of the controller state machine, by
6
+ # disconnecting from the croupier.
7
+ class FinishHandler
8
+ def initialize(driver)
9
+ @driver = driver
10
+ end
11
+
12
+ def run
13
+ puts 'terminating...'
14
+ @driver.socket.close
15
+ nil
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,84 @@
1
+ require 'process/roulette/controller/command_handler'
2
+
3
+ module Process
4
+ module Roulette
5
+ module Controller
6
+
7
+ # Handles the GAME state of the controller state machine.
8
+ class GameHandler
9
+ def initialize(driver)
10
+ @driver = driver
11
+ end
12
+
13
+ def run
14
+ puts 'BOUT BEGINS'
15
+
16
+ @bout_active = true
17
+ @round = 1
18
+
19
+ while @bout_active
20
+ _process_input if _wait_for_input
21
+ @driver.socket.send_packet('PING')
22
+ end
23
+
24
+ Controller::CommandHandler
25
+ end
26
+
27
+ def _wait_for_input
28
+ ready, = IO.select([@driver.socket], [], [], 0.2)
29
+ ready ? ready.first : nil
30
+ end
31
+
32
+ def _process_input
33
+ packet = @driver.socket.read_packet
34
+
35
+ case packet
36
+ when nil then abort 'disconnected!'
37
+ when 'GO' then _start_next_round
38
+ when 'DONE' then _finish_bout
39
+ when /^UPDATE:(.*)/ then _report_update(Regexp.last_match(1))
40
+ else _handle_unexpected(packet)
41
+ end
42
+ end
43
+
44
+ def _start_next_round
45
+ @round += 1
46
+ puts "- ROUND #{@round}"
47
+ end
48
+
49
+ def _finish_bout
50
+ puts 'BOUT FINISHED'
51
+ @bout_active = false
52
+
53
+ scoreboard = @driver.socket.read_packet
54
+ _display_scoreboard(scoreboard)
55
+
56
+ puts
57
+ end
58
+
59
+ def _report_update(message)
60
+ puts "- #{message}"
61
+ end
62
+
63
+ def _handle_unexpected(packet)
64
+ puts "- unexpected message from croupier (#{packet.inspect})"
65
+ end
66
+
67
+ def _display_scoreboard(scoreboard)
68
+ _scoreboard_header
69
+ scoreboard.each.with_index do |player, index|
70
+ puts format('%2d | %-10s | %-10s | %5d',
71
+ index + 1, player[:name],
72
+ player[:killer], player[:victims].length)
73
+ end
74
+ end
75
+
76
+ def _scoreboard_header
77
+ puts format(' | %-10s | %-10s | %-6s', 'Name', 'Killer', 'Rounds')
78
+ puts format('---+-%10s-+-%10s-+-%6s', '-' * 10, '-' * 10, '-' * 6)
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,16 @@
1
+ require 'process/roulette/croupier/driver'
2
+
3
+ module Process
4
+ module Roulette
5
+
6
+ # The Croupier is actually backed by Croupier::Driver
7
+ module Croupier
8
+
9
+ def self.new(port, password)
10
+ Driver.new(port, password)
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Process
2
+ module Roulette
3
+
4
+ # Enhances controller sockets so that controllers can be differentiated
5
+ # from mere spectators.
6
+ module ControllerSocket
7
+ def spectator!
8
+ @spectator = true
9
+ end
10
+
11
+ def spectator?
12
+ @spectator
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,77 @@
1
+ require 'socket'
2
+ require 'process/roulette/croupier/join_handler'
3
+ require 'process/roulette/enhance_socket'
4
+
5
+ module Process
6
+ module Roulette
7
+ module Croupier
8
+
9
+ # The croupier is the person who runs a roulette table
10
+ #
11
+ # Croupier is started with a password (see WAIT state, below)
12
+ #
13
+ # STATES
14
+ # - JOIN
15
+ # * accept connections
16
+ # * if a connection says :OK, they are added to player list
17
+ # - should include a desired username
18
+ # - if username is already taken, reject connection
19
+ # - if accepted, server sends :OK
20
+ # * if a connection gives password, they are added to controller list
21
+ # * all connections must send a :PING at least every 1000ms or be
22
+ # discarded
23
+ # * when controller says :EXIT, advance to FINISH
24
+ # * when controller says :GO, state advances to START and no further
25
+ # connections are accepted (listening socket is closed)
26
+ # - START
27
+ # * sends :GO to all players
28
+ # * players reply with name/pid of process they will kill
29
+ # * players must next respond with :OK after killing the process
30
+ # * if player does not reply within 1000ms they are flagged "DEAD"
31
+ # * if all players are flagged "DEAD", advance to RESTART
32
+ # * when either all players have responded with :OK, or 1000ms have
33
+ # elapsed, advance to START
34
+ # - RESTART
35
+ # * send 'DONE' to controllers
36
+ # * send final score info to controllers
37
+ # * cleanup
38
+ # * advance to JOIN
39
+ # - FINISH
40
+ # * notify all players and controllers that server is ending
41
+ # * cleanup
42
+ # * exit
43
+ class Driver
44
+ attr_reader :port, :password
45
+ attr_reader :players, :controllers
46
+
47
+ def initialize(port, password)
48
+ @port = port
49
+ @password = password
50
+
51
+ @players = []
52
+ @controllers = []
53
+ end
54
+
55
+ def reap!
56
+ @players.delete_if(&:dead?)
57
+ @controllers.delete_if(&:dead?)
58
+ end
59
+
60
+ def sockets
61
+ @players + @controllers
62
+ end
63
+
64
+ def broadcast_update(message)
65
+ payload = "UPDATE:#{message}"
66
+ @controllers.each { |s| s.send_packet(payload) }
67
+ end
68
+
69
+ def run
70
+ next_state = Croupier::JoinHandler
71
+ next_state = next_state.new(self).run while next_state
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,25 @@
1
+ module Process
2
+ module Roulette
3
+ module Croupier
4
+
5
+ # The FinishHandler encapsulates the "finish" state of the croupier state
6
+ # machine. It closes all player and controller sockets and terminates
7
+ # the state machine.
8
+ class FinishHandler
9
+ def initialize(croupier)
10
+ @croupier = croupier
11
+ end
12
+
13
+ def run
14
+ @croupier.sockets.each do |socket|
15
+ socket.send_packet('EXIT')
16
+ socket.close
17
+ end
18
+
19
+ nil
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,139 @@
1
+ require 'process/roulette/croupier/join_pending'
2
+ require 'process/roulette/croupier/start_handler'
3
+ require 'process/roulette/croupier/finish_handler'
4
+
5
+ module Process
6
+ module Roulette
7
+ module Croupier
8
+
9
+ # The JoinHandler encapsulates the "join" state of the croupier state
10
+ # machine. It listens for new connections, builds up the lists of players
11
+ # controllers, and indicates the next state (either 'start' or
12
+ # 'finish') based on the input from the controllers.
13
+ class JoinHandler
14
+ def initialize(driver)
15
+ @driver = driver
16
+ @pending = JoinPending.new(driver)
17
+ @next_state = nil
18
+ end
19
+
20
+ def run
21
+ @driver.players.clear
22
+
23
+ puts 'listening...'
24
+ listener = TCPServer.new(@driver.port)
25
+
26
+ _process_current_state(listener)
27
+ @pending.cleanup!
28
+
29
+ listener.close
30
+
31
+ @next_state
32
+ end
33
+
34
+ def _process_current_state(listener)
35
+ until @next_state
36
+ ready = _wait_for_connections(listener)
37
+ _process_ready_list(ready, listener)
38
+
39
+ @pending.reap!
40
+ @driver.reap!
41
+ end
42
+ end
43
+
44
+ def _wait_for_connections(*extras)
45
+ ready, = IO.select(
46
+ [*extras, *@pending, *@driver.sockets],
47
+ [], [], 1)
48
+
49
+ ready || []
50
+ end
51
+
52
+ def _process_ready_list(list, listener)
53
+ list.each do |socket|
54
+ if socket == listener
55
+ _process_new_connection(socket)
56
+ else
57
+ _process_participant_connection(socket)
58
+ end
59
+ end
60
+ end
61
+
62
+ def _process_new_connection(socket)
63
+ puts 'new pending connection...'
64
+ client = socket.accept
65
+ @pending << Process::Roulette::EnhanceSocket(client)
66
+ end
67
+
68
+ def _process_participant_connection(socket)
69
+ packet = socket.read_packet
70
+ socket.ping! if packet
71
+
72
+ if @pending.include?(socket)
73
+ @pending.process(socket, packet)
74
+ elsif @driver.controllers.include?(socket)
75
+ _process_controller_packet(socket, packet)
76
+ else
77
+ _process_player_packet(socket, packet)
78
+ end
79
+ end
80
+
81
+ def _process_controller_packet(socket, packet)
82
+ if socket.spectator?
83
+ _process_spectator_packet(socket, packet)
84
+ else
85
+ _process_real_controller_packet(socket, packet)
86
+ end
87
+ end
88
+
89
+ def _process_real_controller_packet(socket, packet)
90
+ case packet
91
+ when nil then _controller_disconnected(socket)
92
+ when 'GO' then _controller_go
93
+ when 'EXIT' then _controller_exit
94
+ when 'PING' then nil
95
+ else puts "unexpected command from controller (#{packet.inspect})"
96
+ end
97
+ end
98
+
99
+ def _process_spectator_packet(socket, packet)
100
+ case packet
101
+ when nil then _controller_disconnected(socket)
102
+ when 'PING' then # do nothing
103
+ else puts "unexpected comment from spectator (#{packet.inspect})"
104
+ end
105
+ end
106
+
107
+ def _controller_disconnected(socket)
108
+ type = socket.spectator? ? 'spectator' : 'controller'
109
+ @driver.broadcast_update("#{type} has disconnected")
110
+ @driver.controllers.delete(socket)
111
+ end
112
+
113
+ def _controller_go
114
+ puts 'command given to start'
115
+ @next_state = StartHandler
116
+ end
117
+
118
+ def _controller_exit
119
+ puts 'command given to exit'
120
+ @next_state = FinishHandler
121
+ end
122
+
123
+ def _process_player_packet(socket, packet)
124
+ case packet
125
+ when nil then _player_disconnected(socket)
126
+ when 'PING' then # do nothing
127
+ else puts "unexpected comment from player (#{packet.inspect})"
128
+ end
129
+ end
130
+
131
+ def _player_disconnected(socket)
132
+ @driver.broadcast_update "player #{socket.username} has disconnected"
133
+ @driver.players.delete(socket)
134
+ end
135
+ end
136
+
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,103 @@
1
+ require 'process/roulette/croupier/controller_socket'
2
+
3
+ module Process
4
+ module Roulette
5
+ module Croupier
6
+
7
+ # The JoinPending class encapsulates the handling of pending connections
8
+ # during the 'join' phase of the croupier state machine. It explicitly
9
+ # handles new player and new controller connections, moving them from
10
+ # the pending list to the appropriate collections of the croupier itself,
11
+ # depending on their handshake.
12
+ class JoinPending < Array
13
+ def initialize(driver)
14
+ super()
15
+ @driver = driver
16
+ end
17
+
18
+ def reap!
19
+ delete_if(&:dead?)
20
+ end
21
+
22
+ def cleanup!
23
+ return unless any?
24
+
25
+ puts 'closing pending connections'
26
+ each(&:close)
27
+ end
28
+
29
+ def process(socket, packet)
30
+ _handle_nil(socket, packet) ||
31
+ _handle_new_player(socket, packet) ||
32
+ _handle_new_controller(socket, packet, @driver.password) ||
33
+ _handle_new_controller(socket, packet, 'OK') ||
34
+ _handle_ping(socket, packet) ||
35
+ _handle_unexpected(socket, packet)
36
+ end
37
+
38
+ def _handle_nil(socket, packet)
39
+ return false unless packet.nil?
40
+ puts 'pending socket closed'
41
+ delete(socket)
42
+ true
43
+ end
44
+
45
+ def _handle_new_player(socket, packet)
46
+ return false unless /^OK:(?<username>.*)/ =~ packet
47
+
48
+ socket.username = username
49
+ delete(socket)
50
+
51
+ if @driver.players.any? { |p| p.username == socket.username }
52
+ _player_username_taken(socket)
53
+ else
54
+ _player_accepted(socket)
55
+ end
56
+
57
+ true
58
+ end
59
+
60
+ def _handle_new_controller(socket, packet, password)
61
+ return false unless packet == password
62
+
63
+ socket.extend(ControllerSocket)
64
+ socket.spectator! if password == 'OK'
65
+
66
+ puts format('accepting new %s',
67
+ socket.spectator? ? 'spectator' : 'controller')
68
+ socket.send_packet('OK')
69
+ delete(socket)
70
+ @driver.controllers << socket
71
+
72
+ true
73
+ end
74
+
75
+ def _handle_ping(_socket, packet)
76
+ return false unless packet == 'PING'
77
+ true
78
+ end
79
+
80
+ def _handle_unexpected(_socket, packet)
81
+ puts "unexpected input from pending socket (#{packet.inspect})"
82
+ true
83
+ end
84
+
85
+ def _player_username_taken(socket)
86
+ puts 'rejecting: username already taken'
87
+ socket.send_packet('username already taken')
88
+ socket.close
89
+ end
90
+
91
+ def _player_accepted(socket)
92
+ puts "accepting new player #{socket.username}"
93
+ socket.send_packet('OK')
94
+ @driver.players << socket
95
+ @driver.broadcast_update(
96
+ "player '#{socket.username}' added" \
97
+ " (#{@driver.players.length} total)")
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,45 @@
1
+ require 'process/roulette/croupier/join_handler'
2
+
3
+ module Process
4
+ module Roulette
5
+ module Croupier
6
+
7
+ # The RestartHandler encapsulates the "restart" state of the croupier
8
+ # state machine. It builds a scoreboard of results from the most recent
9
+ # game and sends it to all controllers, and then advances the state
10
+ # machine to the "join" state.
11
+ class RestartHandler
12
+ def initialize(croupier)
13
+ @croupier = croupier
14
+ end
15
+
16
+ def run
17
+ scoreboard = _sorted_players.map do |player|
18
+ _results_for(player)
19
+ end
20
+
21
+ @croupier.controllers.each do |controller|
22
+ controller.send_packet('DONE')
23
+ controller.send_packet(scoreboard)
24
+ end
25
+
26
+ JoinHandler
27
+ end
28
+
29
+ def _sorted_players
30
+ @croupier.players.sort_by { |player| -player.victims.length }
31
+ end
32
+
33
+ def _results_for(player)
34
+ {
35
+ name: player.username,
36
+ killed_at: player.killed_at,
37
+ killer: player.victims.last,
38
+ victims: player.victims
39
+ }
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,109 @@
1
+ require 'process/roulette/croupier/restart_handler'
2
+
3
+ module Process
4
+ module Roulette
5
+ module Croupier
6
+
7
+ # The StartHandler encapsulates the "start" state of the croupier state
8
+ # machine. It sends "GO" to all players, listens for their replies
9
+ # with the processes they intend to kill, and waits for all players to
10
+ # check in afterward. It manages the list if active players (removing
11
+ # those that fail to check-in) and transitions to the "restart" state
12
+ # when all players are dead.
13
+ class StartHandler
14
+ def initialize(driver)
15
+ @driver = driver
16
+ end
17
+
18
+ def run
19
+ @standing = _prepare_live_players
20
+ @started = Time.now
21
+
22
+ until _all_players_confirmed? || _time_elapsed?
23
+ ready = _wait_for_input
24
+ _process_ready_sockets(ready)
25
+ end
26
+
27
+ remaining = _kill_unconfirmed_players
28
+ remaining.any? ? StartHandler : RestartHandler
29
+ end
30
+
31
+ def _wait_for_input
32
+ ready, = IO.select([*@standing, *@driver.controllers], [], [], 1)
33
+ ready || []
34
+ end
35
+
36
+ def _time_elapsed?
37
+ (Time.now - @started) > 1.0
38
+ end
39
+
40
+ def _all_players_confirmed?
41
+ @standing.all?(&:confirmed?)
42
+ end
43
+
44
+ def _prepare_live_players
45
+ standing = @driver.players.select { |s| !s.killed? }
46
+
47
+ @driver.controllers.each do |s|
48
+ s.send_packet('GO')
49
+ end
50
+
51
+ standing.each do |s|
52
+ s.victim = nil
53
+ s.confirmed! false
54
+ s.send_packet('GO')
55
+ end
56
+ end
57
+
58
+ def _kill_unconfirmed_players
59
+ @standing.select do |s|
60
+ next true if s.confirmed?
61
+ _player_died(s, remove: false)
62
+ false
63
+ end
64
+ end
65
+
66
+ def _process_ready_sockets(list)
67
+ list.each do |socket|
68
+ packet = socket.read_packet
69
+ socket.ping! if packet
70
+
71
+ if packet.nil? then _handle_closed_socket(socket)
72
+ elsif _is_player?(socket) then _handle_player(socket, packet)
73
+ elsif packet != 'PING'
74
+ puts "unexpected comment from controller #{packet.inspect}"
75
+ end
76
+ end
77
+ end
78
+
79
+ def _is_player?(socket)
80
+ @standing.include?(socket)
81
+ end
82
+
83
+ def _handle_closed_socket(socket)
84
+ if @standing.include?(socket)
85
+ _player_died(socket)
86
+ else
87
+ @controllers.delete(socket)
88
+ end
89
+ end
90
+
91
+ def _player_died(socket, remove: true)
92
+ socket.killed_at = Time.now
93
+ socket.close
94
+ @driver.broadcast_update("#{socket.username} died")
95
+ @standing.delete(socket) if remove
96
+ end
97
+
98
+ def _handle_player(socket, packet)
99
+ if socket.has_victim? && packet == 'OK'
100
+ socket.confirmed!
101
+ else
102
+ socket.victim = packet
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,82 @@
1
+ module Process
2
+ module Roulette # rubocop:disable Style/Documentation
3
+
4
+ # A factory method for applying the EnhanceSocket module to a socket.
5
+ # It adds the module, and automatically calls #ping!, to ensure that
6
+ # the socket begins in an "alive" state.
7
+ def self.EnhanceSocket(socket) # rubocop:disable Style/MethodName
8
+ socket.tap do |s|
9
+ s.extend(EnhanceSocket)
10
+ s.ping!
11
+ end
12
+ end
13
+
14
+ # A module that adds helper methods to socket objects. In particular,
15
+ # it makes it easier to read and write entire packets (where a packet is
16
+ # defined as a 4-byte length field, followed by a variable length body,
17
+ # and the body is a marshalled Ruby object.)
18
+ module EnhanceSocket
19
+ def read_packet
20
+ raw = recv(4, 0)
21
+ return nil if raw.empty?
22
+
23
+ length = raw.unpack('N').first
24
+ raw = recv(length, 0)
25
+ return nil if raw.empty?
26
+
27
+ Marshal.load(raw)
28
+ end
29
+
30
+ def send_packet(payload)
31
+ body = Marshal.dump(payload)
32
+ len = [body.length].pack('N')
33
+ send(len, 0)
34
+ send(body, 0)
35
+ body.length
36
+ end
37
+
38
+ def wait_with_ping
39
+ loop do
40
+ ready, = IO.select([self], [], [], 0.2)
41
+ return read_packet if ready && ready.any?
42
+
43
+ send_packet('PING')
44
+ end
45
+ end
46
+
47
+ attr_accessor :username
48
+ attr_accessor :victims
49
+ attr_accessor :killed_at
50
+
51
+ def killed?
52
+ @killed_at != nil
53
+ end
54
+
55
+ def has_victim?
56
+ @current_victim != nil
57
+ end
58
+
59
+ def victim=(v)
60
+ @current_victim = v
61
+ (@victims ||= []).push(v) if v
62
+ end
63
+
64
+ def confirmed?
65
+ @confirmed
66
+ end
67
+
68
+ def confirmed!(confirm = true)
69
+ @confirmed = confirm
70
+ end
71
+
72
+ def ping!
73
+ @last_ping = Time.now.to_f
74
+ end
75
+
76
+ def dead?
77
+ Time.now.to_f - @last_ping > 1.0
78
+ end
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,79 @@
1
+ require 'socket'
2
+ require 'process/roulette/enhance_socket'
3
+
4
+ require 'sys/proctable'
5
+
6
+ module Process
7
+ module Roulette
8
+
9
+ # Encapsulates the player entity, participating in the roulette game by
10
+ # killing random processes in coordination with the croupier server.
11
+ class Player
12
+ def initialize(host, port, username)
13
+ @host = host
14
+ @port = port
15
+ @username = username
16
+ end
17
+
18
+ def play
19
+ puts 'connecting...'
20
+ socket = Process::Roulette::EnhanceSocket(TCPSocket.new(@host, @port))
21
+
22
+ _handshake(socket)
23
+ _play_loop(socket)
24
+
25
+ puts 'finishing...'
26
+ socket.close
27
+ end
28
+
29
+ def _handshake(socket)
30
+ socket.send_packet("OK:#{@username}")
31
+
32
+ packet = socket.wait_with_ping
33
+ abort 'lost connection' unless packet
34
+
35
+ return if packet == 'OK'
36
+
37
+ socket.close
38
+ abort 'username already taken!'
39
+ end
40
+
41
+ def _play_loop(socket)
42
+ loop do
43
+ packet = socket.wait_with_ping
44
+ abort 'lost connection' unless packet
45
+
46
+ break if _handle_packet(socket, packet)
47
+ end
48
+ end
49
+
50
+ def _handle_packet(socket, packet)
51
+ if packet == 'GO'
52
+ _pull_trigger(socket)
53
+
54
+ puts 'survived!'
55
+ socket.send_packet('OK')
56
+ else
57
+ puts "unexpected packet: #{packet.inspect}"
58
+ end
59
+
60
+ false
61
+ end
62
+
63
+ def _pull_trigger(socket)
64
+ processes = ::Sys::ProcTable.ps
65
+ victim = processes.sample
66
+
67
+ # alternatively: write to random memory locations?
68
+ socket.send_packet(victim.comm)
69
+
70
+ puts "pulling the trigger on \##{victim.pid} (#{victim.comm})"
71
+ Process.kill('KILL', victim.pid)
72
+
73
+ # give some time to make sure the kill has its effect
74
+ sleep 0.1
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,5 @@
1
+ module Process
2
+ module Roulette
3
+ VERSION = '1.0.0'.freeze
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: process-roulette
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jamis Buck
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sys-proctable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |
28
+ Play roulette with your computer! Randomly kill processes until your machine
29
+ crashes. The person who lasts the longest, wins! (Best enjoyed in a VM, and
30
+ with friends.)
31
+ email:
32
+ - jamis@jamisbuck.org
33
+ executables:
34
+ - croupier
35
+ - roulette-ctl
36
+ - roulette-player
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - LICENSE
41
+ - README.md
42
+ - bin/croupier
43
+ - bin/roulette-ctl
44
+ - bin/roulette-player
45
+ - lib/process/roulette/controller.rb
46
+ - lib/process/roulette/controller/command_handler.rb
47
+ - lib/process/roulette/controller/connect_handler.rb
48
+ - lib/process/roulette/controller/driver.rb
49
+ - lib/process/roulette/controller/finish_handler.rb
50
+ - lib/process/roulette/controller/game_handler.rb
51
+ - lib/process/roulette/croupier.rb
52
+ - lib/process/roulette/croupier/controller_socket.rb
53
+ - lib/process/roulette/croupier/driver.rb
54
+ - lib/process/roulette/croupier/finish_handler.rb
55
+ - lib/process/roulette/croupier/join_handler.rb
56
+ - lib/process/roulette/croupier/join_pending.rb
57
+ - lib/process/roulette/croupier/restart_handler.rb
58
+ - lib/process/roulette/croupier/start_handler.rb
59
+ - lib/process/roulette/enhance_socket.rb
60
+ - lib/process/roulette/player.rb
61
+ - lib/process/roulette/version.rb
62
+ homepage: https://github.com/jamis/process_roulette
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.5.1
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: A roulette party game for devs
86
+ test_files: []