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.
- checksums.yaml +7 -0
- data/LICENSE +18 -0
- data/README.md +67 -0
- data/bin/croupier +26 -0
- data/bin/roulette-ctl +24 -0
- data/bin/roulette-player +26 -0
- data/lib/process/roulette/controller.rb +16 -0
- data/lib/process/roulette/controller/command_handler.rb +123 -0
- data/lib/process/roulette/controller/connect_handler.rb +39 -0
- data/lib/process/roulette/controller/driver.rb +56 -0
- data/lib/process/roulette/controller/finish_handler.rb +21 -0
- data/lib/process/roulette/controller/game_handler.rb +84 -0
- data/lib/process/roulette/croupier.rb +16 -0
- data/lib/process/roulette/croupier/controller_socket.rb +17 -0
- data/lib/process/roulette/croupier/driver.rb +77 -0
- data/lib/process/roulette/croupier/finish_handler.rb +25 -0
- data/lib/process/roulette/croupier/join_handler.rb +139 -0
- data/lib/process/roulette/croupier/join_pending.rb +103 -0
- data/lib/process/roulette/croupier/restart_handler.rb +45 -0
- data/lib/process/roulette/croupier/start_handler.rb +109 -0
- data/lib/process/roulette/enhance_socket.rb +82 -0
- data/lib/process/roulette/player.rb +79 -0
- data/lib/process/roulette/version.rb +5 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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>).
|
data/bin/croupier
ADDED
@@ -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
|
data/bin/roulette-ctl
ADDED
@@ -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
|
data/bin/roulette-player
ADDED
@@ -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
|
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: []
|