nutella_framework 0.3.1 → 0.4.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +3 -4
  4. data/VERSION +1 -1
  5. data/data/startup +3 -3
  6. data/framework_components/example_framework_interface/dandelion-flowers-card.jpg +0 -0
  7. data/framework_components/example_framework_interface/index.html +18 -0
  8. data/framework_components/main_interface/main_interface_bot.rb +183 -0
  9. data/framework_components/main_interface/public/index.html +54 -0
  10. data/framework_components/main_interface/views/index.erb +63 -0
  11. data/framework_components/order.json.example +6 -0
  12. data/framework_components/runs_list_bot/app_runs_list_bot.rb +15 -0
  13. data/lib/{core/commands → commands}/broker.rb +2 -2
  14. data/lib/{core/commands → commands}/checkup.rb +2 -2
  15. data/lib/commands/compile.rb +13 -0
  16. data/lib/commands/dependencies.rb +13 -0
  17. data/lib/commands/help.rb +35 -0
  18. data/lib/{core/commands → commands}/install.rb +19 -15
  19. data/lib/commands/meta/command.rb +19 -0
  20. data/lib/commands/meta/run_command.rb +114 -0
  21. data/lib/{core → commands/meta}/template_command.rb +1 -1
  22. data/lib/commands/new.rb +60 -0
  23. data/lib/commands/runs.rb +54 -0
  24. data/lib/commands/start.rb +321 -0
  25. data/lib/commands/stop.rb +101 -0
  26. data/lib/{core/commands → commands}/template.rb +59 -39
  27. data/lib/config/current_app_utils.rb +51 -0
  28. data/lib/config/persisted_hash.rb +14 -12
  29. data/lib/config/runlist.rb +116 -16
  30. data/lib/{cli → core}/nutella_cli.rb +1 -1
  31. data/lib/core/nutella_core.rb +2 -6
  32. data/lib/nutella_framework.rb +5 -3
  33. data/lib/nutella_lib_framework/api.rb +333 -0
  34. data/lib/tmux/tmux.rb +76 -0
  35. data/nutella_framework.gemspec +42 -29
  36. data/test/commands/test_cmd_cli_params_parsing.rb +56 -0
  37. data/test/commands/test_command_template.rb +31 -0
  38. data/test/config/test_current_app_utils.rb +34 -0
  39. data/test/config/test_persisted_hash.rb +48 -0
  40. data/test/config/test_runlist.rb +15 -23
  41. data/test/framework_apis/test_framework_api.rb +74 -0
  42. metadata +74 -27
  43. data/actors/main_interface/main_interface_bot.rb +0 -163
  44. data/actors/main_interface/public/index.html +0 -51
  45. data/actors/main_interface/views/index.erb +0 -45
  46. data/lib/config/current_project.rb +0 -58
  47. data/lib/core/command.rb +0 -12
  48. data/lib/core/commands/compile.rb +0 -21
  49. data/lib/core/commands/dependencies.rb +0 -21
  50. data/lib/core/commands/help.rb +0 -28
  51. data/lib/core/commands/new.rb +0 -60
  52. data/lib/core/commands/runs.rb +0 -52
  53. data/lib/core/commands/start.rb +0 -271
  54. data/lib/core/commands/stop.rb +0 -100
  55. data/lib/core/run_command.rb +0 -106
  56. data/lib/core/tmux.rb +0 -38
  57. data/test/config/test_config.rb +0 -48
  58. data/test/config/test_project.rb +0 -34
  59. data/test/test_run_command.rb +0 -54
  60. /data/{actors → framework_components}/main_interface/startup +0 -0
  61. /data/{actors → framework_components}/main_interface/views/not_found_404.erb +0 -0
