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 +7 -0
- data/.rspec +3 -0
- data/Gemfile +11 -0
- data/README.md +27 -0
- data/Rakefile +8 -0
- data/lib/subspawn/win32/ffi.rb +252 -0
- data/lib/subspawn/win32/lazy-hndl.rb +16 -0
- data/lib/subspawn/win32/pty.rb +172 -0
- data/lib/subspawn/win32/raw_status.rb +92 -0
- data/lib/subspawn/win32/version.rb +7 -0
- data/lib/subspawn/win32.rb +579 -0
- data/subspawn-win32.gemspec +39 -0
- metadata +73 -0
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
data/Gemfile
ADDED
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,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,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: []
|