subspawn 0.1.1 → 0.2.0.pre1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae4884bb09c6eb0bc3adb178baabd6898ee4b42cf9e8f2e1295d9619f47c7bba
4
- data.tar.gz: 47b9ea6752f556e0427008d0c2ba8310801f079820ed01927db264348500a97a
3
+ metadata.gz: d07057f5bd5fe708a6690914e4c71f391bd1caf008ce6393c3be6a2b70e4c941
4
+ data.tar.gz: '08af14e9800ce17abee24dd76094625da44b0151b8fbe141b85bcad4e3749653'
5
5
  SHA512:
6
- metadata.gz: eeb7880f1b0a380f846459698e4961882f4903cc90a506da20b41807ecab1fb8620efb04a05999564075728ef661cf7ec25a47cb1e60001fa28c33a753ee9799
7
- data.tar.gz: 4da0e3abb99b47f485a72dcea0263743836ae1578020fe84bc2dad5b71e6302b6a388b5b5cf8093a0ff9d30bc17b7990832cd4d42a6316480d9dd4554b9c2741
6
+ metadata.gz: c5ccf97896122ce4570ae0536b6fc3b8bdd10c458f1dd5c8cd35641f62516338f1896634940c52871ddaab6519e8aa4b84243f4308d0af1339daa4e05a62f5b6
7
+ data.tar.gz: d9befadc8ce0b8631012e425617195367e922c7db738a15034c3c183bb96ce8db7e2852a41f2644b6ae114148d7b17ceae6324cfa9d0b032711be54ac18de190
data/.rspec CHANGED
@@ -3,4 +3,5 @@
3
3
  --require spec_helper
4
4
  -I ../ffi-binary-libfixposix/lib
5
5
  -I ../subspawn-posix/lib
6
+ -I ../subspawn-common/lib
6
7
  -I ../ffi-bindings-libfixposix/lib
@@ -4,10 +4,11 @@ require_relative './pipes'
4
4
 
5
5
  module SubSpawn::Internal
6
6
  # argument value to int (or :tty)
7
- def self.parse_fd(fd, allow_pty=false)
7
+ def self.parse_fd(fd, allow_pty=false, dests: nil)
8
8
  # fd = if fd.respond_to? :to_path
9
9
  # fd = if fd.respond_to? :to_file
10
10
  # fd = if fd.respond_to? :to_file
11
+ fd = split_parse(fd, dests) if dests != nil and fd.respond_to? :underlying_write_io
11
12
  case fd
12
13
  when Integer then fd
13
14
  when IO then fd.fileno
@@ -23,6 +24,14 @@ module SubSpawn::Internal
23
24
  end
24
25
  end
25
26
 
27
+ # TODO: does this support [:in, :out, :pty] => bidi as well as :child?
28
+ # Only on windows, we have "split IO" to fake a bidirectional pipe for PTYs
29
+ # This picks the right underlying IO
30
+ def self.split_parse(fd, d)
31
+ mode = guess_mode(d)
32
+ fd.send(:"underlying_#{mode}_io")
33
+ end
34
+
26
35
  # mode conversion
27
36
  def self.guess_mode(d)
28
37
  read = d.include? 0 # stdin
@@ -38,11 +47,34 @@ module SubSpawn::Internal
38
47
  end
39
48
  end
40
49
 
50
+ def self.modestr_parse(str)
51
+ str = str.to_str
52
+ out = 0
53
+ if str.include? "b"
54
+ out = IO::BINARY
55
+ str = str.gsub("b", "")
56
+ end
57
+ if str.include? "x"
58
+ out = IO::EXCL
59
+ str = str.gsub("b", "")
60
+ end
61
+ out | case str
62
+ when "w" then IO::WRONLY | IO::CREAT | IO::TRUNC
63
+ when "r" then IO::RDONLY
64
+ when "r+", "w+" then IO::RDWR | IO::CREAT
65
+ when "a" then IO::WRONLY | IO::CREAT | IO::APPEND
66
+ when "a+" then IO::RDWR | IO::APPEND
67
+ else
68
+ raise ArgumentError "Unknown File mode"
69
+ end
70
+ end
71
+
41
72
  # make FdSource objects of each redirection
42
73
  def self.parse_fd_opts(fds, &settty)