@@ -0,0 +1,114 @@
1
+ require 'commands/meta/command'
2
+ require 'slop'
3
+
4
+ module Nutella
5
+ # This class describes a run command which can be either start or stop.
6
+ # It is mostly a commodity class for code reuse.
7
+ class RunCommand < Command
8
+
9
+ def run (args=nil)
10
+ console.error 'Running the generic RunCommand!!! WAT? https://www.destroyallsoftware.com/talks/wat'
11
+ end
12
+
13
+
14
+ # Extracts the run_id from the parameters passed to the command line
15
+ # @param [Array<String>] args command line arguments passed to the command
16
+ # @return [String, String ] the run_id
17
+ def parse_run_id_from( args )
18
+ # Simple `nutella start/stop` (no args)
19
+ if args.nil? || args.empty?
20
+ return 'default'
21
+ end
22
+ # If the first argument is a parameter, set the run_id to default
23
+ if args[0].start_with? '-'
24
+ run_id = 'default'
25
+ else
26
+ # If the first argument is a run_id, check that it's not 'default' and return it
27
+ # and shift the arguments so we are left with only the parameters in args
28
+ run_id = args[0]
29
+ raise StandardError.new 'Unfortunately you can\'t use `default` as a run_id because it is reserved :(' if run_id=='default'
30
+ args.shift
31
+ end
32
+ run_id
33
+ end
34
+
35
+
36
+ # Parse the command line parameters
37
+ # @param [Array<String>] args command line arguments passed to the command
38
+ # @return [Hash] an hash containing the parameters
39
+ def parse_cli_parameters( args )
40
+ begin
41
+ opts = Slop::Options.new
42
+ opts.array '-wo', '--without', 'A list of components NOT to start'
43
+ opts.array '-w', '--with', 'A list of components that needs to be started'
44
+ parser = Slop::Parser.new(opts)
45
+ result = parser.parse(args)
46
+ result.to_hash
47
+ rescue
48
+ raise StandardError.new 'The only supported parameters are --with (-w) and --without (-wo)'
49
+ end
50
+ end
51
+
52
+
53
+ # Returns all the components in a certain directory
54
+ def components_in_dir( dir )
55
+ Dir.entries(dir).select {|entry| File.directory?(File.join(dir, entry)) && !(entry =='.' || entry == '..') }
56
+ end
57
+
58
+
59
+ # Executes a code block for each component in a certain directory
60
+ # @param [String] dir directory where we are iterating
61
+ # @yield [component] Passes the component name to the block
62
+ def for_each_component_in_dir( dir, &block )
63
+ components_in_dir(dir).each do |component|
64
+ block.call component
65
+ end
66
+ end
67
+
68
+
69
+ # Runs a script for each bot in a certain directory.
70
+ # Message is displayed in case something goes wrong
71
+ def run_script_for_all_bots_in( dir, script, message )
72
+ for_each_component_in_dir dir do |bot|
73
+ # Skip bot if there is no script
74
+ next unless File.exist? "#{dir}/bots/#{bot}/#{script}"
75
+ # Output message
76
+ console.info "#{message} bot #{bot}."
77
+ # Execute 'script' script
78
+ cur_dir = Dir.pwd
79
+ Dir.chdir "#{dir}/bots/#{bot}"
80
+ system "./#{script}"
81
+ Dir.chdir cur_dir
82
+ end
83
+ true
84
+ end
85
+
86
+
87
+ # Prints a success message if the command completes correctly
88
+ def print_success_message(app_id, run_id, action)
89
+ if run_id == 'default'
90
+ console.success "Application #{app_id} #{action}!"
91
+ else
92
+ console.success "Application #{app_id}, run #{run_id} #{action}!"
93
+ end
94
+ end
95
+
96
+
97
+ def compile_and_dependencies( script , in_progress_message, complete_message)
98
+ # If the current directory is not a nutella application, return
99
+ unless Nutella.current_app.exist?
100
+ console.warn 'The current directory is not a nutella application'
101
+ return
102
+ end
103
+
104
+ # Run script
105
+ return unless run_script_for_all_bots_in( Dir.pwd, script, in_progress_message )
106
+
107
+ # Output success message
108
+ console.success "All #{complete_message} for #{Nutella.current_app.config['name']}"
109
+ end
110
+
111
+
112
+ end
113
+
114
+ end
@@ -1,4 +1,4 @@
1
- require 'core/command'
1
+ require 'commands/meta/command'
2
2
  require 'json'
