expect-pty 0.9.1 → 0.9.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: b56d5e855684574710a2b3de08554ff517979a5ad8a96ff455780f55b5313b4a
4
- data.tar.gz: 7f9b80ed4cbe5b0bc98bee0b1400941cecbfefe4229c23fedd9837ce8550d393
3
+ metadata.gz: 0742e1462f3858067396edb88d2e401ba621869882091cddee3237cedd86eaf5
4
+ data.tar.gz: d054194b5f0f54740d86ccdca2d684d7ee8faeae77dbbf50595cc0b3516d4308
5
5
  SHA512:
6
- metadata.gz: ff9e7769c5ecfd1b4d6df2e6405b37acb9c8bcf5edbc5b0fbeea570bd50c774b1020a53d4593fb344efdd2404b8de0cce03f14c503011ab8079ac1649e4234be
7
- data.tar.gz: 40d52a9b7388f2c740842b3c6abcd4c1d04787b4cf1c1ed20d7e855113e9d333330820f1404475a91698f9870426da2d85aeece9b728ed989b4628d3ef00b965
6
+ metadata.gz: 5a3be17c347139230dfe4395292ef7eeb58d0f432445c6d234542e3d83ac0f87e0e08d3131dc6842f4f1639a7a979868b84dd51ea9c9b206809901e232faac0d
7
+ data.tar.gz: e3f6508feaaa07172147b4c26bbc850e22bc918f377213ae4b1403947446cd15141f6d43860d26c28dac62a95628b72df1e09ae9ea2819781eef5cdb7fa93d54
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.3 - 2026-10-18
6
+
7
+ - 结果的 inspect 与 to_s 仅输出匹配序号和错误状态,避免诊断、字符串插值泄露匹配文本、捕获内容或异常消息;原始结果字段保持不变。
8
+ - 工厂块异常、取消或非局部退出时跳过 graceful 读尾,直接硬关闭;正常返回(包括 nil/false)仍可收取尾部输出。
9
+
10
+ ## 0.9.2 - 2026-10-18
11
+
12
+ - 减少显式搜索窗口下字面量匹配的内存分配,保留原始字节偏移、声明优先级和正则窗口语义。
13
+ - 修复脱敏尾部、匹配缓冲上限、转接历史及失败后的待写数据仍保留大块底层存储的问题,释放已丢弃或交付的字节并保留恢复进度。
14
+
5
15
  ## 0.9.1 - 2026-10-18
6
16
 
7
17
  - 修复自定义 Numeric 转换失败时泄漏常规异常的问题;时间参数统一抛出 ArgumentError 并保留 cause,Interrupt/SystemExit 仍原样传播。
data/docs/API.md CHANGED
@@ -19,6 +19,8 @@ Session 构造器不接收命令或操作参数。同一会话只能启动一次
19
19
  `Expect.open(io, writer: io, own: false, graceful: false, **settings)` 适配真实可 select 的 IO,默认借用。
20
20
  `own: true` 接管关闭责任,包括初始化中已知设置的非法值导致的失败;未知关键字在 Ruby 方法入口拒绝,此时不接管 IO。
21
21
  `spawn`、`open` 无块时返回 Session,有块时返回块结果并确保按 `graceful:` 关闭;非局部返回也清理。
22
+ 工厂块正常返回时才执行 graceful 读尾;异常、线程取消以及 return/break/throw 等非局部退出直接硬关闭,避免读尾延迟退出。
23
+ 块正常返回 nil 或 false 仍属于正常完成;调用方 rescue 中已处理的异常不影响此判断。
22
24
  清理中的 StandardError 不覆盖本次作用域已有的主异常;没有主异常时照常传播。新发生的 Interrupt/SystemExit 不会被清理包装吞掉。
23
25
 
24
26
  `close(graceful: false)` 最终硬关闭兜底,返回 nil;`soft_close(timeout: 15, term_timeout: 1)` 收取尾部、关闭句柄并最多发送
@@ -65,6 +67,8 @@ nil 保留全缓冲扫描;指定时,字符串和正则只搜索最新的尾
65
67
  固定 UTF-8 正则跳过边界切开的合法首字符,等待合法未完成尾字符;窗口内非法字节及 EOF 未完成字符仍报错。
66
68
  二进制正则按原始字节匹配。
67
69
 
70
+ Result 的 `inspect` 与 `to_s` 仅显示匹配序号和错误状态,不打印原始文本、异常消息或来源对象。
71
+ 需要原始结果时显式读取字段;`redact` 不改变这些字段。
68
72
  Result 字段为 `number`、`error`、`match`、`before`、`after`、`session`、`captures`。对象、文本及捕获数组均不可变,未参与捕获为
69
73
  nil。
70
74
  错误为 nil、`:timeout`、`:eof` 或原始 IO 异常;来源会话与异常不复制、不冻结。使用 `matched?`、`timeout?`、`eof?` 查询,
@@ -46,7 +46,8 @@ PID。`hard_close` 不等待输出,必要时 TERM、KILL 并回收子进程。
46
46
 
47
47
  Ruby 使用关键字指定各阶段期限,关闭方法返回 `Process::Status` 或 `nil`。`close(graceful: true)` 或工厂块的
48
48
  `graceful: true`
49
- 先尝试软关闭,再在 `ensure` 中硬关闭;这对应原版销毁时可选软关闭、随后硬关闭的清理策略。`close` 返回 `nil`;GC
49
+ 先尝试软关闭,再在 `ensure` 中硬关闭;工厂块仅正常返回时读尾,异常、取消及非局部退出直接硬关闭。
50
+ 显式 `close(graceful: true)` 仍按调用参数读尾。`close` 返回 `nil`;GC
50
51
  兜底直接强制清理,不阻塞等待。
51
52
 
52
53
  ## 有意采用的 Ruby 语义
@@ -192,7 +192,8 @@ module Expect
192
192
  limit = [limit, 3].max if utf8
193
193
  return unless history.bytesize > limit
194
194
 
195
- history.replace(history.byteslice(-limit, limit))
195
+ # 保留历史对象身份,但独立复制尾部,释放已经转发的大前缀存储。
196
+ history.replace(history.unpack1("a*", offset: history.bytesize - limit))
196
197
  return unless utf8
197
198
 
198
199
  history.slice!(0) while (byte = history.getbyte(0)) && (0x80..0xBF).cover?(byte)
@@ -52,9 +52,12 @@ module Expect
52
52
 
53
53
  private
54
54
 
55
- # 显式尾部窗口单独匹配,返回偏移仍属于原始缓冲;字节协议不做转码。
55
+ # 只匹配显式尾部窗口内的候选,返回偏移仍属于原始缓冲;字节协议不做转码。
56
56
  def locate_window(buffer, size, final:, captures:)
57
57
  start = buffer.bytesize - size
58
+ # 字面匹配没有锚点或回看,直接限制候选起点,复用原缓冲而不复制尾窗。
59
+ return locate(buffer, offset: start, final:, captures:) if value.is_a?(String)
60
+
58
61
  start = utf8_window_start(buffer, start, final:) if value.is_a?(Regexp) && value.fixed_encoding? &&
59
62
  value.encoding == Encoding::UTF_8
60
63
  return nil unless start
data/lib/expect/pty.rb CHANGED
@@ -66,12 +66,12 @@ module Expect
66
66
  timeout: nil, write_timeout: nil, buffer_limit: nil, logger: nil, transcript: nil, outputs: [])
67
67
  session = nil
68
68
  spawned = false
69
- cleanup = lambda do
69
+ cleanup = lambda do |completed|
70
70
  if session && (block_given? || !spawned)
71
- session.cleanup_session(nil, writer: nil, own: false, graceful: spawned && graceful)
71
+ session.cleanup_session(nil, writer: nil, own: false, graceful: spawned && graceful && completed)
72
72
  end
73
73
  end
