subspawn-posix 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 +7 -0
- data/.rspec +5 -0
- data/Gemfile +10 -0
- data/README.md +27 -0
- data/Rakefile +8 -0
- data/lib/subspawn/binary.rb +2 -0
- data/lib/subspawn/posix/ffi_helper.rb +39 -0
- data/lib/subspawn/posix/pty.rb +58 -0
- data/lib/subspawn/posix/signals.rb +85 -0
- data/lib/subspawn/posix/version.rb +7 -0
- data/lib/subspawn/posix.rb +375 -0
- data/sig/subspawn/posix.rbs +100 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e1e9a8cd307acbb03c05440d9300a7e48a3636d36734f2b4cb189aae0ab98bdd
|
4
|
+
data.tar.gz: e176b1617b2c674da1de970e1b9e7ecf7776ab9c418e746374ec3983e4d6a31b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 47fc587e35e40da9bffc3b8a8877363441c98dd25cbe1c46509c92478bae2651661194ad080271226b23446eb3f1de089af9e132e212f29e05940d5adecc86d5
|
7
|
+
data.tar.gz: 540342bd3dc2c875a0f94a8d6d0a6f58698a8cdc456cf707e09f8c5c1251d71294342a2d802d220ad6feea64f03d3f482c4f3c00781a88a2b3a19614a34bde3a
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# SubSpawn Native POSIX API
|
2
|
+
|
3
|
+
SubSpawn wrapper over libfixposix. Install `ffi-binary-libfixposix` if you need binaries.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
$ bundle add subspawn-posix
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
$ gem install subspawn-posix
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
```rb
|
18
|
+
require 'subspawn/posix'
|
19
|
+
```
|
20
|
+
|
21
|
+
See SubSpawn for more details.
|
22
|
+
|
23
|
+
An RBS file exists for this gem.
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
See parent SubSpawn readme
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module SubSpawn::POSIX::Internal
|
4
|
+
module SignalFn
|
5
|
+
extend FFI::Library
|
6
|
+
ffi_lib FFI::Library::LIBC
|
7
|
+
|
8
|
+
attach_function :emptyset, :sigemptyset, [:pointer], :int
|
9
|
+
attach_function :fillset, :sigfillset, [:pointer], :int
|
10
|
+
attach_function :addset, :sigaddset, [:pointer, :int], :int
|
11
|
+
attach_function :delset, :sigdelset, [:pointer, :int], :int
|
12
|
+
attach_function :ismember, :sigismember, [:pointer, :int], :int
|
13
|
+
|
14
|
+
|
15
|
+
ffi_lib %w{pthread.so.0 pthread pthread.dylib}
|
16
|
+
|
17
|
+
attach_function :mask, :pthread_sigmask, %i{int pointer pointer}, :int
|
18
|
+
end
|
19
|
+
|
20
|
+
module OpenPTY
|
21
|
+
extend FFI::Library
|
22
|
+
|
23
|
+
ffi_lib FFI::Platform.mac? ? FFI::Library::LIBC : "util"
|
24
|
+
attach_function :openpty, [:buffer_out, :buffer_out, :buffer_out, :buffer_in, :buffer_in], :int
|
25
|
+
|
26
|
+
ffi_lib FFI::Library::LIBC
|
27
|
+
attach_function :close, [:int], :int
|
28
|
+
|
29
|
+
def self.call(termios: nil, winsize: nil)
|
30
|
+
FFI::MemoryPointer.new(:int, 2) do |fds|
|
31
|
+
FFI::MemoryPointer.new(:char, 4096) do |name| # max on linux = 4096
|
32
|
+
ret = self.openpty(fds[0], fds[1], name, termios, winsize)
|
33
|
+
raise SystemCallError.new("OpenPTY Error", LFP.errno) unless ret == 0
|
34
|
+
return [*fds.read_array_of_int(2), name.read_string]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
|
5
|
+
module SubSpawn
|
6
|
+
class POSIX
|
7
|
+
module PtyHelper
|
8
|
+
class PtyIO < IO
|
9
|
+
def inspect
|
10
|
+
"#<IO:masterpty:#{@slave_path}>"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Subspawn-specific feature
|
14
|
+
attr_reader :slave_path
|
15
|
+
|
16
|
+
private
|
17
|
+
def __subspawn_init(name)
|
18
|
+
# All other files are opened cloexec, this one isn't yet as it came from native code
|
19
|
+
self.close_on_exec = true
|
20
|
+
@slave_path = name.freeze
|
21
|
+
self.sync = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.open_internal(chmod_for_open = false)
|
26
|
+
m, s, name = Internal::OpenPTY.call
|
27
|
+
|
28
|
+
# chmod the slave path, but only if were were called as ::open, not as ::spawn
|
29
|
+
# I don't understand why, but doing this just to mirror MRI
|
30
|
+
File.chmod(0o600, name) if chmod_for_open
|
31
|
+
|
32
|
+
master = PtyIO.for_fd(m, IO::RDWR | IO::SYNC)
|
33
|
+
master.send(:__subspawn_init, name)
|
34
|
+
|
35
|
+
# we could shim the slave to be a fake file, or we could just re-open the path
|
36
|
+
# which fixes #inspect, #tty?, #path, and #close_on_exec, all in one go
|
37
|
+
# https://bugs.ruby-lang.org/issues/19036
|
38
|
+
slave = File.open(name, IO::RDWR | IO::SYNC)
|
39
|
+
slave.sync = true # still must do this manually though, as IO::SYNC seems to be mostly ignored
|
40
|
+
Internal::OpenPTY.close(s)
|
41
|
+
|
42
|
+
[master, slave, name]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.open
|
46
|
+
*files, name = open_internal(true)
|
47
|
+
return files unless block_given?
|
48
|
+
|
49
|
+
begin
|
50
|
+
return yield files.dup # Array, not splatted
|
51
|
+
ensure
|
52
|
+
files.reject(&:closed?).each(&:close)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class SubSpawn::POSIX::SigSet
|
2
|
+
def initialize(base=:empty)
|
3
|
+
base = base.to_sym
|
4
|
+
raise ArgumentError, "SigSet only accepts :full, :empty, or :current" unless %i{full empty current}.include? base
|
5
|
+
# TODO: warn about current?
|
6
|
+
@base = base
|
7
|
+
@ops = []
|
8
|
+
@ptr = nil
|
9
|
+
end
|
10
|
+
def self.empty
|
11
|
+
self.new(:empty)
|
12
|
+
end
|
13
|
+
def self.full
|
14
|
+
self.new(:full)
|
15
|
+
end
|
16
|
+
def self.current
|
17
|
+
self.new(:current)
|
18
|
+
end
|
19
|
+
def include(*signals)
|
20
|
+
clean_signals(signals).each do |sig|
|
21
|
+
@ops << [:add, sig]
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
def exclude(*signals)
|
26
|
+
clean_signals(signals).each do |sig|
|
27
|
+
@ops << [:rm, sig]
|
28
|
+
end
|
29
|
+
self
|
30
|
+
end
|
31
|
+
alias :- :exclude
|
32
|
+
alias :+ :include
|
33
|
+
alias :add :exclude
|
34
|
+
alias :del :include
|
35
|
+
alias :delete :include
|
36
|
+
|
37
|
+
def to_ptr(&block)
|
38
|
+
if @ptr.nil?
|
39
|
+
# sigset_t is largest on linux, at 128 bytes, so always allocate that much
|
40
|
+
if block_given?
|
41
|
+
ret = nil
|
42
|
+
FFI::MemoryPointer.new(:uint8, 128) {|ptr| alloc_internal(ptr); ret = block.call(ptr)}
|
43
|
+
ret
|
44
|
+
else
|
45
|
+
FFI::MemoryPointer.new(:uint8, 128).tap {|ptr| @ptr = ptr; alloc_internal(ptr)}
|
46
|
+
end
|
47
|
+
else
|
48
|
+
if block_given?
|
49
|
+
block.call(@ptr)
|
50
|
+
else
|
51
|
+
@ptr
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
private
|
56
|
+
def clean_signals(signals)
|
57
|
+
signals.flatten.map do |sig|
|
58
|
+
if Integer === sig
|
59
|
+
sig
|
60
|
+
else
|
61
|
+
Signal.list[sig.to_s.upcase].tap do |good|
|
62
|
+
raise ArgumentError, "#{sig} cannot be converted to a signal" if good.nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
def alloc_internal ptr
|
68
|
+
sig = SubSpawn::POSIX::Internal::SignalFn
|
69
|
+
case @base
|
70
|
+
when :full then sig.fillset(ptr)
|
71
|
+
when :empty then sig.emptyset(ptr)
|
72
|
+
when :current
|
73
|
+
sig.emptyset(ptr)
|
74
|
+
sig.mask(0, nil, ptr) # get the current (old) set
|
75
|
+
else raise "Invalid State"
|
76
|
+
end
|
77
|
+
@ops.each do |op, num|
|
78
|
+
if op == :add
|
79
|
+
sig.addset(ptr, num)
|
80
|
+
else
|
81
|
+
sig.delset(ptr, num)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,375 @@
|
|
1
|
+
require 'libfixposix'
|
2
|
+
require 'subspawn/posix/version'
|
3
|
+
require 'subspawn/posix/ffi_helper'
|
4
|
+
require 'subspawn/posix/signals'
|
5
|
+
require 'subspawn/posix/pty'
|
6
|
+
module SubSpawn
|
7
|
+
class SpawnError < RuntimeError
|
8
|
+
end
|
9
|
+
class POSIX
|
10
|
+
|
11
|
+
OpenFD = Struct.new(:fd, :path, :mode, :flags)
|
12
|
+
|
13
|
+
def initialize(command, *args, arg0: command)
|
14
|
+
@path = command
|
15
|
+
#raise SpawnError, "Command not found: #{command}" unless @path
|
16
|
+
# TODO: we use envp, so can't check this now
|
17
|
+
@argv = [arg0, *args.map(&:to_str)]
|
18
|
+
@fd_map = {}
|
19
|
+
@fd_keeps = []
|
20
|
+
@fd_closes = []
|
21
|
+
@fd_opens = []
|
22
|
+
@signal_mask = @signal_default = nil
|
23
|
+
@cwd = nil
|
24
|
+
@sid = false
|
25
|
+
@pgroup = nil
|
26
|
+
@env = :default
|
27
|
+
@ctty = nil
|
28
|
+
@rlimits = {}
|
29
|
+
@umask = nil
|
30
|
+
end
|
31
|
+
attr_writer :cwd, :ctty
|
32
|
+
|
33
|
+
StdIn = 0
|
34
|
+
StdOut= 1
|
35
|
+
StdErr = 2
|
36
|
+
Std = {in: StdIn, out: StdOut, err: StdErr}.freeze
|
37
|
+
|
38
|
+
def validate!
|
39
|
+
@argv.map!(&:to_str) # By spec
|
40
|
+
raise SpawnError, "Invalid argv" unless @argv.length > 0
|
41
|
+
@fd_map = @fd_map.map do |number, source|
|
42
|
+
raise SpawnError, "Invalid FD map: Not a number: #{number.inspect}" unless number.is_a? Integer
|
43
|
+
[number, fd_check(source)]
|
44
|
+
end.to_h
|
45
|
+
@fd_keeps.each{|x| fd_check(x)}
|
46
|
+
@fd_closes.each{|x| fd_check(x)}
|
47
|
+
@fd_opens.each{|x|
|
48
|
+
fd_check(x.fd)
|
49
|
+
raise SpawnError, "Invalid FD open: Not a number: #{x.mode.inspect}" unless x.mode.is_a? Integer
|
50
|
+
raise SpawnError, "Invalid FD open: Not a flag: #{x.flags.inspect}" unless x.flags.is_a? Integer
|
51
|
+
raise SpawnError, "Invalid FD open: Not a file: #{x.file.inspect}" unless File.exist? x.path or Dir.exist?(File.dirname(x.path))
|
52
|
+
}
|
53
|
+
|
54
|
+
raise SpawnError, "Invalid cwd path" unless @cwd.nil? or Dir.exist?(@cwd = ensure_file_string(@cwd))
|
55
|
+
|
56
|
+
@ctty = @ctty.path if !@ctty.nil? and @ctty.is_a? File # PTY.open returns files
|
57
|
+
raise SpawnError, "Invalid controlling tty path" unless @ctty.nil? or File.exist?(@ctty = ensure_file_string(@ctty))
|
58
|
+
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def spawn!
|
63
|
+
validate!
|
64
|
+
sfa = LFP::SpawnFileActions.new
|
65
|
+
sa = LFP::Spawnattr.new
|
66
|
+
raise "Spawn Init Error" if 0 != sfa.init
|
67
|
+
out_pid = nil
|
68
|
+
begin
|
69
|
+
raise "Spawn Init Error" if 0 != sa.init
|
70
|
+
begin
|
71
|
+
# set up file descriptors
|
72
|
+
|
73
|
+
@fd_keeps.each {|fd| sfa.addkeep(fd_number(fd)) }
|
74
|
+
@fd_opens.each {|opn|
|
75
|
+
sfa.addopen(fd_number(opn.fd), opn.path, opn.flags, opn.mode)
|
76
|
+
}
|
77
|
+
@fd_map.map{|k, v| [k, fd_number(v)] }.each do |dest, src|
|
78
|
+
sfa.adddup2(src, dest)
|
79
|
+
end
|
80
|
+
@fd_closes.each {|fd| sfa.addclose(fd_number(fd)) }
|
81
|
+
|
82
|
+
unless @rlimits.empty?
|
83
|
+
# allocate output (pid)
|
84
|
+
FFI::MemoryPointer.new(LFP::Rlimit, @rlimits.length) do |rlimits|
|
85
|
+
# build array
|
86
|
+
@rlimits.each_with_index {|(key, (cur, max)), i|
|
87
|
+
rlimit = LFP::Rlimit.new(rlimits[i])
|
88
|
+
#puts "building rlim at #{i} to #{[cur, max, key]}"
|
89
|
+
rlimit[:rlim_cur] = cur.to_i
|
90
|
+
rlimit[:rlim_max] = max.to_i
|
91
|
+
rlimit[:resource] = key.to_i
|
92
|
+
}
|
93
|
+
sa.setrlimit(rlimits, @rlimits.length)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# set up signals
|
98
|
+
sa.sigmask = @signal_mask.to_ptr if @signal_mask
|
99
|
+
sa.sigdefault = @signal_default.to_ptr if @signal_default
|
100
|
+
|
101
|
+
# set up ownership and groups
|
102
|
+
sa.pgroup = @pgroup.to_i if @pgroup
|
103
|
+
sa.umask = @umask.to_i if @umask
|
104
|
+
|
105
|
+
# Set up terminal control
|
106
|
+
sa.setsid if @sid
|
107
|
+
sa.ctty = @ctty if @ctty
|
108
|
+
|
109
|
+
# set up working dir
|
110
|
+
sa.cwd = @cwd if @cwd
|
111
|
+
|
112
|
+
# allocate output (pid)
|
113
|
+
FFI::MemoryPointer.new(:int, 1) do |pid|
|
114
|
+
argv_str = @argv.map{|a|
|
115
|
+
raise ArgumentError, "Nulls not allowed in command: #{a.inspect}" if a.include? "\0"
|
116
|
+
FFI::MemoryPointer.from_string a
|
117
|
+
} + [nil] # null end of argv
|
118
|
+
FFI::MemoryPointer.new(:pointer, argv_str.length) do |argv_holder|
|
119
|
+
|
120
|
+
# ARGV
|
121
|
+
argv_holder.write_array_of_pointer argv_str
|
122
|
+
|
123
|
+
# ARGP/ENV
|
124
|
+
make_envp do |envp_holder|
|
125
|
+
|
126
|
+
# Launch!
|
127
|
+
ret = LFP.spawnp(pid, @path, argv_holder, envp_holder, sfa, sa)
|
128
|
+
if ret != 0
|
129
|
+
raise SystemCallError.new("Spawn Error: #{ret}", LFP.errno)
|
130
|
+
end
|
131
|
+
out_pid = pid.read_int
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
ensure
|
136
|
+
sa.destroy
|
137
|
+
end
|
138
|
+
ensure
|
139
|
+
sfa.destroy
|
140
|
+
end
|
141
|
+
out_pid
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO: allow io on left?
|
145
|
+
def fd(number, io_or_fd)
|
146
|
+
num = number.is_a?(Symbol) ? Std[number] : number.to_i
|
147
|
+
raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0.. or #{std.keys.inspect}" if num.nil?
|
148
|
+
if fd_number(io_or_fd) == num
|
149
|
+
fd_keep(io_or_fd)
|
150
|
+
else
|
151
|
+
@fd_map[num] = io_or_fd
|
152
|
+
end
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
def fd_open(number, path, flags = 0, mode=0o666) # umask will remove bits
|
157
|
+
num = number.is_a?(Symbol) ? Std[number] : number.to_i
|
158
|
+
raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0.. or #{std.keys.inspect}" if num.nil?
|
159
|
+
@fd_opens << OpenFD.new(number, path, mode, flags)
|
160
|
+
self
|
161
|
+
end
|
162
|
+
def fd_keep(io_or_fd)
|
163
|
+
@fd_keep << io_or_fd
|
164
|
+
self
|
165
|
+
end
|
166
|
+
def fd_close(io_or_fd)
|
167
|
+
@fd_closes << io_or_fd
|
168
|
+
self
|
169
|
+
end
|
170
|
+
def name(string)
|
171
|
+
@argv[0] = string.to_s
|
172
|
+
self
|
173
|
+
end
|
174
|
+
alias :name= :name
|
175
|
+
|
176
|
+
def args(args)
|
177
|
+
@argv = [@argv[0], *args.map(&:to_str)]
|
178
|
+
self
|
179
|
+
end
|
180
|
+
alias :args= :args
|
181
|
+
def command(cmd)
|
182
|
+
@path = cmd
|
183
|
+
self
|
184
|
+
end
|
185
|
+
alias :command= :command
|
186
|
+
|
187
|
+
def env_reset!
|
188
|
+
@env = :default
|
189
|
+
self
|
190
|
+
end
|
191
|
+
def env(key, value)
|
192
|
+
@env = ENV.to_h.dup if @env == :default
|
193
|
+
@env[key.to_s] = value.to_s
|
194
|
+
self
|
195
|
+
end
|
196
|
+
def env=(hash)
|
197
|
+
@env = hash.to_h
|
198
|
+
self
|
199
|
+
end
|
200
|
+
# usage:
|
201
|
+
# signal_mask = SigSet.empty.add(:usr1).delete("USR2")
|
202
|
+
# signal_mask(:full, exclude: [9])
|
203
|
+
def signal_mask(sigmask = :default, add: [], delete: [], block: [], allow: [])
|
204
|
+
sigmask = :empty if sigmask == :default
|
205
|
+
@signal_mask = sigmask.is_a?(Symbol) ? SigSet.send(sigmask) : sigmask
|
206
|
+
@signal_mask.add(add, allow).delete(delete, block)
|
207
|
+
self
|
208
|
+
end
|
209
|
+
alias :signal_mask= :signal_mask
|
210
|
+
alias :sigmask= :signal_mask
|
211
|
+
alias :sigmask :signal_mask
|
212
|
+
|
213
|
+
# usage:
|
214
|
+
# signal_default = SigSet.empty.add(:usr1).delete("USR2")
|
215
|
+
# signal_default(:full, exclude: [9])
|
216
|
+
def signal_default(sigmask = :default, add: [], delete: [], default: [])
|
217
|
+
sigmask = :empty if sigmask == :default
|
218
|
+
@signal_default = sigmask.is_a?(Symbol) ? SigSet.send(sigmask) : sigmask
|
219
|
+
@signal_default.add(add, default).delete(delete)
|
220
|
+
self
|
221
|
+
end
|
222
|
+
alias :signal_default= :signal_default
|
223
|
+
|
224
|
+
def umask=(value)
|
225
|
+
@umask = value.nil? ? nil : value.to_i
|
226
|
+
self
|
227
|
+
end
|
228
|
+
alias :umask :umask=
|
229
|
+
|
230
|
+
def pwd(path)
|
231
|
+
@cwd = path
|
232
|
+
self
|
233
|
+
end
|
234
|
+
alias :cwd :pwd
|
235
|
+
alias :pwd= :cwd=
|
236
|
+
alias :chdir :pwd
|
237
|
+
alias :chdir= :cwd=
|
238
|
+
|
239
|
+
def sid!
|
240
|
+
@sid = true
|
241
|
+
self
|
242
|
+
end
|
243
|
+
def pgroup(pid)
|
244
|
+
raise ArgumentError, "Invalid pgroup: #{pid}" if pid < 0 or !pid.is_a?(Integer)
|
245
|
+
@pgroup = pid.to_i
|
246
|
+
self
|
247
|
+
end
|
248
|
+
alias :pgroup= :pgroup
|
249
|
+
|
250
|
+
def ctty(path)
|
251
|
+
@ctty = path
|
252
|
+
self
|
253
|
+
end
|
254
|
+
alias :tty= :ctty=
|
255
|
+
alias :tty :ctty
|
256
|
+
|
257
|
+
|
258
|
+
def rlimit(key, cur, max=nil)
|
259
|
+
key = if key.is_a? Integer
|
260
|
+
key.to_i
|
261
|
+
else # TODO: these const lookup should have better error handling
|
262
|
+
Process.const_get("RLIMIT_#{key.to_s.upcase}")
|
263
|
+
end
|
264
|
+
cur = ensure_rlimit(key, cur, 0)
|
265
|
+
max = ensure_rlimit(key, max, 1)
|
266
|
+
cur = max if cur > max
|
267
|
+
@rlimits[key] = [cur, max]
|
268
|
+
self
|
269
|
+
end
|
270
|
+
alias :setrlimit :rlimit
|
271
|
+
|
272
|
+
def validate
|
273
|
+
validate! rescue false
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
|
278
|
+
# generator for candidates for an executable name
|
279
|
+
# usage:
|
280
|
+
# SubSpawn::POSIX.each_which("ls", ENV) {|path| ...}
|
281
|
+
# SubSpawn::POSIX.each_which("ls", ENV).to_a
|
282
|
+
def self.expand_which(name, env=ENV)
|
283
|
+
return self.to_enum(:expand_which, name, env) unless block_given?
|
284
|
+
# only allow relative paths if they traverse, and if they traverse, only allow relative paths
|
285
|
+
if name.include? "/"
|
286
|
+
yield File.absolute_path(name)
|
287
|
+
else
|
288
|
+
env['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
289
|
+
yield File.join(path, name)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.shell_command(string)
|
295
|
+
# MRI scans for "basic" commands and if so, just un-expands the shell
|
296
|
+
# we could do that too, and there are 2 tests about that in rubyspec
|
297
|
+
# but we shall ignore them for now
|
298
|
+
# TODO: implement that
|
299
|
+
["sh", "-c", string.to_str]
|
300
|
+
end
|
301
|
+
|
302
|
+
COMPLETE_VERSION = {
|
303
|
+
subspawn_posix: SubSpawn::POSIX::VERSION,
|
304
|
+
libfixposix: LFP::COMPLETE_VERSION,
|
305
|
+
}
|
306
|
+
|
307
|
+
private
|
308
|
+
def none
|
309
|
+
@@none ||= Object.new
|
310
|
+
end
|
311
|
+
def ensure_rlimit(key, value, index)
|
312
|
+
if value.nil?
|
313
|
+
return Process.getrlimit(key)[index] # unspecified, load saved
|
314
|
+
end
|
315
|
+
return value.to_i if value.is_a? Integer
|
316
|
+
Process.const_get("RLIMIT_#{value.to_s.upcase}")
|
317
|
+
end
|
318
|
+
|
319
|
+
def make_envp
|
320
|
+
if @env == :default
|
321
|
+
yield LFP.get_environ
|
322
|
+
else
|
323
|
+
strings = @env.select{|k, v|
|
324
|
+
!k.nil? and !v.nil?
|
325
|
+
}.map{|k,v|
|
326
|
+
k = k.to_str
|
327
|
+
str = "#{k}=#{v.to_str}" # rubyspec says to convert to_str
|
328
|
+
raise ArgumentError, "Nulls not allowed in environment variable: #{str.inspect}" if str.include? "\0" # By Spec
|
329
|
+
raise ArgumentError, "Variable key cannot include '=': #{str.inspect}" if k.include? "=" # By Spec
|
330
|
+
FFI::MemoryPointer.from_string str
|
331
|
+
} + [nil] # null end of argp
|
332
|
+
FFI::MemoryPointer.new(:pointer, strings.length) do |argp_holder|
|
333
|
+
argp_holder.write_array_of_pointer strings
|
334
|
+
yield argp_holder
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
def ensure_file_string(path)
|
339
|
+
if defined? JRUBY_VERSION # accept File and Path java objects
|
340
|
+
path = path.to_file if path.respond_to? :to_file
|
341
|
+
if path.respond_to? :absolute_path
|
342
|
+
path.absoloute_path
|
343
|
+
else
|
344
|
+
path.to_s
|
345
|
+
end
|
346
|
+
else
|
347
|
+
path.to_s
|
348
|
+
end
|
349
|
+
end
|
350
|
+
def fd_check(source)
|
351
|
+
case source
|
352
|
+
when Integer then source
|
353
|
+
when IO then source
|
354
|
+
when :in, :out, :err
|
355
|
+
Std[source]
|
356
|
+
else
|
357
|
+
raise SpawnError, "Invalid FD map: Not a io or number: #{source.inspect}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
def fd_number(source)
|
361
|
+
case source
|
362
|
+
when Integer then source
|
363
|
+
when IO then source.fileno
|
364
|
+
when :in, :out, :err
|
365
|
+
Std[source]
|
366
|
+
else
|
367
|
+
raise SpawnError, "Invalid FD map: Not a io or number: #{source.inspect}"
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
|
375
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module SubSpawn
|
2
|
+
class SpawnError < RuntimeError
|
3
|
+
end
|
4
|
+
class POSIX
|
5
|
+
VERSION: String
|
6
|
+
|
7
|
+
StdIn: Integer
|
8
|
+
StdErr: Integer
|
9
|
+
StdOut: Integer
|
10
|
+
Std: Hash[(:in | :out | :err), Integer]
|
11
|
+
|
12
|
+
def initialize: (String command, *String args, ?arg0: String) -> nil
|
13
|
+
|
14
|
+
# Environmental attributes
|
15
|
+
attr_writer cwd: String | File
|
16
|
+
def cwd: (String | File) -> self
|
17
|
+
alias pwd= cwd=
|
18
|
+
alias pwd cwd
|
19
|
+
alias chdir= cwd=
|
20
|
+
alias chdir cwd
|
21
|
+
|
22
|
+
def env_reset!: () -> self
|
23
|
+
attr_writer env: Hash
|
24
|
+
def env: (key: String, value: String) -> self
|
25
|
+
|
26
|
+
attr_writer umask: Integer
|
27
|
+
def umask: (Integer) -> self
|
28
|
+
|
29
|
+
# File descriptor mapping
|
30
|
+
type FD = IO | :in | :out | :err | Integer
|
31
|
+
def fd: (FD dest,FD source) -> self
|
32
|
+
def fd_open: (FD dest, String path, ?Integer flags, ?Integer create_file_mode) -> self
|
33
|
+
def fd_keep: (FD) -> self
|
34
|
+
def fd_close: (FD) -> self
|
35
|
+
|
36
|
+
# Head control
|
37
|
+
def sid!: () -> self
|
38
|
+
def pgroup: (Integer) -> self
|
39
|
+
attr_writer pgroup: Integer
|
40
|
+
attr_writer tty: String | File
|
41
|
+
def tty: (String | File) -> self
|
42
|
+
alias ctty= tty=
|
43
|
+
alias ctty tty
|
44
|
+
|
45
|
+
# Signals
|
46
|
+
type SignalName = String | Symbol | Integer
|
47
|
+
def signal_mask: (SubSpawn::POSIX::SigSet | :full | :empty | :current | :default, ?add: SignalName | Array[SignalName], ?delete: SignalName | Array[SignalName], ?block: SignalName | Array[SignalName], ?allow: SignalName | Array[SignalName]) -> self
|
48
|
+
attr_writer signal_mask: SubSpawn::POSIX::SigSet
|
49
|
+
alias sigmask signal_mask
|
50
|
+
alias sigmask= signal_mask=
|
51
|
+
|
52
|
+
def signal_default: (SubSpawn::POSIX::SigSet | :full | :empty | :current | :default, ?add: SignalName | Array[SignalName], ?delete: SignalName | Array[SignalName], ?default: SignalName | Array[SignalName]) -> self
|
53
|
+
alias signal_default= signal_mask=
|
54
|
+
|
55
|
+
# Misc
|
56
|
+
type ResourceName = String | Symbol | Integer
|
57
|
+
def name: (String) -> self
|
58
|
+
attr_writer name: String
|
59
|
+
def args: (Array[String]) -> self
|
60
|
+
attr_writer args: Array[String]
|
61
|
+
def command: (String) -> self
|
62
|
+
attr_writer command: String
|
63
|
+
|
64
|
+
def rlimit: (ResourceName resource_type, Integer soft_limit, ?Integer hard_max) -> self
|
65
|
+
alias setrlimit rlimit
|
66
|
+
|
67
|
+
# Action items
|
68
|
+
def validate: () -> bool
|
69
|
+
def validate!: () -> TrueClass
|
70
|
+
def spawn!: () -> Integer
|
71
|
+
|
72
|
+
# class interface methods
|
73
|
+
def self.expand_which: (String, ?Hash[String, String]) -> Enumerable[String]
|
74
|
+
| [T] (String, ?Hash[String, String]) { (String) -> T} -> T
|
75
|
+
|
76
|
+
def self.shell_command: (String) -> Array[String]
|
77
|
+
|
78
|
+
# Nested Types
|
79
|
+
class SigSet
|
80
|
+
# Builders
|
81
|
+
def self.empty: -> SigSet
|
82
|
+
def self.full: -> SigSet
|
83
|
+
def self.current: -> SigSet
|
84
|
+
def initialize: (?:current | :empty | :full base) -> nil
|
85
|
+
|
86
|
+
# Modifiers
|
87
|
+
def include: (*(SignalName | Array[SignalName]) signals) -> self
|
88
|
+
def exclude: (*(SignalName | Array[SignalName]) signals) -> self
|
89
|
+
alias - exclude
|
90
|
+
alias + include
|
91
|
+
alias add exclude
|
92
|
+
alias del include
|
93
|
+
alias delete include
|
94
|
+
|
95
|
+
# Output (library use only)
|
96
|
+
def to_ptr: [T] () {(FFI::MemoryPointer) -> T} -> T
|
97
|
+
| () -> FFI::MemoryPointer
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: subspawn-posix
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patrick Plenefisch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-11-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ffi-bindings-libfixposix
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.5.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.5.0
|
27
|
+
description: A SubSpawn subproject to wrap libfixposix as a mid level API
|
28
|
+
email:
|
29
|
+
- simonpatp@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rspec"
|
35
|
+
- Gemfile
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- lib/subspawn/binary.rb
|
39
|
+
- lib/subspawn/posix.rb
|
40
|
+
- lib/subspawn/posix/ffi_helper.rb
|
41
|
+
- lib/subspawn/posix/pty.rb
|
42
|
+
- lib/subspawn/posix/signals.rb
|
43
|
+
- lib/subspawn/posix/version.rb
|
44
|
+
- sig/subspawn/posix.rbs
|
45
|
+
homepage: https://github.com/byteit101/subspawn
|
46
|
+
licenses: []
|
47
|
+
metadata:
|
48
|
+
homepage_uri: https://github.com/byteit101/subspawn
|
49
|
+
source_code_uri: https://github.com/byteit101/subspawn
|
50
|
+
changelog_uri: https://github.com/byteit101/subspawn
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 2.6.0
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.0.3.1
|
67
|
+
signing_key:
|
68
|
+
specification_version: 4
|
69
|
+
summary: SubSpawn Mid-level API for POSIX systems
|
70
|
+
test_files: []
|