toys-core 0.10.5 → 0.11.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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