main_loop 0.1.4.364822 → 0.1.4.367214

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: ec4bfd3358113dae75080b98525dcec837de50f484b9df20b6d19b0b93cfae12
4
- data.tar.gz: 65e3173cd2c269a8c6ed8a1d2ca0a3ca9a94a7903c363901f3d82fe834f8e160
3
+ metadata.gz: 850bb2f02f1bff7caf3ebca532839b4143a24810948b2ac29446d0321542911b
4
+ data.tar.gz: 88f4324fb104706a0705260d6c82ca7f2f04e66af1a9ca2c88d63e3142bcbe62
5
5
  SHA512:
6
- metadata.gz: a82577aaf2f8c3620a7bf45dd2baa08f1ec3e5da6d2ea9cc280ac7c7b78f974bfd0c5ae85eba516c4fec7d35fa7cb46207efbc9679b85b53d30f1bb5f555cdbc
7
- data.tar.gz: 6c31a0c1f9e9c8294f45e3f15839379533ee5a7a2b40c021c25ff977977133ac1e77a6262d25148039fa675c313d6e403927f7259f33780e75fac6e7f5e667d6
6
+ metadata.gz: 2d2675f76bb8127405617c12f4fa39a5e6ae2f3507de8131b4f878a22dc9884ba46e2d50125729e2471bbaee9c2be2fbd164b047914738c5f2b7f13642b71197
7
+ data.tar.gz: eaa4672e9905b147a25b8a376c871debc1841879e8f486d58f8928c14c7289ce19b3813b6ad7a8042f49a0eca88f5d190270e334b9799371b252468575a2a803
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # MainLoop
2
2
 
