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