toys-core 0.10.5 → 0.11.4

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.
@@ -10,7 +10,7 @@ module Toys
10
10
  # @private
11
11
  #
12
12
  def initialize(parent, context_directory, source_type, source_path, source_proc,
13
- source_name, data_dir, lib_dir)
13
+ source_name, data_dir_name, lib_dir_name)
14
14
  @parent = parent
15
15
  @context_directory = context_directory
16
16
  @source_type = source_type
@@ -18,8 +18,10 @@ module Toys
18
18
  @source_path = source_path
19
19
  @source_proc = source_proc
20
20
  @source_name = source_name
21
- @data_dir = data_dir
22
- @lib_dir = lib_dir
21
+ @data_dir_name = data_dir_name
22
+ @lib_dir_name = lib_dir_name
23
+ @data_dir = find_special_dir(data_dir_name)
24
+ @lib_dir = find_special_dir(lib_dir_name)
23
25
  end
24
26
 
25
27
  ##
@@ -118,15 +120,13 @@ module Toys
118
120
  # Create a child SourceInfo relative to the parent path.
119
121
  # @private
120
122
  #
121
- def relative_child(filename, data_dir_name, lib_dir_name)
122
- raise "no parent path for relative_child" unless source_path
123
+ def relative_child(filename)
124
+ raise "relative_child is valid only on a directory source" unless source_type == :directory
123
125
  child_path = ::File.join(source_path, filename)
124
126
  child_path, type = SourceInfo.check_path(child_path, true)
125
127
  return nil unless child_path
126
- data_dir = SourceInfo.find_special_dir(type, child_path, data_dir_name)
127
- lib_dir = SourceInfo.find_special_dir(type, child_path, lib_dir_name)
128
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path,
129
- data_dir, lib_dir)
128
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
129
+ @data_dir_name, @lib_dir_name)
130
130
  end
131
131
 
132
132
  ##
@@ -135,7 +135,8 @@ module Toys
135
135
  #
136
136
  def absolute_child(child_path)
137
137
  child_path, type = SourceInfo.check_path(child_path, false)
138
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path, nil, nil)
138
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
139
+ @data_dir_name, @lib_dir_name)
139
140
  end
140
141
 
141
142
  ##
@@ -144,25 +145,26 @@ module Toys
144
145
  #
145
146
  def proc_child(child_proc, source_name = nil)
146
147
  source_name ||= self.source_name
147
- SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name, nil, nil)
148
+ SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name,
149
+ @data_dir_name, @lib_dir_name)
148
150
  end
149
151
 
150
152
  ##
151
153
  # Create a root source info for a file path.
152
154
  # @private
153
155
  #
154
- def self.create_path_root(source_path)
156
+ def self.create_path_root(source_path, data_dir_name, lib_dir_name)
155
157
  source_path, type = check_path(source_path, false)
156
158
  context_directory = ::File.dirname(source_path)
157
- new(nil, context_directory, type, source_path, nil, source_path, nil, nil)
159
+ new(nil, context_directory, type, source_path, nil, source_path, data_dir_name, lib_dir_name)
158
160
  end
159
161
 
160
162
  ##
161
163
  # Create a root source info for a proc.
162
164
  # @private
163
165
  #
164
- def self.create_proc_root(source_proc, source_name)
165
- new(nil, nil, :proc, nil, source_proc, source_name, nil, nil)
166
+ def self.create_proc_root(source_proc, source_name, data_dir_name, lib_dir_name)
167
+ new(nil, nil, :proc, nil, source_proc, source_name, data_dir_name, lib_dir_name)
166
168
  end
167
169
 
168
170
  ##
@@ -189,14 +191,11 @@ module Toys
189
191
  end
190
192
  end
191
193
 
192
- ##
193
- # Determine the data directory path, if any.
194
- # @private
195
- #
196
- def self.find_special_dir(type, source_path, dir_name)
197
- return nil if source_path.nil? || dir_name.nil?
198
- source_path = ::File.dirname(source_path) if type == :file
199
- dir = ::File.join(source_path, dir_name)
194
+ private
195
+
196
+ def find_special_dir(dir_name)
197
+ return nil if @source_type != :directory || dir_name.nil?
198
+ dir = ::File.join(@source_path, dir_name)
200
199
  dir if ::File.directory?(dir) && ::File.readable?(dir)
