From 60a235d128e78b43e33a859b2c9534d0d96ebce3 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Nov 2019 15:47:16 +0000 Subject: [PATCH] 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. --- lib/discourse.rb | 46 ++++++++++++++++++++++++++----- spec/components/discourse_spec.rb | 38 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/lib/discourse.rb b/lib/discourse.rb index 759a01a935a..8e8d7f9b5b7 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -23,20 +23,52 @@ module Discourse end class Utils - def self.execute_command(*command, failure_message: "", success_status_codes: [0], chdir: ".") - stdout, stderr, status = Open3.capture3(*command, chdir: chdir) + # Usage: + # 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) - failure_message = "#{failure_message}\n" if !failure_message.blank? - raise "#{caller[0]}: #{failure_message}#{stderr}" + if block_given? + raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present? + yield runner + else + runner.exec(*command) end - - stdout end def self.pretty_logs(logs) logs.join("\n".freeze) 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 # Log an exception. diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index 705195d53cb..0049e1de994 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -391,4 +391,42 @@ describe Discourse do 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