expect-pty 0.6.1 → 0.7.2

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.
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Expect
4
- # 集中校验会话配置。类级默认值以冻结快照发布,每个会话再构造独立副本。
5
- # 这里只保存策略值,不持有 IO、缓冲或日志对象;修改默认配置不会追溯影响已创建的会话。
6
- class Configuration
7
- # 各配置项的初始值;会话仅保存经 setter 验证后的副本。
8
- DEFAULTS = {
9
- timeout: nil, write_timeout: nil, buffer_limit: nil, debug_level: 0,
10
- raw_pty: false, preserve_buffer: false, log_stdout: false,
11
- log_listeners: true, raw_terminal: true, reset_timeout_on_read: false,
12
- graceful_close: false
13
- }.freeze
14
- # 属性委托和快照导出使用同一配置清单。
15
- ATTRIBUTES = DEFAULTS.keys.freeze
16
- # 配置键与读取接口分开:布尔值只提供谓词,导出仍使用原配置键。
17
- READERS = DEFAULTS.to_h do |name, value|
18
- [name, [true, false].include?(value) ? :"#{name}?" : name]
19
- end.freeze
20
-
21
- attr_reader :timeout, :write_timeout, :buffer_limit, :debug_level
22
-
23
- # 布尔读写遵循 Ruby 真值规则,仅提供问号查询和 setter。
24
- def self.boolean_attribute(name)
25
- define_method(:"#{name}?") { instance_variable_get(:"@#{name}") }
26
-
27
- define_method(:"#{name}=") { |value| instance_variable_set(:"@#{name}", !!value) }
28
- end
29
- private_class_method :boolean_attribute
30
-
31
- # 先拒绝未知键,再经 setter 校验;发布中的冻结快照不会被部分修改。
32
- def initialize(**options)
33
- unknown = options.keys - ATTRIBUTES
34
- raise ArgumentError, "unknown configuration: #{unknown.join(", ")}" unless unknown.empty?
35
-
36
- DEFAULTS.merge(options).each { |name, value| public_send(:"#{name}=", value) }
37
- end
38
-
39
- # 设置匹配等待的默认秒数;nil 表示无限等待,0 表示只轮询现有数据。
40
- def timeout=(value)
41
- @timeout = Expect.duration(value)
42
- end
43
-
44
- # 设置写入背压的等待期限;先转换和校验,失败时保留原配置。
45
- def write_timeout=(value)
46
- @write_timeout = Expect.duration(value)
47
- end
48
-
49
- # 限制接收缓冲保留的尾部字节数;正整数为上限,nil 为无限。
50
- def buffer_limit=(value)
51
- unless value.nil? || (value.is_a?(Integer) && value.positive?)
52
- raise ArgumentError, "buffer_limit must be a positive Integer or nil"
53
- end
54
-
55
- @buffer_limit = value
56
- end
57
-
58
- # 设置诊断详细程度:0 关闭,1 生命周期与匹配,2 收发内容,3 缓冲内容。
59
- def debug_level=(value)
60
- unless value.is_a?(Integer) && (0..3).cover?(value)
61
- raise ArgumentError, "debug_level must be an Integer between 0 and 3"
62
- end
63
-
64
- @debug_level = value
65
- end
66
-
67
- # 控制 spawn 前是否将子进程终端设为 raw,关闭回显和换行转换。
68
- # @!method raw_pty?
69
- # 查询或设置此会话策略,使用 Ruby 真值规则。
70
- # @return [Boolean] 当前布尔配置。
71
- # @!method raw_pty=(value)
72
- # 查询或设置此会话策略,使用 Ruby 真值规则。
73
- # @param value [Object] 除 nil/false 外均转换为 true。
74
- # @return [Boolean] 归一化的布尔配置。
75
- boolean_attribute :raw_pty
76
-
77
- # 控制匹配成功后是否保留完整缓冲;启用时由继续回调自行消费匹配内容。
78
- # @!method preserve_buffer?
79
- # 查询或设置此会话策略,使用 Ruby 真值规则。
80
- # @return [Boolean] 当前布尔配置。
81
- # @!method preserve_buffer=(value)
82
- # 查询或设置此会话策略,使用 Ruby 真值规则。
83
- # @param value [Object] 除 nil/false 外均转换为 true。
84
- # @return [Boolean] 归一化的布尔配置。
85
- boolean_attribute :preserve_buffer
86
-
87
- # 控制接收字节是否同步输出到当前 $stdout;默认关闭。
88
- # @!method log_stdout?
89
- # 查询或设置此会话策略,使用 Ruby 真值规则。
90
- # @return [Boolean] 当前布尔配置。
91
- # @!method log_stdout=(value)
92
- # 查询或设置此会话策略,使用 Ruby 真值规则。
93
- # @param value [Object] 除 nil/false 外均转换为 true。
94
- # @return [Boolean] 归一化的布尔配置。
95
- boolean_attribute :log_stdout
96
-
97
- # 控制接收字节是否转发给监听器,与 stdout 和日志目标分别管理。
98
- # @!method log_listeners?
99
- # 查询或设置此会话策略,使用 Ruby 真值规则。
100
- # @return [Boolean] 当前布尔配置。
101
- # @!method log_listeners=(value)
102
- # 查询或设置此会话策略,使用 Ruby 真值规则。
103
- # @param value [Object] 除 nil/false 外均转换为 true。
104
- # @return [Boolean] 归一化的布尔配置。
105
- boolean_attribute :log_listeners
106
-
107
- # 控制 interact 是否临时设置并恢复本地输入终端;通用 interconnect 不修改终端模式。
108
- # @!method raw_terminal?
109
- # 查询或设置此会话策略,使用 Ruby 真值规则。
110
- # @return [Boolean] 当前布尔配置。
111
- # @!method raw_terminal=(value)
112
- # 查询或设置此会话策略,使用 Ruby 真值规则。
113
- # @param value [Object] 除 nil/false 外均转换为 true。
114
- # @return [Boolean] 归一化的布尔配置。
115
- boolean_attribute :raw_terminal
116
-
117
- # 控制收到任何新数据时是否刷新匹配期限,适用于按静默时长判断超时。
118
- # 只刷新相对 timeout;单次等待显式指定的绝对 deadline 仍是不可延长的上限。
119
- # @!method reset_timeout_on_read?
120
- # 查询或设置此会话策略,使用 Ruby 真值规则。
121
- # @return [Boolean] 当前布尔配置。
122
- # @!method reset_timeout_on_read=(value)
123
- # 查询或设置此会话策略,使用 Ruby 真值规则。
124
- # @param value [Object] 除 nil/false 外均转换为 true。
125
- # @return [Boolean] 归一化的布尔配置。
126
- boolean_attribute :reset_timeout_on_read
127
-
128
- # 控制通用 close 是否先软关闭;最终资源清理仍由硬关闭兜底。
129
- # @!method graceful_close?
130
- # 查询或设置此会话策略,使用 Ruby 真值规则。
131
- # @return [Boolean] 当前布尔配置。
132
- # @!method graceful_close=(value)
133
- # 查询或设置此会话策略,使用 Ruby 真值规则。
134
- # @param value [Object] 除 nil/false 外均转换为 true。
135
- # @return [Boolean] 归一化的布尔配置。
136
- boolean_attribute :graceful_close
137
-
138
- # 导出新的属性 Hash,用于构造会话副本或发布下一份默认配置。
139
- def to_h
140
- READERS.to_h { |name, reader| [name, public_send(reader)] }
141
- end
142
- end
143
- end
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "shellwords"
4
-
5
- # 会话终端的模式和窗口尺寸接口;人工接管期间的临时恢复由 interaction.rb 负责。
6
- class Expect
7
- # @api private
8
- module Terminal
9
- # 查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。
10
- # 辅助进程与管道独立记账,不能覆盖主会话 PID;失败时保留原异常并有界回收。
11
- def stty(*modes)
12
- return "" unless tty?
13
-
14
- modes = modes.flat_map { |mode| Shellwords.split(mode.to_s) }
15
- modes = ["-g"] if modes.empty?
16
- reader = sink = resources = nil
17
- Cleanup.always(-> { cleanup_stty(resources, reader, sink) }) do
18
- reader, sink = IO.pipe
19
- resources = SessionResources.new(reader, writer: sink, own: true)
20
- resources.pid = Process.spawn("stty", *modes, in: to_io, out: sink, err: sink)
21
- sink.close
22
- output = reader.read
23
- status = wait_stty(resources.pid)
24
- resources.pid = nil
25
- raise IOError, "stty failed: #{output.strip}" unless status.success?
26
-
27
- output.strip
28
- rescue Errno::ENOENT
29
- raise IOError, "stty executable not found in PATH; install the system terminal utilities"
30
- end
31
- end
32
-
33
- # 读取终端的 [行数, 列数];底层并非终端或句柄已关闭时保留原生 IO 异常。
34
- def winsize = to_io.winsize
35
-
36
- # 更新终端尺寸,由内核通知前台进程。
37
- def winsize=(size)
38
- to_io.winsize = size
39
- end
40
-
41
- private
42
-
43
- # 正常路径阻塞取得真实状态,不额外轮询或固定等待;EINTR 不重新执行 stty。
44
- def wait_stty(pid)
45
- Process.waitpid2(pid).last
46
- rescue Errno::EINTR
47
- retry
48
- end
49
-
50
- # 即使账本初始化被中断,局部变量中的管道仍须关闭;单端失败不跳过其他清理。
51
- # 无主异常时由调用方传播首个常规清理错误。
52
- def cleanup_stty(resources, reader, sink)
53
- failure = nil
54
- [reader, sink].compact.each do |io|
55
- io.close unless io.closed?
56
- rescue IOError, SystemCallError => error
57
- failure ||= error
58
- end
59
- begin
60
- reap_stty(resources) if resources
61
- rescue IOError, SystemCallError => error
62
- failure ||= error
63
- end
64
- raise failure if failure
65
- end
66
-
67
- # 每阶段共享固定的 50ms 单调时钟预算:自然退出、TERM、KILL;中断不续期。
68
- # 仍无法同步回收或系统调用失败时交给 detach 的后台 wait,不伪造退出状态。
69
- def reap_stty(resources)
70
- return unless resources.owner == Process.pid
71
-
72
- [nil, "TERM", "KILL"].each do |signal|
73
- deadline = Expect.monotonic + 0.05
74
- loop do
75
- begin
76
- resources.reap
77
- return resources.status unless resources.pid && resources.owner == Process.pid
78
-
79
- if signal
80
- begin
81
- Process.kill(signal, resources.pid)
82
- rescue Errno::ESRCH
83
- # 退出可发生在非阻塞 wait 与信号之间,仍需继续 wait。
84
- nil
85
- end
86
- signal = nil
87
- end
88
- rescue Errno::EINTR
89
- # 重试沿用本阶段期限,连续 EINTR 也必须交还控制权。
90
- nil
91
- end
92
- remaining = deadline - Expect.monotonic
93
- break unless remaining.positive?
94
-
95
- sleep [remaining, 0.005].min
96
- end
97
- end
98
- ensure
99
- Process.detach(resources.pid) if resources.pid && resources.owner == Process.pid
100
- end
101
- end
102
- end