201
200
  end
202
201
  end
@@ -11,32 +11,51 @@ module Toys
11
11
  # defining the tool. If `false` (the default), installs the bundle just
12
12
  # before the tool runs.
13
13
  #
14
- # * `:groups` (Array<String>) The groups to include in setup
14
+ # * `:groups` (Array\<String\>) The groups to include in setup.
15
15
  #
16
- # * `:search_dirs` (Array<String,Symbol>) Directories to search for a
17
- # Gemfile.
16
+ # * `:gemfile_path` (String) The path to the Gemfile to use. If `nil` or
17
+ # not given, the `:search_dirs` will be searched for a Gemfile.
18
+ #
19
+ # * `:search_dirs` (String,Symbol,Array\<String,Symbol\>) Directories to
20
+ # search for a Gemfile.
18
21
  #
19
22
  # You can pass full directory paths, and/or any of the following:
20
- # * `:context` - the current context directory
21
- # * `:current` - the current working directory
22
- # * `:toys` - the Toys directory containing the tool definition
23
+ # * `:context` - the current context directory.
24
+ # * `:current` - the current working directory.
25
+ # * `:toys` - the Toys directory containing the tool definition, and
26
+ # any of its parents within the Toys directory hierarchy.
23
27
  #
24
28
  # The default is to search `[:toys, :context, :current]` in that order.
29
+ # See {DEFAULT_SEARCH_DIRS}.
30
+ #
31
+ # For most directories, the bundler mixin will look for the files
32
+ # ".gems.rb", "gems.rb", and "Gemfile", in that order. In `:toys`
33
+ # directories, it will look only for ".gems.rb" and "Gemfile", in that
34
+ # order. These can be overridden by setting the `:gemfile_names` and/or
35
+ # `:toys_gemfile_names` arguments.
36
+ #
37
+ # * `:gemfile_names` (Array\<String\>) File names that are recognized as
38
+ # Gemfiles when searching in directories other than Toys directories.
39
+ # Defaults to {Toys::Utils::Gems::DEFAULT_GEMFILE_NAMES}.
40
+ #
41
+ # * `:toys_gemfile_names` (Array\<String\>) File names that are
42
+ # recognized as Gemfiles when wearching in Toys directories.
43
+ # Defaults to {DEFAULT_TOYS_GEMFILE_NAMES}.
25
44
  #
26
45
  # * `:on_missing` (Symbol) What to do if a needed gem is not installed.
27
46
  #
28
47
  # Supported values:
29
- # * `:confirm` - prompt the user on whether to install (default)
30
- # * `:error` - raise an exception
31
- # * `:install` - just install the gem
48
+ # * `:confirm` - prompt the user on whether to install (default).
49
+ # * `:error` - raise an exception.
50
+ # * `:install` - just install the gem.
32
51
  #
33
52
  # * `:on_conflict` (Symbol) What to do if bundler has already been run
34
53
  # with a different Gemfile.
35
54
  #
36
55
  # Supported values:
37
- # * `:error` - raise an exception (default)
38
- # * `:ignore` - just silently proceed without bundling again
39
- # * `:warn` - print a warning and proceed without bundling again
56
+ # * `:error` - raise an exception (default).
57
+ # * `:ignore` - just silently proceed without bundling again.
58
+ # * `:warn` - print a warning and proceed without bundling again.
40
59
  #
41
60
  # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional)
42
61
  # * `:input` (IO) Input IO (optional, defaults to STDIN)
@@ -45,68 +64,106 @@ module Toys
45
64
  module Bundler
46
65
  include Mixin
47
66
 
48
- on_initialize do |static: false, search_dirs: nil, **kwargs|
67
+ on_initialize do |static: false, **kwargs|
49
68
  unless static
