Class: SubSpawn::POSIX

Inherits:
Object
  • Object
show all
Defined in:
lib/subspawn/posix.rb,
lib/subspawn/posix/pty.rb,
lib/subspawn/posix/version.rb

Defined Under Namespace

Modules: Internal, PtyHelper Classes: OpenFD, SigSet

Constant Summary collapse

StdIn =
0
StdOut =
1
StdErr =
2
Std =
{in: StdIn, out: StdOut, err: StdErr}.freeze
COMPLETE_VERSION =
{
	subspawn_posix: SubSpawn::POSIX::VERSION,
	libfixposix: LFP::COMPLETE_VERSION,
}
VERSION =
"0.1.1"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command, *args, arg0: command) ⇒ POSIX

Returns a new instance of POSIX.



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/subspawn/posix.rb', line 13

def initialize(command, *args, arg0: command)
	@path = command
	#raise SpawnError, "Command not found: #{command}" unless @path
	# TODO: we use envp, so can't check this now
	@argv = [arg0, *args.map(&:to_str)]
	@fd_map = {}
	@fd_keeps = []
	@fd_closes = []
	@fd_opens = []
	@signal_mask = @signal_default = nil
	@cwd = nil
	@sid = false
	@pgroup = nil
	@env = :default
	@ctty = nil
	@rlimits = {}
	@umask = nil
end

Instance Attribute Details

#ctty(path) ⇒ Object Also known as: tty



250
251
252
253
# File 'lib/subspawn/posix.rb', line 250

def ctty(path)
	@ctty = path
	self
end

#cwd=(value) ⇒ Object (writeonly) Also known as: pwd=, chdir=

Sets the attribute cwd

Parameters:

  • value

    the value to set the attribute cwd to.



31
32
33
# File 'lib/subspawn/posix.rb', line 31

def cwd=(value)
  @cwd = value
end

Class Method Details

.expand_which(name, env = ENV) ⇒ Object

generator for candidates for an executable name usage: SubSpawn::POSIX.each_which(“ls”, ENV) {|path| …} SubSpawn::POSIX.each_which(“ls”, ENV).to_a



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/subspawn/posix.rb', line 282

def self.expand_which(name, env=ENV)
	return self.to_enum(:expand_which, name, env) unless block_given?
	# only allow relative paths if they traverse, and if they traverse, only allow relative paths
	if name.include? "/"
		yield File.absolute_path(name)
	else
		env['PATH'].split(File::PATH_SEPARATOR).each do |path|
			yield File.join(path, name)
		end
	end
end

.shell_command(string) ⇒ Object



294
295
296
297
298
299
300
# File 'lib/subspawn/posix.rb', line 294

def self.shell_command(string)
	# MRI scans for "basic" commands and if so, just un-expands the shell
	# we could do that too, and there are 2 tests about that in rubyspec
	# but we shall ignore them for now
	# TODO: implement that
	["sh", "-c", string.to_str]
end

Instance Method Details

#args(args) ⇒ Object Also known as: args=



176
177
178
179
# File 'lib/subspawn/posix.rb', line 176

def args(args)
	@argv = [@argv[0], *args.map(&:to_str)]
	self
end

#command(cmd) ⇒ Object Also known as: command=



181
182
183
184
# File 'lib/subspawn/posix.rb', line 181

def command(cmd)
	@path = cmd
	self
end

#env(key, value) ⇒ Object



191
192
193
194
195
# File 'lib/subspawn/posix.rb', line 191

def env(key, value)
	@env = ENV.to_h.dup if @env == :default
	@env[key.to_s] = value.to_s
	self
end

#env=(hash) ⇒ Object



196
197
198
199
# File 'lib/subspawn/posix.rb', line 196

def env=(hash)
	@env = hash.to_h
	self
end

#env_reset!Object



187
188
189
190
# File 'lib/subspawn/posix.rb', line 187

def env_reset!
	@env = :default
	self
end

#fd(number, io_or_fd) ⇒ Object

TODO: allow io on left?

Raises:

  • (ArgumentError)


145
146
147
148
149
150
151
152
153
154
# File 'lib/subspawn/posix.rb', line 145