74
- Cleanup.always(cleanup) do
74
+ Cleanup.always(cleanup, report_completion: true) do
75
75
  # 工厂在构造期间处理失败;成功对象的返回与登记不可被中断拆开。
76
76
  Thread.handle_interrupt(Object => :never) do
77
77
  session = SessionFactory.pty(timeout:, write_timeout:, buffer_limit:, logger:, transcript:, outputs:)
@@ -89,14 +89,14 @@ module Expect
89
89
  session = nil
90
90
  initialized = false
91
91
  constructing = false
92
- cleanup = lambda do
92
+ cleanup = lambda do |completed|
93
93
  if session && (block_given? || !initialized)
94
- session.cleanup_session(io, writer:, own:, graceful: initialized && graceful)
94
+ session.cleanup_session(io, writer:, own:, graceful: initialized && graceful && completed)
95
95
  elsif !session && own && !constructing
96
96
  ResourceOwner.close_handles(io, writer)
97
97
  end
98
98
  end
99
- Cleanup.always(cleanup) do
99
+ Cleanup.always(cleanup, report_completion: true) do
100
100
  Thread.handle_interrupt(Object => :never) do
101
101
  constructing = true
102
102
  session = SessionFactory.io(io, writer:, own:, timeout:, write_timeout:, buffer_limit:,
@@ -146,12 +146,13 @@ module Expect
146
146
  cursor = ending
147
147
  end
148
148
  remaining = @pending.bytesize - length
149
- @pending = @pending.byteslice(length, remaining)
150
- @hidden = @hidden.byteslice(length, remaining)
149
+ # 尾部必须独占存储;共享切片会让几 KiB 的保留窗口继续持有整块输入及掩码。
150
+ @pending = @pending.unpack1("a*", offset: length)
151
+ @hidden = @hidden.unpack1("a*", offset: length)
151
152
  if @suppressed
152
153
  # 只追踪最后一个抑制字节的排空边界,避免每轮扫描字节图;跨块秘密掩码不受影响。
153
154
  @suppressed_until -= length
154
- @suppressed = @suppressed_until.positive? ? @suppressed.byteslice(length, remaining) : nil
155
+ @suppressed = @suppressed_until.positive? ? @suppressed.unpack1("a*", offset: length) : nil
155
156
  end
156
157
  @scanned_bytes = remaining
157
158
  output
@@ -13,6 +13,7 @@ module Expect
13
13
  def initialize(target, data)
14
14
  @target = target
15
15
  @data = data
16
+ @data_start = 0
16
17
  @offset = 0
17
18
  @flushed = false
18
19
  restart_timeout
@@ -33,7 +34,14 @@ module Expect
33
34
  end
34
35
 
35
36
  # 数据交付完但 flush 失败仍未完成;重试只补 flush,不重新发送已接受的数据。
36
- def done? = @offset == @data.bytesize && @flushed
37
+ def done? = @offset == @data_start + @data.bytesize && @flushed
38
+
39
+ # 退出共同循环后只保留未交付字节,解除小切片与原始大输入的共享存储。
40
+ # offset 始终累计本排队块的确认进度;data_start 记录当前独立后缀在原块中的起点。
41
+ def detach_pending_data
42
+ @data = @data.unpack1("a*", offset: @offset - @data_start)
43
+ @data_start = @offset
44
+ end
37
45
 
38
46
  # 每轮每目标至多一次写入;真实 IO 永不在此等待,交给 Relay 的共同 select。
39
47
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- 游标、短写、flush 和期限按单个目标推进。
@@ -41,8 +49,8 @@ module Expect
41
49
  return false if done?
42
50
  raise IOError, "closed Expect session" if target.is_a?(Session) && target.closed?
43
51
 
44
- if @offset < @data.bytesize
45
- bytes = @data.byteslice(@offset, READ_SIZE)
52
+ if @offset < @data_start + @data.bytesize
53
+ bytes = @data.byteslice(@offset - @data_start, READ_SIZE)
46
54
  count = io ? io.write_nonblock(bytes, exception: false) : target.write(bytes)
47
55
  if io && count == :wait_writable
48
56
  check_timeout! if check_timeout
@@ -56,7 +64,7 @@ module Expect
56
64
  # 先登记底层已接受的进度,再通知诊断;logger 失败后恢复只补未发送后缀。
57
65
  trace_delivery(bytes.byteslice(0, count)) if target.is_a?(Session)
58
66
  end
59
- if @offset == @data.bytesize
67
+ if @offset == @data_start + @data.bytesize
60
68
  # write_nonblock 已直接写到底层;普通 write 对象仍遵守自身 flush 协议。
61
69
  target.flush if !io && target.respond_to?(:flush)
62
70
  @flushed = true
data/lib/expect/relay.rb CHANGED
@@ -159,7 +159,10 @@ module Expect
159
159
  # 归还多来源缓冲和释放 token 属于同一交接,不能只恢复前几个来源就响应取消。
160
160
  Thread.handle_interrupt(Object => :never) do
161
161
  @previous.each { |session, buffer| session.interaction_buffer = buffer }
162
- @buffers.each { |session, buffer| session.restore_relay_buffer(buffer) }
162
+ @buffers.each do |session, buffer|
163
+ session.restore_relay_buffer(buffer)
164
+ session.pending_writes.each(&:detach_pending_data)
165
+ end
163
166
  ensure
164
167
  OWNERSHIP_MUTEX.synchronize do
165
168
  @sessions.each do |session|
data/lib/expect/result.rb CHANGED
@@ -30,6 +30,22 @@ module Expect
30
30
  captures: captures.map { |value| value&.dup&.freeze }.freeze)