3
+ <div align="center">
4
+
3
5
  [![Gem Version](https://badge.fury.io/rb/main_loop.svg)](https://rubygems.org/gems/main_loop)
4
6
  [![Gem](https://img.shields.io/gem/dt/main_loop.svg)](https://rubygems.org/gems/main_loop/versions)
5
7
  [![YARD](https://badgen.net/badge/YARD/doc/blue)](http://www.rubydoc.info/gems/main_loop)
@@ -9,34 +11,227 @@
9
11
  [![Outdated](https://lysander.rnds.pro/api/v1/badges/main_loop_outdated.svg)](https://lysander.rnds.pro/api/v1/badges/main_loop_outdated.html)
10
12
  [![Vulnerabilities](https://lysander.rnds.pro/api/v1/badges/main_loop_vulnerable.svg)](https://lysander.rnds.pro/api/v1/badges/main_loop_vulnerable.html)
11
13
 
12
- MainLoop is a simple main application implementation to control subprocesses(children) and threads.
14
+ </div>
15
+
16
+ **MainLoop** — Ruby-библиотека для управления субпроцессами и потоками с функциями:
17
+ - автоматический сбор дочерних процессов (reaping)
18
+ - корректное завершение (SIGTERM/SIGINT) процессов и потоков
19
+ - автоматический перезапуск по количеству повторов
20
+ - принудительное завершение по таймауту
21
+ - обработка завершения процессов и потоков
22
+
23
+ ---
24
+
25
+ **MainLoop** is a Ruby library for managing subprocesses and threads with features:
26
+ - automatic child process reaping
27
+ - graceful shutdown (SIGTERM/SIGINT) for processes and threads
28
+ - automatic restart by retry count
29
+ - timeout-based force termination
30
+ - process/thread completion handling
31
+
32
+ <div align="left">
33
+ <a href="https://rnds.pro/" >
34
+ <img src="https://library.rnds.pro/repository/public-blob/logo/RNDS.svg" alt="Supported by RNDSOFT" height="60">
35
+ </a>
36
+ </div>
37
+
38
+ ## Возможности / Features
39
+
40
+ - Потоко-безопасный канал обмена событиями (IO.pipe) / Thread-safe event bus (IO.pipe)
41
+ - Управление процессами через Kernel.fork / Process management via Kernel.fork
42
+ - Управление потоками через Thread.new / Thread management via Thread.new
43
+ - Обработка сигналов TERM/INT/CLD / SIGTERM/INT/CLD signal handling
44
+ - Автоматический сбор завершенных процессов / Automatic child process reaping
45
+ - Retry-логика для перезапуска / Retry logic for restarts
46
+ - Принудительное завершение по таймауту / Timeout-based force termination
47
+
48
+ ## Начало работы / Getting started
49
+
50
+ > gem install main_loop
51
+
52
+ При установке `MainLoop` через bundler добавьте следующую строку в `Gemfile`:
53
+
54
+ ---
55
+
56
+ If you'd rather install `MainLoop` using bundler, add a line for it in your `Gemfile`:
57
+
58
+ > gem 'main_loop'
59
+
60
+ Затем выполните / Then run:
61
+
62
+ > bundle install # для установки гема / gem installation
63
+
64
+ ## Корневой модуль / Root module
65
+
66
+ `MainLoop` - это корневой модуль, который подключает все компоненты:
67
+
68
+ ---
69
+
70
+ `MainLoop` is the root module that requires all components:
71
+
72
+ ```ruby
73
+ require 'main_loop'
74
+ ```
75
+
76
+ Субмодули / Submodules:
77
+
78
+ - `MainLoop::Bus` — канал обмена событиями (IO.pipe)
79
+ - `MainLoop::Dispatcher` — координация обработчиков и управление жизненным циклом
80
+ - `MainLoop::Loop` — главный цикл обработки событий и сигналов
81
+ - `MainLoop::Handler` — абстрактный базовый класс
82
+ - `MainLoop::ProcessHandler` — управление субпроцессами
83
+ - `MainLoop::ThreadHandler` — управление потоками
84
+
85
+ ## Архитектура / Architecture
86
+
87
+ ```mermaid
88
+ graph TB
89
+ subgraph "MainLoop"
90
+ Loop["Loop<br><i>Главный цикл</i>"]
91
+ Dispatcher["Dispatcher<br><i>Координирует обработчиков</i>"]
92
+ Bus["Bus<br><i>Канал обмена событиями (IO.pipe)</i>"]
93
+ Handler["Handler<br><i>Базовый класс</i>"]
94
+ ProcessHandler["ProcessHandler<br><i>Управляет процессами</i>"]
95
+ ThreadHandler["ThreadHandler<br><i>Управляет потоками</i>"]
96
+
97
+ Loop -->|координирует| Dispatcher
98
+ Dispatcher -->|использует| Bus
99
+ Loop -->|получает события из| Bus
100
+ ProcessHandler -->|наследует| Handler
101
+ ThreadHandler -->|наследует| Handler
102
+ ProcessHandler -->|регистрируется в| Dispatcher
103
+ ThreadHandler -->|регистрируется в| Dispatcher
104
+ end
105
+ ```
106
+
107
+ ## Жизненный цикл обработчика / Handler lifecycle
108
+
109
+ ```mermaid
110
+ sequenceDiagram
111
+ participant Cycle as Loop
112
+ participant Bus
113
+ participant Dispatcher
114
+ participant Handler
115
+
116
+ note over Cycle,Handler: Инициализация / Initialization
117
+ Cycle->>Bus: install_signal_handlers
118
+ Handler->>Dispatcher: add_handler
119
+
120
+ Cycle->>Cycle: start_loop_forever
121
+ loop Forever
122
+ Cycle->>Bus: gets(wait)
123
+ alt sig:TERM/INT
124
+ Bus-->>Cycle: "sig:TERM"
125
+ Cycle->>Dispatcher: term
126
+ Dispatcher->>Dispatcher: @terminating_at = Time.now
127
+ note right of Dispatcher: Graceful termination
128
+ Dispatcher->>Handler: term
129
+ opt @on_term || @runnable.on_term
130
+ Handler->>Handler: @on_term.call(pid/thread)
131
+ end
132
+ else sig:CLD
133
+ Cycle->>Cycle: wait for reap_children
134
+ else reap:pid:status
135
+ Cycle->>Dispatcher: reap_by_id
136
+ Dispatcher->>Handler: reap(status)
137
+ alt retry_count > 0
138
+ Handler->>Handler: handle_retry -> run
139
+ else
140
+ Handler->>Bus: publish(:term)
141
+ end
142
+ end
143
+ Cycle->>Dispatcher: tick
144
+ alt need_force_kill?
145
+ note right of Dispatcher: Превышен timeout / Timeout exceeded
146
+ Dispatcher->>Dispatcher: @killed = true
147
+ Dispatcher->>Handler: kill
148
+ end
149
+ end
150
+ ```
151
+
152
+ ## Способы определения логики
13
153
 
14
- Features:
15
- - reaping children
16
- - handling SIGTERM SIGINT to shutdown children(and threads) gracefully
17
- - restarting children
18
- - termination the children
154
+ Библиотека поддерживает два основных подхода для описания работы процессов и потоков:
19
155
 
20
- # Usage
156
+ ### 1. **Блок (inline block)**
21
157
 
22
- Example usage:
158
+ Передайте блок кода непосредственно в конструктор обработчика (`ProcessHandler` или `ThreadHandler`). В этом блоке размещается основная логика. При завершении (по сигналу, ошибке или таймауту) блок прерывается; вы можете определить дополнительные действия, используя переданный объект обработчика.
159
+
160
+ ```ruby
161
+ MainLoop::ProcessHandler.new(dispatcher, 'my_process', retry_count: 3) do
162
+ # основная логика
163
+ loop { sleep 1 }
164
+ end
165
+
166
+ MainLoop::ThreadHandler.new(dispatcher, 'my_thread', retry_count: 0) do |handler|
167
+ handler.on_term do
168
+ # действия при завершении (очистка, закрытие ресурсов)
169
+ @stop = true
170
+ end
171
+ # основная логика с возможностью проверки флага
172
+ @stop = false
173
+ loop { sleep 1; break if @stop }
174
+ end
175
+ ```
176
+
177
+ ### 2. **Объект с интерфейсом run / on_term**
178
+
179
+ Передайте экземпляр класса, который реализует два обязательных метода:
180
+ - `run` — содержит основную логику; для `ProcessHandler` метод не принимает аргументов, для `ThreadHandler` получает объект потока.
181
+ - `on_term` — вызывается при необходимости завершить процесс/поток; для `ProcessHandler` принимает PID, для `ThreadHandler` — объект потока. В этом методе следует инициировать корректное завершение (например, послать сигнал процессу или установить флаг остановки для потока).
182
+
183
+ ```ruby
184
+ class Worker
185
+ def run
186
+ trap('USR1') { @stop = true; raise Interrupt }
187
+ @stop = false
188
+ loop { sleep 1; break if @stop }
189
+ rescue Interrupt
190
+ # завершаемся
191
+ exit 0
192
+ end
193
+
194
+ def on_term(pid)
195
+ Process.kill('USR1', pid)
196
+ end
197
+ end
198
+
199
+ MainLoop::ProcessHandler.new(dispatcher, 'worker', runnable: Worker.new)
200
+ ```
201
+
202
+ Оба подхода могут комбинироваться с параметрами (`retry_count`, `logger` и т.д.) и одинаково хорошо интегрируются с циклом `MainLoop::Loop`.
203
+
204
+ Выбор зависит от удобства: для простых сценариев подойдёт блок, для сложной логики управления завершением — объект с явными методами.
205
+
206
+ ## Использование / Usage
207
+
208
+ ### Базовая настройка / Basic setup
23
209
 
24
210
  ```ruby
25
211
  require 'main_loop'
212
+ require 'logger'
26
213
 
27
214
  logger = Logger.new(STDOUT)
28
215
  logger.level = Logger::DEBUG
29
216
 
217
+ # Шина и диспетчер. Параметр timeout (в секундах) опционален.
30
218
  bus = MainLoop::Bus.new
31
-
32
- dispatcher = MainLoop::Dispatcher.new(bus, logger: logger)
219
+ dispatcher = MainLoop::Dispatcher.new(bus, timeout: 10, logger: logger)
33
220
  mainloop = MainLoop::Loop.new(bus, dispatcher, logger: logger)
221
+ ```
222
+
223
+ ### Обработка процессов / Process handling
34
224
 
225
+ #### Простейший пример / Simplest example
226
+
227
+ ```ruby
228
+ # Процесс test1: будет перезапущен 3 раза, после завершения выходит с кодом 0
35
229
  MainLoop::ProcessHandler.new dispatcher, 'test1', retry_count: 3, logger: logger do
36
230
  sleep 2
37
231
  exit! 0
38
232
  end
39
233
 
234
+ # Процесс test2: обрабатывает SIGTERM и выходит с кодом 1 после 2 перезапусков
40
235
  MainLoop::ProcessHandler.new dispatcher, 'test2', retry_count: 2, logger: logger do
41
236
  trap 'TERM' do
42
237
  exit(0)
@@ -44,24 +239,228 @@ MainLoop::ProcessHandler.new dispatcher, 'test2', retry_count: 2, logger: logger
44
239
  sleep 2
45
240
  exit! 1
46
241
  end
242
+ ```
243
+
244
+ #### С блоком кода / With code block
245
+
246
+ ```ruby
247
+ MainLoop::ProcessHandler.new dispatcher, 'worker', retry_count: 3, logger: logger do
248
+ loop do
249
+ # основная логика / main logic
250
+ sleep 1
251
+ end
252
+ exit 0
253
+ end
254
+ ```
255
+
256
+ #### С объектом runnable / With runnable object
257
+
258
+ ```ruby
259
+ class Worker
260
+ def run
261
+ # Используем пользовательский сигнал, чтобы прервать системные вызовы (например, sleep)
262
+ trap('USR1') do
263
+ puts "Получен USR1. pid = #{Process.pid}"
264
+ @stop = true
265
+ raise Interrupt # прерывает текущий блок (sleep и т.д.)
266
+ end
267
+
268
+ @stop = false
269
+ loop do
270
+ puts "работаю..."
271
+ sleep 100
272
+ break if @stop
273
+ rescue Interrupt
274
+ puts "прерывание, выходим"
275
+ break
276
+ end
277
+ exit 0
278
+ end
279
+
280
+ def on_term(pid)
281
+ puts "Завершаю работу. pid = #{pid}"
282
+ Process.kill('USR1', pid) # посылаем пользовательский сигнал
283
+
284
+ begin
285
+ Timeout.timeout(5) { Process.wait(pid) }
286
+ rescue Timeout::Error
287
+ puts "не завершился за 5 секунд"
288
+ end
289
+
290
+ puts "Завершил работу"
291
+ end
292
+ end
47
293
 
294
+ worker = Worker.new
295
+ MainLoop::ProcessHandler.new(dispatcher, 'worker', runnable: worker, retry_count: :unlimited, logger: logger)
296
+ ```
297
+
298
+ ### Обработка потоков / Thread handling
299
+
300
+ #### Поток с блоком / Thread with block
301
+
302
+ ```ruby
303
+ # Поток thread2: не перезапускается (retry_count: 0), выполняет внешнюю команду
48
304
  MainLoop::ThreadHandler.new dispatcher, 'thread2', retry_count: 0, logger: logger do
49
305
  system('sleep 15;echo ok')
50
306
  end
307
+ ```
308
+
309
+ #### Поток с блоком и колбэком завершения / Thread with block and termination callback
310
+
311
+ ```ruby
312
+ MainLoop::ThreadHandler.new dispatcher, 'worker', retry_count: 0, logger: logger do |handler|
313
+ # Устанавливаем обработчик завершения потока
314
+ handler.on_term do
315
+ puts "Завершаем поток, выполняем cleanup..."
316
+ @stop = true
317
+ end
318
+
319
+ @stop = false
320
+ loop do
321
+ puts "Работаю..."
322
+ sleep 1
323
+ break if @stop
324
+ end
325
+ end
326
+ ```
327
+
328
+ #### Поток с объектом runnable / Thread with runnable object
51
329
 
330
+ ```ruby
331
+ class Worker
332
+ def run(thread)
333
+ @stop = false
334
+ loop do
335
+ sleep 60
336
+ break if @stop
337
+ end
338
+ end
339
+
340
+ def on_term(thread)
341
+ @stop = true
342
+ thread&.wakeup
343
+ end
344
+ end
345
+
346
+ worker = Worker.new
347
+ MainLoop::ThreadHandler.new dispatcher, 'worker', runnable: worker, logger: logger
348
+ ```
349
+
350
+ ### Запуск цикла / Start loop
351
+
352
+ ```ruby
353
+ # Бесконечный цикл / Infinite loop
52
354
  mainloop.run
355
+
356
+ # С таймаутом (30 секунд) / With timeout (30 seconds)
357
+ mainloop.run(30)
53
358
  ```
54
359
 
360
+ ## Обработка завершения / Termination handling
55
361
 
56
- # Installation
362
+ Когда отправляется сигнал `TERM` или `INT`:
57
363
 
58
- It's a gem:
59
- ```bash
60
- gem install main_loop
364
+ 1. `trap` перехватывает сигнал и отправляет `bus.puts("sig:TERM")`
365
+ 2. `Loop` получает событие из `Bus` и вызывает `Dispatcher#term`
366
+ 3. `Dispatcher` устанавливает `@terminating_at = Time.now`
367
+ 4. Все обработчики получают `term`:
368
+ - `ProcessHandler` посылает `Process.kill('TERM', pid)`
369
+ - `ThreadHandler` вызывает `@on_term` блок
370
+ 5. Если через `timeout` (по умолчанию 5 сек) процессы не завершились:
371
+ - `Dispatcher#tick` проверяет `need_force_kill?`
372
+ - Если `true` — посылает `kill` всем обработчикам
373
+ 6. Когда все обработчики завершаются (`finished?`):
374
+ - `Dispatcher#try_exit!` вызывает `exit(@exit_code)`
375
+
376
+ ---
377
+
378
+ When sending `TERM` or `INT` signal:
379
+
380
+ 1. `trap` catches the signal and sends `bus.puts("sig:TERM")`
381
+ 2. `Loop` gets the event from `Bus` and calls `Dispatcher#term`
382
+ 3. `Dispatcher` sets `@terminating_at = Time.now`
383
+ 4. All handlers receive `term`:
384
+ - `ProcessHandler` sends `Process.kill('TERM', pid)`
385
+ - `ThreadHandler` calls `@on_term` block
386
+ 5. If processes don't terminate within `timeout` (default 5 seconds):
387
+ - `Dispatcher#tick` checks `need_force_kill?`
388
+ - If `true` — sends `kill` to all handlers
389
+ 6. When all handlers finish (`finished?`):
390
+ - `Dispatcher#try_exit!` calls `exit(@exit_code)`
391
+
392
+ ## Повторы (retry) / Retry
393
+
394
+ ```ruby
395
+ retry_count: 3 # повторить 3 раза / retry 3 times
396
+ retry_count: 0 # не повторять / don't retry
397
+ retry_count: :unlimited # бесконечные повторы / infinite retries
61
398
  ```
62
- There's also the wonders of [the Gemfile](http://bundler.io):
399
+
400
+ ## Публикация событий / Publishing events
401
+
402
+ Обработчики могут отправлять события в шину:
403
+
404
+ ---
405
+
406
+ Handlers can publish events to the bus:
407
+
63
408
  ```ruby
64
- gem 'main_loop'
409
+ # Из любого места обработчика / From any handler place:
410
+ publish("reap:#{id}:exited")
411
+ publish(:term)
412
+ ```
413
+
414
+ ## Особенности / Features
415
+
416
+ - **Потоко-безопасность**: `Bus` и `Dispatcher` используют `MonitorMixin`
417
+ - **Таймауты**: `Timeouter` используется для timeout в `Bus#gets` и `Loop#start_loop_forever`
418
+ - **Логирование**: все классы принимают параметр `logger:`, по умолчанию `Logger.new(nil)`
419
+ - **Ошибки**: используйте `rescue StandardError` (не пустой `rescue`)
420
+ - **Коды выхода**: `exit!(code)` в процессах, `exit(code)` в основном потоке
421
+
422
+ ---
423
+
424
+ - **Thread safety**: `Bus` and `Dispatcher` use `MonitorMixin`
425
+ - **Timeouts**: `Timeouter` is used for timeout in `Bus#gets` и `Loop#start_loop_forever`
426
+ - **Logging**: all classes accept `logger:` parameter, default is `Logger.new(nil)`
427
+ - **Error handling**: use `rescue StandardError` (not bare `rescue`)
428
+ - **Exit codes**: `exit!(code)` in processes, `exit(code)` in main thread
429
+
430
+ ## Тестирование / Testing
431
+
432
+ ```bash
433
+ # Запуск всех тестов / Run all tests
434
+ bundle exec rspec
435
+
436
+ # Запуск одного файла / Run single file
437
+ bundle exec rspec spec/bus_spec.rb
438
+
439
+ # Запуск одного теста / Run single test
440
+ bundle exec rspec spec/bus_spec.rb:12
441
+
442
+ # Покрытие кода / Code coverage (96%+)
443
+ bundle exec rspec --format progress
65
444
  ```
66
445
 
446
+ Для запуска примеров из локальной копии репозитория (без установки гема) используйте опцию `-I` интерпретатора Ruby, указав путь к директории `lib` относительно текущей папки:
447
+
448
+ ```bash
449
+ ruby -I ./lib examples/имя_файла.rb
450
+ ```
451
+
452
+ ## Версия / Version
453
+
454
+ Текущая версия / Current version: `0.1.4`
455
+
456
+ ## Автор / Author
457
+
458
+ Юрий Самойленко / Yuri Samoylenko <kinnalru@gmail.com>
459
+
460
+ ## Лицензия / License
461
+
462
+ Библиотека доступна с открытым исходным кодом в соответствии с условиями [лицензии MIT](LICENSE).
463
+
464
+ ---
67
465
 
466
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
data/lib/main_loop/bus.rb CHANGED
@@ -1,15 +1,49 @@
1
1
  require 'monitor'
2
2
  require 'timeouter'
3
3
 
4
+ # = MainLoop::Bus
5
+ #
6
+ # Потоко-безопасный канал обмена событиями между компонентами на основе IO.pipe.
7
+ #
8
+ # Использует {MonitorMixin} для синхронизации доступа и {Timeouter} для таймаутов.
9
+ # Буферизация строк для корректного парсинга построчно.
10
+ #
11
+ # == События
12
+ #
13
+ # События — это строки, отправляемые через {puts}. События читаются методами {gets}
14
+ # и {gets_nonblock}. Разделитель строк — newline ("\n").
15
+ #
16
+ # == Пример использования
17
+ #
18
+ # bus = MainLoop::Bus.new
19
+ # bus.puts("term") # Отправить событие
20
+ # event = bus.gets(5) # Получить событие с таймаутом 5 секунд
21
+ #
22
+ # == См. также
23
+ # - {MainLoop::Loop} — потребитель событий
24
+ # - {MainLoop::Dispatcher} — потребитель событий
25
+ # - {MonitorMixin} — реализация потоко-безопасности
26
+
4
27
  module MainLoop
5
28
  class Bus
6
-
7
29
  include MonitorMixin
30
+ # Константа конца строки (end of line)
31
+ # @return [String]
32
+ EOL = "\n".freeze
8
33
 
9
34
  attr_reader :read, :write
10
35
 
11
- EOL = "\n".freeze
12
-
36
+ # == Инициализация
37
+ #
38
+ # Создает IO.pipe, настраивает синхронный режим, инициализирует буфер.
39
+ #
40
+ # @example
41
+ # bus = MainLoop::Bus.new
42
+ #
43
+ # @!attribute [r] read
44
+ # @return [IO] конец канала для чтения
45
+ # @!attribute [r] write
46
+ # @return [IO] конец канала для записи
13
47
  def initialize
14
48
  super()
15
49
  @read, @write = IO.pipe
@@ -18,29 +52,67 @@ module MainLoop
18
52
  @buffer = ''
19
53
  end
20
54
 
55
+ # == Проверка наличия событий
56
+ #
57
+ # Проверяет, есть ли события в канале.
58
+ #
59
+ # @param timeout [Numeric] таймаут ожидания в секундах
60
+ # @return [Boolean] true если событий нет, false если событие доступно
21
61
  def empty?(timeout = 0)
22
62
  !wait_for_event(timeout)
23
63
  end
24
64
 
65
+ # == Закрытие канала
66
+ #
67
+ # Закрывает оба конца канала (read и write).
68
+ # Ошибки закрытия игнорируются (rescue nil).
69
+ #
70
+ # @example
71
+ # bus.close
25
72
  def close
26
73
  @write.close rescue nil
27
74
  @read.close rescue nil
28
75
  end
29
76
 
77
+ # == Проверка закрытости канала
78
+ #
79
+ # @return [Boolean] true если любой конец канала закрыт
30
80
  def closed?
31
81
  @write.closed? || @read.closed?
32
82
  end
33
83
 
84
+ # == Отправка события
85
+ #
86
+ # Отправляет строку в канал с потоко-безопасной синхронизацией.
87
+ # Автоматически добавляет конец строки.
88
+ #
89
+ # @param str [String] событие для отправки
90
+ # @example
91
+ # bus.puts("term")
92
+ # bus.puts("reap:123:0")
34
93
  def puts(str)
35
94
  synchronize do
36
95
  @write.puts str.to_s
37
96
  end
38
97
  end
39
98
 
99
+ # == Ожидание события
100
+ #
101
+ # Использует IO.select для ожидания события в канале.
102
+ #
103
+ # @param timeout [Numeric] таймаут ожидания в секундах
104
+ # @return [Array<IO>|nil] результат IO.select или nil при таймауте
40
105
  def wait_for_event(timeout)
41
106
  IO.select([@read], [], [], timeout)
42
107
  end
43
108
 
109
+ # == Блокирующее чтение события
110
+ #
111
+ # Блокирует до получения строки или таймаута.
112
+ # Использует {Timeouter.loop} для ограничения времени ожидания.
113
+ #
114
+ # @param timeout [Numeric] таймаут ожидания в секундах
115
+ # @return [String|nil] прочитанная строка (без символа новой строки) или nil при таймауте
44
116
  def gets(timeout)
45
117
  Timeouter.loop(timeout) do |t|
46
118
  line = gets_nonblock if wait_for_event(t.left)
@@ -48,6 +120,12 @@ module MainLoop
48
120
  end
49
121
  end
50
122
 
123
+ # == Неблокирующее чтение символа
124
+ #
125
+ # Читает посимвольно до конца строки (EOL).
126
+ # Буферизует символы и возвращает полную строку.
127
+ #
128
+ # @return [String|nil] строка (без EOL и пробелов по краям) или nil если нет данных
51
129
  def gets_nonblock
52
130
  while (ch = @read.read_nonblock(1))
53
131
  @buffer << ch
@@ -61,7 +139,5 @@ module MainLoop
61
139
  rescue IO::WaitReadable
62
140
  nil
63
141
  end
64
-
65
142
  end
66
143
  end
67
-