process-roulette 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []