subspawn 0.1.0

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