pantry 0.0.0 → 0.1.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 +4 -4
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +15 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/Vagrantfile +86 -0
- data/bin/pantry +11 -0
- data/bin/pantry-client +38 -0
- data/bin/pantry-server +33 -0
- data/dist/client.yml +79 -0
- data/dist/server.yml +56 -0
- data/dist/upstart/pantry-client.conf +12 -0
- data/dist/upstart/pantry-server.conf +12 -0
- data/doc/message_packet.dot +19 -0
- data/doc/message_packet.dot.png +0 -0
- data/doc/network_topology.dot +42 -0
- data/doc/network_topology.dot.png +0 -0
- data/lib/celluloid_zmq_patches.rb +16 -0
- data/lib/opt_parse_plus.rb +184 -0
- data/lib/pantry.rb +197 -0
- data/lib/pantry/cli.rb +154 -0
- data/lib/pantry/client.rb +131 -0
- data/lib/pantry/client_info.rb +34 -0
- data/lib/pantry/client_registry.rb +104 -0
- data/lib/pantry/command.rb +194 -0
- data/lib/pantry/command_handler.rb +53 -0
- data/lib/pantry/command_line.rb +115 -0
- data/lib/pantry/commands/create_client.rb +30 -0
- data/lib/pantry/commands/download_directory.rb +35 -0
- data/lib/pantry/commands/echo.rb +32 -0
- data/lib/pantry/commands/edit_application.rb +60 -0
- data/lib/pantry/commands/register_client.rb +38 -0
- data/lib/pantry/commands/status.rb +78 -0
- data/lib/pantry/commands/sync_directory.rb +50 -0
- data/lib/pantry/commands/update_application.rb +45 -0
- data/lib/pantry/commands/upload_file.rb +68 -0
- data/lib/pantry/communication.rb +20 -0
- data/lib/pantry/communication/client.rb +75 -0
- data/lib/pantry/communication/client_filter.rb +117 -0
- data/lib/pantry/communication/file_service.rb +125 -0
- data/lib/pantry/communication/file_service/file_progress.rb +164 -0
- data/lib/pantry/communication/file_service/receive_file.rb +97 -0
- data/lib/pantry/communication/file_service/send_file.rb +74 -0
- data/lib/pantry/communication/publish_socket.rb +20 -0
- data/lib/pantry/communication/reading_socket.rb +89 -0
- data/lib/pantry/communication/receive_socket.rb +23 -0
- data/lib/pantry/communication/security.rb +44 -0
- data/lib/pantry/communication/security/authentication.rb +98 -0
- data/lib/pantry/communication/security/curve_key_store.rb +120 -0
- data/lib/pantry/communication/security/curve_security.rb +70 -0
- data/lib/pantry/communication/security/null_security.rb +32 -0
- data/lib/pantry/communication/send_socket.rb +19 -0
- data/lib/pantry/communication/serialize_message.rb +84 -0
- data/lib/pantry/communication/server.rb +97 -0
- data/lib/pantry/communication/subscribe_socket.rb +33 -0
- data/lib/pantry/communication/wait_list.rb +45 -0
- data/lib/pantry/communication/writing_socket.rb +46 -0
- data/lib/pantry/config.rb +182 -0
- data/lib/pantry/file_editor.rb +67 -0
- data/lib/pantry/logger.rb +78 -0
- data/lib/pantry/message.rb +134 -0
- data/lib/pantry/multi_command.rb +36 -0
- data/lib/pantry/server.rb +132 -0
- data/lib/pantry/test/acceptance.rb +83 -0
- data/lib/pantry/test/support/fake_fs.rb +31 -0
- data/lib/pantry/test/support/matchers.rb +13 -0
- data/lib/pantry/test/support/minitest.rb +13 -0
- data/lib/pantry/test/support/mock_ui.rb +23 -0
- data/lib/pantry/test/unit.rb +13 -0
- data/lib/pantry/ui.rb +68 -0
- data/lib/pantry/version.rb +3 -0
- data/pantry.gemspec +40 -0
- data/test/acceptance/cli/error_handling_test.rb +7 -0
- data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
- data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
- data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
- data/test/acceptance/communication/heartbeat_test.rb +19 -0
- data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
- data/test/acceptance/communication/security_test.rb +117 -0
- data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/config.yml +22 -0
- data/test/fixtures/empty.yml +2 -0
- data/test/fixtures/file_to_upload +3 -0
- data/test/root_dir/.gitkeep +0 -0
- data/test/unit/cli_test.rb +173 -0
- data/test/unit/client_registry_test.rb +61 -0
- data/test/unit/client_test.rb +128 -0
- data/test/unit/command_handler_test.rb +79 -0
- data/test/unit/command_line_test.rb +5 -0
- data/test/unit/command_test.rb +206 -0
- data/test/unit/commands/create_client_test.rb +25 -0
- data/test/unit/commands/download_directory_test.rb +58 -0
- data/test/unit/commands/echo_test.rb +22 -0
- data/test/unit/commands/edit_application_test.rb +84 -0
- data/test/unit/commands/register_client_test.rb +41 -0
- data/test/unit/commands/status_test.rb +81 -0
- data/test/unit/commands/sync_directory_test.rb +75 -0
- data/test/unit/commands/update_application_test.rb +35 -0
- data/test/unit/commands/upload_file_test.rb +51 -0
- data/test/unit/communication/client_filter_test.rb +262 -0
- data/test/unit/communication/client_test.rb +99 -0
- data/test/unit/communication/file_service/receive_file_test.rb +214 -0
- data/test/unit/communication/file_service/send_file_test.rb +110 -0
- data/test/unit/communication/file_service_test.rb +56 -0
- data/test/unit/communication/publish_socket_test.rb +19 -0
- data/test/unit/communication/reading_socket_test.rb +110 -0
- data/test/unit/communication/receive_socket_test.rb +20 -0
- data/test/unit/communication/security/authentication_test.rb +97 -0
- data/test/unit/communication/security/curve_key_store_test.rb +110 -0
- data/test/unit/communication/security/curve_security_test.rb +44 -0
- data/test/unit/communication/security/null_security_test.rb +15 -0
- data/test/unit/communication/security_test.rb +49 -0
- data/test/unit/communication/send_socket_test.rb +19 -0
- data/test/unit/communication/serialize_message_test.rb +128 -0
- data/test/unit/communication/server_test.rb +106 -0
- data/test/unit/communication/subscribe_socket_test.rb +46 -0
- data/test/unit/communication/wait_list_test.rb +60 -0
- data/test/unit/communication/writing_socket_test.rb +46 -0
- data/test/unit/config_test.rb +150 -0
- data/test/unit/logger_test.rb +79 -0
- data/test/unit/message_test.rb +179 -0
- data/test/unit/multi_command_test.rb +45 -0
- data/test/unit/opt_parse_plus_test.rb +218 -0
- data/test/unit/pantry_test.rb +82 -0
- data/test/unit/server_test.rb +166 -0
- data/test/unit/test_helper.rb +25 -0
- data/test/unit/ui_test.rb +58 -0
- metadata +389 -13
data/lib/pantry/cli.rb
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# Pantry's Command Line Interface.
|
|
4
|
+
class CLI < Client
|
|
5
|
+
|
|
6
|
+
def initialize(command_line, **args)
|
|
7
|
+
@command_line = Pantry::CommandLine.new(command_line)
|
|
8
|
+
|
|
9
|
+
args[:identity] ||= ENV["USER"]
|
|
10
|
+
super(**args)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
prepare_local_pantry_root
|
|
15
|
+
|
|
16
|
+
options, arguments = @command_line.parse!
|
|
17
|
+
if options && process_global_options(options)
|
|
18
|
+
super
|
|
19
|
+
perform(options, arguments)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
terminate
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def prepare_local_pantry_root
|
|
26
|
+
if Pantry.config.root_dir.nil?
|
|
27
|
+
# TODO Find a .pantry up the chain vs building one
|
|
28
|
+
Pantry.config.root_dir = File.join(Dir.pwd, ".pantry")
|
|
29
|
+
FileUtils.mkdir_p(Pantry.root)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def process_global_options(options)
|
|
34
|
+
if options["verbose"]
|
|
35
|
+
Pantry.config.log_level = :info
|
|
36
|
+
Pantry.config.refresh
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if options["debug"]
|
|
40
|
+
Pantry.config.log_level = :debug
|
|
41
|
+
Pantry.config.refresh
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if server_host = options["host"]
|
|
45
|
+
Pantry.config.server_host = server_host
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if curve_key_file = options["curve-key-file"]
|
|
49
|
+
Pantry.config.security = "curve"
|
|
50
|
+
copy_keys_file_into_pantry_root(curve_key_file)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if options["version"]
|
|
54
|
+
Pantry.ui.say Pantry::VERSION
|
|
55
|
+
terminate
|
|
56
|
+
return false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Given the parsed options and the arguments left over,
|
|
63
|
+
# figure out what Command was requested, build up the appropriate structures
|
|
64
|
+
# and start the communication process.
|
|
65
|
+
def perform(options, arguments)
|
|
66
|
+
command_info, command_options = @command_line.triggered_command
|
|
67
|
+
|
|
68
|
+
if command_info
|
|
69
|
+
client_filter = Pantry::Communication::ClientFilter.new(
|
|
70
|
+
application: options['application'],
|
|
71
|
+
environment: options['environment'],
|
|
72
|
+
roles: options['roles']
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
command = command_info[:class].new(*arguments)
|
|
76
|
+
command_options = command_options.merge(options)
|
|
77
|
+
|
|
78
|
+
request(client_filter, command, command_options)
|
|
79
|
+
else
|
|
80
|
+
$stderr.puts "I don't know the #{arguments.first} command"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Fire off the requested Command.
|
|
85
|
+
def request(filter, command, options)
|
|
86
|
+
@command = command
|
|
87
|
+
@command.client = self
|
|
88
|
+
|
|
89
|
+
# We don't use send_request here because we don't want to deal with the
|
|
90
|
+
# wait-list future system. This lets command objects handle responses
|
|
91
|
+
# as they come back to the CLI as the command sees fit.
|
|
92
|
+
# If the command isn't meant directly for the Server, the Server will always
|
|
93
|
+
# respond first with the list of clients who will be executing the command
|
|
94
|
+
# and responding with the results. See Pantry::Commands::Echo for an example of how
|
|
95
|
+
# to work with this flow.
|
|
96
|
+
begin
|
|
97
|
+
message = @command.prepare_message(options)
|
|
98
|
+
message.to = filter.stream
|
|
99
|
+
message.requires_response!
|
|
100
|
+
|
|
101
|
+
send_message(message)
|
|
102
|
+
|
|
103
|
+
@command.wait_for_finish
|
|
104
|
+
|
|
105
|
+
copy_full_keys_back_to_curve_key_file
|
|
106
|
+
rescue Exception => ex
|
|
107
|
+
Pantry.ui.say("Error: #{ex.message}")
|
|
108
|
+
Pantry.logger.debug(ex.backtrace.join("\n"))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# All messages received by this client are assumed to be responses
|
|
113
|
+
# from previous commands.
|
|
114
|
+
def receive_message(message)
|
|
115
|
+
if @command
|
|
116
|
+
@command.receive_response(message)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
protected
|
|
121
|
+
|
|
122
|
+
#
|
|
123
|
+
# For the sake of being a Client, the file mentioned in --curve-key-file is
|
|
124
|
+
# copied into security/curve/client_keys.yml so that we don't have to do anything
|
|
125
|
+
# special to turn on Curve encryption for the Client. We do this for every run to ensure
|
|
126
|
+
# that if the keys or options change, the user isn't confused when the client is unable to
|
|
127
|
+
# connect or auth.
|
|
128
|
+
#
|
|
129
|
+
# To facilitate the first-time-connect situation, where a new Pantry Server has been spun up
|
|
130
|
+
# and the only known key is the server's public key, this will also copy the keys file back
|
|
131
|
+
# up to the file named in --curve-key-file because it will fill that file with a set of
|
|
132
|
+
# generated public and private keys.
|
|
133
|
+
#
|
|
134
|
+
|
|
135
|
+
def copy_keys_file_into_pantry_root(curve_key_file)
|
|
136
|
+
@curve_key_file = curve_key_file
|
|
137
|
+
FileUtils.mkdir_p(Pantry.root.join("security", "curve"))
|
|
138
|
+
FileUtils.cp(
|
|
139
|
+
Pantry.root.join(@curve_key_file),
|
|
140
|
+
Pantry.root.join("security", "curve", "client_keys.yml")
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def copy_full_keys_back_to_curve_key_file
|
|
145
|
+
return unless @curve_key_file
|
|
146
|
+
FileUtils.cp(
|
|
147
|
+
Pantry.root.join("security", "curve", "client_keys.yml"),
|
|
148
|
+
Pantry.root.join(@curve_key_file)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# The Pantry Client.
|
|
4
|
+
# The Client runs on any server that needs provisioning,
|
|
5
|
+
# and communicates to the Server through various channels. Clients can
|
|
6
|
+
# be further configured to manage an application, for a given environment,
|
|
7
|
+
# and across any number of roles.
|
|
8
|
+
class Client
|
|
9
|
+
extend Forwardable
|
|
10
|
+
include Celluloid
|
|
11
|
+
finalizer :shutdown
|
|
12
|
+
|
|
13
|
+
# See Pantry::ClientInfo
|
|
14
|
+
def_delegators :@info, :identity, :application, :environment, :roles, :filter
|
|
15
|
+
|
|
16
|
+
# For testing / debugging purposes, keep hold of the last message this client received
|
|
17
|
+
attr_reader :last_received_message
|
|
18
|
+
|
|
19
|
+
def initialize(application: nil, environment: nil, roles: [], identity: nil, network_stack_class: Communication::Client)
|
|
20
|
+
@info = Pantry::ClientInfo.new(
|
|
21
|
+
application: application,
|
|
22
|
+
environment: environment,
|
|
23
|
+
roles: roles || [],
|
|
24
|
+
identity: identity || current_hostname
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@commands = CommandHandler.new(self, Pantry.client_commands)
|
|
28
|
+
@networking = network_stack_class.new_link(self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Start up the Client.
|
|
32
|
+
# This sets up the appropriate communication channels to the
|
|
33
|
+
# server, sends a registration message so the Server knows who
|
|
34
|
+
# just connected, and then waits for information to come.
|
|
35
|
+
def run
|
|
36
|
+
Pantry.set_proc_title("pantry client #{Pantry::VERSION} :: #{identity}")
|
|
37
|
+
@networking.run
|
|
38
|
+
send_registration_message
|
|
39
|
+
Pantry.logger.info("[#{identity}] Client registered and waiting for commands")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def shutdown
|
|
43
|
+
Pantry.logger.info("[#{identity}] Client Shutting down")
|
|
44
|
+
@registration_timer.cancel if @registration_timer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Callback from Networking when a message is received
|
|
48
|
+
def receive_message(message)
|
|
49
|
+
Pantry.logger.debug("[#{identity}] Received message #{message.inspect}")
|
|
50
|
+
|
|
51
|
+
if message_meant_for_us?(message)
|
|
52
|
+
@last_received_message = message
|
|
53
|
+
results = @commands.process(message)
|
|
54
|
+
|
|
55
|
+
if message.requires_response?
|
|
56
|
+
Pantry.logger.debug("[#{identity}] Responding with #{results.inspect}")
|
|
57
|
+
send_results_back_to_requester(message, results)
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
Pantry.logger.debug("[#{identity}] Message discarded, not for us")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Send a Pantry::Message directly to its intended recipient.
|
|
65
|
+
# For a Client this is almost always the Server.
|
|
66
|
+
def send_message(message)
|
|
67
|
+
@networking.send_message(message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Send a Pantry::Message but mark it as requiring a response.
|
|
71
|
+
# This will set up and return a Celluloid::Future that will contain the
|
|
72
|
+
# response once it is available.
|
|
73
|
+
def send_request(message)
|
|
74
|
+
message.requires_response!
|
|
75
|
+
|
|
76
|
+
Pantry.logger.debug("[#{identity}] Sending request #{message.inspect}")
|
|
77
|
+
|
|
78
|
+
@networking.send_request(message)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# See Pantry::Server#receive_file
|
|
82
|
+
def receive_file(file_size, file_checksum)
|
|
83
|
+
@networking.receive_file(file_size, file_checksum)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# See Pantry::Server#send_file
|
|
87
|
+
def send_file(file_path, receiver_uuid, file_uuid)
|
|
88
|
+
@networking.send_file(file_path, receiver_uuid, file_uuid)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
|
|
93
|
+
def current_hostname
|
|
94
|
+
Socket.gethostname
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def send_registration_message
|
|
98
|
+
@networking.send_message(
|
|
99
|
+
Pantry::Commands::RegisterClient.new(self).to_message
|
|
100
|
+
)
|
|
101
|
+
@registration_timer =
|
|
102
|
+
after(Pantry.config.client_heartbeat_interval) { send_registration_message }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def send_results_back_to_requester(message, results)
|
|
106
|
+
response_message = message.build_response
|
|
107
|
+
|
|
108
|
+
[results].flatten(1).each do |entry|
|
|
109
|
+
response_message << entry
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@networking.send_message(response_message)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ZeroMQ's Pub/Sub topic matching is too simplistic to catch all the cases we
|
|
116
|
+
# need to handle. Given that if *any* topic matches the incoming message, we get
|
|
117
|
+
# the message even if it wasn't exactly meant for us. For example, if this client
|
|
118
|
+
# subscribes to the following topics:
|
|
119
|
+
#
|
|
120
|
+
# * pantry
|
|
121
|
+
# * pantry.test
|
|
122
|
+
# * pantry.test.app
|
|
123
|
+
#
|
|
124
|
+
# This client will receive messages sent to "pantry.test.web" because "pantry" and
|
|
125
|
+
# "pantry.test" both match (string start_with? check) the message. Thus, we add our
|
|
126
|
+
# own handling to the message check as a protective stop gap.
|
|
127
|
+
def message_meant_for_us?(message)
|
|
128
|
+
filter.matches?(message.to)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# Simple class to keep track of a given client's identifying information
|
|
4
|
+
class ClientInfo
|
|
5
|
+
attr_reader :application
|
|
6
|
+
|
|
7
|
+
attr_reader :environment
|
|
8
|
+
|
|
9
|
+
attr_reader :roles
|
|
10
|
+
|
|
11
|
+
# The above gets packaged into a ClientFilter for use elsewhere
|
|
12
|
+
attr_reader :filter
|
|
13
|
+
|
|
14
|
+
# This client's current identity. By default a client's identity is it's `hostname`,
|
|
15
|
+
# but a specific one can be given. These identities should be unique across the set
|
|
16
|
+
# of clients connecting to a single Pantry Server, behavior of multiple clients with
|
|
17
|
+
# the same identity is currently undefined.
|
|
18
|
+
attr_reader :identity
|
|
19
|
+
|
|
20
|
+
def initialize(application: nil, environment: nil, roles: [], identity: nil)
|
|
21
|
+
@application = application
|
|
22
|
+
@environment = environment
|
|
23
|
+
@roles = roles
|
|
24
|
+
@identity = identity
|
|
25
|
+
|
|
26
|
+
@filter = Pantry::Communication::ClientFilter.new(
|
|
27
|
+
application: @application,
|
|
28
|
+
environment: @environment,
|
|
29
|
+
roles: @roles,
|
|
30
|
+
identity: @identity
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# The ClientRegistry keeps track of clients who've checked in and supports
|
|
4
|
+
# various querying requests against the list of known clients.
|
|
5
|
+
class ClientRegistry
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
clear!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Return all known clients
|
|
12
|
+
def all
|
|
13
|
+
@clients.map {|identity, record| record.client }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Clear out the registry entirely
|
|
17
|
+
def clear!
|
|
18
|
+
@clients = Hash.new {|hash, key| hash[key] = ClientRecord.new }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Check in a client
|
|
22
|
+
def check_in(client)
|
|
23
|
+
@clients[client.identity].check_in(client)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Has the given client checked in?
|
|
27
|
+
def include?(client)
|
|
28
|
+
@clients[client.identity].checked_in?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Find info for Client that matches the given identity
|
|
32
|
+
def find(identity)
|
|
33
|
+
if found = @clients[identity]
|
|
34
|
+
found.client
|
|
35
|
+
else
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Find and return all clients who will receive messages
|
|
41
|
+
# on the given stream or ClientFilter.
|
|
42
|
+
#
|
|
43
|
+
# If this method is given a block, the block will be processed as
|
|
44
|
+
# a #map of the list of clients and records. Block expected to be
|
|
45
|
+
# of the form:
|
|
46
|
+
#
|
|
47
|
+
# all_matching(filter) do |client, record|
|
|
48
|
+
# ...
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# The `record` contains internal knowledge of the Client's activity.
|
|
52
|
+
# See ClientRecord for what's contained.
|
|
53
|
+
def all_matching(stream_or_filter)
|
|
54
|
+
found_client_records =
|
|
55
|
+
case stream_or_filter
|
|
56
|
+
when String
|
|
57
|
+
select_records_matching do |record|
|
|
58
|
+
record.client.filter.matches?(stream_or_filter)
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
select_records_matching do |record|
|
|
62
|
+
stream_or_filter.includes?(record.client.filter)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if block_given?
|
|
67
|
+
found_client_records.map do |record|
|
|
68
|
+
yield(record.client, record)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
found_client_records.map(&:client)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
def select_records_matching
|
|
78
|
+
selected_records = @clients.clone.select do |identity, record|
|
|
79
|
+
yield(record)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
selected_records.values
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class ClientRecord
|
|
86
|
+
attr_reader :client, :last_checked_in_at
|
|
87
|
+
|
|
88
|
+
def initialize
|
|
89
|
+
@client = nil
|
|
90
|
+
@last_checked_in_at = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def check_in(client)
|
|
94
|
+
@client = client
|
|
95
|
+
@last_checked_in_at = Time.now
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def checked_in?
|
|
99
|
+
!@last_checked_in_at.nil?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
module Pantry
|
|
2
|
+
|
|
3
|
+
# Commands are where the task-specific functionality is implemented, the
|
|
4
|
+
# core of how Pantry works. All Commands are required to implement the #perform method,
|
|
5
|
+
# which receives the Pantry::Message requesting the Command.
|
|
6
|
+
#
|
|
7
|
+
# All commands must be registered with Pantry before they will be available for execution.
|
|
8
|
+
# Use Pantry.add_client_command or Pantry.add_server_command to register the Command.
|
|
9
|
+
class Command
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Expose this Command to the CLI and configure the options and information
|
|
13
|
+
# that this Command needs from the CLI to function.
|
|
14
|
+
#
|
|
15
|
+
# See OptParsePlus for documentation
|
|
16
|
+
def command(name, &block)
|
|
17
|
+
@command_name = name
|
|
18
|
+
@command_config = block
|
|
19
|
+
end
|
|
20
|
+
attr_reader :command_name
|
|
21
|
+
attr_reader :command_config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Initialize this Command
|
|
25
|
+
# Due to the multiple ways a Command is instantiated (via the CLI and from the Network stack)
|
|
26
|
+
# any Command initializer must support being called with zero parameters if it expects some.
|
|
27
|
+
def initialize(*args)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set up the Message that needs to be created to send this Command
|
|
31
|
+
# to the server to be processed. Used by the CLI. This method is given
|
|
32
|
+
# the ClientFilter of which clients should respond to this message (if any) and
|
|
33
|
+
# the extra arguments given on the command line.
|
|
34
|
+
#
|
|
35
|
+
# If work needs to be done prior to the network communication for CLI use,
|
|
36
|
+
# override method to take care of that logic.
|
|
37
|
+
#
|
|
38
|
+
# The message returned here is then passed through the network to the appropriate
|
|
39
|
+
# recipients (Clients, Server, or both) and used to trigger #perform on said
|
|
40
|
+
# recipient.
|
|
41
|
+
def prepare_message(options)
|
|
42
|
+
to_message
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Run whatever this command needs to do.
|
|
46
|
+
# All Command subclasses must implement this method.
|
|
47
|
+
# If the message triggering this Command expects a response, the return
|
|
48
|
+
# value of this method will be that response.
|
|
49
|
+
def perform(message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# When a message comes back from the server as a response to or because of
|
|
53
|
+
# this command's #perform, the command object on the CLI will receive that
|
|
54
|
+
# message here. This method will dispatch to either #receive_server_response
|
|
55
|
+
# or #receive_client_response depending on the type of Command run.
|
|
56
|
+
# In most cases, Commands should override the server/client specific receivers.
|
|
57
|
+
# Only override this method to fully customize Message response handling.
|
|
58
|
+
def receive_response(response)
|
|
59
|
+
@response_tracker ||= TrackResponses.new
|
|
60
|
+
@response_tracker.new_response(response)
|
|
61
|
+
|
|
62
|
+
if @response_tracker.from_server?
|
|
63
|
+
receive_server_response(response)
|
|
64
|
+
finished
|
|
65
|
+
elsif @response_tracker.from_client?
|
|
66
|
+
receive_client_response(response)
|
|
67
|
+
finished if @response_tracker.all_response_received?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Handle a response from a Server Command. Override this for specific handling
|
|
72
|
+
# of Server Command responses.
|
|
73
|
+
def receive_server_response(response)
|
|
74
|
+
Pantry.ui.say("Server response:")
|
|
75
|
+
Pantry.ui.say(response.body.inspect)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Handle a response from a Client Command. This will be called for each Client
|
|
79
|
+
# executing and responding to the requested Command.
|
|
80
|
+
def receive_client_response(response)
|
|
81
|
+
Pantry.ui.say("Response from #{response.from}:")
|
|
82
|
+
Pantry.ui.say(response.body.inspect)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Send a request out, returning the Future which will eventually
|
|
86
|
+
# contain the response Message
|
|
87
|
+
def send_request(message)
|
|
88
|
+
@server_or_client.send_request(message)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Send a request out and wait for the response. Will return the response
|
|
92
|
+
# once it is received.
|
|
93
|
+
#
|
|
94
|
+
# This is a blocking call.
|
|
95
|
+
def send_request!(message)
|
|
96
|
+
send_request(message).value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Create a new Message from the information in the current Command
|
|
100
|
+
def to_message
|
|
101
|
+
Pantry::Message.new(self.class.message_type)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Blocking call that returns when the command has completed
|
|
105
|
+
# Can be given a timeout (in seconds) to wait for a response
|
|
106
|
+
def wait_for_finish(timeout = nil)
|
|
107
|
+
completion_future.value(timeout)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Notify all listeners that this command has completed its tasks
|
|
111
|
+
def finished
|
|
112
|
+
completion_future.signal(OpenStruct.new(:value => nil))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Is this command finished?
|
|
116
|
+
def finished?
|
|
117
|
+
completion_future.ready?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def completion_future
|
|
121
|
+
@completion_future ||= Celluloid::Future.new
|
|
122
|
+
end
|
|
123
|
+
protected :completion_future
|
|
124
|
+
|
|
125
|
+
# The Type of this command, used to differentiate Messages.
|
|
126
|
+
# Defaults to the full scope of the name, though with the special
|
|
127
|
+
# case of removing any "Pantry" related scoping such as Pantry::
|
|
128
|
+
# and Pantry::Commands::
|
|
129
|
+
#
|
|
130
|
+
# This value must be unique across the system or the messages will not
|
|
131
|
+
# be processed reliably.
|
|
132
|
+
#
|
|
133
|
+
# Override this for a custom name.
|
|
134
|
+
def self.message_type
|
|
135
|
+
self.name.gsub(/Pantry::Commands::/, '').gsub(/Pantry::/, '')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Set a link back to the Server or Client that's handling
|
|
139
|
+
# this command. This will be set by the CommandHandler before calling
|
|
140
|
+
# #perform.
|
|
141
|
+
def server_or_client=(server_or_client)
|
|
142
|
+
@server_or_client = server_or_client
|
|
143
|
+
end
|
|
144
|
+
alias client= server_or_client=
|
|
145
|
+
alias server= server_or_client=
|
|
146
|
+
|
|
147
|
+
# Get the server or client object handling this command
|
|
148
|
+
def server_or_client
|
|
149
|
+
@server_or_client
|
|
150
|
+
end
|
|
151
|
+
alias server server_or_client
|
|
152
|
+
alias client server_or_client
|
|
153
|
+
|
|
154
|
+
protected
|
|
155
|
+
|
|
156
|
+
# Internal state tracking of server and client responses.
|
|
157
|
+
# When a Client Command is triggered, the Server first responses with a message
|
|
158
|
+
# containing the list of Clients who will execute the Command and respond.
|
|
159
|
+
# Then we need to keep track of all the Clients who have responded so we know
|
|
160
|
+
# when the command has fully finished across all Clients.
|
|
161
|
+
class TrackResponses
|
|
162
|
+
def initialize
|
|
163
|
+
@expected_clients = []
|
|
164
|
+
@received_from_clients = []
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def new_response(response)
|
|
168
|
+
@latest_response = response
|
|
169
|
+
|
|
170
|
+
if response[:client_response_list]
|
|
171
|
+
@expected_clients = response.body
|
|
172
|
+
count = @expected_clients.length
|
|
173
|
+
Pantry.ui.say("Expecting response from #{count} client#{count == 1 ? "" : "s"}...")
|
|
174
|
+
elsif from_client?
|
|
175
|
+
@received_from_clients << response
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def from_server?
|
|
180
|
+
@latest_response.from_server? && !@latest_response[:client_response_list]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def from_client?
|
|
184
|
+
!@latest_response.from_server?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def all_response_received?
|
|
188
|
+
!@expected_clients.empty? && @expected_clients.length == @received_from_clients.length
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end
|