50
- require "toys/utils/gems"
51
- search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(
52
- search_dirs,
53
- self[::Toys::Context::Key::CONTEXT_DIRECTORY],
54
- self[::Toys::Context::Key::TOOL_SOURCE]
55
- )
56
- ::Toys::StandardMixins::Bundler.setup_bundle(search_dirs, **kwargs)
69
+ context_directory = self[::Toys::Context::Key::CONTEXT_DIRECTORY]
70
+ source_info = self[::Toys::Context::Key::TOOL_SOURCE]
71
+ ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
57
72
  end
58
73
  end
59
74
 
60
- on_include do |static: false, search_dirs: nil, **kwargs|
75
+ on_include do |static: false, **kwargs|
61
76
  if static
62
- require "toys/utils/gems"
63
- search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(
64
- search_dirs, context_directory, source_info
65
- )
66
- ::Toys::StandardMixins::Bundler.setup_bundle(search_dirs, **kwargs)
77
+ ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
67
78
  end
68
79
  end
69
80
 
70
- ## @private
71
- def self.resolve_search_dirs(search_dirs, context_dir, source_info)
72
- search_dirs ||= [:toys, :context, :current]
73
- Array(search_dirs).flat_map do |dir|
74
- case dir
75
- when :context
76
- context_dir
77
- when :current
78
- ::Dir.getwd
79
- when :toys
80
- toys_dir_stack(source_info)
81
- when ::String
82
- dir
83
- else
84
- raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
85
- end
86
- end
87
- end
81
+ ##
82
+ # Default search directories for Gemfiles.
83
+ # @return [Array<String,Symbol>]
84
+ #
85
+ DEFAULT_SEARCH_DIRS = [:toys, :context, :current].freeze
88
86
 
89
- ## @private
90
- def self.toys_dir_stack(source_info)
91
- dirs = []
92
- while source_info
93
- dirs << source_info.source_path if source_info.source_type == :directory
94
- source_info = source_info.parent
95
- end
96
- dirs
97
- end
87
+ ##
88
+ # The gemfile names that are searched by default in Toys directories.
89
+ # @return [Array<String>]
90
+ #
91
+ DEFAULT_TOYS_GEMFILE_NAMES = [".gems.rb", "Gemfile"].freeze
98
92
 
99
- ## @private
100
- def self.setup_bundle(search_dirs,
93
+ # @private
94
+ def self.setup_bundle(context_directory,
95
+ source_info,
96
+ gemfile_path: nil,
97
+ search_dirs: nil,
98
+ gemfile_names: nil,
99
+ toys_gemfile_names: nil,
101
100
  groups: nil,
102
101
  on_missing: nil,
103
102
  on_conflict: nil,
104
103
  terminal: nil,
105
104
  input: nil,
106
105
  output: nil)
106
+ require "toys/utils/gems"
107
+ gemfile_path ||= begin
108
+ gemfile_finder = GemfileFinder.new(context_directory, source_info,
109
+ gemfile_names, toys_gemfile_names)
110
+ gemfile_finder.search(search_dirs || DEFAULT_SEARCH_DIRS)
111
+ end
107
112
  gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict,
108
113
  terminal: terminal, input: input, output: output)
109
- gems.bundle(groups: groups, search_dirs: search_dirs)
114
+ gems.bundle(groups: groups, gemfile_path: gemfile_path)
115
+ end
116
+
117
+ # @private
118
+ class GemfileFinder
119
+ # @private
120
+ def initialize(context_directory, source_info, gemfile_names, toys_gemfile_names)
121
+ @context_directory = context_directory
122
+ @source_info = source_info
123
+ @gemfile_names = gemfile_names
124
+ @toys_gemfile_names = toys_gemfile_names || DEFAULT_TOYS_GEMFILE_NAMES
125
+ end
126
+
127
+ # @private
128
+ def search(search_dir)
129
+ case search_dir
130
+ when ::Array
131
+ search_array(search_dir)
132
+ when ::String
133
+ ::Toys::Utils::Gems.find_gemfile(search_dir, gemfile_names: @gemfile_names)
134
+ when :context
135
+ search(@context_directory)
136
+ when :current
137
+ search(::Dir.getwd)
138
+ when :toys
139
+ search_toys
140
+ else
141
+ raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ def search_array(search_dirs)
148
+ search_dirs.each do |search_dir|
149
+ result = search(search_dir)
150
+ return result if result
151
+ end
152
+ nil
153
+ end
154
+
155
+ def search_toys
156
+ source_info = @source_info
157
+ while source_info
158
+ if source_info.source_type == :directory
159
+ result = ::Toys::Utils::Gems.find_gemfile(source_info.source_path,
160
+ gemfile_names: @toys_gemfile_names)
161
+ return result if result
162
+ end
163
+ source_info = source_info.parent
164
+ end
165
+ nil
166
+ end
110
167
  end
111
168
  end
112
169
  end
@@ -250,13 +250,14 @@ module Toys
250
250
  exec_opts = Opts.new(@default_opts).add(opts)
251
251
  spawn_cmd =
252
252
  if cmd.is_a?(::Array)
253
- if cmd.size == 1 && cmd.first.is_a?(::String)
254
- [[cmd.first, exec_opts.config_opts[:argv0] || cmd.first]]
253
+ if cmd.size > 1
254
+ binary = canonical_binary_spec(cmd.first, exec_opts)
255
+ [binary] + cmd[1..-1].map(&:to_s)
255
256
  else
256
- cmd
257
+ [canonical_binary_spec(Array(cmd.first), exec_opts)]
257
258
  end
258
259
  else
259
- [cmd]
260
+ [cmd.to_s]
260
261
  end
261
262
  executor = Executor.new(exec_opts, spawn_cmd, block)
262
263
  executor.execute
@@ -492,7 +493,8 @@ module Toys
492
493
  #
493
494
  class Controller
494
495
  ## @private
495
- def initialize(name, controller_streams, captures, pid, join_threads, result_callback)
496
+ def initialize(name, controller_streams, captures, pid, join_threads,
497
+ result_callback, mutex)
496
498
  @name = name
497
499
  @in = controller_streams[:in]
498
500
  @out = controller_streams[:out]
@@ -508,6 +510,7 @@ module Toys
508
510
  end
509
511
  @join_threads = join_threads
510
512
  @result_callback = result_callback
513
+ @mutex = mutex
511
514
  @result = nil
512
515
  end
513
516
 
@@ -547,7 +550,7 @@ module Toys
547
550
  ##
548
551
  # The process ID.
549
552
  #
550
- # Exactly one of `exception` and `pid` will be non-nil.
553
+ # Exactly one of {#exception} and {#pid} will be non-nil.
551
554
  #
552
555
  # @return [Integer] if the process start was successful
553
556
  # @return [nil] if the process could not be started.
@@ -557,7 +560,7 @@ module Toys
557
560
  ##
558
561
  # The exception raised when the process failed to start.
559
562
  #
560
- # Exactly one of `exception` and `pid` will be non-nil.
563
+ # Exactly one of {#exception} and {#pid} will be non-nil.
561
564
  #
562
565
  # @return [Exception] if the process failed to start.
563
566
  # @return [nil] if the process start was successful.
@@ -575,7 +578,10 @@ module Toys
575
578
  stream = stream_for(which)
576
579
  @join_threads << ::Thread.new do
577
580
  begin
578
- @captures[which] = stream.read
581
+ data = stream.read
582
+ @mutex.synchronize do
583
+ @captures[which] = data
584
+ end
579
585
  ensure
580
586
  stream.close
581
587
  end
@@ -722,15 +728,20 @@ module Toys
722
728
  ##
723
729
  # Wait for the subcommand to complete, and return a result object.
724
730
  #
731
+ # Closes the control streams if present. The stdin stream is always
732
+ # closed, even if the call times out. The stdout and stderr streams are
733
+ # closed only after the command terminates.
734
+ #
725
735
  # @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
726
736
  # wait indefinitely.
727
737
  # @return [Toys::Utils::Exec::Result] The result object
728
738
  # @return [nil] if a timeout occurred.
729
739
  #
730
740
  def result(timeout: nil)
741
+ close_streams(:in)
731
742
  return nil if @wait_thread && !@wait_thread.join(timeout)
732
743
  @result ||= begin
733
- close_streams
744
+ close_streams(:out)
734
745
  @join_threads.each(&:join)
735
746
  Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
736
747
  .tap { |result| @result_callback&.call(result) }
