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