ctx_driver 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f464dca50ead7d5fa1f1febf62807a0aa46647176a4c3e7a3b88f68bfe5cd37
4
- data.tar.gz: 2f2df95523cb1f684717696dac14dfbad9c774f80479e5e3513f6a648f2886e9
3
+ metadata.gz: f367f0f7740b02d2812c31c9ec1b53f89425b7c7edf5f5d11543aceb625d2e50
4
+ data.tar.gz: f2a16b61cda4b685acd25e972f4609f6dd5cebc89acfc73e059dd93a18160222
5
5
  SHA512:
6
- metadata.gz: 0452e5b24a4b7f6e2d96546a7babd4e54e87e95cb7eb3dab07a6eab15b37e426a7d0dad8b25ff84cbad4b535c74b590aea5fed95d51170997601ddab02132aed
7
- data.tar.gz: d0ab0794e5c4dfe19937abdb8dac61022822306eba0b849779b88f91a2bba2ef81c02d399835698e0bc1f9141a9a994927bc4be5e1aa0ab23c37a9bc35f14999
6
+ metadata.gz: ce35190d1e7ea2121026ec74cc63014f309896bd4a89be594145027ded978677a99fd3e28a8db23435dbc62408673a4cc49129d3965d67da7bcd09c28039d46f
7
+ data.tar.gz: f0fda5a4aa24f72689229146e19adfc2a7ea0d22af61e77c08b1f8c5f1b751c924718373fdfbe5884b75384053dcfd9f5fa4f8140f33c33bc22984a9255cfb79
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  Changes to the public contract are recorded here. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.2.3 (2026-10-18)
6
+
7
+ - Hide borrowed session, clock and response internals from inspection and pretty printing, including response string interpolation, without changing explicit output access.
8
+ - Reuse frozen clock values directly while retaining defensive snapshots for mutable times.
9
+
10
+ ## 0.2.2 (2026-10-18)
11
+
12
+ - Adapt optional native sessions to net-connector 0.13.0 and newer, including command results, terminal decoding and safe error codes.
13
+ - Preserve response bodies before observation-clock callbacks can reuse their buffers, while retaining source evidence when decoding fails.
14
+ - Remove padded first-line command echoes without changing body indentation, and validate one-command input before consulting native sessions.
15
+ - Centralize command-text validation and safely share frozen command, body and timestamp values to avoid redundant snapshot allocations.
16
+ - Verify both dependency-free offline loading and the optional adapter through an installed package and a local PTY process.
17
+
5
18
  ## 0.2.1 (2026-10-17)
6
19
 
7
20
  - Correct installation documentation to state the required ctx_parser 0.2.x API.
data/README-zh.md CHANGED
@@ -72,17 +72,19 @@ puts "SHA-256: #{capture.evidence.first[:sha256]}"
72
72
 
73
73
  ## 公开契约与限制
74
74
 
75
- `Reader.new(session:, clock:).read(command)` 返回 `Capture`。通过 `success?`、`skipped?`、`failure`、`evidence` 检查结果;正文仅显式通过 `body` 读取,`to_h`、`as_json`、`inspect` 不包含正文。
75
+ `Reader.new(session:, clock:).read(command)` 返回 `Capture`。通过 `success?`、`skipped?`、`failure`、`evidence` 检查结果;正文仅显式通过 `body` 读取,`to_h`、`as_json`、`inspect` 不包含正文。`Reader`、借用会话适配器 `Session` 及其回执的 `inspect`/`PP` 不展开调用方对象,也不查询连接状态;回执的字符串插值同样安全。原始输出与解码正文仍可通过回执的显式 API 访问。
76
76
 
77
77
  自定义会话实现 `connected?` 和 `send_commands([command])`。回执提供 `output`、布尔 `success?`、数组 `steps`、成功正文 `value!` 及失败时的 `error`。成功读取必须只有一个完成步骤。只有会话显式声明 `preserve_session_on_rejection:` 关键字时才传入该选项。
78
78
 
79
+ net-connector 仍为可选依赖,使用适配器的宿主需自行声明 `gem "net-connector", ">= 0.13.0"`。`examples/connector.rb` 通过本地 PTY 子进程验证已安装连接器,不连接网络设备。
80
+
79
81
  单个 reader 顺序使用;跨设备并发分别创建 reader。连接、关闭、凭据和批准命令选择由宿主负责,Reader 不判断任意命令是否只读。普通执行/回执异常转为安全诊断,取消信号继续传播。执行或响应处理被中断后,当前 Reader 记录 `command_aborted`,后续查询跳过;不会凭 `connected?` 仍为 true 就复用同步状态未知的会话,也不会替宿主关闭连接。
80
82
 
81
- 时钟必须可调用且返回 Time,时间在取得时立即复制。正文在检查后续会话状态之前复制。`Capture` 校验字段与状态:成功必须有字符串正文(允许空串),失败没有正文,跳过必须有失败原因且没有执行证据。墙上时钟可能回拨,时间字段不用于推断单调耗时。
83
+ 时钟必须可调用且返回 Time,可变时间在取得时保存副本,已冻结的 Time 直接复用。正文和来源摘要在观察时钟及会话状态回调之前固定,避免回调复用缓冲区后混入另一条响应。`Capture` 校验字段与状态:成功必须有字符串正文(允许空串),失败没有正文,跳过必须有失败原因且没有执行证据。墙上时钟可能回拨,时间字段不用于推断单调耗时。
82
84
 
83
85
  详细厂商语义、历史验证记录和 Motor 专用集成说明保留在[架构参考](docs/architecture-zh.md)。其中宿主集成代码需要 Motor,不属于独立 gem API。适配层位于 [Motor 仓库](https://github.com/gatework/motor/tree/main/backend/lib) 的 `backend/lib/baseline`、`backend/lib/vxlan` 和 `backend/lib/wireless`。
84
86
 
85
- 原生 `Net::Connector::Base` 会话通过 `execute_operation` 自动适配,不连接、不提权。只去除已匹配的提示符和首行命令回显;连接器因拒绝命令而关闭时,后续读取跳过,不自动重连。自定义会话仍可实现 `send_commands` 协议。
87
+ net-connector **0.13.0 及以上版本**的原生 `Net::Connector::Base` 会话通过 `execute_operation` 自动适配,不连接、不提权。只去除已匹配的提示符和首行命令回显;连接器因拒绝命令而关闭时,后续读取跳过,不自动重连。自定义会话仍可实现 `send_commands` 协议。
86
88
 
87
89
  ## 领域术语
88
90
 
@@ -101,7 +103,7 @@ bundle exec rake build
101
103
 
102
104
  `bundle exec rake` 执行 RSpec、RuboCop 及离线示例。测试放在 `spec/`,沿用项目 RSpec 约定;样例采用合成或脱敏证据。新增会话适配时补充成功、拒绝、断链、取消、非法回执及证据保留回归。离线测试不代表所有设备版本均通过实机验证。
103
105
 
104
- `bundle exec ruby -Ilib benchmark/body.rb` 测量合成回显的正文解码时间及对象分配,并校验正文内容;不代表设备网络吞吐。
106
+ `bundle exec ruby -Ilib benchmark/body.rb` 测量正文解码,`bundle exec ruby -Ilib benchmark/reader.rb` 每轮测量 10,000 次顺序采集;两者均输出合成回显下的耗时与对象分配,并校验内容。比较版本时保持 Ruby、输入和主机一致,不据此推断设备网络吞吐。
105
107
 
106
108
  使用声明的最低 Ruby 版本检查兼容性。方法用途统一使用简明中文注释;复杂实现还需说明业务规则、实现思路及关键分支。保持公开返回值与错误契约。真实回显和凭据不可进入示例、fixture、日志或发布包。
107
109
 
data/README.md CHANGED
@@ -72,9 +72,11 @@ puts "SHA-256: #{capture.evidence.first[:sha256]}"
72
72
 
73
73
  ## Public contract and limits
74
74
 
75
- `Reader.new(session:, clock:).read(command)` returns a `Capture`. Inspect `success?`, `skipped?`, `failure`, and `evidence`; access the body explicitly through `body`. `to_h`, `as_json`, and `inspect` omit the body.
75
+ `Reader.new(session:, clock:).read(command)` returns a `Capture`. Inspect `success?`, `skipped?`, `failure`, and `evidence`; access the body explicitly through `body`. `to_h`, `as_json`, and `inspect` omit the body. `Reader`, the borrowed `Session` adapter, and its response use safe `inspect`/`PP` displays that do not traverse caller-owned objects or consult connection state. Response string interpolation is also safe; raw output and decoded bodies remain available through explicit response APIs.
76
76
 
77
- A custom session implements `connected?` and `send_commands([command])`. Already loaded `Net::Connector::Base` sessions are adapted automatically through `execute_operation`, without connecting or requesting privilege. The adapter removes the matched prompt and first-line command echo, and keeps raw bytes only in the explicit capture evidence boundary. A native connector that closes on command rejection stays closed; later reads are skipped. A response exposes `output`, a boolean `success?`, an array `steps`, `value!` for a successful body, and `error` on failure. A successful read must contain exactly one completed step. The optional `preserve_session_on_rejection:` keyword is supplied only when the session explicitly declares it.
77
+ A custom session implements `connected?` and `send_commands([command])`. Already loaded `Net::Connector::Base` sessions from net-connector **0.13.0 or newer** are adapted automatically through `execute_operation`, without connecting or requesting privilege. The adapter removes the matched prompt and first-line command echo, and keeps raw bytes only in the explicit capture evidence boundary. A native connector that closes on command rejection stays closed; later reads are skipped. A response exposes `output`, a boolean `success?`, an array `steps`, `value!` for a successful body, and `error` on failure. A successful read must contain exactly one completed step. The optional `preserve_session_on_rejection:` keyword is supplied only when the session explicitly declares it.
78
+
79
+ net-connector remains optional: applications using this adapter must declare `gem "net-connector", ">= 0.13.0"` themselves. `examples/connector.rb` exercises the installed adapter through a local PTY process without opening network connections.
78
80
 
79
81
  Use each reader sequentially; create separate readers for concurrent devices. The caller owns connection, disconnection, credentials, and approved command selection. Reader does not decide whether arbitrary command text is read-only. Standard execution/response errors become safe diagnostics; cancellation signals propagate.
80
82
 
@@ -86,7 +88,7 @@ A command receipt belongs to one execution; a `Capture` holds its processed body
86
88
 
87
89
  Cancellation during execution or response processing propagates and marks the reader with `command_aborted`. Later reads are skipped even if the borrowed session still reports connected; the host retains responsibility for closing or replacing it.
88
90
 
89
- The clock must be callable and return `Time`. Times and response bodies are copied before later callbacks can mutate them. Successful captures require a string body (including an empty string); failed captures have no body, and skipped captures require a failure without execution evidence. Wall-clock timestamps can move backwards and are not elapsed-time measurements.
91
+ The clock must be callable and return `Time`. Mutable times and response bodies are copied before later callbacks can mutate them; frozen `Time` values are reused directly, and immutable bodies may be shared. The observation clock runs after the source fingerprint and body snapshot have been fixed, so its callback cannot mix two responses. Successful captures require a string body (including an empty string); failed captures have no body, and skipped captures require a failure without execution evidence. Wall-clock timestamps can move backwards and are not elapsed-time measurements.
90
92
 
91
93
  ## Development and tests
92
94
 
@@ -103,7 +105,7 @@ bundle exec rake build
103
105
 
104
106
  Use the declared minimum Ruby version for compatibility checks. Document source methods and non-obvious business logic in concise Chinese, preserve public return/error contracts, and never add raw device captures or credentials to examples, fixtures, logs, or release archives.
105
107
 
106
- Run `bundle exec ruby -Ilib benchmark/body.rb` to measure body decoding and allocations against synthetic output, with semantic checks. This does not measure device network throughput.
108
+ Run `bundle exec ruby -Ilib benchmark/body.rb` to measure body decoding, or `bundle exec ruby -Ilib benchmark/reader.rb` for 10,000 sequential captures per trial. Both report allocations and elapsed time against synthetic output, with semantic checks. Compare versions under the same Ruby, input and host; these do not measure device network throughput.
107
109
 
108
110
  ## Release
109
111
 
@@ -26,7 +26,7 @@ capture.evidence # command / sha256 / bytes / observed_at
26
26
  会话协议为 `connected?` 和 `send_commands([command])`。返回值须提供 `output`、布尔 `success?`、数组 `steps`;
27
27
  成功必须只有一个步骤,`value!` 必须返回正文字符串,失败通过 `error` 提供原因。
28
28
  `output` 用于原始来源摘要,`value!` 是连接器处理后的正文,两者不要求相等。
29
- 自定义协议由宿主清理正文;原生 `Net::Connector::Base` 由可选适配器绑定当前完整提示符,并在证据摘要生成后去除命令回显及提示符。适配器不主动加载 net-connector,不连接、不提权。调用方只传目录中已批准的查询。
29
+ 自定义协议由宿主清理正文;net-connector 0.13.0 及以上版本的原生 `Net::Connector::Base` 由可选适配器绑定当前完整提示符,并在证据摘要生成后去除命令回显及提示符。适配器不主动加载 net-connector,不连接、不提权。调用方只传目录中已批准的查询。
30
30
 
31
31
  只有会话方法显式声明 `preserve_session_on_rejection:` 时,才传 `true`。
32
32
  原生 net-connector 关闭拒绝命令后的会话时,Reader 保留该关闭状态;自定义适配器只有确认会话仍可用,才允许下一条独立命令。
@@ -35,15 +35,24 @@ capture.evidence # command / sha256 / bytes / observed_at
35
35
 
36
36
  响应处理异常保留此前取得的 SHA256、字节数与时间。没有回执时不伪造摘要。
37
37
  `Capture` 的正文与元数据分别复制、冻结;正常序列化不包含正文、异常对象、消息或凭据。
38
- 字段 `started_at/completed_at` 为采集阶段时间;证据的 `observed_at` 为实际收到响应的时间。
38
+ 字段 `started_at/completed_at` 为采集阶段时间;证据的 `observed_at` 为响应快照固定后的观察时间,不等同于传输层最后一个字节的到达时间。
39
39
  命令失败与领域健康状态分开,Reader 不产生 pass/fail 或完整库存判断。
40
40
 
41
41
  `StandardError` 在单命令执行、响应验证和状态查询边界转换为安全诊断;取消信号继续抛出。执行或响应处理未能完成回执时,ensure 保留原有失败或记录 `command_aborted`,包括不进入 rescue 的 Thread.kill。此 Reader 后续只返回 skipped,不关闭借用会话、不重放命令。
42
42
 
43
- 开始时间在执行回调之前保存副本,正文在后续状态回调之前保存副本。注入时钟必须可调用并返回 Time;时间允许因墙上时钟校准而回拨。`Capture` 拒绝类型错误以及成功/失败/跳过状态与正文或证据矛盾的回执。
43
+ 开始时间在执行回调之前保存副本,正文和来源摘要在观察时钟及后续状态回调之前固定;已经冻结的字符串和时间允许安全共享。注入时钟必须可调用并返回 Time;时间允许因墙上时钟校准而回拨。`Capture` 拒绝类型错误以及成功/失败/跳过状态与正文或证据矛盾的回执。
44
44
 
45
45
  原生适配器只读取渲染后正文的首行来判断回显,不构造全部行数组;成功路径不再调用连接器 `value!` 重复拼接输出,失败仍由连接器原样抛出错误。`benchmark/body.rb` 使用合成回显验证内容、耗时和分配量,不提供真实设备网络性能结论。
46
46
  解析异常由领域 Collector 在本条命令或本次 AP 子任务内隔离。
47
47
  调用方须顺序使用同一个 Reader;跨设备并发各自建立 Reader。
48
48
 
49
49
  运行依赖由 gemspec 声明;会话所有权与命令来源响应时间契约保持不变。没有环境变量、数据库表或调度入口。
50
+
51
+
52
+ ## 当前执行边界
53
+
54
+ 内部 `Command` 模块统一 Reader、Capture 与原生 Session 的单行命令校验及不可变文本快照,不判断命令是否只读。宿主仍决定批准的查询与会话所有权。
55
+
56
+ 来源摘要先固定,正文提取失败仍保留有效证据;观察时钟在正文快照之后调用。时钟响应无效时返回 response 阶段失败并不发布缺少时间的证据,取消则传播并抑制后续查询。适配器仅在首行和命令去除首尾空白后相等时去除回显,正文缩进和后续同名行保留。
57
+
58
+ `script/verify_package.rb` 先验证不含 net-connector 的安装和离线示例,再独立安装可选连接器,通过包内本地 PTY 示例验证适配器,避免工作区或旧锁文件掩盖已发布依赖的接口变化。
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ctx_driver"
4
+ require "net/connector"
5
+
6
+ # 用本地 PTY 驱动已安装连接器的完整会话;不连接网络设备。
7
+ class ExampleConnectorTransport < Net::Connector::Transports::Pty
8
+ # 子进程先给出提示符,随后逐条回显查询和合成正文。
9
+ def argv
10
+ [ RbConfig.ruby, "-e", '$stdout.sync = true; print "device>"; while (line = gets); print line, " interface Ethernet1/1 up\r\ndevice>"; end' ]
11
+ end
12
+ end
13
+
14
+ configuration = Net::Connector::Configuration.new(host: "192.0.2.1", username: "audit", command_timeout: 2, login_timeout: 2)
15
+ device = Net::Connector.build(:cisco_ios, configuration: configuration, transport: ExampleConnectorTransport.new(configuration))
16
+ begin
17
+ device.connect
18
+ reader = CtxDriver::Reader.new(session: device)
19
+ 2.times do
20
+ capture = reader.read(" show interfaces ")
21
+ abort "Connector capture failed" unless capture.success? && capture.body == " interface Ethernet1/1 up\n"
22
+ abort "Connector evidence missing" unless capture.evidence.one? && capture.evidence.first[:bytes].positive?
23
+ abort "Borrowed session was closed" unless device.connected?
24
+ end
25
+ puts "Connector adapter: PTY, command echo, evidence and borrowed ownership passed"
26
+ ensure
27
+ device.close
28
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ctx_parser/immutable"
4
+ require_relative "command"
4
5
 
5
6
  module CtxDriver
6
7
  # 原始正文须显式读取;默认检查与序列化仅展示采集元数据。
@@ -9,9 +10,7 @@ module CtxDriver
9
10
 
10
11
  # 冻结单条命令回执及其正文、时间、证据和失败元数据。
11
12
  def initialize(command:, body: nil, started_at:, completed_at:, evidence: [], failure: nil, skipped: false)
12
- unless command.is_a?(String) && !command.strip.empty? && !/[\x00-\x1f\x7f]/.match?(command)
13
- raise ArgumentError, "One command without control characters is required"
14
- end
13
+ command = Command.snapshot(command)
15
14
  raise ArgumentError, "Capture body must be text or nil" unless body.nil? || body.is_a?(String)
16
15
  unless started_at.is_a?(Time) && completed_at.is_a?(Time)
17
16
  raise ArgumentError, "Capture timestamps must be Time values"
@@ -25,9 +24,10 @@ module CtxDriver
25
24
  raise ArgumentError, "Failed captures cannot contain a body" if failure && !body.nil?
26
25
  raise ArgumentError, "Skipped captures require failure without evidence" if skipped && (failure.nil? || evidence.any?)
27
26
 
28
- @command = command.dup.freeze
29
- @body = body&.dup&.freeze
30
- @started_at, @completed_at = started_at.dup.freeze, completed_at.dup.freeze
27
+ @command = command
28
+ @body = body.nil? || body.frozen? ? body : body.dup.freeze
29
+ @started_at = started_at.frozen? ? started_at : started_at.dup.freeze
30
+ @completed_at = completed_at.frozen? ? completed_at : completed_at.dup.freeze
31
31
  @evidence, @failure = [ evidence, failure ].map { |value| CtxParser::Immutable.copy(value) }
32
32
  @skipped = skipped
33
33
  freeze
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CtxDriver
4
+ # 单条命令的值边界;只校验文本完整性,是否为只读查询由宿主决定。
5
+ module Command
6
+ # 校验并固定命令;已有冻结文本可以安全共享。
7
+ def self.snapshot(value)
8
+ unless value.is_a?(String) && !value.strip.empty? && !/[\x00-\x1f\x7f]/.match?(value)
9
+ raise ArgumentError, "One command without control characters is required"
10
+ end
11
+
12
+ value.frozen? ? value : value.dup.freeze
13
+ end
14
+ end
15
+ private_constant :Command
16
+ end
@@ -29,7 +29,7 @@ module CtxDriver
29
29
  # 已加载 net-connector 时复用其稳定诊断词表,否则使用调用方的回退代码。
30
30
  def self.code(error, fallback)
31
31
  value = error.code if error.respond_to?(:code)
32
- defined?(Net::Connector::ErrorMetadata) ? Net::Connector::ErrorMetadata.code(value) || fallback : fallback
32
+ defined?(Net::Connector::Error::Metadata) ? Net::Connector::Error::Metadata.code(value) || fallback : fallback
33
33
  rescue StandardError
34
34
  fallback
35
35
  end
@@ -26,10 +26,7 @@ module CtxDriver
26
26
 
27
27
  # 执行一条经过校验的查询,保留独立结果或失败。
28
28
  def read(command)
29
- unless command.is_a?(String) && !command.strip.empty? && !/[\x00-\x1f\x7f]/.match?(command)
30
- raise ArgumentError, "One command without control characters is required"
31
- end
32
- command = command.dup.freeze
29
+ command = Command.snapshot(command)
33
30
  started_at = current_time
34
31
  if failure
35
32
  return Capture.new(command: command, started_at: started_at, completed_at: current_time,
@@ -44,6 +41,9 @@ module CtxDriver
44
41
  @session_failure ||= detect_session_failure
45
42
  end
46
43
 
44
+ # 仅显示边界类型,避免遍历借用会话、时钟或触发外部状态查询。
45
+ def inspect = "#<#{self.class.name}>"
46
+
47
47
  private
48
48
 
49
49
  # 立即保存时钟快照,避免后续用户回调改变已记录的时区或时间。
@@ -51,7 +51,7 @@ module CtxDriver
51
51
  time = @clock.call
52
52
  raise TypeError, "Clock must return a Time" unless time.is_a?(Time)
53
53
 
54
- time.dup.freeze
54
+ time.frozen? ? time : time.dup.freeze
55
55
  end
56
56
 
57
57
  # 分离执行与回执校验;后续校验失败时仍保留已取得的证据。
@@ -68,8 +68,7 @@ module CtxDriver
68
68
  output = receipt.output
69
69
  raise TypeError, "Command output must be text" unless output.is_a?(String)
70
70
 
71
- evidence = [ { command: command, sha256: Digest::SHA256.hexdigest(output), bytes: output.bytesize,
72
- observed_at: current_time.getutc.iso8601(6) } ]
71
+ evidence = [ { command: command, sha256: Digest::SHA256.hexdigest(output), bytes: output.bytesize } ]
73
72
  success = receipt.success?
74
73
  unless [ true, false ].include?(success) && receipt.steps.is_a?(Array) && (!success || receipt.steps.one?)
75
74
  raise TypeError, "One query must return one completed command"
@@ -78,7 +77,7 @@ module CtxDriver
78
77
  body = receipt.value!
79
78
  raise TypeError, "Command body must be text" unless body.is_a?(String)
80
79
 
81
- body = body.dup.freeze
80
+ body = body.dup.freeze unless body.frozen?
82
81
  else
83
82
  command_failure = Failure.details(receipt.error, fallback: :command_failed, stage: :execute)
84
83
  end
@@ -86,6 +85,16 @@ module CtxDriver
86
85
  fallback = stage == :execute ? :command_execution_failed : :invalid_command_result
87
86
  command_failure = Failure.details(error, fallback: fallback, stage: stage)
88
87
  end
88
+ # 外部时钟回调只在正文与摘要固定后调用,不能复用本条响应缓冲区。
89
+ begin
90
+ unless evidence.empty?
91
+ observed_at = current_time.getutc.iso8601(6)
92
+ evidence = [ evidence.first.merge(observed_at: observed_at) ]
93
+ end
94
+ rescue StandardError => error
95
+ evidence = []
96
+ command_failure ||= Failure.details(error, fallback: :invalid_command_result, stage: :response)
97
+ end
89
98
  state_failure = detect_session_failure
90
99
  # 单命令失败但会话仍可用时继续独立查询;断链或状态不明才阻止后续提交。
91
100
  @session_failure = command_failure || state_failure if state_failure
@@ -1,10 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "command"
4
+
3
5
  module CtxDriver
4
6
  # 适配已加载的 net-connector;不连接、不提权,也不恢复已关闭的借用会话。
5
7
  class Session
6
8
  # 先交出原始回执,Reader 建立证据后才按需解码正文。
7
9
  Response = Data.define(:result) do
10
+ # 不依赖原始回执自身的脱敏实现,默认检查不展开来源结果。
11
+ def inspect = "#<#{self.class.name}>"
12
+
13
+ # Data 的默认 to_s 保留原始成员检查实现,日志插值也须脱敏。
14
+ def to_s = inspect
15
+
16
+ # Data 的 PP 默认按成员展开,显式限制为安全表示。
17
+ def pretty_print(printer) = printer.text(inspect)
18
+
8
19
  def output = result.output
9
20
  def steps = result.steps
10
21
  def success? = result.success?
@@ -15,9 +26,9 @@ module CtxDriver
15
26
  # 成功路径无需再次拼接全部输出;失败仍交由连接器保留异常契约。
16
27
  result.value! unless result.success?
17
28
  step = result.steps.fetch(0)
18
- body = Net::Connector::TerminalText.render(step.output.delete_suffix(step.prompt.to_s))
29
+ body = Net::Connector::Terminal::Text.render(step.output.delete_suffix(step.prompt.to_s))
19
30
  first = body.each_line("\n").first
20
- first && first.strip == step.command.text ? body.delete_prefix(first) : body
31
+ first && first.strip == step.command.text.strip ? body.delete_prefix(first) : body
21
32
  end
22
33
  end
23
34
  private_constant :Response
@@ -27,18 +38,23 @@ module CtxDriver
27
38
  @session = session
28
39
  end
29
40
 
41
+ # 不展开调用方拥有的连接器,也不查询连接状态。
42
+ def inspect = "#<#{self.class.name}>"
43
+
30
44
  # 复用连接器的真实状态,避免失败后隐式重连。
31
45
  def connected? = @session.connected?
32
46
 
33
47
  # 单命令操作保护响应正文,并绑定当前完整提示符。
34
48
  def send_commands(commands)
35
- raise ArgumentError, "Expected one query" unless commands.one?
49
+ raise ArgumentError, "Expected one query" unless commands.is_a?(Array) && commands.one?
50
+
51
+ text = Command.snapshot(commands.first)
36
52
 
37
- prompt = Net::Connector::TerminalText.render(@session.current_prompt.to_s).strip
53
+ prompt = Net::Connector::Terminal::Text.render(@session.current_prompt.to_s).strip
38
54
  raise ArgumentError, "Session prompt is unavailable" if prompt.empty?
39
55
 
40
56
  pattern = /(?:\A|(?<=[\r\n]))[ \t\x00]*#{Regexp.escape(prompt)}[ \t\x00]*[\r\n]*\z/
41
- command = Net::Connector::Command.new(commands.first, prompt: pattern, output_sensitive: true)
57
+ command = Net::Connector::Command.new(text, prompt: pattern, output_sensitive: true)
42
58
  result = @session.execute_operation(Net::Connector::Script.new([ command ]), name: :ctx_driver, privilege: false)
43
59
  Response.new(result)
44
60
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module CtxDriver
4
4
  # 公开包版本,供 RubyGems 元数据和运行时调用方共用。
5
- VERSION = "0.2.1"
5
+ VERSION = "0.2.3"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ctx_driver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gatework contributors
@@ -63,9 +63,11 @@ files:
63
63
  - README.md
64
64
  - docs/architecture-zh.md
65
65
  - docs/release.md
66
+ - examples/connector.rb
66
67
  - examples/offline.rb
67
68
  - lib/ctx_driver.rb
68
69
  - lib/ctx_driver/capture.rb
70
+ - lib/ctx_driver/command.rb
69
71
  - lib/ctx_driver/failure.rb
70
72
  - lib/ctx_driver/reader.rb
71
73
  - lib/ctx_driver/session.rb