3
3
 
4
4
  module Nutella
@@ -0,0 +1,60 @@
1
+ require 'commands/meta/command'
2
+ require 'fileutils'
3
+
4
+ module Nutella
5
+ class New < Command
6
+ @description = 'Creates a new nutella application'
7
+
8
+ def run(args=nil)
9
+ app_id = args[0]
10
+
11
+ # If no other arguments, show help and quit here
12
+ if args.empty?
13
+ console.warn 'You need to specify a name for your new application'
14
+ return
15
+ end
16
+
17
+ # Check that a directory (i.e. an app) with the same name doesn't already exist
18
+ # If it does it looks into it to see if there is a nutella.json file and displays
19
+ # the proper error message
20
+ if File.directory? app_id
21
+ if File.exist? "#{app_id}/nutella.json"
22
+ console.warn "An application named #{app_id} already exists"
23
+ return
24
+ else
25
+ console.warn "A directory named #{app_id} already exists"
26
+ return
27
+ end
28
+ end
29
+
30
+ # If all seems good, generate the application skeleton
31
+ create_dir_structure app_id
32
+
33
+ # Display a nice success message and return
34
+ console.success "Your new nutella application #{app_id} is ready!"
35
+ end
36
+
37
+
38
+ private
39
+
40
+
41
+ def create_dir_structure( app_id )
42
+ # Create directories
43
+ FileUtils.mkdir_p("#{app_id}/bots")
44
+ FileUtils.mkdir_p("#{app_id}/interfaces")
45
+ # Create nutella.json hash
46
+ config_file_hash = {
47
+ :name => app_id,
48
+ :version => '0.1.0',
49
+ :nutella_version => File.open("#{Nutella::NUTELLA_HOME}VERSION", 'rb').read,
50
+ :type => 'application',
51
+ :description => 'A quick description of your application'
52
+ }
53
+ # Write nutella.json hash
54
+ File.open("#{app_id}/nutella.json", 'w') do |f|
55
+ f.write(JSON.pretty_generate(config_file_hash))
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ require 'commands/meta/command'
2
+
3
+
4
+ module Nutella
5
+ class Runs < Command
6
+ @description = 'Displays a list of runs for the current application or all applications'
7
+
8
+ def run(args=nil)
9
+
10
+ # If invoked with "all" it will show all the runs under this instance of nutella
11
+ if args[0]=='--all' || args[0]=='-a'
12
+ display_all_runs
13
+ else
14
+ # If the current directory is not a nutella application, return
15
+ unless Nutella.current_app.exist?
16
+ console.warn 'The current directory is not a nutella application'
17
+ return
18
+ end
19
+ # Display list of runs for current nutella application
20
+ display_app_runs
21
+ end
22
+ end
23
+
24
+
25
+ private
26
+
27
+
28
+ def display_all_runs
29
+ if Nutella.runlist.empty?
30
+ console.info 'You are not running any nutella apps'
31
+ else
32
+ console.info 'Currently running:'
33
+ Nutella.runlist.all_runs.each do |app_id, _|
34
+ console.info "#{app_id}:"
35
+ Nutella.runlist.runs_for_app(app_id).each do |run_id|
36
+ console.info " #{run_id}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def display_app_runs
43
+ app_id = Nutella.current_app.config['name']
44
+ app_runs = Nutella.runlist.runs_for_app app_id
45
+ console.info "Currently running #{app_runs.length} instances of app '#{app_id}':"
46
+ app_runs.each do |run_id|
47
+ console.info " #{run_id}"
48
+ end
49
+ end
50
+
51
+
52
+ end
53
+ end
54
+
@@ -0,0 +1,321 @@
1
+ require 'commands/meta/run_command'
2
+ require 'tmux/tmux'
3
+
4
+ module Nutella
5
+ class Start < RunCommand
6
+ @description = 'Starts all or some of the bots in the current application'
7
+
8
+ def run(args=nil)
9
+
10
+ # If the current directory is not a nutella application, return
11
+ unless Nutella.current_app.exist?
12
+ console.warn 'The current directory is not a nutella application'
13
+ return
14
+ end
15
+
16
+ begin
17
+ run_id, params = parse_cli args
18
+ rescue StandardError => e
19
+ console.error e.message
20
+ return
21
+ end
22
+
23
+ app_id, app_path = fetch_app_details
24
+
25
+ if no_bot_to_start(app_id, app_path, params)
26
+ console.warn "Run #{run} not created: your application bots are already started and you specified no regular bots exclusively for this run"
27
+ return
28
+ end
29
+
30
+ return unless add_to_run_list( app_id, run_id, app_path )
31
+
32
+ return unless start_all_components(app_id, app_path, run_id, params)
33
+
34
+ # Output messages
35
+ print_confirmation(run_id, params, app_id, app_path)
36
+ end
37
+
38
+
39
+
40
+ private
41
+
42
+
43
+ # Parses command line arguments
44
+ def parse_cli( args )
45
+ # Parse run_id
46
+ run_id = parse_run_id_from args
47
+ # Extract parameters
48
+ params = parse_cli_parameters args
49
+ # Check that we are not using 'with' and 'without' options at the same time
50
+ unless params[:with].empty? || params[:without].empty?
51
+ raise StandardError.new 'You can\'t use both --with and --without at the same time'
52
+ end
53
+ return run_id, params
54
+ end
55
+
56
+
57
+ # Fetches the app_id and app_path
58
+ def fetch_app_details
59
+ # Extract app_id
60
+ app_id = Nutella.current_app.config['name']
61
+ return app_id, Dir.pwd
62
+ end
63
+
64
+
65
+ # Returns true if both the list of run level bots is empty and the app bots
66
+ # have been started already
67
+ def no_bot_to_start(app_id, app_path, params)
68
+ return run_level_bots_list(app_path, params).empty? && app_bots_started?(app_id)
69
+ end
70
+
71
+
72
+ # Returns the list of run-level bots for this run
73
+ # Depending on the mode we are in, we want to start only some bots, exclude only some bots or start all bots
74
+ def run_level_bots_list( app_path, params )
75
+ # Fetch the list of all components in the bots dir
76
+ all_bots = components_in_dir "#{app_path}/bots/"
77
+ # Fetch the list of app bots
78
+ app_bots = Nutella.current_app.config['app_bots']
79
+ # Return correct list based on the mode we are in
80
+ case start_mode(params)
81
+ when :WITH
82
+ return get_with_bots_list params[:with], app_bots
83
+ when :WO
84
+ return get_wo_bots_list all_bots, app_bots, params[:without]
85
+ when :ALL
86
+ return get_all_bots_list all_bots, app_bots
87
+ else
88
+ # If we get here it means we are both in with and without mode and something went very wrong...
89
+ raise 'You are using simultaneously with and without modes. This should not happen. Please contact developers.'
90
+ end
91
+ end
92
+
93
+ def start_mode(params)
94
+ return :WITH unless params[:with].empty?
95
+ return :WO unless params[:without].empty?
96
+ :ALL if params[:with].empty? && params[:without].empty?
97
+ end
98
+
99
+ # If we are in "with mode", we want to run only the bots in the "with list" (minus the ones in app_bots_list)
100
+ def get_with_bots_list( incl_bots, app_bots)
101
+ return app_bots.nil? ? incl_bots : incl_bots - app_bots
102
+ end
103
+
104
+ # If we are in "without mode", we want to run all the bots in the bots_list minus the ones in the "without list" and in the "app bots list"
105
+ def get_wo_bots_list( all_bots, app_bots, excl_bots )
106
+ return app_bots.nil? ? all_bots - excl_bots : all_bots - excl_bots - app_bots
107
+ end
108
+
109
+ # If we are in "all mode", we want to run all the bots minus the ones in the "app bots list"
110
+ def get_all_bots_list( all_bots, app_bots )
111
+ return app_bots.nil? ? all_bots : all_bots - app_bots
112
+ end
113
+
114
+
115
+
116
+ # Returns true if the app bots have been started already
117
+ def app_bots_started?( app_id )
118
+ Tmux.session_exist? Tmux.app_bot_session_name app_id
119
+ end
120
+
121
+
122
+ # Check that the run is unique and add it to the list of runs
123
+ # If it's not, return (without adding the run to the list of course)
124
+ def add_to_run_list( app_id, run_id, prj_dir )
125
+ unless Nutella.runlist.add?(app_id, run_id, prj_dir)
126
+ # If the run_id is already in the list, check that it's actually live
127
+ if Tmux.session_exist? Tmux.session_name(app_id, run_id)
128
+ console.error 'Impossible to start nutella app: an instance of this app with the same run_id is already running!'
129
+ console.error "You might want to kill it with 'nutella stop #{run_id}'"
130
+ return false
131
+ end
132
+ end
133
+ true
134
+ end
135
+
136
+
137
+ # Starts all the components at all levels for this run
138
+ def start_all_components( app_id, app_path, run_id, params )
139
+ # Start the internal broker
140
+ return false unless start_internal_broker
141
+ # Start all framework-level components (if needed)
142
+ return false unless start_framework_components
143
+ # Start all app-level bots (if any, if needed)
144
+ return false unless start_app_bots(app_id, app_path)
145
+ # Start all run-level bots
146
+ false unless start_run_bots( app_path, app_id, run_id, params )
147
+ end
148
+
149
+
150
+ def start_internal_broker
151
+ pid_file_path = "#{Nutella.config['broker_dir']}bin/.pid"
152
+ # Check if the process with pid indicated in the pidfile is alive
153
+ return true if sanitize_pid_file pid_file_path
154
+ # Check that broker is not running 'unsupervised' (i.e. check port 1883), if it is, return
155
+ return true unless broker_port_free?
156
+ # Broker is not running and there is no pid file so we try to start
157
+ # the internal broker and create a new pid file. Note that the pid file is created by
158
+ # the `startup` script, not here.
159
+ pid = fork
160
+ exec("#{Nutella.config['broker_dir']}/startup") if pid.nil?
161
+ # Wait a bit to give the chance to the broker to actually start up
162
+ sleep 1
163
+ # All went well so we return true
164
+ true
165
+ end
166
+
167
+
168
+ # Cleans the pid file of a given process
169
+ # @param [String] pid_file_path the file storing the pid file of the process
170
+ # @return [Boolean] true if the pid file exists AND the process with that pid is still alive
171
+ def sanitize_pid_file( pid_file_path )
172
+ # Does the pid file exist?
173
+ # If it does we try to see if the process with that pid is still alive
174
+ if File.exist? pid_file_path
175
+ pid_file = File.open(pid_file_path, 'rb')
176
+ pid = pid_file.read.to_i
177
+ pid_file.close
178
+ begin
179
+ # If this statement doesn't throw an exception then a process with
180
+ # this pid is still alive so we do nothing and just return true
181
+ Process.getpgid pid
182
+ return true
183
+ rescue
184
+ # If there is an exception, there is no process with this pid
185
+ # so we have a stale pid file that we need to remove
186
+ File.delete pid_file_path
187
+ return false
188
+ end
189
+ end
190
+ # If there is no pid file, there is no process running
191
+ false
192
+ end
193
+
194
+
195
+ # Checks if port 1883 (MQTT broker port) is free
196
+ # or some other service is already listening on it
197
+ def broker_port_free?
198
+ begin
199
+ s = TCPServer.new('0.0.0.0', 1883)
200
+ s.close
201
+ rescue
202
+ return false
203
+ end
204
+ true
205
+ end
206
+
207
+
208
+ def start_framework_components
209
+ nutella_components_dir = "#{Nutella::NUTELLA_HOME}framework_components"
210
+ if File.exist? "#{nutella_components_dir}/order.json"
211
+ components_list = JSON.parse IO.read "#{nutella_components_dir}/order.json"
212
+ else
213
+ components_list = components_in_dir(nutella_components_dir)
214
+ end
215
+ components_list.each do |component|
216
+ if File.exist? "#{nutella_components_dir}/#{component}/startup"
217
+ unless start_framework_component "#{nutella_components_dir}/#{component}"
218
+ return false
219
+ end
220
+ end
221
+ end
222
+ true
223
+ end
224
+
225
+
226
+ def start_framework_component( component_dir )
227
+ pid_file_path = "#{component_dir}/.pid"
228
+ return true if sanitize_pid_file pid_file_path
229
+ # Component is not running and there is no pid file so we try to start it
230
+ # and create a new pid file. Note that the pid file is created by
231
+ # the startup script!
232
+ nutella_config_file = "#{Nutella.config['config_dir']}config.json"
233
+ runs_list_file = "#{Nutella.config['config_dir']}runlist.json"
234
+ if nutella_config_file==nil || runs_list_file==nil
235
+ return false
236
+ end
237
+ # We are passing the configuration file and the run list files paths to the framework components
238
+ command = "#{component_dir}/startup #{nutella_config_file} #{runs_list_file}"
239
+ pid = fork
240
+ exec(command) if pid.nil?
241
+ # All went well so we return true
242
+ true
243
+ end
244
+
245
+
246
+ def start_app_bots( app_id, app_path )
247
+ app_bots_list = Nutella.current_app.config['app_bots']
248
+ bots_dir = "#{app_path}/bots/"
249
+ # If app bots have been started already, then do nothing
250
+ unless Tmux.session_exist? Tmux.app_bot_session_name app_id
251
+ # Start all app bots in the list into a new tmux session
252
+ tmux = Tmux.new app_id, nil
253
+ for_each_component_in_dir bots_dir do |bot|
254
+ unless app_bots_list.nil? || !app_bots_list.include?( bot )
255
+ # If there is no 'startup' script output a warning (because
256
+ # startup is mandatory) and skip the bot
257
+ unless File.exist?("#{bots_dir}#{bot}/startup")
258
+ console.warn "Impossible to start bot #{bot}. Couldn't locate 'startup' script."
259
+ next
260
+ end
261
+ # Create a new window in the session for this run
262
+ tmux.new_app_bot_window bot
263
+ end
264
+ end
265
+ end
266
+ true
267
+ end
268
+
269
+
270
+ def start_run_bots( app_path, app_id, run_id, params )
271
+ # Create a new tmux instance for this run
272
+ tmux = Tmux.new app_id, run_id
273
+ # Fetch bots dir
274
+ bots_dir = "#{app_path}/bots/"
275
+ # Start the appropriate bots
276
+ run_level_bots_list( app_path, params ).each { |bot| start_run_level_bot(bots_dir, bot, tmux) }
277
+ true
278
+ end
279
+
280
+ # Starts a run level bot
281
+ def start_run_level_bot( bots_dir, bot, tmux )
282
+ # If there is no 'startup' script output a warning (because
283
+ # startup is mandatory) and skip the bot
284
+ unless File.exist?("#{bots_dir}#{bot}/startup")
285
+ console.warn "Impossible to start bot #{bot}. Couldn't locate 'startup' script."
286
+ return
287
+ end
288
+ # Create a new window in the session for this run
289
+ tmux.new_bot_window bot
290
+ end
291
+
292
+
293
+ def print_confirmation( run_id, params, app_id, app_path )
294
+ # If there are no run-level bots to start, do not create the run and error out
295
+ if run_level_bots_list(app_path, params).empty?
296
+ console.warn 'This run doesn\'t seem to have any components. No run was created.'
297
+ return
298
+ end
299
+ print_success_message(app_id, run_id, 'started')
300
+ print_monitoring_details(app_id, run_id)
301
+ end
302
+
303
+
304
+ def print_monitoring_details( app_id, run_id )
305
+ # Output broker info
306
+ console.success "Application is running on broker: #{Nutella.config['broker']}"
307
+ # If some application bots were started, say it
308
+ app_bots_list = Nutella.current_app.config['app_bots']
309
+ unless app_bots_list.nil? || app_bots_list.empty?
310
+ console.success "Do `tmux attach-session -t #{Tmux.app_bot_session_name(app_id)}` to monitor your app bots."
311
+ end
312
+ # Output rest of monitoring info
313
+ console.success "Do `tmux attach-session -t #{Tmux.session_name(app_id,run_id)}` to monitor your bots."
314
+ console.success "Go to http://localhost:#{Nutella.config['main_interface_port']}/#{app_id}/#{run_id} to access your interfaces"
315
+ end
316
+
317
+
318
+ end
319
+
320
+ end
321
+
@@ -0,0 +1,101 @@
1
+ require 'commands/meta/run_command'
2
+ require 'tmux/tmux'
3
+
4
+ module Nutella
5
+ class Stop < RunCommand
6
+ @description = 'Stops all the bots in the current application'
7
+
8
+ def run(args=nil)
9
+
10
+ # If the current directory is not a nutella application, return
11
+ unless Nutella.current_app.exist?
12
+ console.warn 'The current directory is not a nutella application'
13
+ return
14
+ end
15
+
16
+ # Extract run (passed run name) and run_id
17
+ run_id = parse_run_id_from args
18
+ app_id = Nutella.current_app.config['name']
19
+
20
+ # Check that the specified run exists in the list and, if it doesn't, return
21
+ return unless remove_from_run_list app_id, run_id
22
+
23
+ # Stops all run-level bots
24
+ Tmux.kill_run_session app_id, run_id
25
+
26
+ # Stop all app-level bots (if any, if needed)
27
+ stop_app_bots app_id
28
+
29
+ # Stop all framework-level components (if needed)
30
+ if Nutella.runlist.empty?
31
+ stop_framework_components
32
+ end
33
+
34
+ # If running on the internal broker, stop it if needed
35
+ if Nutella.runlist.empty?
36
+ stop_internal_broker
37
+ end
38
+
39
+ # Output success message
40
+ print_success_message(app_id, run_id, 'stopped')
41
+ end
42
+
43
+
44
+ private
45
+
46
+
47
+ def remove_from_run_list( app_id, run_id )
48
+ unless Nutella.runlist.delete? app_id, run_id
49
+ console.warn "Run #{run_id} doesn't exist. Impossible to stop it."
50
+ return false
51
+ end
52
+ true
53
+ end
54
+
55
+
56
+ def stop_app_bots( app_id )
57
+ tmux_session_name = Tmux.app_bot_session_name app_id
58
+ if Tmux.session_exist? tmux_session_name
59
+ # Are there any run of this app hinging on the app bots?
60
+ if Nutella.runlist.runs_for_app(app_id).empty?
61
+ Tmux.kill_app_session app_id
62
+ end
63
+ end
64
+ true
65
+ end
66
+
67
+
68
+ def stop_framework_components
69
+ nutella_components_dir = "#{Nutella::NUTELLA_HOME}framework_components"
70
+ for_each_component_in_dir nutella_components_dir do |component|
71
+ pid_file_path = "#{nutella_components_dir}/#{component}/.pid"
72
+ kill_process_with_pid pid_file_path
73
+ end
74
+ end
75
+
76
+
77
+ def stop_internal_broker
78
+ pid_file_path = "#{Nutella.config['broker_dir']}/bin/.pid"
79
+ kill_process_with_pid pid_file_path
80
+ end
81
+
82
+
83
+ # Does the process pid file exist?
84
+ # If it does we send a SIGKILL to the process with that pid
85
+ # to stop the process and delete the pid file
86
+ def kill_process_with_pid( pid_file_path )
87
+ if File.exist? pid_file_path
88
+ pid_file = File.open( pid_file_path, 'rb' )
89
+ pid = pid_file.read.to_i
90
+ pid_file.close
91
+ begin
92
+ Process.kill( 'SIGKILL', pid )
93
+ rescue
94
+ # Pid file exists but process is dead. Do nothing
95
+ end
96
+ File.delete pid_file_path
97
+ end
98
+ end
99
+
100
+ end
101
+ end