Class: CommandJob

Inherits:
Object
  • Object
show all
Includes:
StandardModel
Defined in:
lib/app/models/command_job.rb

Overview

Base class for all jobs that will be run on builds

Constant Summary collapse

STATE_NEW =

Constants

'new'
STATE_WIP =
'working'
STATE_RETRYING =
'retrying'
STATE_SUCCESS =
'success'
STATE_FAIL =
'failure'
STATE_CANCELLED =
'cancelled'
ALL_STATES =
[STATE_NEW, STATE_WIP, STATE_RETRYING, STATE_CANCELLED, STATE_SUCCESS, STATE_FAIL].freeze

Instance Method Summary collapse

Instance Method Details

#add_log(message) ⇒ Object

Add a job log message



304
305
306
# File 'lib/app/models/command_job.rb', line 304

def add_log(message)
  logs.create!(message: message)
end

#cancelled?Boolean

If we are cancelled

Returns:

  • (Boolean)


108
109
110
# File 'lib/app/models/command_job.rb', line 108

def cancelled?
  STATE_CANCELLED.eql?(state)
end

#check_for_text(output, texts = [], inclusive = true) ⇒ Object

Check if any occurrences were found (or not found)



288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/app/models/command_job.rb', line 288

def check_for_text(output, texts = [], inclusive = true)
  return if texts.blank?

  texts = [texts] if texts.is_a?(String)
  texts.each do |text|
    if inclusive
      raise "Error: found text (#{text}) - #{output}" if output.match?(/#{text}/)
    else
      raise "Error: missing text (#{text}) - #{output}" unless output.match?(/#{text}/)
    end
  end
end

#completed?Boolean

If we is finished, failed or success

Returns:

  • (Boolean)


92
93
94
# File 'lib/app/models/command_job.rb', line 92

def completed?
  [STATE_CANCELLED, STATE_FAIL, STATE_SUCCESS].include?(state)
end

#copy_dir(dir, to_path) ⇒ Object

Copy a given directory to a new location and record the log



211
212
213
214
# File 'lib/app/models/command_job.rb', line 211

def copy_dir(dir, to_path)
  FileUtils.cp_r dir, to_path
  add_log "Copy directory from: #{dir} to: #{to_path}"
end

#copy_file(from_path, to_path) ⇒ Object

Copy a given file to a new location and record the log



199
200
201
202
203
204
205
206
# File 'lib/app/models/command_job.rb', line 199

def copy_file(from_path, to_path)
  if File.exist? from_path
    FileUtils.cp(from_path, to_path)
    add_log "Copy file from: #{from_path} to: #{to_path}"
  else
    add_log "File not found: #{from_path}, copy not performed"
  end
end

#display_started_byObject

Who started this job



43
44
45
# File 'lib/app/models/command_job.rb', line 43

def display_started_by
  started_by.present? ? started_by.display_name : 'System'
end

#download_file(file_url, file_path) ⇒ Object

Download a file to the given path



190
191
192
193
194
# File 'lib/app/models/command_job.rb', line 190

def download_file(file_url, file_path)
  download = URI.parse(file_url).open
  IO.copy_stream(download, file_path)
  add_log "Downloaded file: #{file_url} to #{file_path}"
end

#failed?Boolean

True if in fail status

Returns:

  • (Boolean)


85
86
87
# File 'lib/app/models/command_job.rb', line 85

def failed?
  STATE_FAIL.eql?(state)
end

#failure_or_cancelled?(job = self) ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
123
124
125
126
# File 'lib/app/models/command_job.rb', line 120

def failure_or_cancelled?(job = self)
  job.reload
  job.failed? || job.cancelled?
rescue StandardError => error
  App47Logger.log_warn "Unable to check job cancelled #{job}", error
  true
end

#incomplete?Boolean Also known as: running?

Job has not finished, failure or success

Returns:

  • (Boolean)


99
100
101
# File 'lib/app/models/command_job.rb', line 99

def incomplete?
  !completed?
end

#job_cancelled?(job = self) ⇒ Boolean

Returns:

  • (Boolean)


112
113
114
115
116
117
118
# File 'lib/app/models/command_job.rb', line 112

def job_cancelled?(job = self)
  job.reload
  job.cancelled?
rescue StandardError => error
  App47Logger.log_warn "Unable to check job cancelled #{job}", error
  true
end

#mask_keywords(output, keywords = []) ⇒ Object

Mask keywords if given in the command



275
276
277
278
279
280
281
282
283
# File 'lib/app/models/command_job.rb', line 275

def mask_keywords(output, keywords = [])
  return output if keywords.blank?

  keywords = [keywords] if keywords.is_a?(String)
  keywords.each do |keyword|
    output = output.gsub(keyword, '***********')
  end
  output
end

#mkdir(dir) ⇒ Object Also known as: make_dir

Create a directory and record it



239
240
241
242
243
244
# File 'lib/app/models/command_job.rb', line 239

def mkdir(dir)
  return if File.exist?(dir)

  FileUtils.mkdir dir
  add_log "Created directory: #{dir}"
end

#nameObject

Return the name of this job



57
58
59
# File 'lib/app/models/command_job.rb', line 57

def name
  self.class.to_s.underscore.humanize
end

#new_job?Boolean

True if in new status

Returns:

  • (Boolean)


64
65
66
# File 'lib/app/models/command_job.rb', line 64

def new_job?
  STATE_NEW.eql?(state)
end

#performObject



138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/app/models/command_job.rb', line 138

def perform
  if failed?
    self.retries = 0
    self.error_message = 'Retrying'
  end

  begin
    save!
    run
  rescue StandardError => error
    log_error 'Unable to start job', error
    set state: STATE_FAIL, error_message: error.message
  end
end

#remove_dir(dir_path) ⇒ Object

Remove the given file name



229
230
231
232
233
234
# File 'lib/app/models/command_job.rb', line 229

def remove_dir(dir_path)
  return unless File.exist?(dir_path)

  FileUtils.remove_dir dir_path
  add_log "Removing dir: #{dir_path}"
end

#remove_file(file_path) ⇒ Object

Remove the given file name



219
220
221
222
223
224
# File 'lib/app/models/command_job.rb', line 219

def remove_file(file_path)
  return unless File.exist?(file_path)

  FileUtils.remove_file file_path
  add_log "Removing file: #{file_path}"
end

#runObject



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/app/models/command_job.rb', line 153

def run
  set state: STATE_WIP, started_at: Time.now.utc
  # Run the job
  run!
  set error_message: '', state: STATE_SUCCESS, finished_at: Time.now.utc
rescue StandardError => error
  if retries >= max_retries
    log_error "Unable to run job id: #{id}, done retrying", error
    set state: STATE_FAIL, error_message: "Failed final attempt: #{error.message}", finished_at: Time.now.utc
  else
    log_error "Unable to run job id: #{id}, retrying!!", error
    add_log "Unable to run job: #{error.message}, retrying!!"
    set error_message: "Failed attempt # #{retries}: #{error.message}", retries: retries + 1
    delay(run_at: 10.seconds.from_now).run
  end
end

#run!Object

Determine the correct action to take and get it started



175
176
177
# File 'lib/app/models/command_job.rb', line 175

def run!
  raise 'Incomplete class, concrete implementation should implement #run!'
end

#run_command(command, dir = '/tmp', options = {}) ⇒ Object

Run the command capturing the command output and any standard error to the log.



258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/app/models/command_job.rb', line 258

def run_command(command, dir = '/tmp', options = {})
  command = command.join(' ') if command.is_a?(Array)
  output = Tempfile.open('run-command-', '/tmp') do |f|
    Dir.chdir(dir) { `#{command} > #{f.path} 2>&1` }
    mask_keywords(f.open.read, options[:mask_texts])
  end
  output = 'Success' if output.blank?
  command = mask_keywords(command, options[:mask_texts])
  logs.create!(dir: dir, command: command, message: output)
  check_for_text(output, options[:error_texts], true)
  check_for_text(output, options[:required_texts], false)
  output
end

#sort_fieldsObject

Which to sort by



311
312
313
# File 'lib/app/models/command_job.rb', line 311

def sort_fields
  %i[created_at]
end

#statusObject

Return the job’s status and information in a hash that could be used to return to a calling api



132
133
134
135
136
# File 'lib/app/models/command_job.rb', line 132

def status
  status = { state: state }
  status[:message] = error_message if error_message.present?
  status
end

#succeeded?Boolean

True if in success status

Returns:

  • (Boolean)


78
79
80
# File 'lib/app/models/command_job.rb', line 78

def succeeded?
  STATE_SUCCESS.eql?(state)
end

#ttlObject

Default time to keep a job before auto archiving it



50
51
52
# File 'lib/app/models/command_job.rb', line 50

def ttl
  30
end

#unzip_file(file_path, to_dir) ⇒ Object

Unzip a given file



251
252
253
# File 'lib/app/models/command_job.rb', line 251

def unzip_file(file_path, to_dir)
  run_command "unzip #{file_path}", to_dir, error_texts: 'unzip:'
end

#work_in_progress?Boolean

True if in WIP status

Returns:

  • (Boolean)


71
72
73
# File 'lib/app/models/command_job.rb', line 71

def work_in_progress?
  [STATE_RETRYING, STATE_WIP].include?(state)
end

#write_file(path, contents) ⇒ Object

Write out the contents to the file



182
183
184
185
# File 'lib/app/models/command_job.rb', line 182

def write_file(path, contents)
  File.open(path, 'w') { |f| f.write(contents) }
  add_log "Saving:\n #{contents}\nto: #{path}"
end