minicron 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +187 -0
  4. data/Rakefile +17 -0
  5. data/bin/minicron +26 -0
  6. data/lib/minicron.rb +179 -0
  7. data/lib/minicron/alert.rb +115 -0
  8. data/lib/minicron/alert/email.rb +50 -0
  9. data/lib/minicron/alert/pagerduty.rb +39 -0
  10. data/lib/minicron/alert/sms.rb +47 -0
  11. data/lib/minicron/cli.rb +367 -0
  12. data/lib/minicron/constants.rb +7 -0
  13. data/lib/minicron/cron.rb +192 -0
  14. data/lib/minicron/hub/app.rb +132 -0
  15. data/lib/minicron/hub/assets/app/application.js +151 -0
  16. data/lib/minicron/hub/assets/app/components/schedules.js +280 -0
  17. data/lib/minicron/hub/assets/app/controllers/executions.js +35 -0
  18. data/lib/minicron/hub/assets/app/controllers/hosts.js +129 -0
  19. data/lib/minicron/hub/assets/app/controllers/jobs.js +109 -0
  20. data/lib/minicron/hub/assets/app/controllers/schedules.js +80 -0
  21. data/lib/minicron/hub/assets/app/helpers.js +22 -0
  22. data/lib/minicron/hub/assets/app/models/execution.js +13 -0
  23. data/lib/minicron/hub/assets/app/models/host.js +15 -0
  24. data/lib/minicron/hub/assets/app/models/job.js +15 -0
  25. data/lib/minicron/hub/assets/app/models/job_execution_output.js +11 -0
  26. data/lib/minicron/hub/assets/app/models/schedule.js +32 -0
  27. data/lib/minicron/hub/assets/app/router.js +31 -0
  28. data/lib/minicron/hub/assets/app/routes/executions.js +36 -0
  29. data/lib/minicron/hub/assets/app/routes/hosts.js +42 -0
  30. data/lib/minicron/hub/assets/app/routes/index.js +9 -0
  31. data/lib/minicron/hub/assets/app/routes/jobs.js +52 -0
  32. data/lib/minicron/hub/assets/app/routes/schedules.js +37 -0
  33. data/lib/minicron/hub/assets/css/bootswatch.min.css +9 -0
  34. data/lib/minicron/hub/assets/css/main.scss +323 -0
  35. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  36. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  37. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  38. data/lib/minicron/hub/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  39. data/lib/minicron/hub/assets/fonts/lato-bold-700.woff +0 -0
  40. data/lib/minicron/hub/assets/fonts/lato-italic-400.woff +0 -0
  41. data/lib/minicron/hub/assets/fonts/lato-regular-400.woff +0 -0
  42. data/lib/minicron/hub/assets/js/ansi_up-1.1.1.min.js +6 -0
  43. data/lib/minicron/hub/assets/js/auth/ember-auth-9.0.7.min.js +2 -0
  44. data/lib/minicron/hub/assets/js/auth/ember-auth-request-jquery-1.0.3.min.js +1 -0
  45. data/lib/minicron/hub/assets/js/bootstrap-3.1.1.min.js +6 -0
  46. data/lib/minicron/hub/assets/js/ember-1.4.1.min.js +18 -0
  47. data/lib/minicron/hub/assets/js/ember-data-1.0.0-beta.7.f87cba88.min.js +10 -0
  48. data/lib/minicron/hub/assets/js/faye-browser-1.0.1.min.js +2 -0
  49. data/lib/minicron/hub/assets/js/handlebars-1.3.0.min.js +29 -0
  50. data/lib/minicron/hub/assets/js/jquery-2.1.0.min.js +4 -0
  51. data/lib/minicron/hub/assets/js/moment-2.5.1.min.js +7 -0
  52. data/lib/minicron/hub/controllers/api/executions.rb +34 -0
  53. data/lib/minicron/hub/controllers/api/hosts.rb +150 -0
  54. data/lib/minicron/hub/controllers/api/job_execution_outputs.rb +30 -0
  55. data/lib/minicron/hub/controllers/api/jobs.rb +118 -0
  56. data/lib/minicron/hub/controllers/api/schedule.rb +184 -0
  57. data/lib/minicron/hub/controllers/index.rb +5 -0
  58. data/lib/minicron/hub/db/schema.rb +98 -0
  59. data/lib/minicron/hub/db/schema.sql +158 -0
  60. data/lib/minicron/hub/models/alert.rb +7 -0
  61. data/lib/minicron/hub/models/execution.rb +8 -0
  62. data/lib/minicron/hub/models/host.rb +7 -0
  63. data/lib/minicron/hub/models/job.rb +18 -0
  64. data/lib/minicron/hub/models/job_execution_output.rb +7 -0
  65. data/lib/minicron/hub/models/schedule.rb +25 -0
  66. data/lib/minicron/hub/serializers/execution.rb +75 -0
  67. data/lib/minicron/hub/serializers/host.rb +57 -0
  68. data/lib/minicron/hub/serializers/job.rb +104 -0
  69. data/lib/minicron/hub/serializers/job_execution_output.rb +48 -0
  70. data/lib/minicron/hub/serializers/schedule.rb +68 -0
  71. data/lib/minicron/hub/views/handlebars/application.erb +51 -0
  72. data/lib/minicron/hub/views/handlebars/errors.erb +29 -0
  73. data/lib/minicron/hub/views/handlebars/executions.erb +79 -0
  74. data/lib/minicron/hub/views/handlebars/hosts.erb +205 -0
  75. data/lib/minicron/hub/views/handlebars/jobs.erb +203 -0
  76. data/lib/minicron/hub/views/handlebars/loading.erb +3 -0
  77. data/lib/minicron/hub/views/handlebars/schedules.erb +354 -0
  78. data/lib/minicron/hub/views/index.erb +7 -0
  79. data/lib/minicron/hub/views/layouts/app.erb +15 -0
  80. data/lib/minicron/monitor.rb +116 -0
  81. data/lib/minicron/transport.rb +15 -0
  82. data/lib/minicron/transport/client.rb +80 -0
  83. data/lib/minicron/transport/faye/client.rb +103 -0
  84. data/lib/minicron/transport/faye/extensions/job_handler.rb +184 -0
  85. data/lib/minicron/transport/faye/server.rb +58 -0
  86. data/lib/minicron/transport/server.rb +62 -0
  87. data/lib/minicron/transport/ssh.rb +51 -0
  88. data/spec/invalid_config.toml +2 -0
  89. data/spec/minicron/cli_spec.rb +154 -0
  90. data/spec/minicron/transport/client_spec.rb +8 -0
  91. data/spec/minicron/transport/faye/client_spec.rb +53 -0
  92. data/spec/minicron/transport/server_spec.rb +70 -0
  93. data/spec/minicron/transport_spec.rb +13 -0
  94. data/spec/minicron_spec.rb +133 -0
  95. data/spec/spec_helper.rb +33 -0
  96. data/spec/valid_config.toml +48 -0
  97. metadata +577 -0
