DEV: Allow execute_command to receive a block (#8303)

This makes it easy to run multiple commands with the same keyword arguments. The main use is for using `chdir` across multiple commands. The `Dir.chdir` method is not concurrency safe because it switches the working directory of the entire process.
This commit is contained in:
David Taylor 2019-11-07 15:47:16 +00:00 committed by GitHub
parent f79796fcac
commit 60a235d128
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 77 additions and 7 deletions

View File

@ -23,20 +23,52 @@ module Discourse
end end
class Utils class Utils
def self.execute_command(*command, failure_message: "", success_status_codes: [0], chdir: ".") # Usage:
stdout, stderr, status = Open3.capture3(*command, chdir: chdir) # Discourse::Utils.execute_command("pwd", chdir: 'mydirectory')
# or with a block
# Discourse::Utils.execute_command(chdir: 'mydirectory') do |runner|
# runner.exec("pwd")
# end
def self.execute_command(*command, **args)
runner = CommandRunner.new(**args)
if !status.exited? || !success_status_codes.include?(status.exitstatus) if block_given?
failure_message = "#{failure_message}\n" if !failure_message.blank? raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present?
raise "#{caller[0]}: #{failure_message}#{stderr}" yield runner
else
runner.exec(*command)
end end
stdout
end end
def self.pretty_logs(logs) def self.pretty_logs(logs)
logs.join("\n".freeze) logs.join("\n".freeze)
end end
private
class CommandRunner
def initialize(**init_params)
@init_params = init_params
end
def exec(*command, **exec_params)
raise RuntimeError.new("Cannot specify same parameters at block and command level") if (@init_params.keys & exec_params.keys).present?
execute_command(*command, **@init_params.merge(exec_params))
end
private
def execute_command(*command, failure_message: "", success_status_codes: [0], chdir: ".")
stdout, stderr, status = Open3.capture3(*command, chdir: chdir)
if !status.exited? || !success_status_codes.include?(status.exitstatus)
failure_message = "#{failure_message}\n" if !failure_message.blank?
raise "#{caller[0]}: #{failure_message}#{stderr}"
end
stdout
end
end
end end
# Log an exception. # Log an exception.

View File

@ -391,4 +391,42 @@ describe Discourse do
end end
end end
describe "Utils.execute_command" do
it "works for individual commands" do
expect(Discourse::Utils.execute_command("pwd").strip).to eq(Rails.root.to_s)
expect(Discourse::Utils.execute_command("pwd", chdir: "plugins").strip).to eq("#{Rails.root.to_s}/plugins")
end
it "works with a block" do
Discourse::Utils.execute_command do |runner|
expect(runner.exec("pwd").strip).to eq(Rails.root.to_s)
end
result = Discourse::Utils.execute_command(chdir: "plugins") do |runner|
expect(runner.exec("pwd").strip).to eq("#{Rails.root.to_s}/plugins")
runner.exec("pwd")
end
# Should return output of block
expect(result.strip).to eq("#{Rails.root.to_s}/plugins")
end
it "does not leak chdir between threads" do
has_done_chdir = false
has_checked_chdir = false
thread = Thread.new do
Discourse::Utils.execute_command(chdir: "plugins") do
has_done_chdir = true
sleep(0.01) until has_checked_chdir
end
end
sleep(0.01) until has_done_chdir
expect(Discourse::Utils.execute_command("pwd").strip).to eq(Rails.root.to_s)
has_checked_chdir = true
thread.join
end
end
end end