subspawn 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8517f5eae58feb737016bbdadf7a5fc6df22226834e7531e135a13549fbeb6c0
4
+ data.tar.gz: f57039d8c089adf0859ebea1123c7f216f8955028f2463b7204c04cb43f09272
5
+ SHA512:
6
+ metadata.gz: 0d53923840da47ec6454de99ae34a1c9181a1baf0c4c25cef0e1763facb836c4b910e3bc47c7360f04ab26298dff2fdda994a274a3061b1ae707fb3e443ca0c3
7
+ data.tar.gz: '020817f64ac627bfe26bb044945f5d1cf2770ef5200270a95a501fba0cd7d2d2452f25ebd5a40d4fa3267d2736d3ebf8c7c9fcf34dee7bbb175b8eeaf514176e'
data/.rspec ADDED
@@ -0,0 +1,6 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
4
+ -I ../ffi-binary-libfixposix/lib
5
+ -I ../subspawn-posix/lib
6
+ -I ../ffi-bindings-libfixposix/lib
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in process-wrapper-high.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # SubSpawn Gem (High-level API)
2
+
3
+ Native process launching. See parent SubSpawn readme for details
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add subspawn subspawn-posix
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install subspawn subspawn-posix
14
+
15
+ ## Usage
16
+
17
+ ```rb
18
+ require 'subspawn'
19
+
20
+ Subspawn.spawn(["ls", "/"])
21
+
22
+ # or to augment the built-in ruby methoda
23
+ require 'subspawn/replace'
24
+
25
+ Process.spawn("ls", "/", :setsid=> true)
26
+ PTY.spawn(...)
27
+ PTY.subspawn(..., options...)
28
+ ```
29
+
30
+ ## Development
31
+
32
+ See parent SubSpawn readme
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :build
@@ -0,0 +1,148 @@
1
+ require_relative './fd_types'
2
+ require_relative './graph_helper'
3
+ require_relative './pipes'
4
+
5
+ module SubSpawn::Internal
6
+ # argument value to int (or :tty)
7
+ def self.parse_fd(fd, allow_pty=false)
8
+ # fd = if fd.respond_to? :to_path
9
+ # fd = if fd.respond_to? :to_file
10
+ # fd = if fd.respond_to? :to_file
11
+ case fd
12
+ when Integer then fd
13
+ when IO then fd.fileno
14
+ when :in, :input, :stdin then 0
15
+ when :out, :output, :stdout then 1
16
+ when :err, :error, :stderr then 2
17
+ else
18
+ if allow_pty and %i{pty tty}.include? fd
19
+ :tty
20
+ else
21
+ raise ArgumentError, "Unknown FD type: #{fd.inspect}"
22
+ end
23
+ end
24
+ end
25
+
26
+ # mode conversion
27
+ def self.guess_mode(d)
28
+ read = d.include? 0 # stdin
29
+ write = (d.include? 1 or d.include? 2) # stdout
30
+ if read && write
31
+ raise ArgumentError, "Invalid FD source specification: ambiguious mode (r & w)"
32
+ elsif read
33
+ :read
34
+ elsif write
35
+ :write
36
+ else
37
+ raise ArgumentError, "Invalid FD source specification: ambiguious mode (stdio missing)"
38
+ end
39
+ end
40
+
41
+ # make FdSource objects of each redirection
42
+ def self.parse_fd_opts(fds, &settty)
43
+ child_lookup = {}
44
+ fds.map do |dests, src|
45
+ d = dests.map{|x| parse_fd(x, true)} # TODO: configurable
46
+ src = case src
47
+ when Array
48
+ case src.first
49
+ when String # file
50
+ #raise ArgumentError, "Invalid :child FD source specification" unless src.length == 2
51
+ # TODO: validate
52
+ FdSource::File.new(d, *src)
53
+ when :child
54
+ raise ArgumentError, "Invalid :child FD source specification" unless src.length == 2
55
+ # {a => c, b => [child, a]} is the same as {[a, b] => c}
56
+ # so we can transform the former into the latter
57
+ newfd = parse_fd(src.last)
58
+ # TODO: this isn't an error, create a new one
59
+ raise ArgumentError, "Invalid :child FD source specification" unless child_lookup[newfd]
60
+ child_lookup[newfd].tap{|child|
61
+ # add our destinations to the source's redirection
62
+ d.each { |di| child.dests << di }
63
+ }
64
+ else
65
+ raise ArgumentError, "Invalid FD source specification"
66
+ end
67
+ when String
68
+ FdSource::File.new d, src, ({read: IO::RDONLY, write: IO::WRONLY | IO::CREAT | IO::TRUNC}[guess_mode(d)])
69
+ when :close
70
+ FdSource::Close.new d
71
+ when :pty, :tty
72
+ FdSource::PTY.new d
73
+ when :pipe
74
+ FdSource::Pipe.new d, guess_mode(d)
75
+ when :pipe_r
76
+ FdSource::Pipe.new d, :read # TODO: ensure pipe direction is sensical
77
+ when :pipe_w
78
+ FdSource::Pipe.new d, :read
79
+ else
80
+ if d.include? :tty and src.is_a? File # TODO: is this redundant?
81
+ settty.call(src.path)
82
+ d.delete(:tty)
83
+ end
84
+ FdSource::Basic.new d, parse_fd(src)
85
+ end
86
+ # save redirected fds so we can sneak a child reference in
87
+ src.tap{|x| d.each{|c|
88
+ raise ArgumentError, "Invalid FD source specification: duplicate FDs" if child_lookup[c]
89
+ child_lookup[c] = x
90
+ } }
91
+ end.reject(&:nil?)
92
+ end
93
+
94
+ # convert an unordered list of FdSource's into an ordered list with appropriate
95
+ # temporaries for a correct swap
96
+ def self.graph_order(fds)
97
+ # Note that we keep all dests together. It may be smarter to break dests apart to avoid
98
+ # temporaries, but that is left as an excercise to the reader
99
+ next_temp = [fds.map(&:max).max || 2, 2].max + 1
100
+ graph = Bigraph.new
101
+ # build graphs
102
+ fds.each do |fd|
103
+ from = fd.heads || [Object.new]
104
+ to = fd.tails || [Object.new]
105
+ from.zip(to) do |h, t|
106
+ graph.insert(h, t, fd)
107
+ end
108
+ end
109
+ #puts graph.to_dot
110
+ # find and break cycles
111
+ while point = graph.find_cycle
112
+ # we probably could find the least-cost fd, but this will do for now
113
+ items = graph.delete_outgoing(point)
114
+ #p items
115
+ temp = FdSource::Temp.new([next_temp], point.to_i)
116
+ close = FdSource::Close.new([next_temp])
117
+ items = [temp, *items.map{|x|x.temp_source(next_temp)}, close]
118
+ #p items
119
+ # reinsert
120
+ items.each do |fd|
121
+ from = fd.heads || [Object.new]
122
+ to = fd.tails || [Object.new]
123
+ from.zip(to) do |h, t|
124
+ graph.insert(h, t, fd)
125
+ end
126
+ end
127
+ #puts graph.to_dot
128
+ end
129
+ #puts "removed all cycles, hopeuflly"
130
+ elts = graph.ordered_kahn.reverse # execute in the opposite order of dependencies
131
+ #puts graph.to_dot
132
+ elts
133
+ end
134
+
135
+ # I'd love to use this method, but it doesn't accept env
136
+ # def self.which_jruby(cmd)
137
+ # require 'jruby'
138
+ # org.jruby.util.ShellLauncher.findPathExecutable(JRuby.runtime, cmd)&.absolute_path
139
+ # end
140
+
141
+ def self.which(cmd, env)
142
+ return nil if cmd.nil? or cmd.to_str == ""
143
+ SubSpawn::Platform.
144
+ expand_which(cmd, env).
145
+ lazy.
146
+ find {|x|!File.directory? x and File.executable? x}
147
+ end
148
+ end
@@ -0,0 +1,180 @@
1
+ module SubSpawn::Internal
2
+ class FdSource
3
+ def initialize(dests, all_dests=dests)
4
+ @dests = dests
5
+ @all_dests = all_dests
6
+ raise SpawnError, "Can't provide :tty in this source list" if dests.include? :tty
7
+ end
8
+ attr_reader :dests, :all_dests
9
+
10
+ def destroys? value
11
+ @dests.include? value
12
+ end
13
+ def depends? o
14
+ false
15
+ end
16
+ def before? o
17
+ depends? o
18
+ end
19
+ def max
20
+ @dests.max
21
+ end
22
+ def to_dbg
23
+ [self.class, @dests]
24
+ end
25
+ def heads
26
+ nil
27
+ end
28
+ def tails
29
+ @dests
30
+ end
31
+
32
+ def raw_apply base, value
33
+ @dests.each {|dest|
34
+ base.fd(dest, value)
35
+ }
36
+ nil # TODO: return the io for basics? would need to cache the fds
37
+ end
38
+
39
+ # TODO: :tty in list shouldn't get here!
40
+ class Basic < FdSource
41
+ def initialize(dests, int)
42
+ super(dests)
43
+ @value = int
44
+ end
45
+ def max
46
+ [@dests.max, @value].max
47
+ end
48
+ def source
49
+ @value
50
+ end
51
+ def depend? o
52
+ o.dests.include? @value
53
+ end
54
+ def temp_source(new)
55
+ self.class.new(@dests, new)
56
+ end
57
+ def before? o
58
+ # we are before o if:
59
+ # o destroys any of our sources
60
+ # or we depend on o
61
+ depends? o or o.destroys? @value
62
+ end
63
+ def heads
64
+ [@value]
65
+ end
66
+ def tails
67
+ @dests
68
+ end
69
+ def to_dbg
70
+ [@dests, @value]
71
+ end
72
+ def apply base
73
+ raw_apply base, @value
74
+ end
75
+ end
76
+ class Temp < Basic
77
+ def tails
78
+ super.map{|x|Xchange.new(x)}
79
+ end
80
+ end
81
+ # class Child < Basic # I don't think this is any different?
82
+ # def to_dbg
83
+ # [@dests, :child, @value]
84
+ # end
85
+ # end
86
+
87
+ class Open < FdSource
88
+ end
89
+
90
+ class File < Open
91
+ def initialize(dests, file, mode = ::File::RDONLY, perm = 0o666)
92
+ super(dests)
93
+ @value = file
94
+ @mode = mode || ::File::RDONLY
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
106
+ end
107
+ @perm = perm || 0o666
108
+ end
109
+ def to_dbg
110
+ [@dests, :file, @value, @mode, @perm]
111
+ end
112
+ def apply base
113
+ first, *rest = @dests
114
+ base.fd_open(first, @value, @mode, @perm)
115
+ rest.each {|dest| base.fd(dest, first) }
116
+ nil
117
+ end
118
+ end
119
+
120
+ class Pipe < Open
121
+ def initialize(dests, dir)
122
+ super(dests)
123
+ @dir = dir
124
+ end
125
+ def apply base
126
+ @saved ||= IO.pipe
127
+ r, w = {read: @saved, write: @saved.reverse}[@dir]
128
+ raw_apply base, r
129
+ @dests.each {|dest| base.fd_close(w) } # if you want the other end, pass it in yourself
130
+
131
+ SubSpawn::IoHolder::Pipe.new(w, r)
132
+ end
133
+ #attr_reader :output
134
+ end
135
+ class PTY < Open
136
+ def initialize(dests)
137
+ tty, ntty = dests.partition{|x|x == :tty}
138
+ super(ntty, dests)
139
+ @settty = !tty.empty?
140
+ end
141
+ def apply base
142
+ # MRI doesn't do a chmod for PTY.spawn. Weird, but lets just copy that here
143
+ m,s = (@saved ||= SubSpawn::Platform::PtyHelper.open_internal)
144
+ base.fd_close(m) # if you want the master, pass it in yourself
145
+ raw_apply base, s
146
+ if @settty
147
+ base.tty = s
148
+ end
149
+ SubSpawn::IoHolder::PTY.new(m,s)
150
+ end
151
+ end
152
+
153
+ class Close < FdSource
154
+ def to_dbg
155
+ [:close, @dests]
156
+ end
157
+
158
+ def heads
159
+ nil
160
+ end
161
+ def tails
162
+ @dests
163
+ end
164
+ def apply base
165
+ @dests.each {|dest| base.fd_close(dest) }
166
+ nil
167
+ end
168
+ end
169
+
170
+ Xchange = Struct.new(:fd)
171
+ class Xchange
172
+ def to_s
173
+ "xc_#{fd}"
174
+ end
175
+ def to_i
176
+ fd
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,152 @@
1
+ require 'set'
2
+
3
+ module SubSpawn::Internal
4
+ class Bigraph
5
+ def initialize
6
+ @fwd = {}
7
+ @rev = {}
8
+ end
9
+ def insert(head, tail, data)
10
+ node = BiNode.new(head, tail, data)
11
+ ((@fwd[head] ||= {})[tail] ||= []) << node
12
+ ((@rev[tail] ||= {})[head] ||= []) << node
13
+ end
14
+ def unmark!
15
+ @fwd.each do |h, ts|
16
+ ts.each do |t, lst|
17
+ lst.each {|n| n.marker = nil}
18
+ end
19
+ end
20
+ end
21
+ def roots
22
+ unmark!
23
+ roots = []
24
+ @rev.each do |t, hs|
25
+ hs.each do |h, lst|
26
+ roots += lst unless @rev.key? h
27
+ end
28
+ end
29
+ roots
30
+ end
31
+ def roots_h
32
+ unmark!
33
+ roots = []
34
+ @rev.each do |t, hs|
35
+ hs.each do |h, lst|
36
+ roots << h unless @rev.key? h
37
+ end
38
+ end
39
+ roots.uniq
40
+ end
41
+ def leaves
42
+ unmark!
43
+ leaves = []
44
+ @fwd.each do |h, ts|
45
+ ts.each do |t, lst|
46
+ leaves += lst unless @fwd.key? t
47
+ end
48
+ end
49
+ leaves
50
+ end
51
+ def find_cycle
52
+ unmark!
53
+ # we have a forest, do for each forestry
54
+ rts = roots_h
55
+ if rts.empty? # probably have a loop
56
+ #puts "empt"
57
+ rts = [@fwd.keys.first]
58
+ end
59
+ at, rts = *rts
60
+ while at
61
+ #puts "searching at #{at} with #{rts}"
62
+ tmp = find_cycle_next(at)
63
+ return tmp unless tmp.nil?
64
+ at, rts = *rts
65
+ end
66
+ end
67
+ def find_cycle_next(start)
68
+ @fwd[start]&.each do |t, list|
69
+ #puts "cc = [#{start} -> #{t}]"
70
+ if list.any?(&:marker)
71
+ return t
72
+ end
73
+ list.each(&:mark)
74
+ tmp = find_cycle_next(t)
75
+ return tmp unless tmp.nil?
76
+ end
77
+ return nil
78
+ end
79
+ def delete_outgoing(head)
80
+ @fwd[head].flat_map{|t, k|delete(head, t)}
81
+ end
82
+ def delete(head, tail)
83
+ @fwd[head].delete(tail)
84
+ @rev[tail].delete(head).map(&:data)
85
+ end
86
+ def find_unmarked
87
+ @fwd.each do |h, ts|
88
+ ts.each do |t, lst|
89
+ return [h, t] unless lst.first.marker
90
+ end
91
+ end
92
+ return nil
93
+ end
94
+ def ordered_kahn
95
+ unmark!
96
+ # now traverse the tree until we hit a junction, ensuring
97
+ # dependencies are met
98
+ # Note this assumes you removed all the cycles from your code
99
+ # returns each graph seperately as a sort of forest
100
+ l = []
101
+ s = Set.new(roots_h)
102
+
103
+ until s.empty?
104
+ n = s.first
105
+ #puts "checking #{n} in #{s}"
106
+ s.delete(n)
107
+ @fwd[n]&.each do |e, ms|
108
+ next if ms.any?(&:marker) # already visited
109
+ ms.each(&:mark)
110
+ # no incoming edges
111
+ unless @rev[e]&.any?{|nh, lst| !lst.first.marker}
112
+ # nil -> add
113
+ # any(unmarked) true -> none
114
+ # all(marked) -> add
115
+ s << e
116
+ l += ms
117
+ #puts "blank incoming #{e}"
118
+ else
119
+ #puts "had incoming #{e}: #{@rev[e]}"
120
+ end
121
+ end
122
+ end
123
+
124
+ if find_unmarked#.tap{|x| p x, "um"}
125
+ puts to_dot
126
+ #raise "Unmarked graph edges, an unresolved cycle probably exists. This is a bug in SubSpawn"
127
+ nil
128
+ else
129
+ return l.map(&:data)
130
+ end
131
+ end
132
+
133
+ # for debugging
134
+ def to_dot
135
+ body = @fwd.map do |h, ts|
136
+ ts.map do |t, lst|
137
+ "\"#{h}\" -> \"#{t}\" ;" #{lst.first&.marker};"
138
+ end
139
+ end.join("\n")
140
+ return "digraph G {\n#{body}\n}"
141
+ end
142
+ end
143
+ class BiNode
144
+ def initialize(head, tail, data)
145
+ @head, @tail, @data, @marker = head, tail, data, nil
146
+ end
147
+ attr_accessor :head, :tail, :data, :marker
148
+ def mark
149
+ @marker = true
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,49 @@
1
+
2
+ class SubSpawn::IoHolder
3
+ def initialize(map)
4
+ @map = map
5
+ end
6
+
7
+ def [](key)
8
+ tmp = get(key)
9
+ tmp = tmp.to_io if tmp.respond_to? :to_io
10
+ tmp
11
+ end
12
+ def get(key)
13
+ @map[SubSpawn::Internal.parse_fd(key, true)]
14
+ end
15
+ def composite? key
16
+ self.get(key).is_a? Composite
17
+ end
18
+ def empty?
19
+ @map.values.reject(&:nil?).empty?
20
+ end
21
+
22
+ class Composite
23
+ end
24
+
25
+ class Pipe < Composite
26
+ def initialize(parent, child)
27
+ @parent, @child = parent, child
28
+ end
29
+ attr_reader :parent, :child
30
+ alias :to_io :parent
31
+ def fileno
32
+ @parent.fileno
33
+ end
34
+ end
35
+
36
+ class PTY < Composite
37
+ def initialize(master, slave)
38
+ @master, @slave = master, slave
39
+ end
40
+ attr_reader :master, :slave
41
+ alias :to_io :master
42
+ def to_ary
43
+ [@master, @slave]
44
+ end
45
+ def fileno
46
+ @master.fileno
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ require 'subspawn'
2
+
3
+ module Kernel
4
+ class << self
5
+ alias :builtin_spawn :spawn
6
+ def spawn(*args)
7
+ SubSpawn.spawn_compat(*args)
8
+ end
9
+ end
10
+
11
+ private
12
+ alias :builtin_spawn :spawn
13
+ def spawn(*args)
14
+ SubSpawn.spawn_compat(*args)
15
+ end
16
+ end
17
+
18
+ module Process
19
+ class << self
20
+ alias :builtin_spawn :spawn
21
+
22
+ def spawn(*args)
23
+ SubSpawn.spawn_compat(*args)
24
+ end
25
+
26
+ def subspawn(args, opt={})
27
+ SubSpawn.spawn(args, opt)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ require 'subspawn'
2
+
3
+ $_ss_overwrite = defined? PTY
4
+
5
+ module PTY
6
+ unless $_ss_overwrite
7
+ class ChildExited < RuntimeError
8
+ def initialize(status)
9
+ @status = status
10
+ end
11
+ attr_reader :status
12
+ end
13
+ end
14
+ class << self
15
+ if $_ss_overwrite
16
+ alias :builtin_spawn :spawn
17
+ alias :builtin_getpty :getpty
18
+ end
19
+
20
+ def spawn(*args, &block)
21
+ SubSpawn.pty_spawn_compat(*args, &block)
22
+ end
23
+ def subspawn(command, opts={}, &block)
24
+ SubSpawn.pty_spawn(command, opts, &block)
25
+ end
26
+ alias :getpty :spawn
27
+
28
+ def open(&blk)
29
+ SubSpawn::Platform::PtyHelper.open(&blk)
30
+ end
31
+
32
+ def check(pid, do_raise=false)
33
+ return if Process.waitpid(pid, Process::WNOHANG | Process::WUNTRACED).nil?
34
+ return $? unless do_raise
35
+ raise ::PTY::ChildExited.new($?)
36
+ end
37
+ end
38
+ end
39
+
40
+ $_ss_overwrite = nil
@@ -0,0 +1,2 @@
1
+ require 'subspawn/replace-builtin'
2
+ require 'subspawn/replace-pty'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SubSpawn
4
+ VERSION = "0.1.0"
5
+ end
data/lib/subspawn.rb ADDED
@@ -0,0 +1,221 @@
1
+ require 'ffi'
2
+ require 'subspawn/version'
3
+ require 'subspawn/fd_parse'
4
+ if FFI::Platform.unix?
5
+ require 'subspawn/posix'
6
+ SubSpawn::Platform = SubSpawn::POSIX
7
+ elsif FFI::Platform.windows?
8
+ raise "SubSpawn Win32 is not yet implemented"
9
+ else
10
+ raise "Unknown FFI platform"
11
+ end
12
+
13
+ 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+')
17
+
18
+ # return just the pid
19
+ delta_env = nil
20
+ # check for env
21
+ if command.respond_to? :to_hash
22
+ delta_env = command.to_hash
23
+ command = command2
24
+ else # 2-arg ctor
25
+ command = [command] + command2
26
+ end
27
+ opt = {}
28
+ if command.last.respond_to? :to_hash
29
+ *command, opt = *command
30
+ end
31
+ if command.first.is_a? Array and command.first.length != 2
32
+ raise ArgumentError, "First argument must be an pair TODO: check this"
33
+ end
34
+ raise ArgumentError, "Must provide a command to execute" if command.empty?
35
+ raise ArgumentError, "Must provide options as a hash" unless opt.is_a? Hash
36
+ if opt.key? :env and delta_env
37
+ # TODO: warn?
38
+ raise SpawnError, "SubSpawn.spawn_compat doesn't allow :env key, try SubSpawn.spawn instead"
39
+ # unsupported
40
+ else
41
+ opt[:env] = delta_env if delta_env
42
+ end
43
+ copt = {:__ss_compat => true }
44
+ copt[:__ss_compat_testing] = opt.delete(:__ss_compat_testing)
45
+ begin
46
+ cf = nil
47
+ if command.length == 1 and (cf = command.first).respond_to? :to_str
48
+ # and ((cf = cf.to_str).include? " " or (Internal.which(cmd)))
49
+ #command = ["sh", "-c", cf] # TODO: refactor
50
+ command = [command.first.to_str]
51
+ copt[:__ss_compat_shell] = true
52
+ end
53
+ rescue NoMethodError => e # by spec
54
+ raise TypeError.new(e)
55
+ end
56
+ SubSpawn.__spawn_internal(command, opt, copt).first
57
+ end
58
+ # TODO: accept block mode?
59
+ def self.spawn(command, opt={})
60
+ __spawn_internal(command, opt, {})
61
+ end
62
+ def self.spawn_shell(command, opt={})
63
+ __spawn_internal(Platform.shell_command(command), opt, {})
64
+ end
65
+ def self.__spawn_internal(command, opt, copt)
66
+ unless command.respond_to? :to_ary # TODO: fix this check up with new parsing
67
+ raise ArgumentError, "First argument must be an array" unless command.is_a? String
68
+ # not the cleanest check, but should be better than generic exec errors
69
+ raise SpawnError, "SubSpawn only accepts arrays #LINK TODO" if command.include? " "
70
+ command = [command]
71
+ else
72
+ command = command.to_ary.dup
73
+ end
74
+ unless opt.respond_to? :to_hash # TODO: fix this check up with new parsing
75
+ raise ArgumentError, "Second argument must be a hash, did you mean to use spawn([#{command.inspect}, #{opt.inspect}]) ?"
76
+ end
77
+ fds = []
78
+ env_opts = {base: ENV, set: false, deltas: nil, only: false}
79
+ begin
80
+ if command.first.respond_to? :to_ary
81
+ warn "argv0 and array syntax both provided to SubSpawn. Preferring argv0" if opt[:argv0]
82
+ command[0], tmp = *command.first.to_ary.map(&:to_str) # by spec
83
+ opt[:argv0] = opt[:argv0] || tmp
84
+ end
85
+ command = command.map(&:to_str) # by spec
86
+ rescue NoMethodError => e # by spec
87
+ raise TypeError.new(e)
88
+ end
89
+ arg0 = command.first
90
+ raise ArgumentError, "Cannot spawn with null bytes: OS uses C-style strings" if command.any? {|x|x.include? "\0"}
91
+ base = SubSpawn::Platform.new(*command, arg0: (opt[:argv0] || arg0).to_s)
92
+ opt.each do |key, value|
93
+ case key
94
+ when Array # P.s
95
+ fds << [key,value]
96
+ # TODO: ,:output, :input, :error, :stderr, :stdin, :stdout, :pty, :tty ?
97
+ when Integer, IO, :in, :out, :err # P.s: in, out, err, IO, Integer
98
+ fds << [[key], value]
99
+ # TODO: , :cwd
100
+ when :chdir # P.s: :chdir
101
+ base.cwd = value.respond_to?(:to_path) ? value.to_path : value
102
+ when :tty, :pty
103
+ if value == :tty || value == :pty
104
+ fds << [[key], value] # make a new pty this way
105
+ else
106
+ base.tty = value
107
+ #base.sid!# TODO: yes? no?
108
+ end
109
+ when :sid
110
+ base.sid! if value
111
+ when :env
112
+ if env_opts[:deltas]
113
+ warn "Provided multiple ENV options"
114
+ end
115
+ env_opts[:deltas] = value
116
+ env_opts[:set] ||= value != nil
117
+ when :setenv, :set_env, :env=
118
+ if env_opts[:deltas]
119
+ warn "Provided multiple ENV options"
120
+ end
121
+ env_opts[:deltas] = env_opts[:base] = value
122
+ env_opts[:set] = value != nil
123
+ env_opts[:only] = true
124
+
125
+ # Difference: new_pgroup is linux too?
126
+ when :pgroup, :new_pgroup, :process_group # P.s: pgroup, :new_pgroup
127
+ raise TypeError, "pgroup must be boolean or integral" if value.is_a? Symbol
128
+ base.pgroup = value == true ? 0 : value if value
129
+ when :signal_mask # TODO: signal_default
130
+ base.signal_mask(value)
131
+ when /rlimit_(.*)/ # P.s
132
+ name = $1
133
+ keys = [value].flatten
134
+ base.rlimit(name, *keys)
135
+ when :rlimit # NEW?
136
+ 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)
139
+ end
140
+ when :umask # P.s
141
+ raise ArgumentError, "umask must be numeric" unless value.is_a? Integer
142
+ base.umask = value
143
+ when :unsetenv_others # P.s
144
+ env_opts[:only] = !!value
145
+ env_opts[:set] ||= !!value
146
+ when :close_others # P.s
147
+ warn "CLOEXEC is set by default, :close_others is a no-op in SubSpawn.spawn call. Consider :keep"
148
+ when :argv0
149
+ # Alraedy processed
150
+ else
151
+ # TODO: exception always?
152
+ if copt[:__ss_compat]
153
+ raise ArgumentError, "Unknown SubSpawn argument #{key.inspect}. Ignoring"
154
+ else
155
+ warn "Unknown SubSpawn argument #{key.inspect}. Ignoring"
156
+ end
157
+ end
158
+ end
159
+ working_env = if env_opts[:set]
160
+ base.env = if env_opts[:only]
161
+ env_opts[:deltas].to_hash
162
+ else
163
+ env_opts[:base].to_hash.merge(env_opts[:deltas].to_hash)
164
+ end.to_h
165
+ else
166
+ ENV
167
+ end
168
+ # now that we have the working env, we can finally update the command
169
+ unless copt[:__ss_compat_testing]
170
+ if copt[:__ss_compat_shell] && Internal.which(command.first, working_env).nil? && command.first.include?(" ") # ruby specs don't allow builtins, apparently
171
+ command = Platform.shell_command(command.first)
172
+ base.args = command[1..-1]
173
+ base.command = base.name = command.first
174
+ end
175
+ newcmd = Internal.which(command.first, working_env)
176
+ # if newcmd is null, let the systemerror shine from below
177
+ if command.first!= "" && !newcmd.nil? && newcmd != command.first
178
+ base.command = newcmd
179
+ end
180
+ end
181
+
182
+ # parse and clean up fd descriptors
183
+ fds = Internal.parse_fd_opts(fds) {|path| base.tty = path }
184
+ # now make a graph and add temporaries
185
+ ordering = Internal.graph_order(fds)
186
+ # configure them in order, saving new io descriptors
187
+ created_pipes = ordering.flat_map do |fd|
188
+ result = fd.apply(base)
189
+ fd.all_dests.map{|x| [x, result] }
190
+ end.to_h
191
+ # Spawn and return any new pipes
192
+ [base.spawn!, IoHolder.new(created_pipes)]
193
+ end
194
+
195
+ def self.pty_spawn_compat(*args, &block)
196
+ pty_spawn(args, &block)
197
+ end
198
+ def self.pty_spawn(args, opts={}, &block)
199
+ # TODO: setsid?
200
+ # TODO: MRI tries to pull the shell out of the ENV var, but that seems wrong
201
+ pid, args = SubSpawn.spawn(args, {[:in, :out, :err, :tty] => :pty, :sid => true}.merge(opts))
202
+ tty = args[:tty]
203
+ list = [tty, tty, pid]
204
+ return list unless block_given?
205
+
206
+ begin
207
+ return block.call(*list)
208
+ ensure
209
+ tty.close unless tty.closed?
210
+ # MRI waits this way to ensure the process is reaped
211
+ if Process.waitpid(pid, Process::WNOHANG)
212
+ Process.detach(pid)
213
+ end
214
+ end
215
+ end
216
+
217
+ COMPLETE_VERSION = {
218
+ subspawn: SubSpawn::VERSION,
219
+ platform: SubSpawn::Platform::COMPLETE_VERSION,
220
+ }
221
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subspawn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Patrick Plenefisch
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-11-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: subspawn-posix
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: ffi
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: Advanced native subprocess spawning on MRI, JRuby, and TruffleRuby
42
+ email:
43
+ - simonpatp@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".rspec"
49
+ - Gemfile
50
+ - README.md
51
+ - Rakefile
52
+ - lib/subspawn.rb
53
+ - lib/subspawn/fd_parse.rb
54
+ - lib/subspawn/fd_types.rb
55
+ - lib/subspawn/graph_helper.rb
56
+ - lib/subspawn/pipes.rb
57
+ - lib/subspawn/replace-builtin.rb
58
+ - lib/subspawn/replace-pty.rb
59
+ - lib/subspawn/replace.rb
60
+ - lib/subspawn/version.rb
61
+ homepage: https://github.com/byteit101/subspawn
62
+ licenses: []
63
+ metadata:
64
+ homepage_uri: https://github.com/byteit101/subspawn
65
+ source_code_uri: https://github.com/byteit101/subspawn
66
+ changelog_uri: https://github.com/byteit101/subspawn
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.6.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.0.3.1
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Advanced native subprocess spawning
86
+ test_files: []