@@ -0,0 +1,7 @@
1
+ module Minicron
2
+ VERSION = '0.1.0'
3
+ DEFAULT_CONFIG_FILE = '/etc/minicron.toml'
4
+ BASE_PATH = File.expand_path('../../../', __FILE__)
5
+ LIB_PATH = File.expand_path('../../', __FILE__)
6
+ HUB_PATH = File.expand_path('../../minicron/hub', __FILE__)
7
+ end
@@ -0,0 +1,192 @@
1
+ require 'shellwords'
2
+
3
+ module Minicron
4
+ # Used to interact with the crontab on hosts over an ssh connection
5
+ # TODO: I've had a moment of clarity, I don't need to do all the CRUD
6
+ # using unix commands. I can cat the crontab, manipulate it in ruby
7
+ # and then echo it back!
8
+ class Cron
9
+ # Initialise the cron class
10
+ #
11
+ # @param ssh [Minicron::Transport::SSH] instance
12
+ def initialize(ssh)
13
+ @ssh = ssh
14
+ end
15
+
16
+ # Escape the quotes in a command
17
+ #
18
+ # @param command [String]
19
+ # @return [String]
20
+ def escape_command(command)
21
+ command.gsub(/\\|'/) { |c| "\\#{c}" }
22
+ end
23
+
24
+ # Build the minicron command to be used in the crontab
25
+ #
26
+ # @param command [String]
27
+ # @param schedule [String]
28
+ # @return [String]
29
+ def build_minicron_command(command, schedule)
30
+ # Escape any quotes in the command
31
+ command = escape_command(command)
32
+
33
+ "#{schedule} root minicron run '#{command}'"
34
+ end
35
+
36
+ # Used to find a string and replace it with another in the crontab by
37
+ # using the sed command
38
+ #
39
+ # @param conn an instance of an open ssh connection
40
+ # @param find [String]
41
+ # @param replace [String]
42
+ def find_and_replace(conn, find, replace)
43
+ # TODO: move ssh test here for crontab permissions
44
+
45
+ # Get the full crontab
46
+ crontab = conn.exec!('cat /etc/crontab').to_s.strip
47
+
48
+ # Replace the full string with the replacement string
49
+ begin
50
+ crontab[find] = replace
51
+ rescue Exception => e
52
+ raise Exception, "Unable to replace '#{find}' with '#{replace}' in the crontab, reason: #{e}"
53
+ end
54
+
55
+ # Echo the crontab back to the tmp crontab
56
+ update = conn.exec!("echo #{crontab.shellescape} > /etc/crontab.tmp && echo 'y' || echo 'n'").to_s.strip
57
+
58
+ # Throw an exception if it failed
59
+ if update != 'y'
60
+ raise Exception, "Unable to replace '#{find}' with '#{replace}' in the crontab"
61
+ end
62
+
63
+ # If it's a delete
64
+ if replace == ''
65
+ # Check the original line is no longer there
66
+ grep = conn.exec!("grep -F \"#{find}\" /etc/crontab.tmp").to_s.strip
67
+
68
+ # Throw an exception if we can't see our new line at the end of the file
69
+ if grep != replace
70
+ raise Exception, "Expected to find nothing when grepping crontab but found #{grep}"
71
+ end
72
+ else
73
+ # Check the updated line is there
74
+ grep = conn.exec!("grep -F \"#{replace}\" /etc/crontab.tmp").to_s.strip
75
+
76
+ # Throw an exception if we can't see our new line at the end of the file
77
+ if grep != replace
78
+ raise Exception, "Expected to find '#{replace}' when grepping crontab but found #{grep}"
79
+ end
80
+ end
81
+
82
+ # And finally replace the crontab with the new one now we now the change worked
83
+ move = conn.exec!("mv /etc/crontab.tmp /etc/crontab && echo 'y' || echo 'n'").to_s.strip
84
+
85
+ if move != 'y'
86
+ raise Exception, 'Unable to move tmp crontab with updated crontab'
87
+ end
88
+ end
89
+
90
+ # Add the schedule for this job to the crontab
91
+ #
92
+ # @param job [Minicron::Hub::Job] an instance of a job model
93
+ # @param schedule [String] the job schedule as a string
94
+ # @param conn an instance of an open ssh connection
95
+ def add_schedule(job, schedule, conn = nil)
96
+ # Open an SSH connection
97
+ conn ||= @ssh.open
98
+
99
+ # Prepare the line we are going to write to the crontab
100
+ line = build_minicron_command(job.command, schedule)
101
+ echo_line = "echo \"#{line}\" >> /etc/crontab && echo 'y' || echo 'n'"
102
+
103
+ # Append it to the end of the crontab
104
+ write = conn.exec!(echo_line).strip
105
+
106
+ # Throw an exception if it failed
107
+ if write != 'y'
108
+ raise Exception, "Unable to write '#{line}' to the crontab"
109
+ end
110
+
111
+ # Check the line is there
112
+ tail = conn.exec!('tail -n 1 /etc/crontab').strip
113
+
114
+ # Throw an exception if we can't see our new line at the end of the file
115
+ if tail != line
116
+ raise Exception, "Expected to find '#{line}' at eof but found '#{tail}'"
117
+ end
118
+ end
119
+
120
+ # Update the schedule for this job in the crontab
121
+ #
122
+ # @param job [Minicron::Hub::Job] an instance of a job model
123
+ # @param old_schedule [String] the old job schedule as a string
124
+ # @param new_schedule [String] the new job schedule as a string
125
+ # @param conn an instance of an open ssh connection
126
+ def update_schedule(job, old_schedule, new_schedule, conn = nil)
127
+ # Open an SSH connection
128
+ conn ||= @ssh.open
129
+
130
+ # We are looking for the current value of the schedule
131
+ find = build_minicron_command(job.command, old_schedule)
132
+
133
+ # And replacing it with the updated value
134
+ replace = build_minicron_command(job.command, new_schedule)
135
+
136
+ # Replace the old schedule with the new schedule
137
+ find_and_replace(conn, find, replace)
138
+ end
139
+
140
+ # Remove the schedule for this job from the crontab
141
+ #
142
+ # @param job [Minicron::Hub::Job] an instance of a job model
143
+ # @param schedule [String] the job schedule as a string
144
+ # @param conn an instance of an open ssh connection
145
+ def delete_schedule(job, schedule, conn = nil)
146
+ # Open an SSH connection
147
+ conn ||= @ssh.open
148
+
149
+ # We are looking for the current value of the schedule
150
+ find = build_minicron_command(job.command, schedule)
151
+
152
+ # Replace the old schedule with nothing i.e deleting it
153
+ find_and_replace(conn, find, '')
154
+ end
155
+
156
+ # Delete a job and all it's schedules from the crontab
157
+ #
158
+ # @param job [Minicron::Hub::Job] a job instance with it's schedules
159
+ # @param conn an instance of an open ssh connection
160
+ def delete_job(job, conn = nil)
161
+ conn ||= @ssh.open
162
+
163
+ # Loop through each schedule and delete them one by one
164
+ # TODO: share the ssh connection for this so it's faster when
165
+ # many schedules exist
166
+ # TODO: what if one schedule removal fails but others don't? Should
167
+ # we try and rollback somehow or just return the job with half its
168
+ # schedules deleted?
169
+ job.schedules.each do |schedule|
170
+ delete_schedule(job, schedule.formatted, conn)
171
+ end
172
+ end
173
+
174
+ # Delete a host and all it's jobs from the crontab
175
+ #
176
+ # @param job [Minicron::Hub::Job] a job instance with it's schedules
177
+ # @param conn an instance of an open ssh connection
178
+ def delete_host(host, conn = nil)
179
+ conn ||= @ssh.open
180
+
181
+ # Loop through each job and delete them one by one
182
+ # TODO: share the ssh connection for this so it's faster when
183
+ # many schedules exist
184
+ # TODO: what if one schedule removal fails but others don't? Should
185
+ # we try and rollback somehow or just return the job with half its
186
+ # schedules deleted?
187
+ host.jobs.each do |job|
188
+ delete_job(job, conn)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,132 @@
1
+ require 'minicron'
2
+ require 'sinatra/base'
3
+ require 'sinatra/activerecord'
4
+ require 'sinatra/assetpack'
5
+ require 'erubis'
6
+ require 'oj'
7
+
8
+ module Minicron::Hub
9
+ class App < Sinatra::Base
10
+ register Sinatra::ActiveRecordExtension
11
+ register Sinatra::AssetPack
12
+
13
+ # Set the application root
14
+ set :root, Minicron::HUB_PATH
15
+
16
+ configure do
17
+ # Don't log them. We'll do that ourself
18
+ set :dump_errors, false
19
+
20
+ # Don't capture any errors. Throw them up the stack
21
+ set :raise_errors, true
22
+
23
+ # Disable internal middleware for presenting errors
24
+ # as useful HTML pages
25
+ set :show_exceptions, false
26
+ end
27
+
28
+ # Configure how we server assets
29
+ assets do
30
+ serve '/css', :from => 'assets/css'
31
+ serve '/js', :from => 'assets/js'
32
+ serve '/fonts', :from => 'assets/fonts'
33
+ serve '/app', :from => 'assets/app'
34
+
35
+ # Set up the application css
36
+ css :app, '/css/all.css', [
37
+ '/css/bootswatch.min.css',
38
+ '/css/main.css'
39
+ ]
40
+
41
+ # Set up the application javascript
42
+ js :app, '/js/all.js', [
43
+ # Dependencies, the order of these is important
44
+ '/js/jquery-2.1.0.min.js',
45
+ '/js/handlebars-1.3.0.min.js',
46
+ '/js/ember-1.4.1.min.js',
47
+ '/js/ember-data-1.0.0-beta.7.f87cba88.min.js',
48
+ '/js/faye-browser-1.0.1.min.js',
49
+ '/js/ansi_up-1.1.1.min.js',
50
+ '/js/bootstrap-3.1.1.min.js',
51
+ '/js/moment-2.5.1.min.js',
52
+
53
+ # Ember application files
54
+ '/app/**/*.js'
55
+ ]
56
+ end
57
+
58
+ # Called on class initilisation, sets up the database and requires all
59
+ # the application files
60
+ def initialize
61
+ super
62
+
63
+ # Initialize the db
64
+ Minicron::Hub::App.setup_db
65
+
66
+ # Load all our model serializers
67
+ Dir[File.dirname(__FILE__) + '/serializers/*.rb'].each do |serializer|
68
+ require serializer
69
+ end
70
+
71
+ # Load all our models
72
+ Dir[File.dirname(__FILE__) + '/models/*.rb'].each do |model|
73
+ require model
74
+ end
75
+
76
+ # Load all our controllers
77
+ Dir[File.dirname(__FILE__) + '/controllers/**/*.rb'].each do |controller|
78
+ require controller
79
+ end
80
+ end
81
+
82
+ # Used to set up the database connection
83
+ def self.setup_db
84
+ # Configure the database
85
+ case Minicron.config['database']['type']
86
+ when 'mysql'
87
+ set :database, {
88
+ :adapter => 'mysql2',
89
+ :host => Minicron.config['database']['host'],
90
+ :database => Minicron.config['database']['database'],
91
+ :username => Minicron.config['database']['username'],
92
+ :password => Minicron.config['database']['password']
93
+ }
94
+ else
95
+ raise Exception, "The database #{Minicron.config['database']['type']} is not supported"
96
+ end
97
+ end
98
+ end
99
+
100
+ # Based on http://hawkins.io/2013/06/error-handling-in-sinatra-apis/
101
+ class ExceptionHandling
102
+ def initialize(app)
103
+ @app = app
104
+ end
105
+
106
+ def handle_exception(env, e, status)
107
+ if Minicron.config['global']['trace']
108
+ env['rack.errors'].puts(e)
109
+ env['rack.errors'].puts(e.backtrace.join("\n"))
110
+ env['rack.errors'].flush
111
+ end
112
+
113
+ # Display the error message
114
+ hash = { :error => e.to_s }
115
+
116
+ # Display the full trace if tracing is enabled
117
+ hash[:trace] = e.backtrace if Minicron.config['global']['trace']
118
+
119
+ [status, { 'Content-Type' => 'application/json' }, [hash.to_json]]
120
+ end
121
+
122
+ def call(env)
123
+ begin
124
+ @app.call env
125
+ rescue ActiveRecord::RecordNotFound => e
126
+ handle_exception(env, e, 404)
127
+ rescue Exception => e
128
+ handle_exception(env, e, 500)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ (function() {
4
+ window.Minicron = Ember.Application.create({
5
+ LOG_TRANSITIONS: true,
6
+ LOG_ACTIVE_GENERATION: true,
7
+ LOG_VIEW_LOOKUPS: true,
8
+ LOG_TRANSITIONS_INTERNAL: true
9
+ });
10
+
11
+ // This should be called by all views when they are loaded
12
+ Minicron.onViewLoad = function() {
13
+ // Twitter tooltip plugin
14
+ Ember.$('body').tooltip({
15
+ selector: '[data-toggle=tooltip]'
16
+ });
17
+ };
18
+
19
+ // Configure Ember Data so it can find the API
20
+ Minicron.ApplicationAdapter = DS.RESTAdapter.extend({
21
+ namespace: 'api',
22
+ });
23
+
24
+ Minicron.ApplicationController = Ember.ArrayController.extend({
25
+ sortedExecutions: function() {
26
+ return this.get('content').toArray().sort(function(a, b) {
27
+ a = +moment(a.get('created_at'));
28
+ b = +moment(b.get('created_at'));
29
+
30
+ if (a > b) {
31
+ return -1;
32
+ } else if (a < b) {
33
+ return 1;
34
+ }
35
+
36
+ return 0;
37
+ });
38
+ }.property('content.@each').cacheable()
39
+ });
40
+
41
+ Minicron.ApplicationRoute = Ember.Route.extend({
42
+ actions: {
43
+ error: function(error) {
44
+ console.log(error);
45
+
46
+ // Create a new error to be passed as the controller
47
+ var ember_error = new Ember.Error();
48
+
49
+ // Set the details of the error we need
50
+ ember_error.name = 'Error';
51
+ ember_error.message = error.responseJSON.error;
52
+ ember_error.number = error.status;
53
+
54
+ this.render('fatal-error', {
55
+ controller: ember_error
56
+ });
57
+ }
58
+ },
59
+ model: function() {
60
+ return this.store.find('execution');
61
+ },
62
+ setupController: function(controller, model) {
63
+ controller.set('application', model);
64
+ this._super(controller, model);
65
+ },
66
+ afterModel: function(model, transition) {
67
+ var client = new Faye.Client(window.location.protocol + '//' + window.location.host + '/faye'),
68
+ store = model.store,
69
+ self = this;
70
+ window.store = store;
71
+
72
+ client.addExtension({
73
+ incoming: function(message, callback) {
74
+ // We only care about job messages
75
+ if (message.channel.substr(1, 3) === 'job') {
76
+ var segments = message.channel.split('/');
77
+ var job_id = segments[2];
78
+ var job_execution_id = segments[3];
79
+ var type = segments[4];
80
+ var message_data = message.data.message;
81
+
82
+ // TODO: remove this!
83
+ console.log(job_id, job_execution_id, type, message);
84
+
85
+ // Is it a status message?
86
+ if (type === 'status') {
87
+ // Is it a setup message
88
+ if (typeof message_data.action != 'undefined' && message_data.action === 'SETUP') {
89
+ // The SETUP message is defined slightly differently, segment 2 contains the
90
+ // job hash and segment 3 contains '*job_id*-*job_execution_id*'
91
+ var ids = job_execution_id.split('-');
92
+ job_id = ids[0];
93
+ job_execution_id = ids[1];
94
+
95
+ // Append the job relationship to it
96
+ store.find('job', job_id).then(function(job) {
97
+ // Create the execution
98
+ store.push('execution', {
99
+ id: job_execution_id,
100
+ created_at: moment.utc(message.data.ts).format('YYYY-MM-DDTHH:mm:ss[Z]'),
101
+ job: job
102
+ }, true);
103
+ });
104
+ // Is it a start message?
105
+ } else if (message_data.substr(0, 5) === 'START') {
106
+ // Set the execution start time
107
+ store.find('execution', job_execution_id).then(function(execution) {
108
+ execution.set('started_at', moment.utc(message_data.substr(6)).format('YYYY-MM-DDTHH:mm:ss[Z]'));
109
+ });
110
+ // Is it a finish message?
111
+ } else if (message_data.substr(0, 6) === 'FINISH') {
112
+ // Set the execution finish time
113
+ store.find('execution', job_execution_id).then(function(execution) {
114
+ execution.set('finished_at', moment.utc(message_data.substr(7)).format('YYYY-MM-DDTHH:mm:ss[Z]'));
115
+ });
116
+ // Is it an exit message?
117
+ } else if (message_data.substr(0, 4) === 'EXIT') {
118
+ // Set the execution exit status
119
+ store.find('execution', job_execution_id).then(function(execution) {
120
+ execution.set('exit_status', +message_data.substr(5));
121
+ });
122
+ }
123
+ // Is it an output message?
124
+ } else if (type === 'output') {
125
+ store.find('execution', job_execution_id).then(function(execution) {
126
+ // Add this bit of job execution output
127
+ var output = store.createRecord('job_execution_output', {
128
+ id: message.data.job_execution_output_id,
129
+ output: message_data,
130
+ seq: message.data.seq,
131
+ timestamp: moment(message.data.ts).format()
132
+ });
133
+
134
+ output.set('execution', execution);
135
+
136
+ execution.get('job_execution_outputs').then(function(outputs) {
137
+ outputs.pushObject(output);
138
+ });
139
+ });
140
+ }
141
+ }
142
+
143
+ // We're done with the message, pass it back to Faye
144
+ callback(message);
145
+ }
146
+ });
147
+
148
+ client.subscribe('/job/**');
149
+ }
150
+ });
151
+ })();