@@ -738,13 +749,13 @@ module Toys
738
749
  end
739
750
 
740
751
  ##
741
- # Close all the controller's streams.
752
+ # Close the controller's streams.
742
753
  # @private
743
754
  #
744
- def close_streams
745
- @in.close if @in && !@in.closed?
746
- @out.close if @out && !@out.closed?
747
- @err.close if @err && !@err.closed?
755
+ def close_streams(which)
756
+ @in.close if which != :out && @in && !@in.closed?
757
+ @out.close if which != :in && @out && !@out.closed?
758
+ @err.close if which != :in && @err && !@err.closed?
748
759
  self
749
760
  end
750
761
 
@@ -773,7 +784,21 @@ module Toys
773
784
  end
774
785
 
775
786
  ##
776
- # The result returned from a subcommand execution.
787
+ # The result returned from a subcommand execution. This includes the
788
+ # identifying name of the execution (if any), the result status of the
789
+ # execution, and any captured stream output.
790
+ #
791
+ # Possible result statuses are:
792
+ #
793
+ # * The process failed to start. {Result#failed?} will return true, and
794
+ # {Result#exception} will return an exception describing the failure
795
+ # (often an errno).
796
+ # * The process executed and exited with a normal exit code. Either
797
+ # {Result#success?} or {Result#error?} will return true, and
798
+ # {Result.exit_code} will return the numeric exit code.
799
+ # * The process executed but was terminated by an uncaught signal.
800
+ # {Result#signaled?} will return true, and {Result#term_signal} will
801
+ # return the numeric signal code.
777
802
  #
778
803
  class Result
779
804
  ## @private
@@ -809,11 +834,13 @@ module Toys
809
834
  attr_reader :captured_err
810
835
 
811
836
  ##
812
- # The status code object.
837
+ # The Ruby process status object, providing various information about
838
+ # the ending state of the process.
813
839
  #
814
- # Exactly one of `exception` and `status` will be non-nil.
840
+ # Exactly one of {#exception} and {#status} will be non-nil.
815
841
  #
816
- # @return [Process::Status] The status code.
842
+ # @return [Process::Status] The status, if the process was successfully
843
+ # spanwed and terminated.
817
844
  # @return [nil] if the process could not be started.
818
845
  #
819
846
  attr_reader :status
@@ -821,7 +848,7 @@ module Toys
821
848
  ##
822
849
  # The exception raised if a process couldn't be started.
823
850
  #
824
- # Exactly one of `exception` and `status` will be non-nil.
851
+ # Exactly one of {#exception} and {#status} will be non-nil.
825
852
  #
826
853
  # @return [Exception] The exception raised from process start.
827
854
  # @return [nil] if the process started successfully.
@@ -829,33 +856,76 @@ module Toys
829
856
  attr_reader :exception
830
857
 
831
858
  ##
832
- # The numeric status code.
859
+ # The numeric status code for a process that exited normally,
833
860
  #
834
- # This will be a nonzero integer if the process failed to start. That
835
- # is, `exit_code` will never be `nil`, even if `status` is `nil`.
861
+ # Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
862
+ # non-nil.
836
863
  #
837
- # @return [Integer]
864
+ # @return [Integer] the numeric status code, if the process started
865
+ # successfully and exited normally.
866
+ # @return [nil] if the process did not start successfully, or was
867
+ # terminated by an uncaught signal.
838
868
  #
839
869
  def exit_code
840
- status ? status.exitstatus : 127
870
+ status&.exitstatus
871
+ end
872
+
873
+ ##
874
+ # The numeric signal code that caused process termination.
875
+ #
876
+ # Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
877
+ # non-nil.
878
+ #
879
+ # @return [Integer] The signal that caused the process to terminate.
880
+ # @return [nil] if the process did not start successfully, or executed
881
+ # and exited with a normal exit code.
882
+ #
883
+ def term_signal
884
+ status&.termsig
885
+ end
886
+
887
+ ##
888
+ # Returns true if the subprocess failed to start, or false if the
889
+ # process was able to execute.
890
+ #
891
+ # @return [Boolean]
892
+ #
893
+ def failed?
894
+ status.nil?
895
+ end
896
+
897
+ ##
898
+ # Returns true if the subprocess terminated due to an unhandled signal,
899
+ # or false if the process failed to start or exited normally.
900
+ #
901
+ # @return [Boolean]
902
+ #
903
+ def signaled?
904
+ !term_signal.nil?
841
905
  end