31
31
  end
32
32
 
33
+ # 诊断只显示匹配序号和错误状态,不检查原始文本、异常或来源对象。
34
+ # @return [String] 不包含协议内容的结果摘要。
35
+ def inspect
36
+ state = if error.nil? then "none"
37
+ elsif error.equal?(:eof) then "eof"
38
+ elsif error.equal?(:timeout) then "timeout"
39
+ else "exception"
40
+ end
41
+ index = number.is_a?(Integer) ? number.to_s : "nil"
42
+ "#<#{self.class} number=#{index} error=#{state}>"
43
+ end
44
+
45
+ # Data 的默认 to_s 独立于 inspect;字符串插值同样只使用安全摘要。
46
+ # @return [String] 与 inspect 相同的结果摘要。
47
+ def to_s = inspect
48
+
33
49
  # 是否命中文本;EOF 和超时没有模式序号。
34
50
  def matched? = !number.nil?
35
51
 
@@ -549,7 +549,8 @@ module Expect
549
549
 
550
550
  # 只累计匹配窗口裁剪,消费、清空及转接交接不算丢弃;关闭后仍可读取累计值。
551
551
  @buffer_discarded_bytes += @buffer.bytesize - limit
552
- @buffer = @buffer.byteslice(-limit, limit)
552
+ # 小尾部不共享已丢弃前缀的底层存储,使上限同时约束长期保留的字节。
553
+ @buffer = @buffer.unpack1("a*", offset: @buffer.bytesize - limit)
553
554
  @buffer_generation += 1
554
555
  # 丢弃的前缀使当前窗口与已转发历史之间不再连续。
555
556
  @relay_history.clear
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Expect
4
4
  # Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
5
- VERSION = "0.9.1"
5
+ VERSION = "0.9.3"
6
6
  end
data/sig/expect.rbs CHANGED
@@ -238,6 +238,10 @@ module Expect
238
238
 
239
239
  def eof?: () -> bool
240
240
 
241
+ def inspect: () -> String
242
+
243
+ def to_s: () -> String
244
+
241
245
  def deconstruct: () -> [Integer?, result_error, String?, String?, String?, Session?, Array[String?]]
242
246
 
243
247
  def with: (
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expect-pty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - expect-pty contributors