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