43
74
  child_lookup = {}
44
75
  fds.map do |dests, src|
45
76
  d = dests.map{|x| parse_fd(x, true)} # TODO: configurable
77
+ src = src.to_io if src.respond_to? :to_io
46
78
  src = case src
47
79
  when Array
48
80
  case src.first
@@ -54,7 +86,7 @@ module SubSpawn::Internal
54
86
  raise ArgumentError, "Invalid :child FD source specification" unless src.length == 2
55
87
  # {a => c, b => [child, a]} is the same as {[a, b] => c}
56
88
  # so we can transform the former into the latter
57
- newfd = parse_fd(src.last)
89
+ newfd = parse_fd(src.last, dests: d)
58
90
  # TODO: this isn't an error, create a new one
59
91
  raise ArgumentError, "Invalid :child FD source specification" unless child_lookup[newfd]
60
92
  child_lookup[newfd].tap{|child|
@@ -81,7 +113,7 @@ module SubSpawn::Internal
81
113
  settty.call(src.path)
82
114
  d.delete(:tty)
83
115
  end
84
- FdSource::Basic.new d, parse_fd(src)
116
+ FdSource::Basic.new d, parse_fd(src, dests: d)
85
117
  end
86
118
  # save redirected fds so we can sneak a child reference in
87
119
  src.tap{|x| d.each{|c|
@@ -93,16 +93,7 @@ module SubSpawn::Internal
93
93
  @value = file
94
94
  @mode = mode || ::File::RDONLY
95
95
  if @mode.respond_to? :to_str
96
- @mode = case @mode.to_str
97
- when "w" then IO::WRONLY | IO::CREAT | IO::TRUNC
98
- when "r" then IO::RDONLY
99
- when "rb" then IO::RDONLY | IO::BINARY
100
- when "wb" then IO::WRONLY | IO::BINARY | IO::CREAT | IO::TRUNC
101
- when "r+", "w+" then IO::RDWR | IO::CREAT
102
- # TODO: all!
103
- else
104
- raise ArgumentError "Unknown File mode"
105
- end
96
+ @mode = SubSpawn::Internal.modestr_parse @mode.to_str
106
97
  end
107
98
  @perm = perm || 0o666
108
99
  end
@@ -1,4 +1,7 @@
1
1
  require 'subspawn'
2
+ require 'engine-hacks'
3
+
4
+ EngineHacks.use_child_status :subspawn_child_status
2
5
 
3
6
  module Kernel
4
7
  class << self
@@ -13,6 +16,11 @@ module Kernel
13
16
  def spawn(*args)
14
17
  SubSpawn.spawn_compat(*args)
15
18
  end
19
+ alias :builtin_backtick :`
20
+ def `(str)
21
+ require 'open3'
22
+ Open3.capture2(str).first
23
+ end
16
24
  end
17
25
 
18
26
  module Process
@@ -26,5 +34,33 @@ module Process
26
34
  def subspawn(args, opt={})
27
35
  SubSpawn.spawn(args, opt)
28
36
  end
37
+
38
+ # don't make a loop if waitpid isn't defined
39
+ if SubSpawn::Platform.method(:waitpid2)
40
+ def wait(*args)
41
+ SubSpawn.wait *args
42
+ end
43
+ def waitpid(*args)
44
+ SubSpawn.waitpid *args
45
+ end
46
+ def wait2(*args)
47
+ SubSpawn.wait2 *args
48
+ end
49
+ def waitpid2(*args)
50
+ SubSpawn.waitpid2 *args
51
+ end
52
+ def last_status
53
+ SubSpawn.last_status
54
+ end
55
+ def detach pid
56
+ SubSpawn.detach(pid)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class IO
63
+ def self.popen(*args, &block)
64
+ SubSpawn.popen_compat(*args, &block)
29
65
  end
30
66
  end
@@ -2,6 +2,10 @@ require 'subspawn'
2
2
 
3
3
  $_ss_overwrite = defined? PTY
4
4
 
5
+ if FFI::Platform.windows? && !$_ss_overwrite
6
+ require 'subspawn/win32/pty'
7
+ end
8
+
5
9
  module PTY
6
10
  unless $_ss_overwrite
7
11
  class ChildExited < RuntimeError
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SubSpawn
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0.pre1"
5
5
  end
data/lib/subspawn.rb CHANGED
@@ -5,17 +5,17 @@ if FFI::Platform.unix?
5
5
  require 'subspawn/posix'
6
6
  SubSpawn::Platform = SubSpawn::POSIX
7
7
  elsif FFI::Platform.windows?
8
- raise "SubSpawn Win32 is not yet implemented"
8
+ require 'subspawn/win32'
9
+ SubSpawn::Platform = SubSpawn::Win32
9
10
  else
10
11
  raise "Unknown FFI platform"
11
12
  end
13
+ require 'subspawn/common'
12
14
 
13
15
  module SubSpawn
14
- # TODO: things to check: set $?
15
- def self.spawn_compat(command, *command2)
16
- #File.write('/tmp/spawn.trace', [command, *command2].inspect + "\n", mode: 'a+')
16
+ # Parse and convert the weird Ruby spawn API into something nicer
17
+ def self.__compat_parser(is_popen, command, command2)
17
18
 
18
- # return just the pid
19
19
  delta_env = nil
20
20
  # check for env
21
21
  if command.respond_to? :to_hash
@@ -31,6 +31,9 @@ module SubSpawn
31
31
  if command.first.is_a? Array and command.first.length != 2
32
32
  raise ArgumentError, "First argument must be an pair TODO: check this"
33
33
  end
34
+ popen = if is_popen && command.length > 1
35
+ command.pop
36
+ end
34
37
  raise ArgumentError, "Must provide a command to execute" if command.empty?
35
38
  raise ArgumentError, "Must provide options as a hash" unless opt.is_a? Hash
36
39
  if opt.key? :env and delta_env
@@ -53,7 +56,14 @@ module SubSpawn
53
56
  rescue NoMethodError => e # by spec
54
57
  raise TypeError.new(e)
55
58
  end
56
- SubSpawn.__spawn_internal(command, opt, copt).first
59
+ return [popen, command, opt, copt]
60
+ end
61
+
62
+ # Parse and convert the weird Ruby spawn API into something nicer
63
+ def self.spawn_compat(command, *command2)
64
+ #File.write('/tmp/spawn.trace', [command, *command2].inspect + "\n", mode: 'a+')
65
+
66
+ __spawn_internal(*__compat_parser(false, command, command2)[1..-1]).first
57
67
  end
58
68
  # TODO: accept block mode?
59
69
  def self.spawn(command, opt={})
@@ -107,7 +117,11 @@ module SubSpawn
107
117
  #base.sid!# TODO: yes? no?
108
118
  end
109
119
  when :sid
110
- base.sid! if value
120
+ if base.respond_to? :sid!
121
+ base.sid! if value
122
+ else
123
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'sid'"
124
+ end
111
125
  when :env
112
126
  if env_opts[:deltas]
113
127
  warn "Provided multiple ENV options"
@@ -127,19 +141,44 @@ module SubSpawn
127
141
  raise TypeError, "pgroup must be boolean or integral" if value.is_a? Symbol
128
142
  base.pgroup = value == true ? 0 : value if value
129
143
  when :signal_mask # TODO: signal_default
130
- base.signal_mask(value)
144
+ if base.respond_to? :signal_mask
145
+ base.signal_mask(value)
146
+ else
147
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'signal_mask'"
148
+ end
131
149
  when /rlimit_(.*)/ # P.s
150
+ unless base.respond_to? :rlimit
151
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'rlimit_*'"
152
+ else
153
+ name = $1
154
+ keys = [value].flatten
155
+ base.rlimit(name, *keys)
156
+ end
157
+ when /w32_(.*)/ # NEW
132
158
  name = $1
133
- keys = [value].flatten
134
- base.rlimit(name, *keys)
159
+ raise ArgumentError, "Unknown win32 argument: #{name}" unless %w{desktop title show_window window_pos window_size console_size window_fill start_flags}.include? name
160
+ unless base.respond_to? :name
161
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'w32_#{$1}'"
162
+ else
163
+ base.send(name, *value)
164
+ end
135
165
  when :rlimit # NEW?
136
166
  raise ArgumentError, "rlimit as a hash must be a hash" unless value.respond_to? :to_h
137
- value.to_h.each do |key, values|
138
- base.rlimit(key, *[values].flatten)
167
+
168
+ unless base.respond_to? :rlimit
169
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'rlimit_*'"
170
+ else
171
+ value.to_h.each do |key, values|
172
+ base.rlimit(key, *[values].flatten)
173
+ end
139
174
  end
140
175
  when :umask # P.s
141
176
  raise ArgumentError, "umask must be numeric" unless value.is_a? Integer
142
- base.umask = value
177
+ unless base.respond_to? :umask
178
+ warn "SubSpawn Platform (#{base.class}) doesn't support 'umask'"
179
+ else
180
+ base.umask = value
181
+ end
143
182
  when :unsetenv_others # P.s
144
183
  env_opts[:only] = !!value
145
184
  env_opts[:set] ||= !!value
@@ -208,12 +247,113 @@ module SubSpawn
208
247
  ensure
209
248
  tty.close unless tty.closed?
210
249
  # MRI waits this way to ensure the process is reaped
211
- if Process.waitpid(pid, Process::WNOHANG)
250
+ if Process.waitpid(pid, Process::WNOHANG).nil?
251
+ Process.detach(pid)
252
+ end
253
+ end
254
+ end
255
+
256
+ def self.popen(command, mode="r", opt={}, &block)
257
+ #Many modes, and "-" is not supported at this time
258
+ __popen_internal(command, mode, opt, {}, &block)
259
+ end
260
+ def self.popen_compat(command, *command2, &block)
261
+ #Many modes, and "-" is not supported at this time
262
+ mode, command, opt, copt = __compat_parser(true, command, command2)
263
+ mode ||= "r"
264
+ __popen_internal(command, mode, opt, copt, &block)
265
+ end
266
+ #Many modes, and "-" is not supported at this time
267
+ def self.__popen_internal(command, mode, opt, copt, &block)
268
+ outputs = {}
269
+ # parse, but ignore irrelevant bits
270
+ parsed = Internal.modestr_parse(mode) & (~(IO::TRUNC | IO::CREAT | IO::APPEND | IO::EXCL))
271
+ looking = if parsed & IO::WRONLY != 0
272
+ outputs[:in] = :pipe
273
+ looking = [:in]
274
+ elsif parsed & IO::RDWR != 0
275
+ outputs[:out] = :pipe
276
+ outputs[:in] = :pipe
277
+ looking = [:out, :in] # read, write, from our POV
278
+ else # read only
279
+ outputs[:out] = :pipe
280
+ looking = [:out]
281
+ end
282
+ # do normal spawning. Note: we only chose the internal spawn for popen_compat
283
+ pid, rawio = __spawn_internal(command, outputs.merge(opt), copt)
284
+
285
+ # create a proxy to close the process
286
+ io_proxy = looking.length == 1 ? SubSpawn::Common::ClosableIO : SubSpawn::Common::BidiMergedIOClosable
287
+ io = io_proxy.new(*looking.map{|x|rawio[x]}) do
288
+ # MRI waits this way to ensure the process is reaped
289
+ Process.waitpid(pid) # TODO: I think there isn't a WNOHANG here
290
+ end
291
+
292
+ # return or call
293
+ return io unless block_given?
294
+ begin
295
+ return yield(io)
296
+ ensure
297
+ io.close unless io.closed?
298
+ # MRI waits this way to ensure the process is reaped
299
+ if Process.waitpid(pid, Process::WNOHANG).nil?
212
300
  Process.detach(pid)
213
301
  end
214
302
  end
215
303
  end
216
304
 
305
+ # Windows doesn't like mixing and matching who is spawning and who is waiting, so use
306
+ # subspawn.wait* if you used subspawn.spawn*, while using process.wait* if you used Process.spawn*
307
+ # though if you replace process, then it's a moot point
308
+ if SubSpawn::Platform.method_defined? :waitpid2
309
+ def self.wait(*args)
310
+ waitpid *args
311
+ end
312
+ def self.waitpid(*args)
313
+ waitpid2(*args)&.first
314
+ end
315
+ def self.wait2(*args)
316
+ waitpid2 *args
317
+ end
318
+ def self.waitpid2(*args)
319
+ SubSpawn::Platform.waitpid2 *args
320
+ end
321
+ def self.last_status
322
+ SubSpawn::Platform.last_status
323
+ end
324
+ else
325
+ def self.wait(*args)
326
+ Process.wait *args
327
+ end
328
+ def self.waitpid(*args)
329
+ Process.waitpid *args
330
+ end
331
+ def self.wait2(*args)
332
+ Process.wait2 *args
333
+ end
334
+ def self.waitpid2(*args)
335
+ Process.waitpid2 *args
336
+ end
337
+ def self.last_status
338
+ Process.last_status
339
+ end
340
+ end
341
+
342
+ def self.detach(pid)
343
+ Thread.new do
344
+ pid, status = *SubSpawn.waitpid2(pid)
345
+ # TODO: ensure this loop isn't necessary
346
+ # while pid.nil?
347
+ # sleep 0.01
348
+ # pid, status = *SubSpawn.waitpid2(pid)
349
+ # end
350
+ status
351
+ end.tap do |thr|
352
+ thr[:pid] = pid
353
+ # TODO: does thread.pid need to exist?
354
+ end
355
+ end
356
+
217
357
  COMPLETE_VERSION = {
218
358
  subspawn: SubSpawn::VERSION,
219
359
  platform: SubSpawn::Platform::COMPLETE_VERSION,
data/subspawn.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/subspawn/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "subspawn"
7
+ spec.version = SubSpawn::VERSION
8
+ spec.authors = ["Patrick Plenefisch"]
9
+ spec.email = ["simonpatp@gmail.com"]
10
+
11
+ spec.summary = "Advanced native subprocess spawning"
12
+ spec.description = "Advanced native subprocess spawning on MRI, JRuby, and TruffleRuby"
13
+ final_github = "https://github.com/byteit101/subspawn"
14
+ spec.homepage = final_github
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = final_github
21
+ spec.metadata["changelog_uri"] = final_github
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ spec.add_dependency "subspawn-common", "~> 0.2.0.pre1"
36
+ spec.add_dependency "subspawn-posix", "~> 0.2.0.pre1"
37
+ spec.add_dependency "subspawn-win32", "~> 0.2.0.pre1"
38
+ spec.add_dependency "ffi", "~> 1.0"
39
+
40
+ # For more information and examples about making a new gem, check out our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+
43
+ # You can use Ruby's license, or any of the JRuby tri-license option.
44
+ spec.licenses = ["Ruby", "EPL-2.0", "LGPL-2.1-or-later"]
45
+ end
metadata CHANGED
@@ -1,29 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: subspawn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Plenefisch
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-11-22 00:00:00.000000000 Z
11
+ date: 2024-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: subspawn-common
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0.pre1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0.pre1
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: subspawn-posix
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: 0.1.0
33
+ version: 0.2.0.pre1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.0.pre1
41
+ - !ruby/object:Gem::Dependency
42
+ name: subspawn-win32
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.2.0.pre1
20
48
  type: :runtime
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
52
  - - "~>"
25
53
  - !ruby/object:Gem::Version
26
- version: 0.1.0
54
+ version: 0.2.0.pre1
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: ffi
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -58,6 +86,7 @@ files:
58
86
  - lib/subspawn/replace-pty.rb
59
87
  - lib/subspawn/replace.rb
60
88
  - lib/subspawn/version.rb
89
+ - subspawn.gemspec
61
90
  homepage: https://github.com/byteit101/subspawn
62
91
  licenses:
63
92
  - Ruby
@@ -67,7 +96,7 @@ metadata:
67
96
  homepage_uri: https://github.com/byteit101/subspawn
68
97
  source_code_uri: https://github.com/byteit101/subspawn
69
98
  changelog_uri: https://github.com/byteit101/subspawn
70
- post_install_message:
99
+ post_install_message:
71
100
  rdoc_options: []
72
101
  require_paths:
73
102
  - lib
@@ -82,8 +111,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
111
  - !ruby/object:Gem::Version
83
112
  version: '0'
84
113
  requirements: []
85
- rubygems_version: 3.0.3.1
86
- signing_key:
114
+ rubygems_version: 3.5.3
115
+ signing_key:
87
116
  specification_version: 4
88
117
  summary: Advanced native subprocess spawning
89
118
  test_files: []