def fd(number, io_or_fd)
	num = number.is_a?(Symbol) ? Std[number] : number.to_i
	raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0.. or #{std.keys.inspect}" if num.nil?
	if fd_number(io_or_fd) == num
		fd_keep(io_or_fd)
	else
		@fd_map[num] = io_or_fd
	end
	self
end

#fd_close(io_or_fd) ⇒ Object



166
167
168
169
# File 'lib/subspawn/posix.rb', line 166

def fd_close(io_or_fd)
	@fd_closes << io_or_fd
	self
end

#fd_keep(io_or_fd) ⇒ Object



162
163
164
165
# File 'lib/subspawn/posix.rb', line 162

def fd_keep(io_or_fd)
	@fd_keep << io_or_fd
	self
end

#fd_open(number, path, flags = 0, mode = 0o666) ⇒ Object

umask will remove bits

Raises:

  • (ArgumentError)


156
157
158
159
160
161
# File 'lib/subspawn/posix.rb', line 156

def fd_open(number, path, flags = 0, mode=0o666) # umask will remove bits
	num = number.is_a?(Symbol) ? Std[number] : number.to_i
	raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0.. or #{std.keys.inspect}" if num.nil?
	@fd_opens << OpenFD.new(number, path, mode, flags)
	self
end

#name(string) ⇒ Object Also known as: name=



170
171
172
173
# File 'lib/subspawn/posix.rb', line 170

def name(string)
	@argv[0] = string.to_s
	self
end

#pgroup(pid) ⇒ Object Also known as: pgroup=

Raises:

  • (ArgumentError)


243
244
245
246
247
# File 'lib/subspawn/posix.rb', line 243

def pgroup(pid)
	raise ArgumentError, "Invalid pgroup: #{pid}" if pid < 0 or !pid.is_a?(Integer)
	@pgroup = pid.to_i
	self
end

#pwd(path) ⇒ Object Also known as: cwd, chdir



230
231
232
233
# File 'lib/subspawn/posix.rb', line 230

def pwd(path)
	@cwd = path
	self
end

#rlimit(key, cur, max = nil) ⇒ Object Also known as: setrlimit



258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/subspawn/posix.rb', line 258

def rlimit(key, cur, max=nil)
	key = if key.is_a? Integer
		key.to_i
	else # TODO: these const lookup should have better error handling
		Process.const_get("RLIMIT_#{key.to_s.upcase}")
	end
	cur = ensure_rlimit(key, cur, 0)
	max = ensure_rlimit(key, max, 1)
	cur = max if cur > max
	@rlimits[key] = [cur, max]
	self
end

#sid!Object



239
240
241
242
# File 'lib/subspawn/posix.rb', line 239

def sid!
	@sid = true
	self
end

#signal_default(sigmask = :default, add: [], delete: [], default: []) ⇒ Object Also known as: signal_default=

usage: signal_default = SigSet.empty.add(:usr1).delete(“USR2”) signal_default(:full, exclude: [9])



216
217
218
219
220
221
# File 'lib/subspawn/posix.rb', line 216

def signal_default(sigmask = :default, add: [], delete: [], default: [])
	sigmask = :empty if sigmask == :default
	@signal_default = sigmask.is_a?(Symbol) ? SigSet.send(sigmask) : sigmask
	@signal_default.add(add, default).delete(delete)
	self
end

#signal_mask(sigmask = :default, add: [], delete: [], block: [], allow: []) ⇒ Object Also known as: signal_mask=, sigmask=, sigmask

usage: signal_mask = SigSet.empty.add(:usr1).delete(“USR2”) signal_mask(:full, exclude: [9])



203
204
205
206
207
208
# File 'lib/subspawn/posix.rb', line 203

def signal_mask(sigmask = :default, add: [], delete: [], block: [], allow: [])
	sigmask = :empty if sigmask == :default
	@signal_mask = sigmask.is_a?(Symbol) ? SigSet.send(sigmask) : sigmask
	@signal_mask.add(add, allow).delete(delete, block)
	self
end

#spawn!Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/subspawn/posix.rb', line 62

