bwrap 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,12 @@ require_relative "library"
8
8
  #
9
9
  # @see Config::Features
10
10
  class Bwrap::Args::Features < Hash
11
+ # Requires are here so there is no extra trickiness with namespaces.
12
+ #
13
+ # Feature implementations are not meant to be used outside of this class anyway.
14
+ require_relative "features/binds_base"
15
+ require_relative "features/ruby_binds"
16
+
11
17
  include Bwrap::Output
12
18
 
13
19
  # Implementation for Bash feature set.
@@ -45,44 +51,6 @@ class Bwrap::Args::Features < Hash
45
51
  end
46
52
  end
47
53
 
48
- # Implementation for Ruby feature set.
49
- #
50
- # @api private
51
- class RubyBinds
52
- # Returns mounts needed by Ruby feature set.
53
- attr_reader :mounts
54
-
55
- # Bind system paths so scripts works inside sandbox.
56
- def sitedir_mounts
57
- mounts = []
58
- mounts << "--ro-bind" << RbConfig::CONFIG["sitedir"] << RbConfig::CONFIG["sitedir"]
59
- mounts << "--ro-bind" << RbConfig::CONFIG["rubyhdrdir"] << RbConfig::CONFIG["rubyhdrdir"]
60
- mounts << "--ro-bind" << RbConfig::CONFIG["rubylibdir"] << RbConfig::CONFIG["rubylibdir"]
61
- mounts << "--ro-bind" << RbConfig::CONFIG["vendordir"] << RbConfig::CONFIG["vendordir"]
62
-
63
- mounts
64
- end
65
-
66
- # Create binds for required system libraries.
67
- #
68
- # These are in path like /usr/lib64/ruby/2.5.0/x86_64-linux-gnu/,
69
- # and as they are mostly shared libraries, they may have some extra
70
- # dependencies that also need to be bound inside the sandbox.
71
- def stdlib_mounts stdlib
72
- library_mounts = []
73
- library = Bwrap::Args::Library.new
74
- stdlib.each do |lib|
75
- path = "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
76
-
77
- library.needed_libraries(path).each do |requisite_library|
78
- library_mounts << "--ro-bind" << requisite_library << requisite_library
79
- end
80
- end
81
-
82
- library_mounts
83
- end
84
- end
85
-
86
54
  # `Array` of parameters passed to bwrap.
87
55
  attr_writer :args
88
56
 
@@ -104,7 +72,7 @@ class Bwrap::Args::Features < Hash
104
72
 
105
73
  binds = BashBinds.new
106
74
 
107
- @args.append binds.bash_mounts
75
+ @args.add :feature_binds, binds.bash_mounts
108
76
  end
109
77
 
110
78
  private def nscd_binds
@@ -112,7 +80,7 @@ class Bwrap::Args::Features < Hash
112
80
 
113
81
  binds = NscdBinds.new
114
82
 
115
- @args.append binds.custom_binds
83
+ @args.add :feature_binds, binds.custom_binds
116
84
  end
117
85
 
118
86
  # @note This does not allow development headers needed for compilation for now.
@@ -120,9 +88,9 @@ class Bwrap::Args::Features < Hash
120
88
  private def ruby_binds
121
89
  return unless @config.features.ruby.enabled?
122
90
 
123
- binds = RubyBinds.new
91
+ binds = RubyBinds.new @config
124
92
 
125
- @args.append binds.sitedir_mounts
126
- @args.append binds.stdlib_mounts(@config.features.ruby.stdlib)
93
+ @args.add :feature_binds, binds.sitedir_mounts
94
+ @args.add :feature_binds, binds.stdlib_mounts(@config.features.ruby.stdlib)
127
95
  end
128
96
  end
@@ -66,8 +66,6 @@ class Bwrap::Args::Library
66
66
  @needed_libraries
67
67
  end
68
68
 
69
- # Used by {Bwrap::Args::Bind#libs_command_requires}.
70
- #
71
69
  # @param binary_paths [String, Array] one or more paths to be resolved
72
70
  def needed_libraries binary_paths
73
71
  trace "Finding libraries #{binary_paths} requires"
@@ -105,7 +103,7 @@ class Bwrap::Args::Library
105
103
  libraries = libraries_line.split ","
106
104
 
107
105
  (@needed_libraries & libraries).each do |library|
