subspawn-win32 0.2.0.pre1

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: 4824ace4f995314f87c68497af9b0ab8e45154b383221d9d35a0acd804efd09f
4
+ data.tar.gz: 332afa90e72b124d99482e67144ae8c06f239c3258ddd2038ed540b12ea84e99
5
+ SHA512:
6
+ metadata.gz: 7cc966c090bcd70e901abdc55a3f7ae2dad2590f9ac74f9917dc3f4b2eb1b34da2d89f6022afa4d21e4f7717c63795d4e07187cf2fc4c58649d566b254c39294
7
+ data.tar.gz: fa38824ce6dc9b5891ab72cd59e6e376680ddc5bc60c4f77e3dd293dc5449065a6cc98897359bd287e5e37e0d6c8363b0f61c55381fdee752ec4a3458ab05d23
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in process-wrapper-mid.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+ gem "ffi", "~> 1.0"
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # SubSpawn Native Win32 API
2
+
3
+ SubSpawn wrapper over the Win32 process API. Should work on all recent 32 and 64-bit Windows releases
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add subspawn-win32
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install subspawn-win32
14
+
15
+ ## Usage
16
+
17
+ ```rb
18
+ require 'subspawn/win32'
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,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,252 @@
1
+ require 'ffi'
2
+ module SubSpawn::Win32::FFI
3
+ extend FFI::Library
4
+
5
+ W = SubSpawn::Win32::FFI
6
+
7
+ # Common types
8
+
9
+ # uintptr or :pointer ?
10
+ typedef :intptr_t, :shandle
11
+ typedef :uintptr_t, :handle
12
+ typedef :int, :hresult
13
+
14
+ # TODO: I think this is corect?
15
+ typedef :uint, :dword
16
+ typedef :ushort, :word
17
+
18
+ module MMHelper
19
+ # from https://github.com/ffi/ffi/wiki/Structs
20
+
21
+ # Use the symbol passed from the method for accessing the analogous field.
22
+ # This method can also take a &block, but we would not use it.
23
+ def method_missing( sym, *args )
24
+ # convert symbol to a string to allow regex checks
25
+ str = sym.to_s
26
+
27
+ # derive the member's symbolic name
28
+ member = str.match( /^([a-z0-9_]+)/i )[1].to_sym
29
+
30
+ # raise an exception via the default behavior if that symbol isn't a member!
31
+ super unless members.include? member
32
+
33
+ # this ternary checks for the presence of an equals sign (=) to indicate an
34
+ # assignment operation and changes which method we invoke and whether or not
35
+ # to send the splatted arguments as well.
36
+ (str =~ /=$/) ? send( :[]=, member, *args ) : send( :[], member )
37
+ end
38
+ end
39
+
40
+ class ProcInfo < FFI::Struct
41
+ include MMHelper
42
+ #_PROCESS_INFORMATION
43
+ layout :hProcess, :handle,
44
+ :hThread, :handle,
45
+ :dwProcessId, :dword,
46
+ :dwThreadId, :dword
47
+
48
+ # TODO: if this is a managed struct, we could autofree the handles. Do we want to?
49
+ # def self.release ptr
50
+ # W.free(ptr.hThread)
51
+ # W.free(ptr.hProcess)
52
+ # end
53
+ end
54
+
55
+
56
+ class StartupInfo < FFI::Struct
57
+ include MMHelper
58
+ #_STARTUPINFOEXW
59
+ layout :cb, :dword,
60
+ :lpReserved, :pointer,
61
+ :lpDesktop, :pointer,
62
+ :lpTitle, :pointer,
63
+ :dwX, :dword,
64
+ :dwY, :dword,
65
+ :dwXSize, :dword,
66
+ :dwYSize, :dword,
67
+ :dwXCountChars, :dword,
68
+ :dwYCountChars, :dword,
69
+ :dwFillAttribute, :dword,
70
+ :dwFlags, :dword,
71
+ :wShowWindow, :word,
72
+ :cbReserved2, :word,
73
+ :lpReserved2, :pointer,
74
+ :hStdInput, :handle,
75
+ :hStdOutput, :handle,
76
+ :hStdError, :handle,
77
+ :lpAttributeList, :pointer
78
+
79
+ def initialize()
80
+ super
81
+ self[:cb] = self.class.size
82
+ end
83
+ end
84
+
85
+ class SecurityAttributes < FFI::Struct
86
+ include MMHelper
87
+ # _SECURITY_ATTRIBUTES
88
+ layout :nLength, :dword,
89
+ :lpSecurityDescriptor, :pointer,
90
+ :bInheritHandle, :bool
91
+
92
+ def initialize()
93
+ super
94
+ self[:nLength] = self.class.size
95
+ end
96
+ end
97
+
98
+ class Coord < FFI::Struct
99
+ include MMHelper
100
+ # _COORD
101
+ layout :x, :short,
102
+ :y, :short
103
+
104
+ def initialize(x=0,y=0)
105
+ super()
106
+ self[:x] = x
107
+ self[:y] = y
108
+ end
109
+ def to_a
110
+ [x, y]
111
+ end
112
+ def to_ary
113
+ [x, y]
114
+ end
115
+ def self.[](*keys)
116
+ self.new(*keys.flatten)
117
+ end
118
+ end
119
+
120
+ ffi_lib :kernel32
121
+
122
+ attach_function :GetLastError, [], :dword
123
+ attach_function :WaitForSingleObject, [:handle, :dword], :dword
124
+ attach_function :GetExitCodeProcess, [:handle, :buffer_out], :bool
125
+ attach_function :OpenProcess, [:dword, :bool, :dword], :handle
126
+
127
+ attach_function :CloseHandle, [:handle], :bool
128
+ attach_function :CreateProcess, :CreateProcessW, %i{buffer_in buffer_inout pointer pointer bool dword buffer_in buffer_in pointer pointer}, :bool # TODO: specify the types, not just pointers?
129
+ attach_function :GetStdHandle, [:dword], :handle
130
+
131
+ attach_function :InitializeProcThreadAttributeList, %i{buffer_out dword dword buffer_inout}, :bool
132
+
133
+ # the first pointer/handle should really be a pointer, but we use it as a pointer
134
+ attach_function :UpdateProcThreadAttribute, %i{buffer_inout dword handle handle size_t pointer pointer}, :bool
135
+ attach_function :DeleteProcThreadAttributeList, [:buffer_inout], :void
136
+
137
+ attach_function :SetHandleInformation, [:handle, :dword, :dword], :bool
138
+
139
+ # PTY
140
+ # HPCON == handle
141
+ attach_function :ClosePseudoConsole, [:handle], :void
142
+ attach_function :ResizePseudoConsole, [:handle, :pointer], :int
143
+ attach_function :CreatePseudoConsole, %i{buffer_in handle handle dword buffer_out}, :int
144
+
145
+
146
+ ffi_lib FFI::Library::LIBC
147
+
148
+ attach_function :get_osfhandle, :_get_osfhandle, [:int], :shandle
149
+ attach_function :get_errno, :_get_errno, [:buffer_inout], :int
150
+
151
+ # Constants
152
+ # TODO: are these already somewhere?
153
+
154
+ SIZEOF_HPCON = FFI::Pointer::SIZE
155
+
156
+ INFINITE = 0xFFFFFFFF
157
+ INVALID_HANDLE_VALUE = -1 # unsure if signed or unsigned is better
158
+ HANDLE_NEGATIVE_TWO = -2
159
+ HANDLE_FLAG_INHERIT = 1
160
+
161
+ # Process flags
162
+ DEBUG_PROCESS = 0x00000001
163
+ DEBUG_ONLY_THIS_PROCESS = 0x00000002
164
+ CREATE_SUSPENDED = 0x00000004
165
+ DETACHED_PROCESS = 0x00000008
166
+ CREATE_NEW_CONSOLE = 0x00000010
167
+ NORMAL_PRIORITY_CLASS = 0x00000020 # TODO: where is this listed on MSDN?
168
+ CREATE_NEW_PROCESS_GROUP = 0x00000200
169
+ CREATE_UNICODE_ENVIRONMENT = 0x00000400
170
+ CREATE_SEPARATE_WOW_VDM = 0x00000800
171
+ CREATE_SHARED_WOW_VDM = 0x00001000
172
+ INHERIT_PARENT_AFFINITY = 0x00010000
173
+ CREATE_PROTECTED_PROCESS = 0x00040000
174
+ EXTENDED_STARTUPINFO_PRESENT = 0x00080000
175
+ CREATE_SECURE_PROCESS = 0x00400000
176
+ CREATE_BREAKAWAY_FROM_JOB = 0x01000000
177
+ CREATE_PRESERVE_CODE_AUTHZ_LEVEL= 0x02000000
178
+ CREATE_DEFAULT_ERROR_MODE = 0x04000000
179
+ CREATE_NO_WINDOW = 0x08000000
180
+
181
+ STARTF_USESHOWWINDOW = 0x00000001
182
+ STARTF_USESIZE = 0x00000002
183
+ STARTF_USEPOSITION = 0x00000004
184
+ STARTF_USECOUNTCHARS = 0x00000008
185
+ STARTF_USEFILLATTRIBUTE = 0x00000010
186
+ STARTF_RUNFULLSCREEN = 0x00000020
187
+ STARTF_FORCEONFEEDBACK = 0x00000040
188
+ STARTF_FORCEOFFFEEDBACK = 0x00000080
189
+ STARTF_USESTDHANDLES = 0x00000100
190
+ STARTF_USEHOTKEY = 0x00000200
191
+ #
192
+ STARTF_TITLEISLINKNAME = 0x00000800
193
+ STARTF_TITLEISAPPID = 0x00001000
194
+ STARTF_PREVENTPINNING = 0x00002000
195
+ #
196
+ STARTF_UNTRUSTEDSOURCE = 0x00008000
197
+
198
+ # Status/errors, and mapping to posix errors. Note we only map what CRuby maps
199
+
200
+ STATUS_ACCESS_VIOLATION = 0xC0000005
201
+ STATUS_ILLEGAL_INSTRUCTION = 0xC000001D
202
+ STATUS_FLOAT_DENORMAL_OPERAND = 0xC000008D
203
+ STATUS_FLOAT_DIVIDE_BY_ZERO = 0xC000008E
204
+ STATUS_FLOAT_INEXACT_RESULT = 0xC000008F
205
+ STATUS_FLOAT_INVALID_OPERATION = 0xC0000090
206
+ STATUS_FLOAT_OVERFLOW = 0xC0000091
207
+ STATUS_FLOAT_STACK_CHECK = 0xC0000092
208
+ STATUS_FLOAT_UNDERFLOW = 0xC0000093
209
+ STATUS_PRIVILEGED_INSTRUCTION = 0xC0000096
210
+ STATUS_CONTROL_C_EXIT = 0xC000013A
211
+ STATUS_FLOAT_MULTIPLE_FAULTS = 0xC00002B4
212
+ STATUS_FLOAT_MULTIPLE_TRAPS = 0xC00002B5
213
+
214
+ StatusPosixMap = {
215
+ STATUS_ACCESS_VIOLATION => "SEGV",
216
+ STATUS_ILLEGAL_INSTRUCTION => "ILL",
217
+ STATUS_PRIVILEGED_INSTRUCTION => "ILL",
218
+ STATUS_FLOAT_DENORMAL_OPERAND => "FPE",
219
+ STATUS_FLOAT_DIVIDE_BY_ZERO => "FPE",
220
+ STATUS_FLOAT_INEXACT_RESULT => "FPE",
221
+ STATUS_FLOAT_INVALID_OPERATION => "FPE",
222
+ STATUS_FLOAT_OVERFLOW => "FPE",
223
+ STATUS_FLOAT_STACK_CHECK => "FPE",
224
+ STATUS_FLOAT_UNDERFLOW => "FPE",
225
+ STATUS_FLOAT_MULTIPLE_FAULTS => "FPE",
226
+ STATUS_FLOAT_MULTIPLE_TRAPS => "FPE",
227
+ STATUS_CONTROL_C_EXIT => "INT",
228
+ }
229
+ StatusPosixMap.default = "SEGV"
230
+
231
+ # TODO: expose other values for the proc thread list
232
+ PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
233
+
234
+ # Also unsigned, but this is convienent, and ffi takes care of the rest
235
+ STD_HANDLE = {
236
+ 0 => -10,
237
+ 1 => -11,
238
+ 2 => -12,
239
+ }
240
+
241
+ #PTY
242
+ PSEUDOCONSOLE_INHERIT_CURSOR = 1
243
+
244
+ # waiting
245
+ STILL_ACTIVE = 259
246
+ PROCESS_QUERY_INFORMATION = 0x00000400
247
+
248
+ # TODO: error reporting?
249
+ def self.free hwnd
250
+ W.CloseHandle(hwnd) unless hwnd.nil?
251
+ end
252
+ end
@@ -0,0 +1,16 @@
1
+ class SubSpawn::Win32::LazyHndl
2
+ def self.closer(hndl)
3
+ lambda do |obj_id|
4
+ SubSpawn::Win32::W.CloseHandle(hndl)
5
+ end
6
+ end
7
+
8
+ def initialize(pid, hndl)
9
+ ObjectSpace.define_finalizer(self, self.class.closer(hndl))
10
+ @hndl = hndl
11
+ @pid = pid
12
+ end
13
+
14
+ attr_reader :hndl
15
+ attr_reader :pid
16
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'subspawn/win32'
5
+ require 'subspawn/common'
6
+
7
+ module SubSpawn
8
+ class Win32
9
+ module PtyHelper
10
+
11
+ W = SubSpawn::Win32::FFI
12
+
13
+ class MasterPtyIO < SubSpawn::Common::BidiMergedIO
14
+ def initialize(read, write, pty)
15
+ super(read, write)
16
+ read.sync = true
17
+ write.sync = true
18
+ @con_pty = pty
19
+ end
20
+
21
+ def inspect
22
+ "#<masterpty:#{@con_pty}>"
23
+ end
24
+
25
+ def winsize
26
+ @conpty.winsize
27
+ end
28
+
29
+ def winsize= arg
30
+ @conpty.winsize = arg
31
+ end
32
+ def tty?
33
+ true
34
+ end
35
+ def isatty
36
+ true
37
+ end
38
+
39
+ # Subspawn-specific feature
40
+ attr_reader :con_pty
41
+ end
42
+ class SlavePtyIO < SubSpawn::Common::BidiMergedIO
43
+ def initialize(read, write, pty)
44
+ super(read, write)
45
+ read.sync = true
46
+ write.sync = true
47
+ @con_pty = pty
48
+ end
49
+
50
+ def winsize
51
+ @conpty.winsize
52
+ end
53
+
54
+ def winsize= arg
55
+ @conpty.winsize = arg
56
+ end
57
+ def tty?
58
+ true
59
+ end
60
+ def isatty
61
+ true
62
+ end
63
+
64
+ def inspect
65
+ "#<winpty:#{@con_pty}>"
66
+ end
67
+
68
+ # Subspawn-specific feature
69
+ attr_reader :con_pty
70
+ end
71
+
72
+ # TODO: ensure all handles/resources are cleaned up properly
73
+ class ConPTYHelper
74
+ def initialize(hpc, pipes, size)
75
+ @hpc = hpc
76
+ @close = true
77
+ @pipes = pipes
78
+ @lastsize = size
79
+ end
80
+ def winsize
81
+ @lastsize
82
+ end
83
+ def winsize=(size)
84
+ if W::ResizePseudoConsole(@hpc, W::Coord[size]) < 0
85
+ raise "ConPTY Resize failure"
86
+ else
87
+ @lastsize = size
88
+ end
89
+ end
90
+ def con_pty
91
+ self
92
+ end
93
+ def raw_handle
94
+ @hpc
95
+ end
96
+ def no_gc!
97
+ @close=false
98
+ end
99
+ def close
100
+ if @close
101
+ W::ClosePseudoConsole(@hpc) # TODO!
102
+ @hpc = nil
103
+ @pipes.reject(&:closed?).each(&:close)
104
+ end
105
+ end
106
+ def closed?
107
+ if @close
108
+ @hpc.nil? && @pipes.all?(&:closed?)
109
+ else
110
+ nil # falsy
111
+ end
112
+ end
113
+ end
114
+ module IoHelper
115
+ refine IO do
116
+ def to_hndl
117
+ hndl = if RUBY_ENGINE == "jruby"
118
+ self.fileno # jruby returns the raw handle, not the fd
119
+ else
120
+ W.get_osfhandle(self.fileno)
121
+ end
122
+
123
+ if hndl == W::INVALID_HANDLE_VALUE || hndl == W::HANDLE_NEGATIVE_TWO
124
+ raise SystemCallError.new("Invalid FD/handle for input fd #{self}", FFI.errno)
125
+ end
126
+ hndl
127
+ end
128
+ end
129
+ end
130
+ using IoHelper
131
+
132
+ def self.open_internal(chmod_for_open = false, initial_size: [40,80], flags: 0)
133
+ # TODO: does windows care about permissions? I'm going to assume no for now
134
+
135
+ child_r, us_w = IO.pipe
136
+ us_r, child_w = IO.pipe
137
+ size = W::Coord[initial_size]
138
+ hpc = nil
139
+ ::FFI::MemoryPointer.new(:uintptr_t, 1) do |ptyref|
140
+ hr = W::CreatePseudoConsole(size, child_r.to_hndl, child_w.to_hndl, flags, ptyref)
141
+ if hr < 0 # failure
142
+ raise "ConPTY failure: #{hr}"
143
+ end
144
+ hpc = ptyref.read(:uintptr_t)
145
+ end
146
+
147
+ pty = ConPTYHelper.new(hpc, [child_r, child_w, us_r, us_w], initial_size.flatten)
148
+ master = MasterPtyIO.new(us_r, us_w, pty)
149
+
150
+ slave = SlavePtyIO.new(child_r, child_w, pty)
151
+
152
+ #child_r.close
153
+ #child_w.close
154
+
155
+ [master, slave, pty]
156
+ end
157
+
158
+ def self.open
159
+ *files, pty = open_internal(true)
160
+ return files unless block_given?
161
+
162
+ begin
163
+ return yield files.dup # Array, not splatted
164
+ ensure
165
+ pty.close
166
+ files.reject(&:closed?).each(&:close)
167
+ end
168
+ end
169
+
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,92 @@
1
+ # JRuby and CRuby both do internal things to build the status, so I think we can mostly just override it
2
+ # without checking
3
+ module Process
4
+ class Status
5
+ def initialize(pid, status=nil, termsig=nil)
6
+ @pid = pid
7
+ @stat = status
8
+ @termsig = termsig
9
+ end
10
+
11
+ private :initialize
12
+
13
+ # Public API (it's weird!)
14
+
15
+ def & num
16
+ @stat & num
17
+ end
18
+
19
+ def == other
20
+ if other.is_a? Process::Status
21
+ @stat == other.exitstatus
22
+ # Very weird that we don't compare pids or termination options
23
+ # but the doc says we only need to do this.
24
+ else
25
+ false
26
+ end
27
+ end
28
+
29
+ def >> num
30
+ @stat >> num
31
+ end
32
+
33
+ # no coredumps on windows
34
+ def coredump?
35
+ false
36
+ end
37
+
38
+ def exited?
39
+ !@stat.nil?
40
+ end
41
+
42
+ def exitstatus
43
+ @stat
44
+ end
45
+
46
+ def inspect
47
+ "#<Process::Status: #{to_s}>"
48
+ end
49
+
50
+ attr_reader :pid
51
+
52
+ def signaled?
53
+ !@termsig.nil?
54
+ end
55
+
56
+ # No stopped condition on windows
57
+ def stopped?
58
+ false
59
+ end
60
+
61
+ def stopsig
62
+ nil
63
+ end
64
+
65
+ def success?
66
+ return nil unless exited?
67
+ return exitstatus == 0
68
+ end
69
+
70
+ attr_reader :termsig
71
+
72
+ def to_i
73
+ # a deeply unsettling API here, but it's platform-dependent, so lets make it slightly saner
74
+ ((@stat || 0)) | ((@termsig || 0) << 8)
75
+ end
76
+
77
+ def to_s
78
+ tail = if exited?
79
+ "exit #{exitstatus}"
80
+ else # stopped or signaled, probably
81
+ mid = "signal #{termsig}"
82
+ name = Signal.signame(termsig)
83
+ if name
84
+ "SIG#{name} (#{mid})"
85
+ else
86
+ mid
87
+ end
88
+ end
89
+ "pid #{@pid} #{tail}"
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SubSpawn
4
+ class Win32
5
+ VERSION = "0.2.0.pre1"
6
+ end
7
+ end
@@ -0,0 +1,579 @@
1
+ require 'subspawn/win32/version'
2
+ require 'subspawn/win32/ffi'
3
+ require 'subspawn/win32/raw_status'
4
+ require 'engine-hacks'
5
+ module SubSpawn
6
+ class SpawnError < RuntimeError
7
+ end
8
+ class UnimplementedError < RuntimeError
9
+ end
10
+ class Win32
11
+ W = SubSpawn::Win32::FFI
12
+
13
+ module WinStr
14
+ refine Object do
15
+ def to_wstr
16
+ "#{self.to_str}\0".encode("UTF-16LE")
17
+ end
18
+ def to_wstrp
19
+ ::FFI::MemoryPointer.from_string(to_wstr)
20
+ end
21
+ end
22
+ refine NilClass do
23
+ def to_wstr
24
+ nil
25
+ end
26
+ def to_wstrp
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ using WinStr
32
+
33
+
34
+ OpenFD = Struct.new(:fd, :path, :mode, :flags, :ref)
35
+ def initialize(command, *args, arg0: command)
36
+ @path = command
37
+ #raise SpawnError, "Command not found: #{command}" unless @path
38
+ # TODO: we use envp, so can't check this now
39
+ @argv = [arg0, *args.map(&:to_str)]
40
+ @fd_map = {}
41
+ @fd_keeps = []
42
+ @fd_closes = []
43
+ @fd_opens = []
44
+ @cwd = nil
45
+ @pgroup = nil
46
+ @env = :default
47
+ @ctty = nil
48
+
49
+ @win = {
50
+ reqflags: 0,
51
+ flags: 0,
52
+ title: nil,
53
+ desktop: nil,
54
+ show: 0,
55
+ confil: 0,
56
+ }
57
+
58
+ end
59
+ attr_writer :cwd, :ctty
60
+
61
+ StdIn = 0
62
+ StdOut= 1
63
+ StdErr = 2
64
+ Std = {in: StdIn, out: StdOut, err: StdErr}.freeze
65
+
66
+ def validate!
67
+ @argv.map!(&:to_str) # By spec
68
+ raise SpawnError, "Invalid argv" unless @argv.length > 0
69
+ @fd_map = @fd_map.map do |number, source|
70
+ raise SpawnError, "Invalid FD map: Not a number: #{number.inspect}" unless number.is_a? Integer
71
+ [number, fd_check(source)]
72
+ end.to_h
73
+ @fd_keeps.each{|x| fd_check(x)}
74
+ @fd_closes.each{|x| fd_check(x)}
75
+ @fd_opens.each{|x|
76
+ fd_check(x.fd)
77
+ raise SpawnError, "Invalid FD open: Not a number: #{x.mode.inspect}" unless x.mode.is_a? Integer
78
+ raise SpawnError, "Invalid FD open: Not a flag: #{x.flags.inspect}" unless x.flags.is_a? Integer
79
+ raise SpawnError, "Invalid FD open: Not a file: #{x.file.inspect}" unless File.exist? x.path or Dir.exist?(File.dirname(x.path))
80
+ raise SpawnError, "Invalid FD open: overwrites existing mapping #{x.file.inspect}" unless @fd_map[fd_number(x.fd)].nil?
81
+ @fd_map[fd_number(x.fd)] = x
82
+ }
83
+
84
+ @path = @path.gsub("/", "\\")
85
+ @cwd = @cwd.gsub("/", "\\") unless @cwd.nil?
86
+
87
+ raise SpawnError, "Invalid cwd path" unless @cwd.nil? or Dir.exist?(@cwd = ensure_file_string(@cwd))
88
+
89
+ argv_str = build_argstr
90
+ raise SpawnError, "Argument string is too long. #{argv_str.size} must be less than (1 << 15)" if argv_str.size >= (1 << 15)
91
+
92
+ raise SpawnError, "Invalid controlling tty" unless @ctty.nil? or @ctty.respond_to? :con_pty
93
+
94
+ true
95
+ end
96
+
97
+ def spawn!
98
+ validate!
99
+ startupinfo = W::StartupInfo.new # ffi gem zeros memory for us
100
+ proc_info = W::ProcInfo.new
101
+ out_pid = nil
102
+ # set up file descriptors
103
+
104
+ @fd_keeps.each {|fd| process_handle(fd, true) }
105
+ @fd_closes.each {|fd| process_handle(fd, false) }
106
+
107
+ # startup info
108
+ use_stdio = W::STARTF_USESTDHANDLES
109
+ startupinfo.dwFlags = use_stdio | @win[:reqflags] | @win[:flags]
110
+ if use_stdio != 0
111
+ startupinfo.hStdInput = handle_for(StdIn)
112
+ startupinfo.hStdOutput = handle_for(StdOut)
113
+ startupinfo.hStdError = handle_for(StdErr)
114
+ end
115
+ cap1 = startupinfo.lpDesktop = @win[:desktop].to_wstrp if @win[:desktop]
116
+ cap2 = startupinfo.lpTitle = @win[:title].to_wstrp if @win[:title]
117
+ startupinfo.wShowWindow = @win[:show] if @win[:show]
118
+ startupinfo.dwFillAttribute = @win[:confill] if @win[:confill]
119
+ if @win[:consize]
120
+ startupinfo.dwXCountChars, startupinfo.dwYCountChars = *@win[:consize]
121
+ end
122
+ if @win[:winsize]
123
+ startupinfo.dwXSize, startupinfo.dwYSize = *@win[:winsize]
124
+ end
125
+ if @win[:winpos]
126
+ startupinfo.dwX, startupinfo.dwY = *@win[:winpos]
127
+ end
128
+
129
+ # TODO: does windows have rlimits?
130
+
131
+ # TODO: allow configuring inherit handles. CRuby force this to true, so we will copy that for now
132
+ hndl_inheritance = true
133
+ sa = W::SecurityAttributes.new
134
+ sa.bInheritHandle = hndl_inheritance
135
+
136
+ flags = 0
137
+ flags |= W::CREATE_UNICODE_ENVIRONMENT if @env != :default # ENVP
138
+ flags |= W::NORMAL_PRIORITY_CLASS # TODO: allow configuring priority
139
+ flags |= W::CREATE_NEW_PROCESS_GROUP if @pgroup
140
+
141
+
142
+
143
+ # ARGV
144
+ argv_str = build_argstr
145
+ # TODO: move this validation to validate!
146
+ raise SpawnError, "Argument string is too long. #{argv_str.size} must be less than (1 << 15)" if argv_str.size >= (1 << 15)
147
+
148
+ # Add extra attributes (pty)
149
+ numAttribs = 0
150
+ numAttribs +=1 if @ctty != nil
151
+
152
+ ::FFI::MemoryPointer.new(:size_t, 1) do |sizeref|
153
+ if numAttribs == 0
154
+ sizeref.write(:size_t, 5)
155
+ else
156
+ W::InitializeProcThreadAttributeList(nil, numAttribs, 0, sizeref)
157
+ end
158
+ ::FFI::MemoryPointer.new(:uint8_t, sizeref.read(:size_t)) do |attribList|
159
+
160
+ if numAttribs > 0 && !W::InitializeProcThreadAttributeList(attribList, numAttribs,0, sizeref)
161
+ raise SpawnError, "Couldn't initialize attribute list"
162
+ end
163
+ startupinfo.lpAttributeList = nil
164
+ if numAttribs > 0
165
+ flags |= W::EXTENDED_STARTUPINFO_PRESENT
166
+ startupinfo.lpAttributeList = attribList
167
+ end
168
+ unless @ctty.nil?
169
+ if !W::UpdateProcThreadAttribute(attribList, 0, W::PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, @ctty.con_pty.raw_handle, W::SIZEOF_HPCON, nil, nil)
170
+ raise SpawnError, "Couldn't add pty to list"
171
+ end
172
+ end
173
+ # ARGP/ENV
174
+ envp_holder = make_envp
175
+ # Launch!
176
+ # Note that @path can be null on windows, but we will always enforce otherwise
177
+ ret = W.CreateProcess(
178
+ @path.to_wstr, # DONE
179
+ argv_str, #DONE
180
+ sa, # proc_sec, DONE, but unexposed
181
+ sa, # thread_sec, DONE, but unexposed
182
+ hndl_inheritance, # DONE, but unexposed
183
+ flags, # DONE, but unexposed
184
+ envp_holder, # DONE
185
+ @cwd.to_wstr, # DONE
186
+ startupinfo,
187
+ proc_info # DONE
188
+ )
189
+ if !ret
190
+ # TODO: CRuby does map_errno(GetLastError()) Do we need to do that for does FFI.errno do that already?
191
+ raise SystemCallError.new("Spawn Error: CreateProcess: #{self.class.errno}", self.class.errno[:win])
192
+ # TODO: come up with a better errno
193
+ end
194
+ # Don't close the handle now, but save it in a thread-local for waitpid
195
+ # in case it's a quick exit
196
+ #W.CloseHandle(proc_info.hProcess)
197
+ Thread.current[:subspawn_win32hndl] = LazyHndl.new(proc_info.dwProcessId, proc_info.hProcess)
198
+
199
+ W.CloseHandle(proc_info.hThread)
200
+ # being a spawn clone, we don't normally expose the thread, but assign it if anyone wants it
201
+ @out_thread = proc_info.dwThreadId
202
+ out_pid = proc_info.dwProcessId
203
+
204
+ # close our half of the files
205
+ @fd_map.values.select{|x|x.is_a? OpenFD}.map{|x|x.ref.close; x.ref = nil}
206
+
207
+ if numAttribs > 0
208
+ W::DeleteProcThreadAttributeList(attribList)
209
+ end
210
+ end
211
+ end
212
+ out_pid
213
+ end
214
+
215
+ # TODO: allow io on left?
216
+ def fd(number, io_or_fd)
217
+ num = number.is_a?(Symbol) ? Std[number] : number.to_i
218
+ raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0, 1, 2" unless [0,1,2].include? num
219
+ @fd_map[num] = io_or_fd
220
+ self
221
+ end
222
+
223
+ def fd_open(number, path, flags = 0, mode=0o666) # umask will remove bits
224
+ num = number.is_a?(Symbol) ? Std[number] : number.to_i
225
+ raise ArgumentError, "Invalid file descriptor number: #{number}. Supported values = 0.. or #{std.keys.inspect}" if num.nil?
226
+ @fd_opens << OpenFD.new(number, path, mode, flags)
227
+ self
228
+ end
229
+ def fd_keep(io_or_fd)
230
+ @fd_keep << io_or_fd
231
+ self
232
+ end
233
+ def fd_close(io_or_fd)
234
+ @fd_closes << io_or_fd
235
+ self
236
+ end
237
+ def name(string)
238
+ @argv[0] = string.to_s
239
+ self
240
+ end
241
+ alias :name= :name
242
+
243
+ def args(args)
244
+ @argv = [@argv[0], *args.map(&:to_str)]
245
+ self
246
+ end
247
+ alias :args= :args
248
+ def command(cmd)
249
+ @path = cmd
250
+ self
251
+ end
252
+ alias :command= :command
253
+
254
+ def env_reset!
255
+ @env = :default
256
+ self
257
+ end
258
+ def env(key, value)
259
+ @env = ENV.to_h.dup if @env == :default
260
+ @env[key.to_s] = value.to_s
261
+ self
262
+ end
263
+ def env=(hash)
264
+ @env = hash.to_h
265
+ self
266
+ end
267
+
268
+ def desktop(str)
269
+ @win[:desktop] = str
270
+ self
271
+ end
272
+ def title(str)
273
+ @win[:title] = str
274
+ self
275
+ end
276
+ def show_window(show) # TODO: translate ruby to flags
277
+ @win[:show] = show
278
+ @win[:reqflags] |= W::STARTF_USESHOWWINDOW
279
+ self
280
+ end
281
+ def window_pos(x,y)
282
+ @win[:winpos] = [x,y]
283
+ @win[:reqflags] |= W::STARTF_USEPOSITION
284
+ self
285
+ end
286
+ def window_size(w,h)
287
+ @win[:winsize] = [w,h]
288
+ @win[:reqflags] |= W::STARTF_USESIZE
289
+ self
290
+ end
291
+ def console_size(x,y)
292
+ @win[:conpos] = [x,y]
293
+ @win[:reqflags] |= W::STARTF_USECOUNTCHARS
294
+ self
295
+ end
296
+ def window_fill(attrib)
297
+ @win[:confill] = attrib
298
+ @win[:reqflags] |= W::STARTF_USEFILLATTRIBUTE
299
+ self
300
+ end
301
+ def start_flags(flags)
302
+ @win[:flags] = flags
303
+ self
304
+ end
305
+
306
+ # TODO: I don't think windows has a umask equivalent?
307
+ # alias :umask :umask=
308
+
309
+ def pwd(path)
310
+ @cwd = path
311
+ self
312
+ end
313
+ alias :cwd :pwd
314
+ alias :pwd= :cwd=
315
+ alias :chdir :pwd
316
+ alias :chdir= :cwd=
317
+
318
+ def pgroup(isnew)
319
+ raise ArgumentError, "Invalid new_pgroup: #{isnew} (expecting boolean)" unless [true, false, nil, 0].include? isnew
320
+ @pgroup = !!isnew
321
+ self
322
+ end
323
+ alias :pgroup= :pgroup
324
+
325
+ def ctty(path)
326
+ @ctty = path
327
+ self
328
+ end
329
+ alias :tty= :ctty=
330
+ alias :tty :ctty
331
+
332
+
333
+ def validate
334
+ validate! rescue false
335
+ end
336
+
337
+
338
+
339
+ # generator for candidates for an executable name
340
+ # usage:
341
+ # SubSpawn::Win32.each_which("ls", ENV) {|path| ...}
342
+ # SubSpawn::Win32.each_which("ls", ENV).to_a
343
+ # TODO: fix this!
344
+ def self.expand_which(name, env=ENV)
345
+ name = name.gsub("/","\\") # windows-ify the name
346
+ return self.to_enum(:expand_which, name, env) unless block_given?
347
+ extensions = ["", *(ENV['PATHEXT'] || "").split(';')]
348
+ # only allow relative paths if they traverse, and if they traverse, only allow relative paths
349
+ if name.include? "/"
350
+ extensions.each do |ext|
351
+ yield File.absolute_path(name + ext)
352
+ end
353
+ else
354
+ # If we were to follow
355
+ # https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
356
+ # in the path search process, we should look in CWD and dirname($0) first,
357
+ # and possibly the app paths registry key. Ignore for now
358
+ env['PATH'].split(File::PATH_SEPARATOR).each do |path|
359
+ extensions.each do |ext|
360
+ yield File.join(path, name+ext)
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ def self.shell_command(string)
367
+ # MRI scans for "basic" commands and if so, just un-expands the shell
368
+ # we could do that too, and there are 2 tests about that in rubyspec
369
+ # but we shall ignore them for now
370
+ # TODO: implement that
371
+ # https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
372
+ # I think this should work, maybe?
373
+ ["cmd.exe", "/c", string.to_str]#.gsub(/([)(%!^"><&|)])/, "^\\1")]
374
+ end
375
+
376
+ def self.errno
377
+ errno = 0
378
+ ::FFI::MemoryPointer.new(:int, 1) do |errnoholder|
379
+ W.get_errno(errnoholder)
380
+ errno = errnoholder.read_int
381
+ end
382
+ return {win: W.GetLastError(), c: errno}
383
+ end
384
+
385
+ # Ruby raises EChild, so we have to reimplement wait/waitpid2
386
+ def self.waitpid2(pid, options=0)
387
+ # only support pid == -1 (any of our children), and pid > 0 for windows
388
+ # TODO: -1 requres keeping a list of our children that could grow to 4GiB
389
+ # TODO: Think that through
390
+ raise SubSpawn::UnimplementedError("PID <= 0 not yet implemented in subspawn-win32") if pid <= 0
391
+
392
+ # TODO: allow cross-thread support
393
+ last = Thread.current[:subspawn_win32hndl]
394
+ hndl = if last.nil? or last.pid != pid
395
+ last = Thread.current[:subspawn_win32hndl] = nil
396
+ # TODO: process_limited_information maybe?
397
+ W.OpenProcess(W::PROCESS_QUERY_INFORMATION, false, pid)
398
+ else
399
+ last.hndl
400
+ end
401
+ raise Errno::ECHILD.new("No child processes") if hndl == 0 || hndl == W::INVALID_HANDLE_VALUE
402
+
403
+ timeout = (options & Process::WNOHANG) != 0 ? 0 : W::INFINITE
404
+
405
+ begin
406
+ tmp = _single_exit_poll hndl
407
+ while tmp.nil? and (options & Process::WNOHANG) == 0
408
+ sleep 0.01 # Somehow, this still loops sometimes
409
+ W.WaitForSingleObject(hndl, timeout)
410
+ tmp = _single_exit_poll hndl
411
+ end
412
+ _set_status(if tmp.nil?
413
+ nil
414
+ else
415
+ if (tmp & 0xC000_0000) != 0 # if it was a windows exception
416
+ Process::Status.send :new, pid, nil, Signal.list[W::StatusPosixMap[tmp]]
417
+ else
418
+ Process::Status.send :new, pid, tmp, nil
419
+ end
420
+ end)
421
+ ensure
422
+ W.CloseHandle(hndl) if last.nil?
423
+ end
424
+ end
425
+ def self.last_status
426
+ @last_status
427
+ end
428
+
429
+ COMPLETE_VERSION = {
430
+ subspawn_win32: SubSpawn::Win32::VERSION,
431
+ }
432
+
433
+ private
434
+
435
+ def self._single_exit_poll hndl
436
+ # TODO: cleanup error messages
437
+ ::FFI::MemoryPointer.new(:int, 1) do |buf|
438
+ # TODO: This should parsee the errno, I think?
439
+ raise SystemCallError.new("Waitpid Polling Failure", W::GetLastError()) unless W::GetExitCodeProcess(hndl, buf)
440
+ tmp = buf.read(:int)
441
+ return tmp == W::STILL_ACTIVE ? nil : tmp
442
+ end
443
+ end
444
+ def self._set_status status
445
+ EngineHacks.child_status = status
446
+ @last_status = status
447
+ return status.nil? ? nil : [status.pid, status]
448
+ end
449
+
450
+ def none
451
+ @@none ||= Object.new
452
+ end
453
+
454
+ def make_envp
455
+ if @env == :default
456
+ return nil # weirdly easy on windows
457
+ else
458
+ strings = @env.select{|k, v|
459
+ !k.nil? and !v.nil?
460
+ }.map{|k,v|
461
+ k = k.to_str
462
+ str = "#{k}=#{v.to_str}" # rubyspec says to convert to_str
463
+ raise ArgumentError, "Nulls not allowed in environment variable: #{str.inspect}" if str.include? "\0" # By Spec
464
+ raise ArgumentError, "Variable key cannot include '=': #{str.inspect}" if k.include? "=" # By Spec
465
+ "#{str}\0"
466
+ }.join("") + "\0" # null end of argp
467
+ return strings.to_wstr
468
+ end
469
+ end
470
+ def build_argstr
471
+ # TODO: quote this way, or are other characters
472
+ @argv.map{|a|
473
+ raise ArgumentError, "Nulls not allowed in command: #{a.inspect}" if a.include? "\0"
474
+ quote_arg(a)
475
+ }.join(" ").to_wstr
476
+ end
477
+ # windows quoting is horrible and not consistent
478
+ # TODO: test this throughly
479
+ # https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess
480
+ def quote_arg(str)
481
+ # if no whitespace or quote characters, this is a "simple" argument
482
+ return str unless str =~ /[ \t\n\v"]/ # TODO: no \r?
483
+ backslashes = nil
484
+ base = str.each_char.map do |c|
485
+ if c != "\\"
486
+ if backslashes.nil? # we are just a lone character
487
+ c
488
+ else # we are terminating a backslash sequence
489
+ if c == '"'
490
+ backslashes + backslashes + "\\" + c # double escape
491
+ else
492
+ backslashes + c # no escaping necessary (UNC path, etc)
493
+ end.tap{ backslashes = nil } # clear saved escapes
494
+ end
495
+ else
496
+ backslashes << c
497
+ end
498
+ end.join("")
499
+ %Q{"#{base}#{backslashes}#{backslashes}"} # A quote goes next, so double escape again
500
+ end
501
+ def handle_for(fdi)
502
+ mapped = @fd_map[fdi] || fdi
503
+ if mapped.is_a? OpenFD
504
+ mapped.ref = File.new(mapped.path, mapped.flags, mapped.mode)
505
+ mapped = mapped.ref.fileno
506
+ end
507
+ hndl = fd2handle(mapped)
508
+
509
+ if hndl == W::INVALID_HANDLE_VALUE || hndl == W::HANDLE_NEGATIVE_TWO
510
+ if @fd_map.nil?
511
+ hndl = W.GetStdHandle(W::STD_HANDLE[fdi])
512
+ else
513
+ raise SystemCallError.new("Invalid FD/handle for input fd #{fdi}", FFI.errno)
514
+ end
515
+ end
516
+ # ensure the handle is inheritable
517
+ res = W.SetHandleInformation(hndl, W::HANDLE_FLAG_INHERIT, W::HANDLE_FLAG_INHERIT)
518
+ raise SystemCallError.new("Non-inheritable handle for input fd #{fdi} (#{self.class.errno})") unless res
519
+
520
+ #puts "stdio[#{fdi}] = #{@fd_map[fdi]} => #{fd} => #{hndl} (#{res})"
521
+ hndl
522
+ end
523
+
524
+ def process_handle(fdo, share)
525
+ hndl = fd2handle(fdo)
526
+
527
+ if hndl == W::INVALID_HANDLE_VALUE || hndl == W::HANDLE_NEGATIVE_TWO
528
+ raise SystemCallError.new("Invalid FD/handle for keep/close")
529
+ end
530
+ # ensure the handle is inheritable
531
+ res = W.SetHandleInformation(hndl, W::HANDLE_FLAG_INHERIT, share ? W::HANDLE_FLAG_INHERIT : 0)
532
+ raise SystemCallError.new("Couldn't change handle inheritance (#{self.class.errno})") unless res
533
+ hndl
534
+ end
535
+
536
+ def ensure_file_string(path)
537
+ if defined? JRUBY_VERSION # accept File and Path java objects
538
+ path = path.to_file if path.respond_to? :to_file
539
+ if path.respond_to? :absolute_path
540
+ path.absoloute_path
541
+ else
542
+ path.to_s
543
+ end
544
+ else
545
+ path.to_s
546
+ end
547
+ end
548
+ def fd_check(source)
549
+ case source
550
+ when Integer then source
551
+ when IO then source
552
+ when :in, :out, :err
553
+ Std[source]
554
+ else
555
+ raise SpawnError, "Invalid FD map: Not a io or number: #{source.inspect}"
556
+ end
557
+ end
558
+ def fd_number(source)
559
+ case source
560
+ when Integer then source
561
+ when IO then source.fileno
562
+ when :in, :out, :err
563
+ Std[source]
564
+ else
565
+ raise SpawnError, "Invalid FD map: Not a io or number: #{source.inspect}"
566
+ end
567
+ end
568
+ def fd2handle(fdo)
569
+ if RUBY_ENGINE == "jruby" && ((fdo.is_a?(IO) && !fdo.is_a?(File)) || (fdo.is_a? Integer and fdo > 9)) # handles are (usually?) greater than 9
570
+ tmp = fd_number(fdo) # JRuby IO.pipe.fileno returns a proper handle!
571
+ else
572
+ W.get_osfhandle(fd_number(fdo))
573
+ end
574
+ end
575
+
576
+ end
577
+ end
578
+
579
+ require 'subspawn/win32/lazy-hndl'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/subspawn/win32/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "subspawn-win32"
7
+ spec.version = SubSpawn::Win32::VERSION
8
+ spec.authors = ["Patrick Plenefisch"]
9
+ spec.email = ["simonpatp@gmail.com"]
10
+
11
+ spec.summary = "SubSpawn Mid-level API for Windows systems"
12
+ spec.description = "A SubSpawn subproject to wrap Win32 as a mid level API"
13
+ final_github = "https://github.com/byteit101/subspawn"
14
+ spec.homepage = final_github
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = final_github
21
+ spec.metadata["changelog_uri"] = final_github
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.require_paths = ["lib"]
31
+
32
+ spec.add_dependency "subspawn-common", "~> 0.2.0.pre1"
33
+
34
+ # For more information and examples about making a new gem, check out our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+
37
+ # You can use Ruby's license, or any of the JRuby tri-license options
38
+ spec.licenses = ["Ruby", "EPL-2.0", "LGPL-2.1-or-later"]
39
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subspawn-win32
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0.pre1
5
+ platform: ruby
6
+ authors:
7
+ - Patrick Plenefisch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: subspawn-common
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0.pre1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0.pre1
27
+ description: A SubSpawn subproject to wrap Win32 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/win32.rb
39
+ - lib/subspawn/win32/ffi.rb
40
+ - lib/subspawn/win32/lazy-hndl.rb
41
+ - lib/subspawn/win32/pty.rb
42
+ - lib/subspawn/win32/raw_status.rb
43
+ - lib/subspawn/win32/version.rb
44
+ - subspawn-win32.gemspec
45
+ homepage: https://github.com/byteit101/subspawn
46
+ licenses:
47
+ - Ruby
48
+ - EPL-2.0
49
+ - LGPL-2.1-or-later
50
+ metadata:
51
+ homepage_uri: https://github.com/byteit101/subspawn
52
+ source_code_uri: https://github.com/byteit101/subspawn
53
+ changelog_uri: https://github.com/byteit101/subspawn
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.6.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.5.3
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: SubSpawn Mid-level API for Windows systems
73
+ test_files: []