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.
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