108
- verb "Binding #{library} as dependency of #{binary_path}"
106
+ debug "Binding #{library} as dependency of #{binary_path}"
109
107
  end
110
108
 
111
109
  @needed_libraries |= libraries
@@ -9,25 +9,25 @@ module Bwrap::Args::Mount
9
9
  return unless @config.root
10
10
 
11
11
  debug "Binding #{@config.root} as /"
12
- @args.append %W{ --bind #{@config.root} / }
13
- @args.append %w{ --chdir / }
12
+ @args.add :root_mount, "--bind", @config.root, "/"
13
+ @args.add :root_mount, "--chdir", "/"
14
14
  end
15
15
 
16
16
  # Arguments for mounting devtmpfs to /dev.
17
17
  private def dev_mount
18
18
  debug "Mounting new devtmpfs to /dev"
19
- @args.append %w{ --dev /dev }
19
+ @args.add :dev_mounts, "--dev", "/dev"
20
20
  end
21
21
 
22
22
  # Arguments for mounting procfs to /proc.
23
23
  private def proc_mount
24
24
  debug "Mounting new procfs to /proc"
25
- @args.append %w{ --proc /proc }
25
+ @args.add :proc_mount, "--proc", "/proc"
26
26
  end
27
27
 
28
28
  # Arguments for mounting tmpfs to /tmp.
29
29
  private def tmp_as_tmpfs
30
30
  debug "Mounting tmpfs to /tmp"
31
- @args.append %w{ --tmpfs /tmp }
31
+ @args.add :tmp_mount, "--tmpfs", "/tmp"
32
32
  end
33
33
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/output"
4
+ require_relative "args"
5
+
6
+ # Network related binds.
7
+ class Bwrap::Args::Network
8
+ include Bwrap::Output
9
+
10
+ # Instance of {Config}.
11
+ attr_writer :config
12
+
13
+ # @param args [Bwrap::Args::Args] Arguments to be passed to bwrap.
14
+ def initialize args
15
+ @args = args
16
+ end
17
+
18
+ # Arguments to set hostname to whatever is configured.
19
+ def hostname
20
+ return unless @config.hostname
21
+
22
+ debug "Setting hostname to #{@config.hostname}"
23
+ @args.add :hostname, %W{ --hostname #{@config.hostname} }
24
+ end
25
+
26
+ # Arguments to read-only bind /etc/resolv.conf.
27
+ def resolv_conf
28
+ # We can’t really bind symlinks, so let’s resolve real path to resolv.conf, in case it is symlinked.
29
+ source_resolv_conf = Pathname.new "/etc/resolv.conf"
30
+ source_resolv_conf = source_resolv_conf.realpath
31
+
32
+ debug "Binding #{source_resolv_conf} as /etc/resolv.conf"
33
+ @args.add :resolv_conf, %W{ --ro-bind #{source_resolv_conf} /etc/resolv.conf }
34
+ end
35
+
36
+ # Arguments to allow network connection inside sandbox.
37
+ def share_net
38
+ return unless @config.share_net
39
+
40
+ verb "Sharing network"
41
+ @args.add :network, %w{ --share-net }
42
+ end
43
+ end
data/lib/bwrap/bwrap.rb CHANGED
@@ -55,7 +55,9 @@ class Bwrap::Bwrap
55
55
  construct = Bwrap::Args::Construct.new
56
56
  construct.command = command
57
57
  construct.config = @config
58
- bwrap_args = construct.construct_bwrap_args
58
+
59
+ construct.calculate
60
+ bwrap_args = construct.bwrap_arguments
59
61
 
60
62
  exec_command = [ "bwrap" ]
61
63
  exec_command += bwrap_args
@@ -89,7 +91,9 @@ class Bwrap::Bwrap
89
91
  construct = Bwrap::Args::Construct.new
90
92
  construct.command = command
91
93
  construct.config = config
92
- bwrap_args = construct.construct_bwrap_args
94
+
95
+ construct.calculate
96
+ bwrap_args = construct.bwrap_arguments
93
97
 
94
98
  exec_command = [ "bwrap" ]
95
99
  exec_command += bwrap_args
@@ -134,6 +138,9 @@ class Bwrap::Bwrap
134
138
  opt :trace,
135
139
  "Show trace output (noisiest, probably not useful for most of time)",
136
140
  short: :none
141
+ opt [ :quiet, :silent ],
142
+ "Hides notice messages. Warnings and errors are still shown.",
143
+ short: :none
137
144
  opt :version,
138
145
  "Print version and exit",
139
146
  short: "V"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @abstract
4
+ #
5
+ # Base of all features.
6
+ class Bwrap::Config::Features::Base
7
+ # @param features [Bwrap::Config::Features] Instance of features object in {Config}
8
+ def initialize features
9
+ @features = features
10
+ end
11
+
12
+ # Checks if the feature has been enabled.
13
+ #
14
+ # @return [Boolean] whether feature is enabled
15
+ def enabled?
16
+ @enabled
17
+ end
18
+
19
+ # Enable the feature.
20
+ def enable
21
+ @enabled = true
22
+ end
23
+
24
+ # Disable the feature.
25
+ def disable
26
+ @enabled = false
27
+ end
28
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bwrap/execution"
4
+
5
+ # Defines Ruby feature set.
6
+ #
7
+ # Implies {Nscd} feature.
8
+ class Bwrap::Config::Features::Ruby < Bwrap::Config::Features::Base
9
+ include Bwrap::Execution
10
+
11
+ def initialize features
12
+ super features
13
+
14
+ @gem_env_paths = true
15
+ @stdlib = []
16
+ end
17
+
18
+ # @return true if bindirs from “gem environment” should be added to sandbox.
19
+ def gem_env_paths?
20
+ @gem_env_paths
21
+ end
22
+
23
+ # Enable Ruby feature set.
24
+ #
25
+ # Among others, binds `RbConfig::CONFIG["sitedir"]` so scripts works.
26
+ #
27
+ # @note This does not allow development headers needed for compilation for now.
28
+ # I’ll look at it after I have an use for it.
29
+ #
30
+ # @note Also enables {Nscd} feature.
31
+ def enable
32
+ super
33
+
34
+ @features.nscd.enable
35
+ end
36
+
37
+ # @return [Pathname|nil] path to Ruby interpreter.
38
+ def interpreter
39
+ @features.mime&.executable_path
40
+ end
41
+
42
+ # @return [Hash(String, String)] RbConfig::CONFIG from interpreter returned by {#interpreter}
43
+ def ruby_config
44
+ unless interpreter
45
+ raise "Interpreter is not set, so ruby_config() can’t be called yet."
46
+ end
47
+
48
+ return @ruby_config if @ruby_config
49
+
50
+ script = %q{RbConfig::CONFIG.each { |k, v| puts "#{k}=#{v}" }}
51
+ config_string = execvalue %W{ #{interpreter} -e #{script} }
52
+
53
+ ruby_config = {}
54
+ config_string.split("\n").each do |line|
55
+ key, value = line.split "=", 2
56
+ ruby_config[key] = value
57
+ end
58
+
59
+ @ruby_config = ruby_config
60
+ end
61
+
62
+ # Extra libraries to be loaded from script’s interpreter’s `RbConfig::CONFIG["rubyarchdir"]`.
63
+ #
64
+ # @note Existence of library paths are checked here, and not in {#stdlib=},
65
+ # to avoid error about missing interpreter, as it is set in {Bwrap::Bwrap#run},
66
+ # and config is meant to be created beforehand.
67
+ #
68
+ # @note This is only required to be called if extra dependencies are necessary.
69
+ # For example, psych.so requires libyaml.so.
70
+ #
71
+ # @return [Array] list of needed libraries.
72
+ def stdlib
73
+ # Just a little check to have error earlier.
74
+ @stdlib.each do |lib|
75
+ unless File.exist? "#{ruby_config["rubyarchdir"]}/#{lib}.so"
76
+ raise "Library “#{lib}” passed to Bwrap::Config::Ruby.stdlib= does not exist."
77
+ end
78
+ end
79
+
80
+ @stdlib
81
+ end
82
+
83
+ def stdlib= libs
84
+ @stdlib = libs
85
+ end
86
+ end
@@ -3,32 +3,14 @@
3
3
  class Bwrap::Config
4
4
  # Methods to enable or disable feature sets to control various aspects of sandboxing.
5
5
  class Features
6
- # @abstract
6
+ # Requires are here so there is no extra trickiness with namespaces.
7
7
  #
8
- # Base of all features.
9
- class Base
10
- # @param features [Bwrap::Config::Features] Instance of features object in {Config}
11
- def initialize features
12
- @features = features
13
- end
8
+ # Feature implementations are not meant to be used outside of this class anyway.
9
+ require_relative "features/base"
10
+ require_relative "features/ruby"
14
11
 
15
- # Checks if the feature has been enabled.
16
- #
17
- # @return [Boolean] whether feature is enabled
18
- def enabled?
19
- @enabled
20
- end
21
-
22
- # Enable the feature.
23
- def enable
24
- @enabled = true
25
- end
26
-
27
- # Disable the feature.
28
- def disable
29
- @enabled = false
30
- end
31
- end
12
+ # Instance of {Bwrap::Args::Bind::Mime}.
13
+ attr_accessor :mime
32
14
 
33
15
  # Defines Bash feature set.
34
16
  class Bash < Base
@@ -44,60 +26,6 @@ class Bwrap::Config
44
26
  # Nya.
45
27
  end
46
28
 
47
- # Defines Ruby feature set.
48
- #
49
- # Implies {Nscd} feature.
50
- class Ruby < Base
51
- # Extra libraries to be loaded from `RbConfig::CONFIG["rubyarchdir"]`.
52
- #
53
- # @note This is only required to be called if extra dependencies are necessary.
54
- # For example, psych.so requires libyaml.so.
55
- #
56
- # @return [Array] list of needed libraries.
57
- #
58
- # @overload stdlib
59
- # @overload stdlib=(libs)
60
- attr_reader :stdlib
61
-
62
- def initialize features
63
- super features
64
-
65
- @gem_env_paths = true
66
- @stdlib = []
67
- end
68
-
69
- # @return true if bindirs from “gem environment” should be added to sandbox.
70
- def gem_env_paths?
71
- @gem_env_paths
72
- end
73
-
74
- # Enable Ruby feature set.
75
- #
76
- # Among others, binds `RbConfig::CONFIG["sitedir"]` so scripts works.
77
- #
78
- # @note This does not allow development headers needed for compilation for now.
79
- # I’ll look at it after I have an use for it.
80
- #
81
- # @note Also enables {Nscd} feature.
82
- def enable
83
- super
84
-
85
- @features.nscd.enable
86
- end
87
-
88
- # @see #stdlib
89
- def stdlib= libs
90
- # Just a little check to have error earlier.
91
- libs.each do |lib|
92
- unless File.exist? "#{RbConfig::CONFIG["rubyarchdir"]}/#{lib}.so"
93
- raise "Library “#{lib}” passed to Bwrap::Config::Ruby.stdlib= does not exist."
94
- end
95
- end
96
-
97
- @stdlib = libs
98
- end
99
- end
100
-
101
29
  # @return [Bash] Instance of feature class for Bash
102
30
  def bash
103
31
  @bash ||= Bash.new self
@@ -7,6 +7,18 @@ module Bwrap::Execution
7
7
 
8
8
  # Signifies that command execution has failed.
9
9
  class ExecutionFailed < CommandError
10
+ # The command that was executed.
11
+ attr_reader :command
12
+
13
+ # Output of the command.
14
+ attr_reader :output
15
+
16
+ def initialize msg, command:, output:
17
+ @command = command
18
+ @output = output
19
+
20
+ super msg
21
+ end
10
22
  end
11
23
 
12
24
  # Thrown if given command was not found.
@@ -40,6 +40,13 @@ class Bwrap::Execution::Execute
40
40
 
41
41
  # @return formatted command.
42
42
  def self.format_command command, rootcmd:
43
+ # Flatten the command if required, so nils can be converted in more of cases.
44
+ # Flattenization is also done in executions, but they also take in account
45
+ # for example rootcmd, so they probably should be done in addition to this one.
46
+ if command.respond_to? :flatten!
47
+ command.flatten!
48
+ end
49
+
43
50
  replace_nils command
44
51
  return command if rootcmd.nil?
45
52
 
@@ -63,13 +70,17 @@ class Bwrap::Execution::Execute
63
70
  end
64
71
 
65
72
  # Checks whether execution failed and acts accordingly.
66
- def self.handle_execution_fail fail:, error:, output:
67
- return unless fail and !$CHILD_STATUS.success?
73
+ def self.handle_execution_fail fail:, error:, output:, command:
74
+ return unless fail and !execution_success?
68
75
 
69
76
  if error == :show and !output.empty?
70
77
  Bwrap::Output.warn_output "Command failed with output:\n“#{output}”"
71
78
  end
72
- raise Bwrap::Execution::ExecutionFailed, "Command execution failed.", caller
79
+
80
+ exception = Bwrap::Execution::ExecutionFailed.new "Command execution failed",
81
+ command: command,
82
+ output: output
83
+ raise exception, caller
73
84
  end
74
85
 
75
86
  # @note It makes sense for caller to just return if wait has been set and not check output.
@@ -101,6 +112,13 @@ class Bwrap::Execution::Execute
101
112
  "to add “self.prepend_rootcmd(command, rootcmd:)” method."
102
113
  end
103
114
 
115
+ # A wrapper to get status of an execution.
116
+ #
117
+ # Mainly here so test implementation is easier.
118
+ private_class_method def self.execution_success?
119
+ $CHILD_STATUS.success?
120
+ end
121
+
104
122
  # Used by `#handle_logging`.
105
123
  private_class_method def self.calculate_log_command command
106
124
  return command.dup unless command.respond_to?(:join)
@@ -5,6 +5,7 @@ require "bwrap/output"
5
5
  require_relative "exceptions"
6
6
  require_relative "execute"
7
7
  require_relative "path"
8
+ require_relative "popen2e"
8
9
 
9
10
  # Provides methods to execute commands and handle its output.
10
11
  #
@@ -61,7 +62,7 @@ module Bwrap::Execution
61
62
  end
62
63
 
63
64
  # If command is string, splat operator (the *) does not do anything. If array, it expand the arguments.
64
- # This causes spawning work correctly, as that’s how spawn() expects to have the argu
65
+ # This causes spawning work correctly, as that’s how spawn() expects to have the arguments.
65
66
  pid = spawn(env, *command, err: [ :child, :out ], out: Execute.w, unsetenv_others: clear_env)
66
67
  output = Execute.finish_execution(log: log, wait: wait, direct_output: direct_output)
67
68
  return pid unless wait
@@ -71,12 +72,35 @@ module Bwrap::Execution
71
72
  @last_status = $CHILD_STATUS
72
73
 
73
74
  output = Execute.process_output output: output
74
- Execute.handle_execution_fail fail: fail, error: error, output: output
75
+ Execute.handle_execution_fail fail: fail, error: error, output: output, command: command
75
76
  output
76
77
  ensure
77
78
  Execute.clean_variables
78
79
  end
79
80
 
81
+ # Works similarly to {Open3.popen2e}.
82
+ #
83
+ # TODO: If there will be any difference to input syntax, document those differences here.
84
+ # For now, rootcmd option has been implemented.
85
+ #
86
+ # A block is accepted, as does {Open3.popen2e}. For now, at least.
87
+ #
88
+ # TODO: Implement this so that this uses same execution things as other things here.
89
+ # This way bwrap actually can be integrated to this...
90
+ #
91
+ # TODO: Verify default log_callback is correct
92
+ #
93
+ # @warning Only array style commands are accepted. For example, `ls /`
94
+ # is not ok, but `ls` or `%w{ ls / }` is ok.
95
+ def popen2e *cmd, rootcmd: nil, log_callback: 1, log: true, &block
96
+ popen = Bwrap::Execution::Popen2e.new
97
+ popen.dry_run = @dry_run
98
+ popen.popen2e(*cmd, rootcmd: rootcmd, log_callback: log_callback, log: log, &block)
99
+ ensure
100
+ @last_status = $CHILD_STATUS
101
+ end
102
+ module_function :popen2e
103
+
80
104
  # Returns Process::Status instance of last execution.
81
105
  #
82
106
  # @note This only is able to return the status if wait is true, as otherwise caller is assumed to
@@ -20,7 +20,7 @@ module Bwrap::Execution::Path
20
20
  #
21
21
  # @yield Command appended to each path in PATH environment variable
22
22
  # @yieldparam path [String] Full path to executable
23
- def self.each_env_path command, env_path_var: ENV["PATH"]
23
+ def self.each_env_path command, env_path_var: ENV.fetch("PATH", nil)
24
24
  exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [ "" ]
25
25
 
26
26
  env_path_var.split(File::PATH_SEPARATOR).each do |env_path|
@@ -39,7 +39,7 @@ module Bwrap::Execution::Path
39
39
  # @param command [String] executable to be resolved
40
40
  # @param env_path_var [String] PATH environment variable as string.
41
41
  # Defaults to `ENV["PATH"]`
42
- private def command_available? command, env_path_var: ENV["PATH"]
42
+ private def command_available? command, env_path_var: ENV.fetch("PATH", nil)
43
43
  # Special handling for absolute paths.
44
44
  path = Pathname.new command
45
45
  if path.absolute?
@@ -60,7 +60,7 @@ module Bwrap::Execution::Path
60
60
  # Returns path to given executable.
61
61
  #
62
62
  # @param (see #command_available?)
63
- private def which command, fail: true, env_path_var: ENV["PATH"]
63
+ private def which command, fail: true, env_path_var: ENV.fetch("PATH", nil)
64
64
  # Special handling for absolute paths.
65
65
  path = Pathname.new command
66
66
  if path.absolute?
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @see {Bwrap::Execution.popen2e}
4
+ class Bwrap::Execution::Popen2e
5
+ attr_writer :dry_run
6
+
7
+ # @see {Bwrap::Execution.popen2e}
8
+ # TODO: Add a test for this (does Ruby have anything I could use here?).
9
+ #
10
+ # @note Also options accepted by {Open3.popen2e} are accepted
11
+ # here, in addition to those specified here.
12
+ def popen2e *cmd, rootcmd: nil, log_callback: 1, log: true, &block
13
+ @rootcmd = rootcmd
14
+ @log_callback = log_callback + 1 # Passing to another method, so need to add one more.
15
+ @log = log
16
+ self.opts_from_input = cmd
17
+
18
+ resolve_actual_command cmd
19
+
20
+ return if @dry_run || Bwrap::Execution::Execute.dry_run
21
+
22
+ open_pipes
23
+ popen_run(@actual_command, [ @in_read, @out_write ], [ @in_write, @out_read ], &block)
24
+ ensure
25
+ if block
26
+ @in_read.close
27
+ @in_write.close
28
+ @out_read.close
29
+ @out_write.close
30
+ end
31
+ end
32
+
33
+ # Sets @opts from `cmd` given to {#popen2e}, if any extra options have been given.
34
+ private def opts_from_input= cmd
35
+ @opts = cmd.last.is_a?(Hash) && cmd.pop.dup || {}
36
+ end
37
+
38
+ private def open_pipes
39
+ @in_read, @in_write = IO.pipe
40
+ @opts[:in] = @in_read
41
+ @in_write.sync = true
42
+
43
+ @out_read, @out_write = IO.pipe
44
+ @opts[[ :out, :err ]] = @out_write
45
+ end
46
+
47
+ # First element may be optional environment variables.
48
+ private def resolve_actual_command cmd
49
+ temp_cmd = cmd.dup
50
+
51
+ # Delete environment hash.
52
+ env_hash = temp_cmd.shift if temp_cmd.first.is_a? Hash
53
+
54
+ # Delete option hash.
55
+ option_hash = temp_cmd.pop if temp_cmd.last.is_a? Hash
56
+
57
+ temp_cmd = Bwrap::Execution::Execute.format_command temp_cmd, rootcmd: @rootcmd
58
+ Bwrap::Execution::Execute.handle_logging temp_cmd, log_callback: @log_callback, log: @log, dry_run: @dry_run
59
+
60
+ temp_cmd.unshift env_hash if env_hash
61
+ temp_cmd.push option_hash if option_hash
62
+
63
+ # If command is an array, there can’t be arrays inside the array (hashes are preserved).
64
+ # For convenience, the array is flattened here, so callers can construct commands more easily.
65
+ temp_cmd.flatten!
66
+
67
+ @actual_command = temp_cmd
68
+ end
69
+
70
+ private def popen_run cmd, child_io, parent_io
71
+ pid = spawn(*cmd, @opts)
72
+ wait_thr = Process.detach(pid)
73
+ child_io.each(&:close)
74
+ result = [ *parent_io, wait_thr ]
75
+
76
+ if defined? yield
77
+ begin
78
+ return yield(*result)
79
+ ensure
80
+ parent_io.each(&:close)
81
+ wait_thr.join
82
+ end
83
+ end
84
+
85
+ result
86
+ end
87
+ end