expect-pty 0.5.3 → 0.6.1

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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
@@ -4,99 +4,99 @@ require "shellwords"
4
4
 
5
5
  # 会话终端的模式和窗口尺寸接口;人工接管期间的临时恢复由 interaction.rb 负责。
6
6
  class Expect
7
- # 查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。
8
- # 辅助进程与管道独立记账,不能覆盖主会话 PID;失败时保留原异常并有界回收。
9
- def stty(*modes)
10
- return "" unless tty?
7
+ # @api private
8
+ module Terminal
9
+ # 查询可恢复的终端模式字符串,或通过系统 stty 设置模式;参数按数组传递,不经 shell。
10
+ # 辅助进程与管道独立记账,不能覆盖主会话 PID;失败时保留原异常并有界回收。
11
+ def stty(*modes)
12
+ return "" unless tty?
11
13
 
12
- modes = modes.flat_map { |mode| Shellwords.split(mode.to_s) }
13
- modes = ["-g"] if modes.empty?
14
- reader, sink = IO.pipe
15
- resources = SessionResources.new(reader, writer: sink, own: true)
16
- resources.pid = Process.spawn("stty", *modes, in: to_io, out: sink, err: sink)
17
- sink.close
18
- output = reader.read
19
- status = wait_stty(resources.pid)
20
- resources.pid = nil
21
- raise IOError, "stty failed: #{output.strip}" unless status.success?
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?
22
26
 
23
- output.strip
24
- rescue Errno::ENOENT
25
- failed = true
26
- raise IOError, "stty executable not found in PATH; install the system terminal utilities"
27
- rescue Exception # rubocop:disable Lint/RescueException -- 中断同样先回收辅助进程,再原样传播;不读取调用者的旧 $!。
28
- failed = true
29
- raise
30
- ensure
31
- cleanup(failed:) { cleanup_stty(resources, reader, sink) }
32
- end
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
33
32
 
34
- # 读取终端的 [行数, 列数];底层并非终端或句柄已关闭时保留原生 IO 异常。
35
- def winsize = to_io.winsize
33
+ # 读取终端的 [行数, 列数];底层并非终端或句柄已关闭时保留原生 IO 异常。
34
+ def winsize = to_io.winsize
36
35
 
37
- # 更新终端尺寸,由内核通知前台进程。
38
- def winsize=(size)
39
- to_io.winsize = size
40
- end
41
-
42
- private
36
+ # 更新终端尺寸,由内核通知前台进程。
37
+ def winsize=(size)
38
+ to_io.winsize = size
39
+ end
43
40
 
44
- # 正常路径阻塞取得真实状态,不额外轮询或固定等待;EINTR 不重新执行 stty。
45
- def wait_stty(pid)
46
- Process.waitpid2(pid).last
47
- rescue Errno::EINTR
48
- retry
49
- end
41
+ private
50
42
 
51
- # 即使账本初始化被中断,局部变量中的管道仍须关闭;单端失败不跳过其他清理。
52
- # 无主异常时由调用方传播首个常规清理错误。
53
- def cleanup_stty(resources, reader, sink)
54
- failure = nil
55
- [reader, sink].compact.each do |io|
56
- io.close unless io.closed?
57
- rescue IOError, SystemCallError => error
58
- failure ||= error
43
+ # 正常路径阻塞取得真实状态,不额外轮询或固定等待;EINTR 不重新执行 stty。
44
+ def wait_stty(pid)
45
+ Process.waitpid2(pid).last
46
+ rescue Errno::EINTR
47
+ retry
59
48
  end
60
- begin
61
- reap_stty(resources) if resources
62
- rescue IOError, SystemCallError => error
63
- failure ||= error
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
64
65
  end
65
- raise failure if failure
66
- end
67
66
 
68
- # 每阶段共享固定的 50ms 单调时钟预算:自然退出、TERM、KILL;中断不续期。
69
- # 仍无法同步回收或系统调用失败时交给 detach 的后台 wait,不伪造退出状态。
70
- def reap_stty(resources)
71
- return unless resources.owner == Process.pid
67
+ # 每阶段共享固定的 50ms 单调时钟预算:自然退出、TERM、KILL;中断不续期。
68
+ # 仍无法同步回收或系统调用失败时交给 detach 的后台 wait,不伪造退出状态。
69
+ def reap_stty(resources)
70
+ return unless resources.owner == Process.pid
72
71
 
73
- [nil, "TERM", "KILL"].each do |signal|
74
- deadline = Expect.monotonic + 0.05
75
- loop do
76
- begin
77
- resources.reap
78
- return resources.status unless resources.pid && resources.owner == Process.pid
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
79
78
 
80
- if signal
81
- begin
82
- Process.kill(signal, resources.pid)
83
- rescue Errno::ESRCH
84
- # 退出可发生在非阻塞 wait 与信号之间,仍需继续 wait。
85
- nil
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
86
87
  end
87
- signal = nil
88
+ rescue Errno::EINTR
89
+ # 重试沿用本阶段期限,连续 EINTR 也必须交还控制权。
90
+ nil
88
91
  end
89
- rescue Errno::EINTR
90
- # 重试沿用本阶段期限,连续 EINTR 也必须交还控制权。
91
- nil
92
- end
93
- remaining = deadline - Expect.monotonic
94
- break unless remaining.positive?
92
+ remaining = deadline - Expect.monotonic
93
+ break unless remaining.positive?
95
94
 
96
- sleep [remaining, 0.005].min
95
+ sleep [remaining, 0.005].min
96
+ end
97
97
  end
98
+ ensure
99
+ Process.detach(resources.pid) if resources.pid && resources.owner == Process.pid
98
100
  end
99
- ensure
100
- Process.detach(resources.pid) if resources.pid && resources.owner == Process.pid
101
101
  end
102
102
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  class Expect
4
4
  # Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
5
- VERSION = "0.5.3"
5
+ VERSION = "0.6.1"
6
6
  end