pantry 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +19 -0
  5. data/Gemfile +15 -0
  6. data/Guardfile +16 -0
  7. data/LICENSE +20 -0
  8. data/README.md +53 -0
  9. data/Rakefile +18 -0
  10. data/Vagrantfile +86 -0
  11. data/bin/pantry +11 -0
  12. data/bin/pantry-client +38 -0
  13. data/bin/pantry-server +33 -0
  14. data/dist/client.yml +79 -0
  15. data/dist/server.yml +56 -0
  16. data/dist/upstart/pantry-client.conf +12 -0
  17. data/dist/upstart/pantry-server.conf +12 -0
  18. data/doc/message_packet.dot +19 -0
  19. data/doc/message_packet.dot.png +0 -0
  20. data/doc/network_topology.dot +42 -0
  21. data/doc/network_topology.dot.png +0 -0
  22. data/lib/celluloid_zmq_patches.rb +16 -0
  23. data/lib/opt_parse_plus.rb +184 -0
  24. data/lib/pantry.rb +197 -0
  25. data/lib/pantry/cli.rb +154 -0
  26. data/lib/pantry/client.rb +131 -0
  27. data/lib/pantry/client_info.rb +34 -0
  28. data/lib/pantry/client_registry.rb +104 -0
  29. data/lib/pantry/command.rb +194 -0
  30. data/lib/pantry/command_handler.rb +53 -0
  31. data/lib/pantry/command_line.rb +115 -0
  32. data/lib/pantry/commands/create_client.rb +30 -0
  33. data/lib/pantry/commands/download_directory.rb +35 -0
  34. data/lib/pantry/commands/echo.rb +32 -0
  35. data/lib/pantry/commands/edit_application.rb +60 -0
  36. data/lib/pantry/commands/register_client.rb +38 -0
  37. data/lib/pantry/commands/status.rb +78 -0
  38. data/lib/pantry/commands/sync_directory.rb +50 -0
  39. data/lib/pantry/commands/update_application.rb +45 -0
  40. data/lib/pantry/commands/upload_file.rb +68 -0
  41. data/lib/pantry/communication.rb +20 -0
  42. data/lib/pantry/communication/client.rb +75 -0
  43. data/lib/pantry/communication/client_filter.rb +117 -0
  44. data/lib/pantry/communication/file_service.rb +125 -0
  45. data/lib/pantry/communication/file_service/file_progress.rb +164 -0
  46. data/lib/pantry/communication/file_service/receive_file.rb +97 -0
  47. data/lib/pantry/communication/file_service/send_file.rb +74 -0
  48. data/lib/pantry/communication/publish_socket.rb +20 -0
  49. data/lib/pantry/communication/reading_socket.rb +89 -0
  50. data/lib/pantry/communication/receive_socket.rb +23 -0
  51. data/lib/pantry/communication/security.rb +44 -0
  52. data/lib/pantry/communication/security/authentication.rb +98 -0
  53. data/lib/pantry/communication/security/curve_key_store.rb +120 -0
  54. data/lib/pantry/communication/security/curve_security.rb +70 -0
  55. data/lib/pantry/communication/security/null_security.rb +32 -0
  56. data/lib/pantry/communication/send_socket.rb +19 -0
  57. data/lib/pantry/communication/serialize_message.rb +84 -0
  58. data/lib/pantry/communication/server.rb +97 -0
  59. data/lib/pantry/communication/subscribe_socket.rb +33 -0
  60. data/lib/pantry/communication/wait_list.rb +45 -0
  61. data/lib/pantry/communication/writing_socket.rb +46 -0
  62. data/lib/pantry/config.rb +182 -0
  63. data/lib/pantry/file_editor.rb +67 -0
  64. data/lib/pantry/logger.rb +78 -0
  65. data/lib/pantry/message.rb +134 -0
  66. data/lib/pantry/multi_command.rb +36 -0
  67. data/lib/pantry/server.rb +132 -0
  68. data/lib/pantry/test/acceptance.rb +83 -0
  69. data/lib/pantry/test/support/fake_fs.rb +31 -0
  70. data/lib/pantry/test/support/matchers.rb +13 -0
  71. data/lib/pantry/test/support/minitest.rb +13 -0
  72. data/lib/pantry/test/support/mock_ui.rb +23 -0
  73. data/lib/pantry/test/unit.rb +13 -0
  74. data/lib/pantry/ui.rb +68 -0
  75. data/lib/pantry/version.rb +3 -0
  76. data/pantry.gemspec +40 -0
  77. data/test/acceptance/cli/error_handling_test.rb +7 -0
  78. data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
  79. data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
  80. data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
  81. data/test/acceptance/communication/heartbeat_test.rb +19 -0
  82. data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
  83. data/test/acceptance/communication/security_test.rb +117 -0
  84. data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
  85. data/test/acceptance/test_helper.rb +25 -0
  86. data/test/fixtures/config.yml +22 -0
  87. data/test/fixtures/empty.yml +2 -0
  88. data/test/fixtures/file_to_upload +3 -0
  89. data/test/root_dir/.gitkeep +0 -0
  90. data/test/unit/cli_test.rb +173 -0
  91. data/test/unit/client_registry_test.rb +61 -0
  92. data/test/unit/client_test.rb +128 -0
  93. data/test/unit/command_handler_test.rb +79 -0
  94. data/test/unit/command_line_test.rb +5 -0
  95. data/test/unit/command_test.rb +206 -0
  96. data/test/unit/commands/create_client_test.rb +25 -0
  97. data/test/unit/commands/download_directory_test.rb +58 -0
  98. data/test/unit/commands/echo_test.rb +22 -0
  99. data/test/unit/commands/edit_application_test.rb +84 -0
  100. data/test/unit/commands/register_client_test.rb +41 -0
  101. data/test/unit/commands/status_test.rb +81 -0
  102. data/test/unit/commands/sync_directory_test.rb +75 -0
  103. data/test/unit/commands/update_application_test.rb +35 -0
  104. data/test/unit/commands/upload_file_test.rb +51 -0
  105. data/test/unit/communication/client_filter_test.rb +262 -0
  106. data/test/unit/communication/client_test.rb +99 -0
  107. data/test/unit/communication/file_service/receive_file_test.rb +214 -0
  108. data/test/unit/communication/file_service/send_file_test.rb +110 -0
  109. data/test/unit/communication/file_service_test.rb +56 -0
  110. data/test/unit/communication/publish_socket_test.rb +19 -0
  111. data/test/unit/communication/reading_socket_test.rb +110 -0
  112. data/test/unit/communication/receive_socket_test.rb +20 -0
  113. data/test/unit/communication/security/authentication_test.rb +97 -0
  114. data/test/unit/communication/security/curve_key_store_test.rb +110 -0
  115. data/test/unit/communication/security/curve_security_test.rb +44 -0
  116. data/test/unit/communication/security/null_security_test.rb +15 -0
  117. data/test/unit/communication/security_test.rb +49 -0
  118. data/test/unit/communication/send_socket_test.rb +19 -0
  119. data/test/unit/communication/serialize_message_test.rb +128 -0
  120. data/test/unit/communication/server_test.rb +106 -0
  121. data/test/unit/communication/subscribe_socket_test.rb +46 -0
  122. data/test/unit/communication/wait_list_test.rb +60 -0
  123. data/test/unit/communication/writing_socket_test.rb +46 -0
  124. data/test/unit/config_test.rb +150 -0
  125. data/test/unit/logger_test.rb +79 -0
  126. data/test/unit/message_test.rb +179 -0
  127. data/test/unit/multi_command_test.rb +45 -0
  128. data/test/unit/opt_parse_plus_test.rb +218 -0
  129. data/test/unit/pantry_test.rb +82 -0
  130. data/test/unit/server_test.rb +166 -0
  131. data/test/unit/test_helper.rb +25 -0
  132. data/test/unit/ui_test.rb +58 -0
  133. metadata +389 -13
@@ -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