rubik 0.1.6 → 0.2.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 +4 -4
- data/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +88 -0
- data/README.md +30 -17
- data/lib/rubik/ast/extraction_result.rb +37 -0
- data/lib/rubik/ast/extractor.rb +35 -9
- data/lib/rubik/ast/graph.rb +27 -1
- data/lib/rubik/ast/treesitter/c_walker.rb +136 -28
- data/lib/rubik/ast/treesitter_extractor.rb +45 -17
- data/lib/rubik/buffer/apply_contract.rb +78 -0
- data/lib/rubik/buffer/manager.rb +61 -5
- data/lib/rubik/concurrency/event_loop.rb +56 -44
- data/lib/rubik/concurrency/scheduler.rb +7 -24
- data/lib/rubik/context/git_lister.rb +6 -1
- data/lib/rubik/context/scanner.rb +15 -3
- data/lib/rubik/mcp/transport/stdio.rb +14 -11
- data/lib/rubik/platform/process.rb +74 -2
- data/lib/rubik/plugin/manager.rb +148 -71
- data/lib/rubik/plugin/package_inventory.rb +84 -0
- data/lib/rubik/plugin/requires_dag.rb +40 -0
- data/lib/rubik/plugin/storage.rb +100 -14
- data/lib/rubik/repl/commands/buffer.rb +28 -18
- data/lib/rubik/runtime/repair_probe.rb +98 -0
- data/lib/rubik/security/blocked_guidance.rb +19 -0
- data/lib/rubik/security/guard.rb +115 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +129 -136
- data/lib/rubik/version.rb +1 -1
- metadata +6 -1
|
@@ -20,7 +20,8 @@ module Rubik
|
|
|
20
20
|
# the loop thread wakes immediately.
|
|
21
21
|
# * Wake staleness is handled by park generations: every registration
|
|
22
22
|
# stamps @gen[fiber]; timer fires and io dispatch only push when the
|
|
23
|
-
# generation still matches
|
|
23
|
+
# generation still matches at dispatch, including queued wakeups,
|
|
24
|
+
# so a fiber that timed out and re-parked
|
|
24
25
|
# elsewhere can never be resumed by a stale source.
|
|
25
26
|
#
|
|
26
27
|
# Drain model (waiters, not liveness — Fiber#alive? does not exist on
|
|
@@ -48,8 +49,8 @@ module Rubik
|
|
|
48
49
|
|
|
49
50
|
def initialize
|
|
50
51
|
@mutex = ::Mutex.new
|
|
51
|
-
@runnable = [] # [[fiber, resume_value]] — guarded by @mutex
|
|
52
|
-
@parked = {} # fiber =>
|
|
52
|
+
@runnable = [] # [[fiber, resume_value, park_generation]] — guarded by @mutex
|
|
53
|
+
@parked = {} # fiber => generation (block-hook parks) — guarded
|
|
53
54
|
@waiter_threads = [] # process-wait threads — guarded
|
|
54
55
|
# Stale timers (fiber re-parked or resumed since registration) must
|
|
55
56
|
# not gate draining — @gen is bumped on every resume, so any of this
|
|
@@ -77,11 +78,11 @@ module Rubik
|
|
|
77
78
|
|
|
78
79
|
# Queues a fiber to be resumed with value. Safe from foreign threads:
|
|
79
80
|
# pushes write a self-pipe byte so the loop's IO.select wakes at once.
|
|
80
|
-
def push(fiber, value: true)
|
|
81
|
+
def push(fiber, value: true, generation: nil)
|
|
81
82
|
@mutex.synchronize do
|
|
82
83
|
return if @closed
|
|
83
84
|
|
|
84
|
-
@runnable << [fiber, value]
|
|
85
|
+
@runnable << [fiber, value, generation]
|
|
85
86
|
end
|
|
86
87
|
kick unless ::Thread.current.equal?(@loop_thread)
|
|
87
88
|
end
|
|
@@ -95,8 +96,8 @@ module Rubik
|
|
|
95
96
|
@mutex.synchronize do
|
|
96
97
|
return if @closed
|
|
97
98
|
|
|
98
|
-
@parked.delete(fiber)
|
|
99
|
-
@runnable << [fiber, value]
|
|
99
|
+
generation = @parked.delete(fiber)
|
|
100
|
+
@runnable << [fiber, value, generation]
|
|
100
101
|
end
|
|
101
102
|
kick unless ::Thread.current.equal?(@loop_thread)
|
|
102
103
|
end
|
|
@@ -149,8 +150,8 @@ module Rubik
|
|
|
149
150
|
# timeout of 0 still registers a timer (fires immediately) — truthiness
|
|
150
151
|
# guards would park join(0)/wait(0) polls forever.
|
|
151
152
|
def park(_blocker, timeout = nil)
|
|
152
|
-
@mutex.synchronize { @parked[::Fiber.current] = true }
|
|
153
153
|
value = park_fiber do |fiber, gen|
|
|
154
|
+
@mutex.synchronize { @parked[fiber] = gen }
|
|
154
155
|
@timers.add(monotonic + timeout, fiber, :timeout, gen) unless timeout.nil?
|
|
155
156
|
end
|
|
156
157
|
value == :timeout ? false : value
|
|
@@ -172,49 +173,52 @@ module Rubik
|
|
|
172
173
|
value
|
|
173
174
|
end
|
|
174
175
|
|
|
175
|
-
#
|
|
176
|
-
# the
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# itself routed back through scheduler.io_read once the hook exists.
|
|
184
|
-
# The Fiber.blocking wrap is the VM-sanctioned escape (ruby's own test
|
|
185
|
-
# scheduler wraps its primitives the same way): it runs the primitive
|
|
186
|
-
# immediately, bypassing interception, and read_nonblock(exception:
|
|
187
|
-
# false) never blocks the OS thread.
|
|
188
|
-
def read_io(io, buffer)
|
|
189
|
-
length = buffer.size
|
|
190
|
-
return 0 if length.zero?
|
|
176
|
+
# A zero minimum means one nonblocking attempt: return -errno to
|
|
177
|
+
# the VM instead of parking here, so read_nonblock preserves the
|
|
178
|
+
# caller's subsequent IO#wait_readable deadline (e.g. Net::HTTP).
|
|
179
|
+
# Fiber.blocking prevents the primitive from re-entering this hook.
|
|
180
|
+
def read_io(io, buffer, minimum = 0, offset = 0)
|
|
181
|
+
transferred = 0
|
|
182
|
+
capacity = buffer.size - offset
|
|
183
|
+
return 0 if capacity.zero?
|
|
191
184
|
|
|
192
185
|
loop do
|
|
193
|
-
chunk = ::Fiber.blocking { io.read_nonblock(
|
|
186
|
+
chunk = ::Fiber.blocking { io.read_nonblock(capacity - transferred, exception: false) }
|
|
194
187
|
case chunk
|
|
195
|
-
when :wait_readable
|
|
196
|
-
|
|
197
|
-
|
|
188
|
+
when :wait_readable, :wait_writable
|
|
189
|
+
return transferred.positive? ? transferred : -Errno::EAGAIN::Errno if transferred >= minimum
|
|
190
|
+
|
|
191
|
+
wait_io(io, chunk == :wait_readable ? READABLE : WRITABLE, nil)
|
|
192
|
+
when nil then return transferred
|
|
198
193
|
else
|
|
199
|
-
buffer.
|
|
200
|
-
|
|
194
|
+
buffer.set_string(chunk, offset + transferred)
|
|
195
|
+
transferred += chunk.bytesize
|
|
196
|
+
return transferred if transferred >= minimum || transferred == capacity
|
|
201
197
|
end
|
|
202
198
|
end
|
|
199
|
+
rescue SystemCallError => e
|
|
200
|
+
-e.errno
|
|
203
201
|
end
|
|
204
202
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
data = buffer.get_string(0, buffer.size)
|
|
210
|
-
return 0 if data.empty?
|
|
203
|
+
def write_io(io, buffer, minimum = 0, offset = 0)
|
|
204
|
+
transferred = 0
|
|
205
|
+
capacity = buffer.size - offset
|
|
206
|
+
return 0 if capacity.zero?
|
|
211
207
|
|
|
212
208
|
loop do
|
|
209
|
+
data = buffer.get_string(offset + transferred, capacity - transferred)
|
|
213
210
|
written = ::Fiber.blocking { io.write_nonblock(data, exception: false) }
|
|
214
|
-
|
|
211
|
+
if written.is_a?(::Integer)
|
|
212
|
+
transferred += written
|
|
213
|
+
return transferred if transferred >= minimum || transferred == capacity
|
|
214
|
+
else
|
|
215
|
+
return transferred.positive? ? transferred : -Errno::EAGAIN::Errno if transferred >= minimum
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
wait_io(io, written == :wait_readable ? READABLE : WRITABLE, nil)
|
|
218
|
+
end
|
|
217
219
|
end
|
|
220
|
+
rescue SystemCallError => e
|
|
221
|
+
-e.errno
|
|
218
222
|
end
|
|
219
223
|
|
|
220
224
|
# DNS resolution parks the fiber; the DnsPool worker delivers the list
|
|
@@ -345,7 +349,9 @@ module Rubik
|
|
|
345
349
|
|
|
346
350
|
def run_batch
|
|
347
351
|
while (item = @mutex.synchronize { @runnable.shift })
|
|
348
|
-
fiber, value = item
|
|
352
|
+
fiber, value, generation = item
|
|
353
|
+
next if generation && @gen[fiber] != generation
|
|
354
|
+
|
|
349
355
|
# A resumed fiber is definitionally no longer parked; dropping the
|
|
350
356
|
# registration here keeps interrupt-style wakes from leaking parks.
|
|
351
357
|
# Bumping the park generation invalidates every OTHER outstanding
|
|
@@ -369,7 +375,9 @@ module Rubik
|
|
|
369
375
|
end
|
|
370
376
|
|
|
371
377
|
def select_or_wait
|
|
372
|
-
read_set = @io_waiters.
|
|
378
|
+
read_set = @io_waiters.each_with_object([]) do |(io, slot), acc|
|
|
379
|
+
acc << io unless slot[:read].empty?
|
|
380
|
+
end
|
|
373
381
|
write_set = @io_waiters.each_with_object([]) do |(io, slot), acc|
|
|
374
382
|
acc << io unless slot[:write].empty?
|
|
375
383
|
end
|
|
@@ -407,7 +415,7 @@ module Rubik
|
|
|
407
415
|
next if gen != STICKY && @gen[fiber] != gen # stale: fiber moved on after registering
|
|
408
416
|
|
|
409
417
|
drop_io_interest(fiber)
|
|
410
|
-
push(fiber, value: value)
|
|
418
|
+
push(fiber, value: value, generation: gen == STICKY ? nil : gen)
|
|
411
419
|
end
|
|
412
420
|
end
|
|
413
421
|
|
|
@@ -420,7 +428,9 @@ module Rubik
|
|
|
420
428
|
slot = @io_waiters[io]
|
|
421
429
|
return unless slot
|
|
422
430
|
|
|
423
|
-
waiters =
|
|
431
|
+
waiters = {}
|
|
432
|
+
waiters.merge!(slot[:read]) if events.anybits?(READABLE)
|
|
433
|
+
waiters.merge!(slot[:write]) if events.anybits?(WRITABLE)
|
|
424
434
|
waiters.each do |fiber, gen|
|
|
425
435
|
if @gen[fiber] != gen
|
|
426
436
|
# Stale interest (the fiber timed out or moved on): remove it so
|
|
@@ -434,7 +444,7 @@ module Rubik
|
|
|
434
444
|
# it re-registers with a fresh generation.
|
|
435
445
|
slot[:read].delete(fiber)
|
|
436
446
|
slot[:write].delete(fiber)
|
|
437
|
-
push(fiber, value: events)
|
|
447
|
+
push(fiber, value: events, generation: gen)
|
|
438
448
|
end
|
|
439
449
|
@io_waiters.delete(io) if slot[:read].empty? && slot[:write].empty?
|
|
440
450
|
end
|
|
@@ -460,7 +470,9 @@ module Rubik
|
|
|
460
470
|
slot = @io_waiters.delete(io)
|
|
461
471
|
next unless slot
|
|
462
472
|
|
|
463
|
-
|
|
473
|
+
slot[:read].merge(slot[:write]).each do |fiber, gen|
|
|
474
|
+
push(fiber, value: 0, generation: gen)
|
|
475
|
+
end
|
|
464
476
|
pruned += 1
|
|
465
477
|
end
|
|
466
478
|
pruned
|
|
@@ -5,23 +5,9 @@ require_relative "process_waiter"
|
|
|
5
5
|
|
|
6
6
|
module Rubik
|
|
7
7
|
module Concurrency
|
|
8
|
-
# Adapter for the VM's Fiber::Scheduler
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# fiber 4.0: fiber(&blk) — scheduler creates and returns the fiber.
|
|
13
|
-
# 3.3: fiber(fiber, block) — VM pre-created it.
|
|
14
|
-
# io_read integer args differ (3.3 passes length; 4.0 passes 0, 0)
|
|
15
|
-
# io_write but the buffer is always sized to the operation, so both
|
|
16
|
-
# hooks drive off buffer.size — see EventLoop#read_io.
|
|
17
|
-
#
|
|
18
|
-
# io_read/io_write are required on 4.0: without them a plain IO#read
|
|
19
|
-
# inside a scheduled fiber falls back to a whole-thread BLOCKING read
|
|
20
|
-
# (measured: 20 × 0.2 s child pipes serialized into 13 s). The buffer is
|
|
21
|
-
# filled/drained with read_nonblock/write_nonblock + IO::Buffer views —
|
|
22
|
-
# IO#nonblock= and IO::Buffer#read(io,...) are NOT usable on Windows
|
|
23
|
-
# builds (NoMethodError / constant-zero reads, verified), while the
|
|
24
|
-
# nonblock(…, exception: false) family works for pipes and sockets alike.
|
|
8
|
+
# Adapter for the VM's Fiber::Scheduler hooks. IO hooks receive a
|
|
9
|
+
# minimum transfer length and a buffer offset. A zero minimum requests
|
|
10
|
+
# one nonblocking attempt; the VM owns subsequent waits and deadlines.
|
|
25
11
|
class Scheduler
|
|
26
12
|
attr_reader :loop
|
|
27
13
|
|
|
@@ -42,15 +28,12 @@ module Rubik
|
|
|
42
28
|
@loop.wait_io(io, events, timeout)
|
|
43
29
|
end
|
|
44
30
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# EventLoop drives both hooks off buffer.size — see read_io/write_io.
|
|
48
|
-
def io_read(io, buffer, *_protocol_args)
|
|
49
|
-
@loop.read_io(io, buffer)
|
|
31
|
+
def io_read(io, buffer, length, offset = 0)
|
|
32
|
+
@loop.read_io(io, buffer, length, offset)
|
|
50
33
|
end
|
|
51
34
|
|
|
52
|
-
def io_write(io, buffer,
|
|
53
|
-
@loop.write_io(io, buffer)
|
|
35
|
+
def io_write(io, buffer, length, offset = 0)
|
|
36
|
+
@loop.write_io(io, buffer, length, offset)
|
|
54
37
|
end
|
|
55
38
|
|
|
56
39
|
def kernel_sleep(duration = nil)
|
|
@@ -21,7 +21,12 @@ module Rubik
|
|
|
21
21
|
)
|
|
22
22
|
return nil unless status.success?
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
# PERF: NUL-split with conditional in-place backslash translation and
|
|
25
|
+
# in-place deduplication avoids intermediate array and string allocations.
|
|
26
|
+
res = out.split("\x00", -1).reject(&:empty?)
|
|
27
|
+
res.map! { |p| p.tr("\\", "/") } if out.include?("\\")
|
|
28
|
+
res.uniq!
|
|
29
|
+
res
|
|
25
30
|
rescue StandardError
|
|
26
31
|
nil
|
|
27
32
|
end
|
|
@@ -196,8 +196,9 @@ module Rubik
|
|
|
196
196
|
}
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
# PERF: integer math avoids Rational allocation from Time#to_r (~5x faster, zero allocation)
|
|
199
200
|
def mtime_micros(stat)
|
|
200
|
-
(stat.mtime.
|
|
201
|
+
(stat.mtime.tv_sec * 1_000_000) + stat.mtime.tv_usec
|
|
201
202
|
end
|
|
202
203
|
|
|
203
204
|
def walk_dir(base_dir, rel_prefix, key_prefix, ignore_patterns, previous, needles)
|
|
@@ -325,6 +326,15 @@ module Rubik
|
|
|
325
326
|
# depth. subtree_ignored? is a pure function of (dir, needles), so
|
|
326
327
|
# the memo is exact.
|
|
327
328
|
dir_verdicts = Hash.new { |h, dir| h[dir] = subtree_ignored?(dir, needles) }
|
|
329
|
+
|
|
330
|
+
# PERF: filter ignore_patterns to exclude prunable subtree patterns that
|
|
331
|
+
# dir_verdicts already checked on ancestor directory segments.
|
|
332
|
+
file_patterns = ignore_patterns.reject do |pat|
|
|
333
|
+
m = PRUNABLE_PATTERN.match(pat)
|
|
334
|
+
m && !m[1].include?("*") && !m[1].include?("?") && !m[1].include?("[")
|
|
335
|
+
end
|
|
336
|
+
file_rules = compile_ignore_regexes(file_patterns)
|
|
337
|
+
|
|
328
338
|
order_like_walker(rel_paths).each do |rel|
|
|
329
339
|
# PERF: zero-allocation ancestor check using string slicing instead of Array#split and Array#join
|
|
330
340
|
ignored_dir = false
|
|
@@ -337,7 +347,8 @@ module Rubik
|
|
|
337
347
|
idx += 1
|
|
338
348
|
end
|
|
339
349
|
next if ignored_dir
|
|
340
|
-
next if
|
|
350
|
+
next if rel == ".rubik" || rel.start_with?(".rubik/") || rel == ".git" || rel.start_with?(".git/")
|
|
351
|
+
next if file_rules.any? { |r| pattern_match?(r, rel) }
|
|
341
352
|
next unless included?(rel, @custom_includes)
|
|
342
353
|
|
|
343
354
|
full = "#{base_dir}/#{rel}"
|
|
@@ -370,8 +381,9 @@ module Rubik
|
|
|
370
381
|
# PERF: replacing "/" with NUL byte ("\x00") is equivalent to array component
|
|
371
382
|
# comparison because NUL (0x00) is strictly less than any valid path byte,
|
|
372
383
|
# avoiding intermediate Array allocations per path (~8.4x faster, ~62.5% fewer allocations).
|
|
384
|
+
# PERF: in-place sort_by! avoids array re-allocation
|
|
373
385
|
def order_like_walker(paths)
|
|
374
|
-
paths.sort_by { |p| p.tr("/", "\x00") }
|
|
386
|
+
paths.sort_by! { |p| p.tr("/", "\x00") }
|
|
375
387
|
end
|
|
376
388
|
|
|
377
389
|
def parse_gitignore(dir = @root_dir)
|
|
@@ -29,6 +29,7 @@ module Rubik
|
|
|
29
29
|
@mutex = Mutex.new
|
|
30
30
|
@cv = ConditionVariable.new
|
|
31
31
|
@responses = {}
|
|
32
|
+
@closed = false
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def open
|
|
@@ -78,6 +79,7 @@ module Rubik
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
@running = true
|
|
82
|
+
@closed = false
|
|
81
83
|
@req_id = 0
|
|
82
84
|
@responses.clear
|
|
83
85
|
|
|
@@ -118,8 +120,9 @@ module Rubik
|
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
def close
|
|
121
|
-
return
|
|
123
|
+
return if @closed
|
|
122
124
|
|
|
125
|
+
@closed = true
|
|
123
126
|
@running = false
|
|
124
127
|
|
|
125
128
|
begin
|
|
@@ -128,6 +131,16 @@ module Rubik
|
|
|
128
131
|
nil
|
|
129
132
|
end
|
|
130
133
|
|
|
134
|
+
if @wait_thr&.alive?
|
|
135
|
+
Rubik::Platform.kill_tree(@wait_thr.pid)
|
|
136
|
+
begin
|
|
137
|
+
@wait_thr.join(0.5)
|
|
138
|
+
rescue StandardError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
@wait_thr = nil
|
|
143
|
+
|
|
131
144
|
[@stdin, @stdout, @stderr].each do |io|
|
|
132
145
|
io&.close unless io&.closed?
|
|
133
146
|
rescue StandardError
|
|
@@ -142,16 +155,6 @@ module Rubik
|
|
|
142
155
|
@reader_thr = nil
|
|
143
156
|
end
|
|
144
157
|
|
|
145
|
-
if @wait_thr&.alive?
|
|
146
|
-
Rubik::Platform.kill_tree(@wait_thr.pid)
|
|
147
|
-
begin
|
|
148
|
-
@wait_thr.join(0.5)
|
|
149
|
-
rescue StandardError
|
|
150
|
-
nil
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
@wait_thr = nil
|
|
154
|
-
|
|
155
158
|
@mutex.synchronize do
|
|
156
159
|
@responses.clear
|
|
157
160
|
@cv.broadcast
|
|
@@ -21,18 +21,90 @@ module Rubik
|
|
|
21
21
|
nil
|
|
22
22
|
end
|
|
23
23
|
else
|
|
24
|
+
# On Unix/Linux, collect descendant PIDs so child processes are not
|
|
25
|
+
# orphaned when the parent process is killed.
|
|
26
|
+
descendants = descendant_pids(pid)
|
|
27
|
+
descendants.each do |cpid|
|
|
28
|
+
begin
|
|
29
|
+
::Process.kill("TERM", cpid)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
begin
|
|
34
|
+
::Process.kill("KILL", cpid)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
24
39
|
begin
|
|
25
40
|
::Process.kill("TERM", pid)
|
|
26
|
-
rescue
|
|
41
|
+
rescue StandardError
|
|
27
42
|
nil
|
|
28
43
|
end
|
|
29
44
|
begin
|
|
30
45
|
::Process.kill("KILL", pid)
|
|
31
|
-
rescue
|
|
46
|
+
rescue StandardError
|
|
32
47
|
nil
|
|
33
48
|
end
|
|
34
49
|
end
|
|
35
50
|
end
|
|
51
|
+
|
|
52
|
+
# Collect all descendant process IDs recursively (leaves first).
|
|
53
|
+
def descendant_pids(pid)
|
|
54
|
+
return [] unless pid&.positive?
|
|
55
|
+
|
|
56
|
+
collected = []
|
|
57
|
+
queue = [pid]
|
|
58
|
+
until queue.empty?
|
|
59
|
+
curr = queue.shift
|
|
60
|
+
children = direct_child_pids(curr)
|
|
61
|
+
children.each do |cpid|
|
|
62
|
+
next if collected.include?(cpid) || cpid == pid
|
|
63
|
+
|
|
64
|
+
collected << cpid
|
|
65
|
+
queue << cpid
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
collected.reverse
|
|
69
|
+
rescue StandardError
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def direct_child_pids(pid)
|
|
76
|
+
task_dir = "/proc/#{pid}/task"
|
|
77
|
+
if File.directory?(task_dir)
|
|
78
|
+
pids = []
|
|
79
|
+
Dir.glob("#{task_dir}/*/children").each do |path|
|
|
80
|
+
content = begin
|
|
81
|
+
File.read(path)
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
pids.concat(content.split.map(&:to_i)) if content
|
|
86
|
+
end
|
|
87
|
+
return pids.reject(&:zero?) unless pids.empty?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
out = begin
|
|
91
|
+
`pgrep -P #{pid} 2>/dev/null`
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
return out.lines.map(&:to_i).reject(&:zero?) if out && !out.empty?
|
|
96
|
+
|
|
97
|
+
out = begin
|
|
98
|
+
`ps --ppid #{pid} -o pid= 2>/dev/null`
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
return out.lines.map(&:to_i).reject(&:zero?) if out && !out.empty?
|
|
103
|
+
|
|
104
|
+
[]
|
|
105
|
+
rescue StandardError
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
36
108
|
end
|
|
37
109
|
end
|
|
38
110
|
end
|