subspawn 0.1.1 → 0.2.0.pre1

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