def spawn!
	validate!
	sfa = LFP::SpawnFileActions.new
	sa = LFP::Spawnattr.new
	raise "Spawn Init Error" if 0 != sfa.init
	out_pid = nil
	begin
		raise "Spawn Init Error" if 0 != sa.init
		begin
			# set up file descriptors
			
			@fd_keeps.each {|fd| sfa.addkeep(fd_number(fd)) }
			@fd_opens.each {|opn|
				sfa.addopen(fd_number(opn.fd), opn.path, opn.flags, opn.mode)
			}
			@fd_map.map{|k, v| [k, fd_number(v)] }.each do |dest, src|
				sfa.adddup2(src, dest)
			end
			@fd_closes.each {|fd| sfa.addclose(fd_number(fd)) }

			unless @rlimits.empty?
				# allocate output (pid)
				FFI::MemoryPointer.new(LFP::Rlimit, @rlimits.length) do |rlimits|
					# build array
					@rlimits.each_with_index {|(key, (cur, max)), i|
						rlimit = LFP::Rlimit.new(rlimits[i])
						#puts "building rlim at #{i} to #{[cur, max, key]}"
						rlimit[:rlim_cur] = cur.to_i
						rlimit[:rlim_max] = max.to_i
						rlimit[:resource] = key.to_i
					}
					sa.setrlimit(rlimits, @rlimits.length)
				end
			end
			
			# set up signals
			sa.sigmask = @signal_mask.to_ptr if @signal_mask
			sa.sigdefault = @signal_default.to_ptr if @signal_default
			
			# set up ownership and groups
			sa.pgroup = @pgroup.to_i if @pgroup
			sa.umask = @umask.to_i if @umask
			
			# Set up terminal control
			sa.setsid if @sid
			sa.ctty = @ctty if @ctty
			
			# set up working dir
			sa.cwd = @cwd if @cwd
			
			# allocate output (pid)
			FFI::MemoryPointer.new(:int, 1) do |pid|
				argv_str = @argv.map{|a|
					raise ArgumentError, "Nulls not allowed in command: #{a.inspect}" if a.include? "\0"
					FFI::MemoryPointer.from_string a
				} + [nil] # null end of argv
				FFI::MemoryPointer.new(:pointer, argv_str.length) do |argv_holder|
				
					# ARGV
					argv_holder.write_array_of_pointer argv_str
					
					# ARGP/ENV
					make_envp do |envp_holder|

						# Launch!
						ret = LFP.spawnp(pid, @path, argv_holder, envp_holder, sfa, sa)
						if ret != 0
							raise SystemCallError.new("Spawn Error: #{ret}", LFP.errno)
						end
						out_pid = pid.read_int
					end
				end
			end
		ensure
			sa.destroy
		end
	ensure
		sfa.destroy
	end
	out_pid
end

#umask=(value) ⇒ Object Also known as: umask



224
225
226
227
# File 'lib/subspawn/posix.rb', line 224

def umask=(value)
	@umask = value.nil? ? nil : value.to_i
	self
end

#validateObject



272
273
274
# File 'lib/subspawn/posix.rb', line 272

def validate
	validate! rescue false
end

#validate!Object

Raises:



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/subspawn/posix.rb', line 38

def validate!
	@argv.map!(&:to_str) # By spec
	raise SpawnError, "Invalid argv" unless @argv.length > 0
	@fd_map = @fd_map.map do |number, source|
		raise SpawnError, "Invalid FD map: Not a number: #{number.inspect}" unless number.is_a? Integer
		[number, fd_check(source)]
	end.to_h
	@fd_keeps.each{|x| fd_check(x)}
	@fd_closes.each{|x| fd_check(x)}
	@fd_opens.each{|x|
		fd_check(x.fd)
		raise SpawnError, "Invalid FD open: Not a number: #{x.mode.inspect}" unless x.mode.is_a? Integer
		raise SpawnError, "Invalid FD open: Not a flag: #{x.flags.inspect}" unless x.flags.is_a? Integer
		raise SpawnError, "Invalid FD open: Not a file: #{x.file.inspect}" unless File.exist? x.path or Dir.exist?(File.dirname(x.path))
	}
	
	raise SpawnError, "Invalid cwd path" unless @cwd.nil? or Dir.exist?(@cwd = ensure_file_string(@cwd))
	
	@ctty = @ctty.path if !@ctty.nil? and @ctty.is_a? File # PTY.open returns files
	raise SpawnError, "Invalid controlling tty path" unless @ctty.nil? or File.exist?(@ctty = ensure_file_string(@ctty))
	
	true
end