842
906
 
843
907
  ##
844
- # Returns true if the subprocess terminated with a zero status.
908
+ # Returns true if the subprocess terminated with a zero status, or
909
+ # false if the process failed to start, terminated due to a signal, or
910
+ # returned a nonzero status.
845
911
  #
846
912
  # @return [Boolean]
847
913
  #
848
914
  def success?
849
- exit_code.zero?
915
+ code = exit_code
916
+ !code.nil? && code.zero?
850
917
  end
851
918
 
852
919
  ##
853
- # Returns true if the subprocess terminated with a nonzero status.
920
+ # Returns true if the subprocess terminated with a nonzero status, or
921
+ # false if the process failed to start, terminated due to a signal, or
922
+ # returned a zero status.
854
923
  #
855
924
  # @return [Boolean]
856
925
  #
857
926
  def error?
858
- !exit_code.zero?
927
+ code = exit_code
928
+ !code.nil? && !code.zero?
859
929
  end
860
930
  end
861
931
 
@@ -876,6 +946,7 @@ module Toys
876
946
  @parent_streams = []
877
947
  @block = block
878
948
  @default_stream = @config_opts[:background] ? :null : :inherit
949
+ @mutex = ::Mutex.new
879
950
  end
880
951
 
881
952
  def execute
@@ -887,10 +958,10 @@ module Toys
887
958
  return controller if @config_opts[:background]
888
959
  begin
889
960
  @block&.call(controller)
961
+ controller.result
890
962
  ensure
891
- controller.close_streams
963
+ controller.close_streams(:both)
892
964
  end
893
- controller.result
894
965
  end
895
966
 
896
967
  private
@@ -898,12 +969,19 @@ module Toys
898
969
  def log_command
899
970
  logger = @config_opts[:logger]
900
971
  if logger && @config_opts[:log_level] != false
901
- cmd_str = @config_opts[:log_cmd]
902
- cmd_str ||= @spawn_cmd.size == 1 ? @spawn_cmd.first : @spawn_cmd.inspect if @spawn_cmd
972
+ cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
903
973
  logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
904
974
  end
905
975
  end
906
976
 
977
+ def default_log_str(spawn_cmd)
978
+ return nil unless spawn_cmd
979
+ return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
980
+ cmd_binary = spawn_cmd.first
981
+ cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
982
+ ([cmd_binary] + spawn_cmd[1..-1]).inspect
983
+ end
984
+
907
985
  def start_with_controller
908
986
  pid =
909
987
  begin
@@ -913,7 +991,7 @@ module Toys
913
991
  end
914
992
  @child_streams.each(&:close)
915
993
  Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
916
- @join_threads, @config_opts[:result_callback])
994
+ @join_threads, @config_opts[:result_callback], @mutex)
917
995
  end
918
996
 
919
997
  def start_process
@@ -1220,13 +1298,27 @@ module Toys
1220
1298
  stream = make_out_pipe(key)
1221
1299
  @join_threads << ::Thread.new do
1222
1300
  begin
1223
- @captures[key] = stream.read
1301
+ data = stream.read
1302
+ @mutex.synchronize do
1303
+ @captures[key] = data
1304
+ end
1224
1305
  ensure
1225
1306
  stream.close
1226
1307
  end
1227
1308
  end
1228
1309
  end
1229
1310
  end
1311
+
1312
+ private
1313
+
1314
+ def canonical_binary_spec(cmd, exec_opts)
1315
+ config_argv0 = exec_opts.config_opts[:argv0]
1316
+ return cmd.to_s if !config_argv0 && !cmd.is_a?(::Array)
1317
+ cmd = Array(cmd)
1318
+ actual_cmd = cmd.first
1319
+ argv0 = cmd[1] || config_argv0 || actual_cmd
1320
+ [actual_cmd.to_s, argv0.to_s]
1321
+ end
1230
1322
  end
1231
1323
  end
1232
1324
  end