scout_agent 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/AUTHORS +4 -0
  2. data/CHANGELOG +3 -0
  3. data/COPYING +340 -0
  4. data/INSTALL +17 -0
  5. data/LICENSE +6 -0
  6. data/README +3 -0
  7. data/Rakefile +123 -0
  8. data/TODO +3 -0
  9. data/bin/scout_agent +11 -0
  10. data/lib/scout_agent.rb +73 -0
  11. data/lib/scout_agent/agent.rb +42 -0
  12. data/lib/scout_agent/agent/communication_agent.rb +85 -0
  13. data/lib/scout_agent/agent/master_agent.rb +301 -0
  14. data/lib/scout_agent/api.rb +241 -0
  15. data/lib/scout_agent/assignment.rb +105 -0
  16. data/lib/scout_agent/assignment/configuration.rb +30 -0
  17. data/lib/scout_agent/assignment/identify.rb +110 -0
  18. data/lib/scout_agent/assignment/queue.rb +95 -0
  19. data/lib/scout_agent/assignment/reset.rb +91 -0
  20. data/lib/scout_agent/assignment/snapshot.rb +92 -0
  21. data/lib/scout_agent/assignment/start.rb +149 -0
  22. data/lib/scout_agent/assignment/status.rb +44 -0
  23. data/lib/scout_agent/assignment/stop.rb +60 -0
  24. data/lib/scout_agent/assignment/upload_log.rb +61 -0
  25. data/lib/scout_agent/core_extensions.rb +260 -0
  26. data/lib/scout_agent/database.rb +386 -0
  27. data/lib/scout_agent/database/mission_log.rb +282 -0
  28. data/lib/scout_agent/database/queue.rb +126 -0
  29. data/lib/scout_agent/database/snapshots.rb +187 -0
  30. data/lib/scout_agent/database/statuses.rb +65 -0
  31. data/lib/scout_agent/dispatcher.rb +157 -0
  32. data/lib/scout_agent/id_card.rb +143 -0
  33. data/lib/scout_agent/lifeline.rb +243 -0
  34. data/lib/scout_agent/mission.rb +212 -0
  35. data/lib/scout_agent/order.rb +58 -0
  36. data/lib/scout_agent/order/check_in_order.rb +32 -0
  37. data/lib/scout_agent/order/snapshot_order.rb +33 -0
  38. data/lib/scout_agent/plan.rb +306 -0
  39. data/lib/scout_agent/server.rb +123 -0
  40. data/lib/scout_agent/tracked.rb +59 -0
  41. data/lib/scout_agent/wire_tap.rb +513 -0
  42. data/setup.rb +1360 -0
  43. data/test/tc_core_extensions.rb +89 -0
  44. data/test/tc_id_card.rb +115 -0
  45. data/test/tc_plan.rb +285 -0
  46. data/test/test_helper.rb +22 -0
  47. data/test/ts_all.rb +7 -0
  48. metadata +171 -0
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class MissionLog < Database
6
+ #
7
+ # A default number of seconds a mission is allowed to run before it is
8
+ # halted.
9
+ #
10
+ DEFAULT_TIMEOUT = 60
11
+ # The default number of minutes before a mission is run again.
12
+ DEFAULT_INTERVAL = 3
13
+ # A size limit for the reports table to prevent data from building up.
14
+ REPORTS_LIMIT = 3000
15
+
16
+ def update_schema(version = schema_version)
17
+ case version
18
+ when 0
19
+ <<-END_INITIAL_SCHEMA.trim
20
+ CREATE TABLE plans (
21
+ last_modified REQUIRED_TEXT_TYPE COLLATE NOCASE
22
+ );
23
+
24
+ CREATE TABLE missions (
25
+ plan_id DEFAULT_INTEGER_TYPE 1,
26
+ id INTEGER NOT NULL PRIMARY KEY,
27
+ name TEXT COLLATE NOCASE,
28
+ code REQUIRED_TEXT_TYPE,
29
+ options TEXT,
30
+ memory TEXT DEFAULT '{}',
31
+ timeout POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT},
32
+ interval POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_INTERVAL},
33
+ last_run_at DATETIME_TYPE,
34
+ next_run_at REQUIRED_DATETIME_TYPE
35
+ );
36
+ FOREIGN_KEY_CHECK_TRIGGER missions plan_id plans ROWID
37
+
38
+ CREATE TABLE reports (
39
+ mission_id INTEGER NOT NULL,
40
+ type TEXT NOT NULL COLLATE NOCASE
41
+ CHECK(type IN ('report', 'hint', 'alert', 'error')),
42
+ fields REQUIRED_TEXT_TYPE,
43
+ created_at DATETIME_TYPE
44
+ );
45
+ FOREIGN_KEY_CHECK_TRIGGER reports mission_id missions id
46
+ DEFAULT_LOCALTIME_TRIGGER reports created_at
47
+ LIMIT_TABLE_SIZE_TRIGGER reports #{REPORTS_LIMIT}
48
+ END_INITIAL_SCHEMA
49
+ end
50
+ end
51
+
52
+ def current_plan
53
+ plan = read_from_sqlite { |sqlite|
54
+ sqlite.first_row_from(<<-END_FIND_PLAN.trim)
55
+ SELECT ROWID AS id, last_modified
56
+ FROM plans
57
+ ORDER BY ROWID DESC
58
+ LIMIT 1
59
+ END_FIND_PLAN
60
+ }
61
+ plan.empty? ? nil : plan
62
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve plan
63
+ log.error("Database current plan error: #{error.message}.")
64
+ nil # not found
65
+ end
66
+
67
+ def update_plan(last_modified, missions)
68
+ write_to_sqlite do |sqlite|
69
+ begin
70
+ sqlite.execute(<<-END_INSERT_PLAN.trim, last_modified)
71
+ INSERT OR REPLACE INTO plans(ROWID, last_modified) VALUES(1, ?)
72
+ END_INSERT_PLAN
73
+
74
+ ids = missions.map { |m| m["id"] }
75
+ sqlite.execute(<<-END_DELETE_MISSIONS.trim, *ids)
76
+ DELETE FROM missions
77
+ WHERE id NOT IN (#{(['?'] * ids.size).join(', ')})
78
+ END_DELETE_MISSIONS
79
+ rescue Amalgalite::SQLite3::Error => error # failed to update plan
80
+ log.error("Database bad plan error: #{error.message}.")
81
+ sqlite.rollback # these changes are all or nothing
82
+ return # try again to update plan later
83
+ end
84
+ missions.each do |mission|
85
+ params = [ mission["name"],
86
+ mission["code"],
87
+ mission["options"].to_json,
88
+ mission["timeout"].to_s =~ /\A\d*[1-9]\z/ ?
89
+ mission["timeout"].to_i :
90
+ DEFAULT_TIMEOUT,
91
+ mission["interval"].to_s =~ /\A\d*[1-9]\z/ ?
92
+ mission["interval"].to_i :
93
+ DEFAULT_INTERVAL,
94
+ mission["id"],
95
+ Time.now.to_db_s(:trim_seconds) ]
96
+ begin
97
+ if sqlite.first_value_from(
98
+ "SELECT id FROM missions WHERE id = ? LIMIT 1",
99
+ mission["id"]
100
+ )
101
+ params.pop # remove next_run_at
102
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, *params)
103
+ UPDATE missions
104
+ SET name = ?, code = ?, options = ?, timeout = ?,
105
+ interval = ?
106
+ WHERE id = ?
107
+ END_UPDATE_MISSION
108
+ else
109
+ sqlite.execute(<<-END_INSERT_MISSION.trim, *params)
110
+ INSERT INTO
111
+ missions( name, code, options, timeout,
112
+ interval, id, next_run_at )
113
+ VALUES( ?, ?, ?, ?,
114
+ ?, ?, ? )
115
+ END_INSERT_MISSION
116
+ end
117
+ rescue Amalgalite::SQLite3::Error => error # failed to set mission
118
+ # do nothing: skip bad mission and move on
119
+ log.error( "Database bad mission (#{mission['name']}) error: " +
120
+ "#{error.message}." )
121
+ end
122
+ end
123
+ end
124
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
125
+ # try again to update plan later
126
+ log.error("Database mission update locking error: #{error.message}.")
127
+ end
128
+
129
+ def current_mission
130
+ mission = read_from_sqlite { |sqlite|
131
+ return nil unless plan = current_plan
132
+ params = [plan[:id], Time.now.to_db_s]
133
+ sqlite.first_row_from(<<-END_FIND_MISSION.trim, *params)
134
+ SELECT id, timeout, interval, last_run_at, name,
135
+ code, options, memory
136
+ FROM missions
137
+ WHERE plan_id = ? AND next_run_at <= ?
138
+ ORDER BY ROWID
139
+ LIMIT 1
140
+ END_FIND_MISSION
141
+ }
142
+ if mission.empty?
143
+ nil # not found
144
+ else
145
+ mission[:last_run_at] = Time.from_db_s(mission[:last_run_at])
146
+ %w[options memory].each do |serialized|
147
+ begin
148
+ mission[serialized] = JSON.parse(mission[serialized].to_s)
149
+ rescue JSON::ParserError
150
+ log.warn("Mission #{serialized} could not be parsed.")
151
+ mission[serialized] = { }
152
+ end
153
+ end
154
+ mission
155
+ end
156
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve mission
157
+ log.error("Database current mission error: #{error.message}.")
158
+ nil # not found
159
+ end
160
+
161
+ def update_mission_memory(mission_id, memory)
162
+ write_to_sqlite do |sqlite|
163
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, memory.to_json, mission_id)
164
+ UPDATE missions SET memory = ? WHERE id = ?
165
+ END_UPDATE_MISSION
166
+ end
167
+ rescue Amalgalite::SQLite3::Error => error # failed to update memory
168
+ # do nothing: mission will receive previous memory state
169
+ log.error("Database memory update error: #{error.message}.")
170
+ end
171
+
172
+ def complete_mission(mission)
173
+ write_to_sqlite do |sqlite|
174
+ run_time = Time.now
175
+ params = [ run_time.to_db_s,
176
+ ( run_time +
177
+ mission[:interval] * 60 ).to_db_s(:trim_seconds),
178
+ mission[:id] ]
179
+ sqlite.execute(<<-END_UPDATE_MISSION.trim, *params)
180
+ UPDATE missions SET last_run_at = ?, next_run_at = ? WHERE id = ?
181
+ END_UPDATE_MISSION
182
+ end
183
+ true # it's safe to continue
184
+ rescue Amalgalite::SQLite3::Error => error # failed to update mission
185
+ log.error("Database complete mission error: #{error.message}.")
186
+ false # warn the caller that the mission will still match
187
+ end
188
+
189
+ def reset_missions(*ids)
190
+ write_to_sqlite do |sqlite|
191
+ sqlite.execute(<<-END_RESET_MISSIONS.trim, *ids)
192
+ UPDATE missions
193
+ SET next_run_at = strftime('%Y-%m-%d %H:%M', 'now', 'localtime')
194
+ WHERE id IN (#{(['?'] * ids.size).join(', ')})
195
+ END_RESET_MISSIONS
196
+ end
197
+ rescue Amalgalite::SQLite3::Error => error # failed to reset missions
198
+ # do nothing: missions will be run at their scheduled time
199
+ log.error("Database mission reset error: #{error.message}.")
200
+ end
201
+
202
+ def write_report(mission_id, type, fields)
203
+ write_to_sqlite do |sqlite|
204
+ params = [mission_id, type, fields.to_json]
205
+ sqlite.execute(<<-END_INSERT_REPORT.trim, *params)
206
+ INSERT INTO reports(mission_id, type, fields) VALUES(?, ?, ?)
207
+ END_INSERT_REPORT
208
+ end
209
+ true # report successfully written
210
+ rescue Amalgalite::SQLite3::Error => error # failed to create report
211
+ log.error("Database write report error: #{error.message}.")
212
+ false # couldn't be written
213
+ end
214
+
215
+ def current_reports
216
+ write_to_sqlite { |sqlite|
217
+ begin
218
+ report_ids = Array.new
219
+ reports = query(<<-END_FIND_REPORTS.trim) { |row|
220
+ SELECT reports.ROWID AS id, reports.type, reports.fields,
221
+ reports.created_at, missions.id AS plugin_id
222
+ FROM reports
223
+ INNER JOIN missions ON reports.mission_id = missions.id
224
+ ORDER BY created_at
225
+ LIMIT 500
226
+ END_FIND_REPORTS
227
+ begin
228
+ row[:fields] = JSON.parse(row[:fields].to_s)
229
+ rescue JSON::ParserError
230
+ # skip the transform since we can't parse it
231
+ log.warn("Report fields malformed.")
232
+ end
233
+ if created = Time.from_db_s(row[:created_at])
234
+ row[:created_at] = created.utc.to_db_s
235
+ else
236
+ log.warn("Report timestamp missing.")
237
+ end
238
+ report_ids << row.delete_at(:id)
239
+ }
240
+ rescue Amalgalite::SQLite3::Error => error # failed to find reports
241
+ log.error("Database reports error: #{error.message}.")
242
+ return Array.new # return empty results
243
+ end
244
+ return reports if reports.empty?
245
+ begin
246
+ sqlite.execute(<<-END_DELETE_REPORTS.trim, *report_ids)
247
+ DELETE FROM reports
248
+ WHERE ROWID IN (#{(['?'] * report_ids.size).join(', ')})
249
+ END_DELETE_REPORTS
250
+ rescue Amalgalite::SQLite3::Error => error # failed to remove reports
251
+ # cancel sending this batch
252
+ log.error("Database delivered reports error: #{error.message}.")
253
+ sqlite.rollback # we can't submit unless we're sure they are gone
254
+ return Array.new # return empty results
255
+ end
256
+ reports # the reports ready for sending
257
+ }
258
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
259
+ # try again to read reports later
260
+ log.error("Database reports locking error: #{error.message}.")
261
+ end
262
+
263
+ def seconds_to_next_mission
264
+ default = DEFAULT_INTERVAL * 60
265
+ next_run_at = read_from_sqlite { |sqlite|
266
+ sqlite.first_value_from(<<-END_FIND_MISSION.trim)
267
+ SELECT next_run_at FROM missions ORDER BY next_run_at LIMIT 1
268
+ END_FIND_MISSION
269
+ }
270
+ if next_run = Time.from_db_s(next_run_at)
271
+ seconds = next_run - Time.now
272
+ seconds > 0 ? seconds : default
273
+ else
274
+ default
275
+ end
276
+ rescue Amalgalite::SQLite3::Error => error # failed to locate last run
277
+ log.error("Database next mission error: #{error.message}.")
278
+ default # use default
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class Queue < Database
6
+ # A size limit for the queue to prevent data from building up.
7
+ QUEUE_LIMIT = 3000
8
+
9
+ def update_schema(version = schema_version)
10
+ case version
11
+ when 0
12
+ <<-END_INITIAL_SCHEMA.trim
13
+ CREATE TABLE queue (
14
+ mission_id TEXT NOT NULL
15
+ CHECK( mission_id IN ('report', 'hint', 'alert', 'error') OR
16
+ CAST(mission_id AS 'integer') > 0 ),
17
+ fields REQUIRED_TEXT_TYPE,
18
+ created_at DATETIME_TYPE
19
+ );
20
+ DEFAULT_LOCALTIME_TRIGGER queue created_at
21
+ LIMIT_TABLE_SIZE_TRIGGER queue #{QUEUE_LIMIT}
22
+ END_INITIAL_SCHEMA
23
+ end
24
+ end
25
+
26
+ def enqueue(mission_id, fields)
27
+ write_to_sqlite do |sqlite|
28
+ sqlite.execute(<<-END_ENQUEUE.trim, mission_id, fields.to_json)
29
+ INSERT INTO queue(mission_id, fields) VALUES(?, ?)
30
+ END_ENQUEUE
31
+ end
32
+ true
33
+ rescue Amalgalite::SQLite3::Error => error # failed to enqueue message
34
+ log.error("Database queuing error: #{error.message}.")
35
+ false # reject bad message
36
+ end
37
+
38
+ def peek(mission_id)
39
+ queued = read_from_sqlite { |sqlite|
40
+ sqlite.first_row_from(<<-END_FIND_QUEUED.trim, mission_id.to_s)
41
+ SELECT ROWID AS id, fields, created_at FROM queue WHERE mission_id = ?
42
+ END_FIND_QUEUED
43
+ }
44
+ if queued.empty?
45
+ nil # not found
46
+ else
47
+ begin
48
+ queued[:fields] = JSON.parse(queued[:fields].to_s)
49
+ rescue JSON::ParserError # failed to parse
50
+ # leave for mission to decode it
51
+ log.warn("Queued fields malformed.")
52
+ end
53
+ if created = Time.from_db_s(queued[:created_at])
54
+ queued[:created_at] = created
55
+ else
56
+ log.warn("Queued timestamp missing.")
57
+ end
58
+ queued
59
+ end
60
+ rescue Amalgalite::SQLite3::Error => error # failed to retrieve message
61
+ log.error("Database peeking error: #{error.message}.")
62
+ nil # not found
63
+ end
64
+
65
+ def queued_reports
66
+ write_to_sqlite { |sqlite|
67
+ begin
68
+ report_ids = Array.new
69
+ reports = query(<<-END_FIND_REPORTS.trim) { |row|
70
+ SELECT ROWID AS id, mission_id AS type, fields, created_at
71
+ FROM queue
72
+ WHERE mission_id IN ('report', 'hint', 'alert', 'error')
73
+ ORDER BY created_at
74
+ LIMIT 500
75
+ END_FIND_REPORTS
76
+ begin
77
+ row[:fields] = JSON.parse(row[:fields].to_s)
78
+ if row[:fields].include? "plugin_id"
79
+ row[:plugin_id] = row[:fields].delete("plugin_id")
80
+ end
81
+ rescue JSON::ParserError # failed to parse
82
+ # skip the transform since we can't parse it
83
+ log.warn("Queued fields malformed.")
84
+ end
85
+ if created = Time.from_db_s(row[:created_at])
86
+ row[:created_at] = created.utc.to_db_s
87
+ else
88
+ log.warn("Queued timestamp missing.")
89
+ end
90
+ report_ids << row.delete_at(:id)
91
+ }
92
+ rescue Amalgalite::SQLite3::Error => error # failed to find reports
93
+ log.error("Database queued reports error: #{error.message}.")
94
+ return Array.new # return empty results
95
+ end
96
+ return reports if reports.empty?
97
+ unless dequeue(*report_ids)
98
+ # cancel sending this batch
99
+ sqlite.rollback # we can't submit unless we're sure they are gone
100
+ return Array.new # return empty results
101
+ end
102
+ reports # the reports ready for sending
103
+ }
104
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
105
+ # try again to read reports later
106
+ log.error("Database queued reports locking error: #{error.message}.")
107
+ end
108
+
109
+ def dequeue(*ids)
110
+ write_to_sqlite do |sqlite|
111
+ sqlite.execute(<<-END_DELETE_QUEUED.trim, *ids)
112
+ DELETE FROM queue WHERE ROWID IN (#{(['?'] * ids.size).join(', ')})
113
+ END_DELETE_QUEUED
114
+ end
115
+ true
116
+ rescue Amalgalite::SQLite3::Error => error # failed to remove messages
117
+ #
118
+ # do nothing: messages will be delivered again,
119
+ # mission can block duplicate
120
+ #
121
+ log.error("Database dequeuing error: #{error.message}.")
122
+ false
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ module ScoutAgent
4
+ class Database
5
+ class Snapshots < Database
6
+ # A maximum time in seconds after which a command run is halted.
7
+ DEFAULT_TIMEOUT = 60
8
+ #
9
+ # A minimum time in minutes that must pass before a command will be run
10
+ # again in the next snapshot.
11
+ #
12
+ DEFAULT_INTERVAL = 10
13
+ # A size limit for the runs table to prevent data from building up.
14
+ RUNS_LIMIT = 3000
15
+
16
+ def update_schema(version = schema_version)
17
+ case version
18
+ when 0
19
+ <<-END_INITIAL_SCHEMA.trim
20
+ CREATE TABLE commands (
21
+ code REQUIRED_TEXT_TYPE PRIMARY KEY,
22
+ timeout POSITIVE_INTEGER_TYPE DEFAULT #{DEFAULT_TIMEOUT},
23
+ interval DEFAULT_INTEGER_TYPE #{DEFAULT_INTERVAL},
24
+ last_run_at DATETIME_TYPE,
25
+ next_run_at DATETIME_TYPE
26
+ );
27
+ DEFAULT_LOCALTIME_TRIGGER commands next_run_at trim_seconds
28
+
29
+ CREATE TABLE runs (
30
+ code REQUIRED_TEXT_TYPE,
31
+ output TEXT,
32
+ exit_status INTEGER,
33
+ snapshot_at REQUIRED_DATETIME_TYPE,
34
+ run_time ZERO_OR_POSITIVE_REAL_TYPE,
35
+ PRIMARY KEY(code, snapshot_at)
36
+ );
37
+ LIMIT_TABLE_SIZE_TRIGGER runs #{RUNS_LIMIT}
38
+ END_INITIAL_SCHEMA
39
+ end
40
+ end
41
+
42
+ def update_commands(commands)
43
+ write_to_sqlite do |sqlite|
44
+ codes = commands.map { |m| m["code"] }
45
+ begin
46
+ sqlite.execute(<<-END_DELETE_COMMANDS.trim, *codes)
47
+ DELETE FROM commands
48
+ WHERE code NOT IN (#{(['?'] * codes.size).join(', ')})
49
+ END_DELETE_COMMANDS
50
+ rescue Amalgalite::SQLite3::Error => error # failed to remove
51
+ log.error("Database command updating error: #{error.message}.")
52
+ return # try again to update commands later
53
+ end
54
+ commands.each do |command|
55
+ params = [ command["timeout"].to_s =~ /\A\d*[1-9]\z/ ?
56
+ command["timeout"].to_i :
57
+ DEFAULT_TIMEOUT,
58
+ (command["interval"] || DEFAULT_INTERVAL).to_i,
59
+ command["code"] ]
60
+ begin
61
+ if sqlite.first_value_from(
62
+ "SELECT ROWID FROM commands WHERE code = ? LIMIT 1",
63
+ command["code"]
64
+ )
65
+ sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
66
+ UPDATE commands SET timeout = ?, interval = ? WHERE code = ?
67
+ END_UPDATE_COMMAND
68
+ else
69
+ sqlite.execute(<<-END_INSERT_COMMAND.trim, *params)
70
+ INSERT INTO commands(timeout, interval, code) VALUES(?, ?, ?)
71
+ END_INSERT_COMMAND
72
+ end
73
+ rescue Amalgalite::SQLite3::Error => error # failed to set command
74
+ # do nothing: skip bad command and move on
75
+ log.error( "Database bad command (#{command['code']}) error: " +
76
+ "#{error.message}." )
77
+ end
78
+ end
79
+ end
80
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
81
+ # try again to update commands later
82
+ log.error("Database command update locking error: #{error.message}.")
83
+ end
84
+
85
+ def current_commands
86
+ query(<<-END_FIND_COMMANDS.trim, Time.now.to_db_s) { |row|
87
+ SELECT ROWID AS id, timeout, interval, last_run_at, code
88
+ FROM commands
89
+ WHERE next_run_at <= ?
90
+ END_FIND_COMMANDS
91
+ row[:last_run_at] = Time.from_db_s(row[:last_run_at])
92
+ }
93
+ rescue Amalgalite::SQLite3::Error => error # failed to find commands
94
+ log.error("Database commands error: #{error.message}.")
95
+ Array.new # return empty results
96
+ end
97
+
98
+ def have_commands?
99
+ read_from_sqlite { |sqlite|
100
+ !!sqlite.first_value_from("SELECT ROWID FROM commands LIMIT 1")
101
+ }
102
+ rescue Amalgalite::SQLite3::Error => error # failed to find commands
103
+ log.error("Database command check error: #{error.message}.")
104
+ nil # commands not found
105
+ end
106
+
107
+ def complete_run(command, output, exit_status, snapshot_at, run_time)
108
+ write_to_sqlite do |sqlite|
109
+ params = [ command[:code],
110
+ output,
111
+ exit_status,
112
+ snapshot_at.to_db_s,
113
+ run_time ]
114
+ begin
115
+ sqlite.execute(<<-END_INSERT_RUN.trim, *params)
116
+ INSERT INTO
117
+ runs( code, output, exit_status, snapshot_at, run_time )
118
+ VALUES( ?, ?, ?, ?, ? )
119
+ END_INSERT_RUN
120
+ rescue Amalgalite::SQLite3::Error => error # failed to add run
121
+ # do nothing: skip bad command and move on
122
+ log.error( "Database bad command run (#{command[:code]}) error: " +
123
+ "#{error.message}." )
124
+ end
125
+ run_time = Time.now
126
+ params = [ run_time.to_db_s,
127
+ ( run_time +
128
+ command[:interval] * 60 ).to_db_s(:trim_seconds),
129
+ command[:id] ]
130
+ begin
131
+ sqlite.execute(<<-END_UPDATE_COMMAND.trim, *params)
132
+ UPDATE commands SET last_run_at = ?, next_run_at = ? WHERE ROWID = ?
133
+ END_UPDATE_COMMAND
134
+ rescue Amalgalite::SQLite3::Error => error # failed to update command
135
+ # do nothing: command will be run again
136
+ log.error( "Database bad command (#{command[:code]}) update " +
137
+ "error: #{error.message}." )
138
+ end
139
+ end
140
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
141
+ # try again to update commands later
142
+ log.error("Database complete command locking error: #{error.message}.")
143
+ end
144
+
145
+ def current_runs
146
+ write_to_sqlite { |sqlite|
147
+ begin
148
+ run_ids = Array.new
149
+ runs = query(<<-END_FIND_RUNS.trim) { |row|
150
+ SELECT ROWID AS id, code, output, exit_status,
151
+ snapshot_at AS created_at, run_time
152
+ FROM runs
153
+ ORDER BY snapshot_at
154
+ LIMIT 500
155
+ END_FIND_RUNS
156
+ if created = Time.from_db_s(row[:created_at])
157
+ row[:created_at] = created.utc.to_db_s
158
+ else
159
+ log.warn("Run timestamp missing.")
160
+ end
161
+ run_ids << row.delete_at(:id)
162
+ }
163
+ rescue Amalgalite::SQLite3::Error => error # failed to find runs
164
+ log.error("Database runs error: #{error.message}.")
165
+ return Array.new # return empty results
166
+ end
167
+ return runs if runs.empty?
168
+ begin
169
+ sqlite.execute(<<-END_DELETE_RUNS.trim, *run_ids)
170
+ DELETE FROM runs
171
+ WHERE ROWID IN (#{(['?'] * run_ids.size).join(', ')})
172
+ END_DELETE_RUNS
173
+ rescue Amalgalite::SQLite3::Error => error # failed to remove runs
174
+ # cancel sending this batch
175
+ log.error("Database delivered runs error: #{error.message}.")
176
+ sqlite.rollback # we can't submit unless we're sure they are gone
177
+ return Array.new # return empty results
178
+ end
179
+ runs # the runs ready for sending
180
+ }
181
+ rescue Amalgalite::SQLite3::Error => error # failed to get a write lock
182
+ # try again to read runs later
183
+ log.error("Database runs locking error: #{error.message}.")
184
+ end
185
+ end
186
+ end
187
+ end