subspawn-win32 0.2.0.pre1

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 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: []