acp_sdk_async 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 +7 -0
- data/LICENSE +24 -0
- data/README.md +147 -0
- data/lib/acp/agent.rb +201 -0
- data/lib/acp/client.rb +213 -0
- data/lib/acp/connection.rb +341 -0
- data/lib/acp/exceptions.rb +32 -0
- data/lib/acp/meta.rb +58 -0
- data/lib/acp/router.rb +119 -0
- data/lib/acp/schema.rb +1365 -0
- data/lib/acp/schema_base.rb +440 -0
- data/lib/acp/stdio.rb +162 -0
- data/lib/acp/transport.rb +134 -0
- data/lib/acp/version.rb +5 -0
- data/lib/acp/wait.rb +129 -0
- data/lib/acp_sdk.rb +3 -0
- data/lib/acp_sdk_async.rb +14 -0
- data/schema/VERSION +1 -0
- data/schema/meta.json +52 -0
- data/schema/schema.json +10295 -0
- metadata +90 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ACP
|
|
6
|
+
module Schema
|
|
7
|
+
class ValidationError < StandardError
|
|
8
|
+
attr_reader :path
|
|
9
|
+
|
|
10
|
+
def initialize(message, path = [])
|
|
11
|
+
@path = path
|
|
12
|
+
super(path.empty? ? message : "#{path.join('.')}: #{message}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.to_camel(snake)
|
|
17
|
+
parts = snake.to_s.split("_")
|
|
18
|
+
parts[0] + parts[1..].map(&:capitalize).join
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.to_snake(camel)
|
|
22
|
+
camel.to_s.gsub(/([A-Z])/) { "_#{$1.downcase}" }.sub(/\A_/, "")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.serialize(value)
|
|
26
|
+
case value
|
|
27
|
+
when Base then value.to_h
|
|
28
|
+
when Array then value.map { |v| serialize(v) }
|
|
29
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = serialize(v) }
|
|
30
|
+
else value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module Types
|
|
35
|
+
def self.resolve(spec)
|
|
36
|
+
case spec
|
|
37
|
+
when Symbol then Scalar.for(spec)
|
|
38
|
+
when String then Ref.new(spec)
|
|
39
|
+
when Array then List.new(resolve(spec.first))
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Unsupported type spec: #{spec.inspect}" unless spec.respond_to?(:coerce)
|
|
42
|
+
|
|
43
|
+
spec
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Scalar
|
|
48
|
+
KINDS = %i[string integer number boolean object any].freeze
|
|
49
|
+
|
|
50
|
+
def self.for(kind)
|
|
51
|
+
@instances ||= KINDS.to_h { |k| [k, new(k)] }
|
|
52
|
+
@instances.fetch(kind) { raise ArgumentError, "Unknown scalar type: #{kind.inspect}" }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :kind
|
|
56
|
+
|
|
57
|
+
def initialize(kind)
|
|
58
|
+
@kind = kind
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def coerce(value, path = [])
|
|
62
|
+
case @kind
|
|
63
|
+
when :any then value
|
|
64
|
+
when :string
|
|
65
|
+
return value if value.is_a?(String)
|
|
66
|
+
|
|
67
|
+
fail!(value, path)
|
|
68
|
+
when :integer
|
|
69
|
+
return value if value.is_a?(Integer)
|
|
70
|
+
return value.to_i if value.is_a?(Float) && value.finite? && value == value.floor
|
|
71
|
+
|
|
72
|
+
fail!(value, path)
|
|
73
|
+
when :number
|
|
74
|
+
return value if value.is_a?(Numeric)
|
|
75
|
+
|
|
76
|
+
fail!(value, path)
|
|
77
|
+
when :boolean
|
|
78
|
+
return value if value == true || value == false
|
|
79
|
+
|
|
80
|
+
fail!(value, path)
|
|
81
|
+
when :object
|
|
82
|
+
return value if value.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
fail!(value, path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def fail!(value, path)
|
|
91
|
+
raise ValidationError.new("expected #{@kind}, got #{value.class}", path)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
STRING = Scalar.for(:string)
|
|
96
|
+
INTEGER = Scalar.for(:integer)
|
|
97
|
+
NUMBER = Scalar.for(:number)
|
|
98
|
+
BOOLEAN = Scalar.for(:boolean)
|
|
99
|
+
OBJECT = Scalar.for(:object)
|
|
100
|
+
ANY = Scalar.for(:any)
|
|
101
|
+
|
|
102
|
+
class ProtocolVersion
|
|
103
|
+
def coerce(value, _path = [])
|
|
104
|
+
return value if value.is_a?(Integer)
|
|
105
|
+
|
|
106
|
+
Integer(value)
|
|
107
|
+
rescue ArgumentError, TypeError
|
|
108
|
+
1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
PROTOCOL_VERSION = ProtocolVersion.new
|
|
113
|
+
|
|
114
|
+
class Ref
|
|
115
|
+
attr_reader :name
|
|
116
|
+
|
|
117
|
+
def initialize(name)
|
|
118
|
+
@name = name
|
|
119
|
+
@target = nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def target
|
|
123
|
+
@target ||= Schema.const_get(@name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def coerce(value, path = [])
|
|
127
|
+
target.coerce(value, path)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class List
|
|
132
|
+
attr_reader :item
|
|
133
|
+
|
|
134
|
+
def initialize(item, skip_invalid: false)
|
|
135
|
+
@item = Types.resolve(item)
|
|
136
|
+
@skip_invalid = skip_invalid
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def coerce(value, path = [])
|
|
140
|
+
raise ValidationError.new("expected array, got #{value.class}", path) unless value.is_a?(Array)
|
|
141
|
+
|
|
142
|
+
result = []
|
|
143
|
+
value.each_with_index do |element, index|
|
|
144
|
+
begin
|
|
145
|
+
result << @item.coerce(element, path + [index])
|
|
146
|
+
rescue ValidationError
|
|
147
|
+
raise unless @skip_invalid
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class Map
|
|
155
|
+
def initialize(value_type)
|
|
156
|
+
@value_type = Types.resolve(value_type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def coerce(value, path = [])
|
|
160
|
+
raise ValidationError.new("expected object, got #{value.class}", path) unless value.is_a?(Hash)
|
|
161
|
+
|
|
162
|
+
value.each_with_object({}) do |(key, element), result|
|
|
163
|
+
result[key.to_s] = @value_type.coerce(element, path + [key.to_s])
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class Enum
|
|
169
|
+
attr_reader :values
|
|
170
|
+
|
|
171
|
+
def initialize(values, open: false)
|
|
172
|
+
@values = values.freeze
|
|
173
|
+
@open = open
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def coerce(value, path = [])
|
|
177
|
+
raise ValidationError.new("expected string, got #{value.class}", path) unless value.is_a?(String)
|
|
178
|
+
return value if @open || @values.include?(value)
|
|
179
|
+
|
|
180
|
+
raise ValidationError.new("unexpected value #{value.inspect}, allowed: #{@values.join(', ')}", path)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
class Union
|
|
185
|
+
attr_reader :tag, :tagged, :untagged
|
|
186
|
+
|
|
187
|
+
def initialize(tag: nil, tagged: {}, untagged: [])
|
|
188
|
+
@tag = tag
|
|
189
|
+
@tagged = tagged.transform_values { |list| Array(list).map { |t| Types.resolve(t) } }
|
|
190
|
+
@untagged = Array(untagged).map { |t| Types.resolve(t) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def variants
|
|
194
|
+
(@tagged.values.flatten + @untagged).map { |t| t.is_a?(Ref) ? t.target : t }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def coerce(value, path = [])
|
|
198
|
+
if value.is_a?(Base)
|
|
199
|
+
return value if variant_classes.any? { |variant| value.is_a?(variant) }
|
|
200
|
+
|
|
201
|
+
return coerce(value.to_h, path)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if @tag && value.is_a?(Hash)
|
|
205
|
+
tag_value = read_tag(value)
|
|
206
|
+
candidates = @tagged[tag_value] if tag_value
|
|
207
|
+
if candidates && !candidates.empty?
|
|
208
|
+
return try_variants(candidates, value, path) { |error| raise error }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
try_variants(@untagged, value, path) do
|
|
213
|
+
raise ValidationError.new(union_error_message(value), path)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def variant_classes
|
|
220
|
+
@variant_classes ||= variants.select { |variant| variant.is_a?(Class) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def read_tag(hash)
|
|
224
|
+
snake = Schema.to_snake(@tag)
|
|
225
|
+
[@tag, @tag.to_sym, snake, snake.to_sym].each do |key|
|
|
226
|
+
return hash[key] if hash.key?(key)
|
|
227
|
+
end
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def try_variants(candidates, value, path)
|
|
232
|
+
last_error = nil
|
|
233
|
+
candidates.each do |candidate|
|
|
234
|
+
return candidate.coerce(value, path)
|
|
235
|
+
rescue ValidationError => e
|
|
236
|
+
last_error = e
|
|
237
|
+
end
|
|
238
|
+
yield(last_error || ValidationError.new(union_error_message(value), path))
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def union_error_message(value)
|
|
242
|
+
if @tag && value.is_a?(Hash)
|
|
243
|
+
"no variant matches #{@tag}=#{read_tag(value).inspect}"
|
|
244
|
+
else
|
|
245
|
+
"no variant matches value of type #{value.class}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
class Base
|
|
252
|
+
FieldDef = Struct.new(
|
|
253
|
+
:name, :key, :type, :required, :default, :const, :default_on_error,
|
|
254
|
+
keyword_init: true
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
class << self
|
|
258
|
+
def fields
|
|
259
|
+
@fields ||= superclass.respond_to?(:fields) ? superclass.fields.dup : []
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def field_names
|
|
263
|
+
fields.map(&:name)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def field(name, type = :any, key: nil, required: false, default: nil, const: nil, default_on_error: false)
|
|
267
|
+
name = name.to_sym
|
|
268
|
+
fields.reject! { |f| f.name == name }
|
|
269
|
+
fields << FieldDef.new(
|
|
270
|
+
name: name,
|
|
271
|
+
key: key || Schema.to_camel(name),
|
|
272
|
+
type: Types.resolve(type),
|
|
273
|
+
required: required,
|
|
274
|
+
default: default,
|
|
275
|
+
const: const,
|
|
276
|
+
default_on_error: default_on_error
|
|
277
|
+
)
|
|
278
|
+
attr_reader name
|
|
279
|
+
define_method(:"#{name}=") do |value|
|
|
280
|
+
instance_variable_set(:"@#{name}", value)
|
|
281
|
+
@set_fields << name
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def coerce(value, path = [])
|
|
286
|
+
return value if value.is_a?(self)
|
|
287
|
+
raise ValidationError.new("expected object, got #{value.class}", path) unless value.is_a?(Hash)
|
|
288
|
+
|
|
289
|
+
instance = allocate
|
|
290
|
+
instance.send(:load_from_hash, value, path)
|
|
291
|
+
instance
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def from_hash(hash)
|
|
295
|
+
return nil if hash.nil?
|
|
296
|
+
|
|
297
|
+
coerce(hash)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
alias parse coerce
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
attr_reader :field_meta
|
|
304
|
+
|
|
305
|
+
def initialize(**kwargs)
|
|
306
|
+
@set_fields = []
|
|
307
|
+
@field_meta = kwargs.delete(:field_meta) || kwargs.delete(:_meta)
|
|
308
|
+
unknown = kwargs.keys - self.class.field_names
|
|
309
|
+
raise ArgumentError, "Unknown fields for #{self.class.name}: #{unknown.join(', ')}" unless unknown.empty?
|
|
310
|
+
|
|
311
|
+
self.class.fields.each do |f|
|
|
312
|
+
if kwargs.key?(f.name)
|
|
313
|
+
value = kwargs[f.name]
|
|
314
|
+
value = f.type.coerce(value, [f.key]) unless value.nil?
|
|
315
|
+
assign(f, value, set: !value.nil?)
|
|
316
|
+
elsif f.const
|
|
317
|
+
assign(f, f.const, set: true)
|
|
318
|
+
else
|
|
319
|
+
assign(f, default_for(f), set: false)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def field_meta=(value)
|
|
325
|
+
@field_meta = value
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def set?(name)
|
|
329
|
+
@set_fields.include?(name.to_sym)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def [](name)
|
|
333
|
+
public_send(name)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def to_h
|
|
337
|
+
result = {}
|
|
338
|
+
ordered_fields.each do |f|
|
|
339
|
+
next unless f.const || @set_fields.include?(f.name)
|
|
340
|
+
|
|
341
|
+
value = instance_variable_get(:"@#{f.name}")
|
|
342
|
+
next if value.nil?
|
|
343
|
+
|
|
344
|
+
result[f.key] = Schema.serialize(value)
|
|
345
|
+
end
|
|
346
|
+
result["_meta"] = Schema.serialize(@field_meta) if @field_meta
|
|
347
|
+
result
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def to_json(*args)
|
|
351
|
+
to_h.to_json(*args)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def ==(other)
|
|
355
|
+
other.class == self.class && other.to_h == to_h
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
alias eql? ==
|
|
359
|
+
|
|
360
|
+
def hash
|
|
361
|
+
[self.class, to_h].hash
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def inspect
|
|
365
|
+
"#<#{self.class.name} #{to_h.inspect}>"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
private
|
|
369
|
+
|
|
370
|
+
def ordered_fields
|
|
371
|
+
fields = self.class.fields
|
|
372
|
+
fields.select(&:const) + fields.reject(&:const)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def assign(field, value, set:)
|
|
376
|
+
instance_variable_set(:"@#{field.name}", value)
|
|
377
|
+
@set_fields << field.name if set
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def default_for(field)
|
|
381
|
+
default = field.default
|
|
382
|
+
return nil if default.nil?
|
|
383
|
+
|
|
384
|
+
default = deep_dup(default)
|
|
385
|
+
begin
|
|
386
|
+
field.type.coerce(default, [field.key])
|
|
387
|
+
rescue ValidationError
|
|
388
|
+
default
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def deep_dup(value)
|
|
393
|
+
case value
|
|
394
|
+
when Hash then value.transform_values { |v| deep_dup(v) }
|
|
395
|
+
when Array then value.map { |v| deep_dup(v) }
|
|
396
|
+
when String then value.dup
|
|
397
|
+
else value
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def load_from_hash(hash, path)
|
|
402
|
+
@set_fields = []
|
|
403
|
+
self.class.fields.each do |f|
|
|
404
|
+
present, raw = lookup(hash, f)
|
|
405
|
+
if present && !raw.nil?
|
|
406
|
+
load_present(f, raw, path)
|
|
407
|
+
elsif f.const
|
|
408
|
+
assign(f, f.const, set: true)
|
|
409
|
+
elsif f.required && !(present && f.default_on_error)
|
|
410
|
+
raise ValidationError.new("missing required field #{f.key}", path)
|
|
411
|
+
else
|
|
412
|
+
assign(f, default_for(f), set: false)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
meta = hash.key?("_meta") ? hash["_meta"] : hash[:_meta]
|
|
416
|
+
@field_meta = meta.is_a?(Hash) ? meta : nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def load_present(field, raw, path)
|
|
420
|
+
value = field.type.coerce(raw, path + [field.key])
|
|
421
|
+
if field.const && value != field.const
|
|
422
|
+
raise ValidationError.new("expected #{field.const.inspect}, got #{value.inspect}", path + [field.key])
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
assign(field, value, set: true)
|
|
426
|
+
rescue ValidationError
|
|
427
|
+
raise unless field.default_on_error
|
|
428
|
+
|
|
429
|
+
assign(field, default_for(field), set: false)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def lookup(hash, field)
|
|
433
|
+
[field.key, field.key.to_sym, field.name.to_s, field.name].each do |candidate|
|
|
434
|
+
return [true, hash[candidate]] if hash.key?(candidate)
|
|
435
|
+
end
|
|
436
|
+
[false, nil]
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
data/lib/acp/stdio.rb
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transport"
|
|
4
|
+
require_relative "client"
|
|
5
|
+
require_relative "wait"
|
|
6
|
+
|
|
7
|
+
module ACP
|
|
8
|
+
module Stdio
|
|
9
|
+
DEFAULT_INHERITED_ENV = %w[HOME LOGNAME PATH SHELL TERM USER].freeze
|
|
10
|
+
|
|
11
|
+
def self.default_environment
|
|
12
|
+
env = {}
|
|
13
|
+
DEFAULT_INHERITED_ENV.each do |key|
|
|
14
|
+
value = ENV.fetch(key, nil)
|
|
15
|
+
next if value.nil? || value.start_with?("()")
|
|
16
|
+
|
|
17
|
+
env[key] = value
|
|
18
|
+
end
|
|
19
|
+
env
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.stdio_streams(input = $stdin, output = $stdout)
|
|
23
|
+
input.binmode if input.respond_to?(:binmode)
|
|
24
|
+
output.binmode if output.respond_to?(:binmode)
|
|
25
|
+
input.set_encoding("UTF-8") if input.respond_to?(:set_encoding)
|
|
26
|
+
output.set_encoding("UTF-8") if output.respond_to?(:set_encoding)
|
|
27
|
+
output.sync = true if output.respond_to?(:sync=)
|
|
28
|
+
[input, output]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.spawn_agent(command, *args, env: nil, cwd: nil, stderr: :log, receive_timeout: nil)
|
|
32
|
+
merged = default_environment
|
|
33
|
+
merged.merge!(env.transform_keys(&:to_s)) if env
|
|
34
|
+
|
|
35
|
+
reader, writer = IO.pipe
|
|
36
|
+
child_reader, child_writer = IO.pipe
|
|
37
|
+
stderr_reader, stderr_writer = stderr == :log ? IO.pipe : [nil, nil]
|
|
38
|
+
|
|
39
|
+
options = { in: child_reader, out: writer }
|
|
40
|
+
options[:chdir] = cwd.to_s if cwd
|
|
41
|
+
options[:err] = case stderr
|
|
42
|
+
when :log then stderr_writer
|
|
43
|
+
when :inherit then $stderr
|
|
44
|
+
when :discard then File::NULL
|
|
45
|
+
when IO then stderr
|
|
46
|
+
else raise ArgumentError, "Unsupported stderr option: #{stderr.inspect}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
pid = Process.spawn(merged, command, *args, **options)
|
|
50
|
+
child_reader.close
|
|
51
|
+
writer.close
|
|
52
|
+
stderr_writer&.close
|
|
53
|
+
|
|
54
|
+
reader.set_encoding("UTF-8")
|
|
55
|
+
child_writer.set_encoding("UTF-8")
|
|
56
|
+
child_writer.sync = true
|
|
57
|
+
|
|
58
|
+
drain = stderr_reader ? start_stderr_drain(stderr_reader, "#{File.basename(command)}[#{pid}]") : nil
|
|
59
|
+
|
|
60
|
+
transport = NdjsonTransport.new(reader, child_writer, receive_timeout: receive_timeout)
|
|
61
|
+
AgentProcess.new(pid: pid, transport: transport, stdin: child_writer, stdout: reader, stderr_drain: drain)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.start_stderr_drain(io, label)
|
|
65
|
+
io.set_encoding("UTF-8")
|
|
66
|
+
Wait.spawn("acp-stderr-#{label}") do
|
|
67
|
+
io.each_line do |line|
|
|
68
|
+
line = line.scrub unless line.valid_encoding?
|
|
69
|
+
ACP.logger.info("acp[#{label}] stderr: #{line.chomp}")
|
|
70
|
+
end
|
|
71
|
+
rescue IOError, Errno::EBADF, ::Async::Cancel
|
|
72
|
+
nil
|
|
73
|
+
ensure
|
|
74
|
+
io.close unless io.closed?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class AgentProcess
|
|
79
|
+
attr_reader :pid, :transport
|
|
80
|
+
|
|
81
|
+
def initialize(pid:, transport:, stdin:, stdout:, stderr_drain: nil)
|
|
82
|
+
@pid = pid
|
|
83
|
+
@transport = transport
|
|
84
|
+
@stdin = stdin
|
|
85
|
+
@stdout = stdout
|
|
86
|
+
@stderr_drain = stderr_drain
|
|
87
|
+
@status = nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def connect(client_handler, start_listening: true, **options)
|
|
91
|
+
connection = Client::Connection.new(client_handler, @transport, **options)
|
|
92
|
+
connection.start if start_listening
|
|
93
|
+
connection
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def alive?
|
|
97
|
+
return false if @status
|
|
98
|
+
|
|
99
|
+
Process.kill(0, @pid)
|
|
100
|
+
true
|
|
101
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def wait
|
|
106
|
+
return @status if @status
|
|
107
|
+
|
|
108
|
+
_, status = Process.wait2(@pid)
|
|
109
|
+
@status = status
|
|
110
|
+
rescue Errno::ECHILD
|
|
111
|
+
@status
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def kill(signal = "TERM")
|
|
115
|
+
Process.kill(signal, @pid)
|
|
116
|
+
rescue Errno::ESRCH
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def close(timeout: 2.0)
|
|
121
|
+
@stdin.close unless @stdin.closed?
|
|
122
|
+
@transport.close
|
|
123
|
+
return finish if wait_with_timeout(timeout)
|
|
124
|
+
|
|
125
|
+
kill
|
|
126
|
+
return finish if wait_with_timeout(timeout)
|
|
127
|
+
|
|
128
|
+
kill("KILL")
|
|
129
|
+
wait
|
|
130
|
+
finish
|
|
131
|
+
rescue StandardError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def finish
|
|
138
|
+
Wait.join(@stderr_drain, 1.0)
|
|
139
|
+
@status
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def wait_with_timeout(timeout)
|
|
143
|
+
return true if @status
|
|
144
|
+
|
|
145
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
146
|
+
loop do
|
|
147
|
+
_, status = Process.wait2(@pid, Process::WNOHANG)
|
|
148
|
+
if status
|
|
149
|
+
@status = status
|
|
150
|
+
return true
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
154
|
+
|
|
155
|
+
sleep 0.02
|
|
156
|
+
end
|
|
157
|
+
rescue Errno::ECHILD
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|