winproc 0.1.0
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/CHANGELOG.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +312 -0
- data/ext/winproc/extconf.rb +33 -0
- data/ext/winproc/winproc.c +1922 -0
- data/lib/winproc/version.rb +5 -0
- data/lib/winproc.rb +454 -0
- metadata +122 -0
data/lib/winproc.rb
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "winproc/version"
|
|
4
|
+
require "winproc/winproc" # native extension: classes + errors + SLOT_*/SW_*/CREATE_* constants
|
|
5
|
+
|
|
6
|
+
# winproc — Windows process control for Ruby: job-object process trees that can
|
|
7
|
+
# never leak, ConPTY terminals, hygienic spawning, and UAC-aware elevation
|
|
8
|
+
# helpers — safe by default, cooperative with a fiber scheduler.
|
|
9
|
+
#
|
|
10
|
+
# require "winproc"
|
|
11
|
+
#
|
|
12
|
+
# Winproc::Job.new do |job| # kill_on_close: true (default)
|
|
13
|
+
# p = Winproc.spawn("ruby", "-e", "puts 6*7", stdout: :pipe, job: job)
|
|
14
|
+
# out = +""
|
|
15
|
+
# while (chunk = p.stdout.read) then out << chunk end
|
|
16
|
+
# p.wait # => 0
|
|
17
|
+
# p.close
|
|
18
|
+
# end # any stragglers die with the job
|
|
19
|
+
module Winproc
|
|
20
|
+
# A Windows API failure carries the originating error code (GetLastError /
|
|
21
|
+
# the failing return), set on the exception in C.
|
|
22
|
+
class OSError
|
|
23
|
+
def code
|
|
24
|
+
@code
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The four privilege names winproc supports (process-wide, scoped by
|
|
29
|
+
# #with_privilege). The token must already HOLD the privilege.
|
|
30
|
+
PRIVILEGE_NAMES = {
|
|
31
|
+
debug: "SeDebugPrivilege",
|
|
32
|
+
backup: "SeBackupPrivilege",
|
|
33
|
+
restore: "SeRestorePrivilege",
|
|
34
|
+
increase_quota: "SeIncreaseQuotaPrivilege"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# Run a blocking native call cooperatively. Under a Fiber scheduler (e.g.
|
|
40
|
+
# winloop) the call is offloaded to a worker Thread so the calling fiber parks
|
|
41
|
+
# (Thread#value routes through the scheduler's block/unblock hooks) and the
|
|
42
|
+
# event loop keeps serving other fibers; with no scheduler it runs inline (the
|
|
43
|
+
# C call already releases the GVL). On fiber unwind the worker is killed +
|
|
44
|
+
# joined so it can't leak or consume data destined for a later op.
|
|
45
|
+
#
|
|
46
|
+
# Caveat (same as winipc): a fiber unwound in the instant after the worker
|
|
47
|
+
# acquired a resource but before the value was delivered loses that
|
|
48
|
+
# acquisition — the inherent cancelled-read-already-pulled-bytes limitation.
|
|
49
|
+
def run_blocking
|
|
50
|
+
sched = Fiber.scheduler
|
|
51
|
+
return yield unless sched
|
|
52
|
+
|
|
53
|
+
worker = Thread.new do
|
|
54
|
+
Thread.current.report_on_exception = false
|
|
55
|
+
yield
|
|
56
|
+
end
|
|
57
|
+
begin
|
|
58
|
+
worker.value
|
|
59
|
+
ensure
|
|
60
|
+
if worker.alive?
|
|
61
|
+
worker.kill
|
|
62
|
+
worker.join
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Seconds -> milliseconds for the native waits. nil => -1 (INFINITE); a tiny
|
|
68
|
+
# positive value rounds up to 1 ms (never collapse a wait into a poll). A
|
|
69
|
+
# finite timeout always yields a finite, in-range ms: the result is clamped to
|
|
70
|
+
# the signed 64-bit ceiling the C waits accept (NUM2LL), so an absurdly large
|
|
71
|
+
# timeout becomes a very long finite wait (~292 million years) instead of a
|
|
72
|
+
# RangeError — never an INFINITE wait. Clamping here also means the in-flight
|
|
73
|
+
# guards in the C waits can never wedge on a conversion raise.
|
|
74
|
+
MS_MAX = 0x7FFF_FFFF_FFFF_FFFF # LLONG_MAX: the largest ms NUM2LL accepts
|
|
75
|
+
|
|
76
|
+
def ms_for(timeout)
|
|
77
|
+
return -1 if timeout.nil?
|
|
78
|
+
|
|
79
|
+
t = Float(timeout)
|
|
80
|
+
raise ArgumentError, "timeout must be non-negative, got #{timeout.inspect}" if t.negative?
|
|
81
|
+
|
|
82
|
+
ms = (t * 1000).round
|
|
83
|
+
return 1 if ms.zero? && t.positive?
|
|
84
|
+
|
|
85
|
+
ms > MS_MAX ? MS_MAX : ms
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Quote an argv Array into a single command line per the CommandLineToArgvW /
|
|
89
|
+
# CRT rules ("everyone quotes command line arguments the wrong way"). Pure,
|
|
90
|
+
# unit-tested. Raises ArgumentError on an empty argv or a NUL in any element.
|
|
91
|
+
#
|
|
92
|
+
# NOTE: these are the rules of the C runtime / CommandLineToArgvW parser.
|
|
93
|
+
# cmd.exe batch files and programs that parse their own command line
|
|
94
|
+
# differently (notably `cmd /c`) have their own metacharacter rules — winproc
|
|
95
|
+
# never invokes a shell and does not escape for cmd.exe.
|
|
96
|
+
def quote_argv(argv)
|
|
97
|
+
raise ArgumentError, "argv must not be empty" if argv.empty?
|
|
98
|
+
|
|
99
|
+
argv.map { |arg| quote_one(arg) }.join(" ")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def quote_one(arg)
|
|
103
|
+
s = String(arg)
|
|
104
|
+
raise ArgumentError, "argument contains a NUL byte: #{s.inspect}" if s.include?("\0")
|
|
105
|
+
return '""' if s.empty?
|
|
106
|
+
return s unless s.match?(/[ \t"]/)
|
|
107
|
+
|
|
108
|
+
out = +'"'
|
|
109
|
+
backslashes = 0
|
|
110
|
+
s.each_char do |ch|
|
|
111
|
+
case ch
|
|
112
|
+
when "\\"
|
|
113
|
+
backslashes += 1
|
|
114
|
+
when '"'
|
|
115
|
+
out << ("\\" * (backslashes * 2 + 1)) << '"'
|
|
116
|
+
backslashes = 0
|
|
117
|
+
else
|
|
118
|
+
out << ("\\" * backslashes) << ch
|
|
119
|
+
backslashes = 0
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
out << ("\\" * (backslashes * 2)) # trailing run, before the closing quote
|
|
123
|
+
out << '"'
|
|
124
|
+
out
|
|
125
|
+
end
|
|
126
|
+
private_class_method :quote_one
|
|
127
|
+
|
|
128
|
+
# Build the merged, sorted, NUL-joined environment block (a single UTF-8
|
|
129
|
+
# String with embedded NULs) from +env+ (Hash{String=>String|nil}) merged over
|
|
130
|
+
# the parent ENV case-insensitively, or nil to inherit unchanged.
|
|
131
|
+
def build_env_block(env)
|
|
132
|
+
return nil if env.nil?
|
|
133
|
+
|
|
134
|
+
merged = ENV.to_h
|
|
135
|
+
env.each do |key, value|
|
|
136
|
+
validate_env_key(key)
|
|
137
|
+
validate_env_value(value)
|
|
138
|
+
# delete any existing key that matches case-insensitively
|
|
139
|
+
merged.delete_if { |k, _| k.casecmp?(key) }
|
|
140
|
+
merged[key] = value unless value.nil?
|
|
141
|
+
end
|
|
142
|
+
# Sorted by upcased UTF-16 code-unit order (CreateProcessW requirement:
|
|
143
|
+
# "sorted alphabetically, case-insensitive, Unicode order, without regard to
|
|
144
|
+
# locale"). encode UTF-16LE to get code-unit ordering.
|
|
145
|
+
pairs = merged.sort_by { |k, _| k.upcase.encode("UTF-16LE") }
|
|
146
|
+
block = +""
|
|
147
|
+
pairs.each { |k, v| block << k << "=" << v << "\0" }
|
|
148
|
+
block
|
|
149
|
+
end
|
|
150
|
+
private_class_method :build_env_block
|
|
151
|
+
|
|
152
|
+
def validate_env_key(key)
|
|
153
|
+
raise TypeError, "env key must be a String, got #{key.class}" unless key.is_a?(String)
|
|
154
|
+
raise ArgumentError, "env key must not be empty" if key.empty?
|
|
155
|
+
raise ArgumentError, "env key must not contain '=': #{key.inspect}" if key.include?("=")
|
|
156
|
+
raise ArgumentError, "env key must not contain NUL: #{key.inspect}" if key.include?("\0")
|
|
157
|
+
end
|
|
158
|
+
private_class_method :validate_env_key
|
|
159
|
+
|
|
160
|
+
def validate_env_value(value)
|
|
161
|
+
return if value.nil?
|
|
162
|
+
raise TypeError, "env value must be a String or nil, got #{value.class}" unless value.is_a?(String)
|
|
163
|
+
raise ArgumentError, "env value must not contain NUL: #{value.inspect}" if value.include?("\0")
|
|
164
|
+
end
|
|
165
|
+
private_class_method :validate_env_value
|
|
166
|
+
|
|
167
|
+
# Encode a stdio kwarg into a [kind, io_or_std] pair for the C layer. +std+ is
|
|
168
|
+
# the GetStdHandle selector used when an INHERIT slot needs a parent std value.
|
|
169
|
+
def stdio_spec(value, allow_merge: false)
|
|
170
|
+
case value
|
|
171
|
+
when nil then [SLOT_INHERIT, nil]
|
|
172
|
+
when :null then [SLOT_NULL, nil]
|
|
173
|
+
when :pipe then [SLOT_PIPE, nil]
|
|
174
|
+
when :stdout
|
|
175
|
+
raise ArgumentError, "stdio :stdout is only valid for stderr:" unless allow_merge
|
|
176
|
+
|
|
177
|
+
[SLOT_MERGE, nil]
|
|
178
|
+
when IO then [SLOT_IO, value]
|
|
179
|
+
else
|
|
180
|
+
if value.respond_to?(:to_io)
|
|
181
|
+
[SLOT_IO, value.to_io]
|
|
182
|
+
else
|
|
183
|
+
raise ArgumentError, "stdio must be nil/:null/:pipe/an IO#{allow_merge ? '/:stdout' : ''}, " \
|
|
184
|
+
"got #{value.inspect}"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
private_class_method :stdio_spec
|
|
189
|
+
|
|
190
|
+
# Spawn a child process from an argv ARRAY (no shell is ever invoked). Returns
|
|
191
|
+
# a Winproc::Process; with a block, yields it and ensure-closes the handles
|
|
192
|
+
# (closing does NOT kill the child — use job: for guaranteed reaping).
|
|
193
|
+
def spawn(*argv, app: nil, cwd: nil, env: nil,
|
|
194
|
+
stdin: nil, stdout: nil, stderr: nil,
|
|
195
|
+
job: nil, new_process_group: false, no_window: false)
|
|
196
|
+
cmdline = quote_argv(argv)
|
|
197
|
+
envblock = build_env_block(env)
|
|
198
|
+
ik, iio = stdio_spec(stdin)
|
|
199
|
+
ok, oio = stdio_spec(stdout)
|
|
200
|
+
ek, eio = stdio_spec(stderr, allow_merge: true)
|
|
201
|
+
|
|
202
|
+
flags = 0
|
|
203
|
+
flags |= CREATE_NEW_PROCESS_GROUP if new_process_group
|
|
204
|
+
flags |= CREATE_NO_WINDOW if no_window
|
|
205
|
+
|
|
206
|
+
process = _spawn(app && String(app), cmdline, cwd && String(cwd), envblock,
|
|
207
|
+
flags, job, 0, 0, ik, iio, ok, oio, ek, eio)
|
|
208
|
+
return process unless block_given?
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
yield process
|
|
212
|
+
ensure
|
|
213
|
+
process.close
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Spawn a child attached to a ConPTY pseudoconsole. Returns a Winproc::PTY;
|
|
218
|
+
# with a block, yields it and ensure-closes (kill: true). stdio redirection
|
|
219
|
+
# kwargs are deliberately absent (ConPTY and pipe redirection are mutually
|
|
220
|
+
# exclusive).
|
|
221
|
+
def pty(*argv, cols: 80, rows: 24, app: nil, cwd: nil, env: nil, job: nil)
|
|
222
|
+
raise Unsupported, "winproc: ConPTY requires Windows 10 1809+" unless pty_available?
|
|
223
|
+
|
|
224
|
+
c = Integer(cols)
|
|
225
|
+
r = Integer(rows)
|
|
226
|
+
raise ArgumentError, "cols/rows must be 1..32767" unless (1..0x7FFF).cover?(c) && (1..0x7FFF).cover?(r)
|
|
227
|
+
|
|
228
|
+
cmdline = quote_argv(argv)
|
|
229
|
+
envblock = build_env_block(env)
|
|
230
|
+
pty = _spawn(app && String(app), cmdline, cwd && String(cwd), envblock,
|
|
231
|
+
0, job, c, r,
|
|
232
|
+
SLOT_INHERIT, nil, SLOT_INHERIT, nil, SLOT_INHERIT, nil)
|
|
233
|
+
return pty unless block_given?
|
|
234
|
+
|
|
235
|
+
begin
|
|
236
|
+
yield pty
|
|
237
|
+
ensure
|
|
238
|
+
pty.close(kill: true)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Is ConPTY available on this OS? (GetProcAddress probe; never raises.)
|
|
243
|
+
def pty_available?
|
|
244
|
+
__pty_available
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Is the current process running with an elevated token? (TokenElevation)
|
|
248
|
+
def elevated?
|
|
249
|
+
__elevated
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Is the current USER an administrator, even if not currently elevated?
|
|
253
|
+
def admin?
|
|
254
|
+
__admin
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Relaunch/launch a program elevated via the shell "runas" verb (UAC prompt).
|
|
258
|
+
# +args+ is an argv-style Array quoted by winproc with the same quote_argv
|
|
259
|
+
# rules as spawn (a String raises TypeError). Returns a Winproc::Process when
|
|
260
|
+
# the shell reports a new process handle; nil when it launched without one.
|
|
261
|
+
def runas(exe, args = [], cwd: nil, show: :normal)
|
|
262
|
+
raise TypeError, "runas args must be an Array, got #{args.class}" unless args.is_a?(Array)
|
|
263
|
+
|
|
264
|
+
params = args.empty? ? nil : quote_argv(args)
|
|
265
|
+
sw = case show
|
|
266
|
+
when :normal then SW_SHOWNORMAL
|
|
267
|
+
when :hide then SW_HIDE
|
|
268
|
+
when :minimized then SW_SHOWMINIMIZED
|
|
269
|
+
when :maximized then SW_SHOWMAXIMIZED
|
|
270
|
+
else raise ArgumentError, "show must be :normal/:hide/:minimized/:maximized, got #{show.inspect}"
|
|
271
|
+
end
|
|
272
|
+
run_blocking { __runas(String(exe), params, cwd && String(cwd), sw) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Enable a process-token privilege for the duration of the block, restoring its
|
|
276
|
+
# previous state afterwards (even on raise). The token is PROCESS-wide.
|
|
277
|
+
def with_privilege(name)
|
|
278
|
+
se = PRIVILEGE_NAMES[name]
|
|
279
|
+
raise ArgumentError, "unknown privilege #{name.inspect}; one of #{PRIVILEGE_NAMES.keys.inspect}" unless se
|
|
280
|
+
|
|
281
|
+
was_enabled = __privilege(se, true) # raises PrivilegeNotHeld if not held
|
|
282
|
+
begin
|
|
283
|
+
yield
|
|
284
|
+
ensure
|
|
285
|
+
__privilege(se, was_enabled)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------- Process ---
|
|
290
|
+
class Process
|
|
291
|
+
# Block until the process exits; returns the exit code (Integer). +timeout+
|
|
292
|
+
# in seconds (nil = infinite); returns nil on timeout. Cooperative under a
|
|
293
|
+
# scheduler. Memoizes the exit code.
|
|
294
|
+
def wait(timeout: nil)
|
|
295
|
+
Winproc.run_blocking { _wait(Winproc.ms_for(timeout)) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Hard kill (TerminateProcess); does NOT touch children (use a Job for trees).
|
|
299
|
+
def kill(exit_code = 1)
|
|
300
|
+
_kill(exit_code)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# The writable/readable pipe ends, present only for :pipe stdio slots.
|
|
304
|
+
def stdin = @stdin
|
|
305
|
+
def stdout = @stdout
|
|
306
|
+
def stderr = @stderr
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# -------------------------------------------------------------------- Job ---
|
|
310
|
+
class Job
|
|
311
|
+
# Create an ANONYMOUS job object and apply limits. A private IOCP is created
|
|
312
|
+
# and associated with the job before any process can join, so #wait_empty
|
|
313
|
+
# can never miss messages.
|
|
314
|
+
def self.new(kill_on_close: true, memory: nil, process_memory: nil,
|
|
315
|
+
cpu_percent: nil, active_processes: nil, cpu_time: nil)
|
|
316
|
+
mem = memory && nonneg_int(memory, "memory")
|
|
317
|
+
pmem = process_memory && nonneg_int(process_memory, "process_memory")
|
|
318
|
+
act = active_processes && positive_int(active_processes, "active_processes")
|
|
319
|
+
cpu = cpu_percent && cpu_percent_int(cpu_percent)
|
|
320
|
+
time = cpu_time && cpu_time_ticks(cpu_time)
|
|
321
|
+
|
|
322
|
+
job = _create(kill_on_close, mem, pmem, cpu, act, time)
|
|
323
|
+
return job unless block_given?
|
|
324
|
+
|
|
325
|
+
begin
|
|
326
|
+
yield job
|
|
327
|
+
ensure
|
|
328
|
+
job.close
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def self.nonneg_int(v, name)
|
|
333
|
+
i = Integer(v)
|
|
334
|
+
raise ArgumentError, "#{name} must be >= 0, got #{v.inspect}" if i.negative?
|
|
335
|
+
|
|
336
|
+
i
|
|
337
|
+
end
|
|
338
|
+
private_class_method :nonneg_int
|
|
339
|
+
|
|
340
|
+
def self.positive_int(v, name)
|
|
341
|
+
i = Integer(v)
|
|
342
|
+
raise ArgumentError, "#{name} must be >= 1, got #{v.inspect}" if i < 1
|
|
343
|
+
|
|
344
|
+
i
|
|
345
|
+
end
|
|
346
|
+
private_class_method :positive_int
|
|
347
|
+
|
|
348
|
+
def self.cpu_percent_int(v)
|
|
349
|
+
i = Integer(v)
|
|
350
|
+
raise ArgumentError, "cpu_percent must be 1..100, got #{v.inspect}" unless (1..100).cover?(i)
|
|
351
|
+
|
|
352
|
+
i
|
|
353
|
+
end
|
|
354
|
+
private_class_method :cpu_percent_int
|
|
355
|
+
|
|
356
|
+
# seconds (Float) -> user-mode CPU 100 ns ticks for PerJobUserTimeLimit.
|
|
357
|
+
def self.cpu_time_ticks(v)
|
|
358
|
+
t = Float(v)
|
|
359
|
+
raise ArgumentError, "cpu_time must be > 0 seconds, got #{v.inspect}" unless t.positive?
|
|
360
|
+
|
|
361
|
+
(t * 10_000_000).round
|
|
362
|
+
end
|
|
363
|
+
private_class_method :cpu_time_ticks
|
|
364
|
+
|
|
365
|
+
# AssignProcessToJobObject — the fallback path for a process NOT placed at
|
|
366
|
+
# creation. Prefer Winproc.spawn(job: job).
|
|
367
|
+
def assign(process)
|
|
368
|
+
_assign(process)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# TerminateJobObject: kill every process in the job (and nested child jobs).
|
|
372
|
+
def terminate(exit_code = 1)
|
|
373
|
+
_terminate(exit_code)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Block until the job has zero active processes. true on empty, false on
|
|
377
|
+
# timeout. Only ONE wait_empty may be in flight per Job.
|
|
378
|
+
def wait_empty(timeout: nil)
|
|
379
|
+
Winproc.run_blocking { _wait_empty(Winproc.ms_for(timeout)) }
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# ----------------------------------------------------------------- Stream ---
|
|
384
|
+
class Stream
|
|
385
|
+
# Up to +maxlen+ bytes as a binary (ASCII-8BIT) String; nil at EOF. Blocks
|
|
386
|
+
# until at least 1 byte. Cooperative under a scheduler.
|
|
387
|
+
def read(maxlen = 65_536)
|
|
388
|
+
Winproc.run_blocking { _read(maxlen) }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Write ALL +bytes+ (loops on partial); returns the byte count. Binary-safe.
|
|
392
|
+
def write(bytes)
|
|
393
|
+
Winproc.run_blocking { _write(bytes) }
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def <<(bytes)
|
|
397
|
+
write(bytes)
|
|
398
|
+
self
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# -------------------------------------------------------------------- PTY ---
|
|
403
|
+
class PTY
|
|
404
|
+
# Raw bytes from the terminal output stream (UTF-8 text interleaved with VT
|
|
405
|
+
# escape sequences, verbatim). Binary (ASCII-8BIT) String; nil at EOF.
|
|
406
|
+
def read(maxlen = 65_536)
|
|
407
|
+
Winproc.run_blocking { @output._read(maxlen) }
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Bytes to the terminal input stream. Plain text = keystrokes; control keys
|
|
411
|
+
# are VT sequences. Send UTF-8. "\r" is Enter (not "\n").
|
|
412
|
+
def write(bytes)
|
|
413
|
+
Winproc.run_blocking { @input._write(bytes) }
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# ResizePseudoConsole. 1..32767 each (COORD is SHORT).
|
|
417
|
+
def resize(cols, rows)
|
|
418
|
+
_resize(Integer(cols), Integer(rows))
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# The child Process; use it for wait/kill/exitstatus.
|
|
422
|
+
def process = @process
|
|
423
|
+
|
|
424
|
+
# Deadlock-free teardown, exact order (§2.6):
|
|
425
|
+
# 1. kill: true and child alive -> process.kill
|
|
426
|
+
# 2. close the OUTPUT stream (cancels any in-flight read; the blocked
|
|
427
|
+
# reader raises Closed) — makes step 4 safe on pre-24H2 Windows
|
|
428
|
+
# 3. close the INPUT stream
|
|
429
|
+
# 4. ClosePseudoConsole(hPC) (GVL released; output pipe gone, no deadlock)
|
|
430
|
+
# 5. memoize the child's exit code if exited, then process.close
|
|
431
|
+
# Idempotent. After close, pty.process.exitstatus still answers (memoized).
|
|
432
|
+
def close(kill: true)
|
|
433
|
+
return nil if closed?
|
|
434
|
+
|
|
435
|
+
begin
|
|
436
|
+
@process.kill if kill && process_alive_safely?
|
|
437
|
+
rescue Winproc::Error
|
|
438
|
+
# best-effort; teardown must proceed
|
|
439
|
+
end
|
|
440
|
+
@output.close
|
|
441
|
+
@input.close
|
|
442
|
+
Winproc.run_blocking { _close_pty }
|
|
443
|
+
@process.close
|
|
444
|
+
nil
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def process_alive_safely?
|
|
448
|
+
@process.alive?
|
|
449
|
+
rescue Winproc::Closed
|
|
450
|
+
false
|
|
451
|
+
end
|
|
452
|
+
private :process_alive_safely?
|
|
453
|
+
end
|
|
454
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: winproc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ned
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: vcvars
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.1.1
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0.1'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.1.1
|
|
74
|
+
description: |
|
|
75
|
+
winproc is a native extension for controlling Windows processes the way the
|
|
76
|
+
OS intends: argv-array spawning with exact quoting and per-handle inheritance
|
|
77
|
+
(PROC_THREAD_ATTRIBUTE_HANDLE_LIST), job objects with kill-on-close so a
|
|
78
|
+
spawned tree can never outlive you (even across a crash), atomic job
|
|
79
|
+
placement at creation, ConPTY pseudoconsoles for real interactive terminal
|
|
80
|
+
I/O, and elevation helpers (elevated?/admin?, ShellExecuteEx "runas",
|
|
81
|
+
scoped token privileges). Blocking waits release the GVL and cooperate with
|
|
82
|
+
a fiber scheduler. Windows MSVC (mswin) Ruby only.
|
|
83
|
+
executables: []
|
|
84
|
+
extensions:
|
|
85
|
+
- ext/winproc/extconf.rb
|
|
86
|
+
extra_rdoc_files: []
|
|
87
|
+
files:
|
|
88
|
+
- CHANGELOG.md
|
|
89
|
+
- LICENSE.txt
|
|
90
|
+
- README.md
|
|
91
|
+
- ext/winproc/extconf.rb
|
|
92
|
+
- ext/winproc/winproc.c
|
|
93
|
+
- lib/winproc.rb
|
|
94
|
+
- lib/winproc/version.rb
|
|
95
|
+
homepage: https://github.com/main-path/winproc
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/main-path/winproc
|
|
100
|
+
source_code_uri: https://github.com/main-path/winproc
|
|
101
|
+
changelog_uri: https://github.com/main-path/winproc/blob/main/CHANGELOG.md
|
|
102
|
+
bug_tracker_uri: https://github.com/main-path/winproc/issues
|
|
103
|
+
rubygems_mfa_required: 'true'
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.1'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.6.9
|
|
119
|
+
specification_version: 4
|
|
120
|
+
summary: 'Windows process control for Ruby: job-object process trees, ConPTY terminals,
|
|
121
|
+
hygienic spawning, and UAC elevation helpers.'
|
|
122
|
+
test_files: []
|