polyphony 0.43.10 → 0.45.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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -1
  3. data/CHANGELOG.md +37 -0
  4. data/Gemfile.lock +16 -6
  5. data/Rakefile +1 -1
  6. data/TODO.md +15 -10
  7. data/docs/_posts/2020-07-26-polyphony-0.44.md +77 -0
  8. data/docs/api-reference/thread.md +1 -1
  9. data/docs/getting-started/overview.md +14 -14
  10. data/docs/getting-started/tutorial.md +1 -1
  11. data/examples/adapters/redis_client.rb +3 -1
  12. data/examples/adapters/redis_pubsub_perf.rb +11 -8
  13. data/examples/adapters/sequel_mysql.rb +23 -0
  14. data/examples/adapters/sequel_mysql_pool.rb +33 -0
  15. data/examples/adapters/sequel_pg.rb +24 -0
  16. data/examples/core/{02-awaiting-fibers.rb → await.rb} +0 -0
  17. data/examples/core/{xx-channels.rb → channels.rb} +0 -0
  18. data/examples/core/deferring-an-operation.rb +16 -0
  19. data/examples/core/{xx-erlang-style-genserver.rb → erlang-style-genserver.rb} +16 -9
  20. data/examples/core/{xx-forking.rb → forking.rb} +1 -1
  21. data/examples/core/handling-signals.rb +11 -0
  22. data/examples/core/{03-interrupting.rb → interrupt.rb} +0 -0
  23. data/examples/core/{xx-pingpong.rb → pingpong.rb} +7 -5
  24. data/examples/core/{xx-recurrent-timer.rb → recurrent-timer.rb} +1 -1
  25. data/examples/core/{xx-resource_delegate.rb → resource_delegate.rb} +3 -4
  26. data/examples/core/{01-spinning-up-fibers.rb → spin.rb} +1 -1
  27. data/examples/core/{xx-spin_error_backtrace.rb → spin_error_backtrace.rb} +1 -1
  28. data/examples/core/{xx-supervise-process.rb → supervise-process.rb} +8 -5
  29. data/examples/core/supervisor.rb +20 -0
  30. data/examples/core/{xx-thread-sleep.rb → thread-sleep.rb} +0 -0
  31. data/examples/core/{xx-thread_pool.rb → thread_pool.rb} +0 -0
  32. data/examples/core/{xx-throttling.rb → throttling.rb} +0 -0
  33. data/examples/core/{xx-timeout.rb → timeout.rb} +0 -0
  34. data/examples/core/{xx-using-a-mutex.rb → using-a-mutex.rb} +0 -0
  35. data/examples/core/{xx-worker-thread.rb → worker-thread.rb} +2 -2
  36. data/examples/io/{xx-backticks.rb → backticks.rb} +0 -0
  37. data/examples/io/{xx-echo_client.rb → echo_client.rb} +1 -1
  38. data/examples/io/{xx-echo_client_from_stdin.rb → echo_client_from_stdin.rb} +2 -2
  39. data/examples/io/{xx-echo_pipe.rb → echo_pipe.rb} +1 -1
  40. data/examples/io/{xx-echo_server.rb → echo_server.rb} +0 -0
  41. data/examples/io/{xx-echo_server_with_timeout.rb → echo_server_with_timeout.rb} +1 -1
  42. data/examples/io/{xx-echo_stdin.rb → echo_stdin.rb} +0 -0
  43. data/examples/io/{xx-happy-eyeballs.rb → happy-eyeballs.rb} +0 -0
  44. data/examples/io/{xx-httparty.rb → httparty.rb} +4 -13
  45. data/examples/io/{xx-irb.rb → irb.rb} +0 -0
  46. data/examples/io/{xx-net-http.rb → net-http.rb} +0 -0
  47. data/examples/io/{xx-open.rb → open.rb} +0 -0
  48. data/examples/io/pry.rb +18 -0
  49. data/examples/io/rack_server.rb +71 -0
  50. data/examples/io/{xx-system.rb → system.rb} +1 -1
  51. data/examples/io/{xx-tcpserver.rb → tcpserver.rb} +0 -0
  52. data/examples/io/{xx-tcpsocket.rb → tcpsocket.rb} +0 -0
  53. data/examples/io/tunnel.rb +6 -1
  54. data/examples/io/{xx-zip.rb → zip.rb} +0 -0
  55. data/examples/performance/fiber_transfer.rb +2 -1
  56. data/examples/performance/fs_read.rb +5 -6
  57. data/examples/{io/xx-switch.rb → performance/switch.rb} +2 -1
  58. data/examples/performance/thread-vs-fiber/{xx-httparty_multi.rb → httparty_multi.rb} +3 -4
  59. data/examples/performance/thread-vs-fiber/{xx-httparty_threaded.rb → httparty_threaded.rb} +0 -0
  60. data/examples/performance/thread-vs-fiber/polyphony_mt_server.rb +1 -1
  61. data/examples/performance/thread-vs-fiber/polyphony_server.rb +1 -1
  62. data/examples/performance/thread-vs-fiber/polyphony_server_read_loop.rb +1 -1
  63. data/examples/performance/thread-vs-fiber/threaded_server.rb +1 -5
  64. data/examples/performance/thread_pool_perf.rb +6 -7
  65. data/ext/polyphony/backend.h +40 -0
  66. data/ext/polyphony/event.c +3 -3
  67. data/ext/polyphony/extconf.rb +1 -1
  68. data/ext/polyphony/fiber.c +66 -6
  69. data/ext/polyphony/{libev_agent.c → libev_backend.c} +239 -235
  70. data/ext/polyphony/polyphony.c +3 -3
  71. data/ext/polyphony/polyphony.h +15 -23
  72. data/ext/polyphony/polyphony_ext.c +3 -4
  73. data/ext/polyphony/queue.c +25 -12
  74. data/ext/polyphony/ring_buffer.c +0 -1
  75. data/ext/polyphony/thread.c +36 -33
  76. data/lib/polyphony.rb +25 -38
  77. data/lib/polyphony/adapters/fs.rb +1 -1
  78. data/lib/polyphony/adapters/irb.rb +2 -17
  79. data/lib/polyphony/adapters/mysql2.rb +19 -0
  80. data/lib/polyphony/adapters/postgres.rb +5 -5
  81. data/lib/polyphony/adapters/process.rb +2 -2
  82. data/lib/polyphony/adapters/readline.rb +17 -0
  83. data/lib/polyphony/adapters/redis.rb +1 -1
  84. data/lib/polyphony/adapters/sequel.rb +45 -0
  85. data/lib/polyphony/core/exceptions.rb +11 -0
  86. data/lib/polyphony/core/global_api.rb +17 -12
  87. data/lib/polyphony/core/resource_pool.rb +20 -7
  88. data/lib/polyphony/core/sync.rb +46 -8
  89. data/lib/polyphony/core/throttler.rb +1 -1
  90. data/lib/polyphony/extensions/core.rb +38 -25
  91. data/lib/polyphony/extensions/fiber.rb +12 -45
  92. data/lib/polyphony/extensions/io.rb +45 -12
  93. data/lib/polyphony/extensions/openssl.rb +6 -6
  94. data/lib/polyphony/extensions/socket.rb +22 -15
  95. data/lib/polyphony/extensions/thread.rb +6 -5
  96. data/lib/polyphony/net.rb +2 -1
  97. data/lib/polyphony/version.rb +1 -1
  98. data/polyphony.gemspec +7 -3
  99. data/test/helper.rb +1 -1
  100. data/test/{test_agent.rb → test_backend.rb} +22 -22
  101. data/test/test_fiber.rb +28 -11
  102. data/test/test_io.rb +17 -1
  103. data/test/test_kernel.rb +5 -0
  104. data/test/test_resource_pool.rb +50 -16
  105. data/test/test_signal.rb +5 -29
  106. data/test/test_socket.rb +17 -0
  107. data/test/test_sync.rb +52 -0
  108. metadata +126 -98
  109. data/.gitbook.yaml +0 -4
  110. data/examples/adapters/concurrent-ruby.rb +0 -9
  111. data/examples/core/04-handling-signals.rb +0 -19
  112. data/examples/core/xx-agent.rb +0 -102
  113. data/examples/core/xx-at_exit.rb +0 -29
  114. data/examples/core/xx-caller.rb +0 -12
  115. data/examples/core/xx-daemon.rb +0 -14
  116. data/examples/core/xx-deadlock.rb +0 -8
  117. data/examples/core/xx-deferring-an-operation.rb +0 -14
  118. data/examples/core/xx-exception-backtrace.rb +0 -40
  119. data/examples/core/xx-fork-cleanup.rb +0 -22
  120. data/examples/core/xx-fork-spin.rb +0 -42
  121. data/examples/core/xx-fork-terminate.rb +0 -27
  122. data/examples/core/xx-move_on.rb +0 -23
  123. data/examples/core/xx-queue-async.rb +0 -120
  124. data/examples/core/xx-readpartial.rb +0 -18
  125. data/examples/core/xx-signals.rb +0 -16
  126. data/examples/core/xx-sleep-forever.rb +0 -9
  127. data/examples/core/xx-sleeping.rb +0 -25
  128. data/examples/core/xx-snooze-starve.rb +0 -16
  129. data/examples/core/xx-spin-fork.rb +0 -49
  130. data/examples/core/xx-state-machine.rb +0 -51
  131. data/examples/core/xx-stop.rb +0 -20
  132. data/examples/core/xx-supervisors.rb +0 -21
  133. data/examples/core/xx-thread-selector-sleep.rb +0 -51
  134. data/examples/core/xx-thread-selector-snooze.rb +0 -46
  135. data/examples/core/xx-thread-snooze.rb +0 -34
  136. data/examples/core/xx-timer-gc.rb +0 -17
  137. data/examples/core/xx-trace.rb +0 -79
  138. data/examples/performance/xx-array.rb +0 -11
  139. data/examples/performance/xx-fiber-switch.rb +0 -9
  140. data/examples/performance/xx-snooze.rb +0 -15
  141. data/examples/xx-spin.rb +0 -32
  142. data/ext/polyphony/agent.h +0 -39
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'fileutils'
4
4
 
5
- require_relative './core/thread_pool'
5
+ require_relative '../core/thread_pool'
6
6
 
7
7
  ::File.singleton_class.instance_eval do
8
8
  alias_method :orig_stat, :stat
@@ -16,7 +16,7 @@ if Object.constants.include?(:Reline)
16
16
  fiber.cancel
17
17
  end
18
18
  read_ios.each do |io|
19
- Thread.current.agent.wait_io(io, false)
19
+ Thread.current.backend.wait_io(io, false)
20
20
  return [io]
21
21
  end
22
22
  rescue Polyphony::Cancel
@@ -26,22 +26,7 @@ if Object.constants.include?(:Reline)
26
26
  end
27
27
  end
28
28
  else
29
- # readline blocks the current thread, so we offload it to the blocking-ops
30
- # thread pool. That way, the reactor loop can keep running while waiting for
31
- # readline to return
32
- module ::Readline
33
- alias_method :orig_readline, :readline
34
-
35
- Workers = Polyphony::ThreadPool.new
36
-
37
- def readline(*args)
38
- p :readline
39
- # caller.each do |l|
40
- # STDOUT.orig_puts l
41
- # end
42
- Workers.process { orig_readline(*args) }
43
- end
44
- end
29
+ require_relative './readline'
45
30
 
46
31
  # RubyLex patches
47
32
  class ::RubyLex
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+ require 'mysql2/client'
5
+
6
+ # Mysql2::Client overrides
7
+ Mysql2::Client.prepend(Module.new do
8
+ def initialize(config)
9
+ config[:async] = true
10
+ super
11
+ @io = ::IO.for_fd(socket)
12
+ end
13
+
14
+ def query(sql, **options)
15
+ super
16
+ Thread.current.backend.wait_io(@io, false)
17
+ async_result
18
+ end
19
+ end)
@@ -15,8 +15,8 @@ module ::PG
15
15
  res = conn.connect_poll
16
16
  case res
17
17
  when PGRES_POLLING_FAILED then raise Error, conn.error_message
18
- when PGRES_POLLING_READING then Thread.current.agent.wait_io(socket_io, false)
19
- when PGRES_POLLING_WRITING then Thread.current.agent.wait_io(socket_io, true)
18
+ when PGRES_POLLING_READING then Thread.current.backend.wait_io(socket_io, false)
19
+ when PGRES_POLLING_WRITING then Thread.current.backend.wait_io(socket_io, true)
20
20
  when PGRES_POLLING_OK then return conn.setnonblocking(true)
21
21
  end
22
22
  end
@@ -42,7 +42,7 @@ class ::PG::Connection
42
42
 
43
43
  def get_result(&block)
44
44
  while is_busy
45
- Thread.current.agent.wait_io(socket_io, false)
45
+ Thread.current.backend.wait_io(socket_io, false)
46
46
  consume_input
47
47
  end
48
48
  orig_get_result(&block)
@@ -59,7 +59,7 @@ class ::PG::Connection
59
59
 
60
60
  def block(_timeout = 0)
61
61
  while is_busy
62
- Thread.current.agent.wait_io(socket_io, false)
62
+ Thread.current.backend.wait_io(socket_io, false)
63
63
  consume_input
64
64
  end
65
65
  end
@@ -97,7 +97,7 @@ class ::PG::Connection
97
97
  return move_on_after(timeout) { wait_for_notify(&block) } if timeout
98
98
 
99
99
  loop do
100
- Thread.current.agent.wait_io(socket_io, false)
100
+ Thread.current.backend.wait_io(socket_io, false)
101
101
  consume_input
102
102
  notice = notifies
103
103
  next unless notice
@@ -7,7 +7,7 @@ module Polyphony
7
7
  def watch(cmd = nil, &block)
8
8
  terminated = nil
9
9
  pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
10
- Thread.current.agent.waitpid(pid)
10
+ Thread.current.backend.waitpid(pid)
11
11
  terminated = true
12
12
  ensure
13
13
  kill_process(pid) unless terminated || pid.nil?
@@ -23,7 +23,7 @@ module Polyphony
23
23
 
24
24
  def kill_and_await(sig, pid)
25
25
  ::Process.kill(sig, pid)
26
- Thread.current.agent.waitpid(pid)
26
+ Thread.current.backend.waitpid(pid)
27
27
  rescue SystemCallError
28
28
  # ignore
29
29
  puts 'SystemCallError in kill_and_await'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'polyphony'
4
+ require 'readline'
5
+
6
+ # readline blocks the current thread, so we offload it to the blocking-ops
7
+ # thread pool. That way, the reactor loop can keep running while waiting for
8
+ # readline to return
9
+ module ::Readline
10
+ alias_method :orig_readline, :readline
11
+
12
+ Worker = Polyphony::ThreadPool.new(1)
13
+
14
+ def readline(*args)
15
+ Worker.process { orig_readline(*args) }
16
+ end
17
+ end
@@ -56,7 +56,7 @@ class Polyphony::RedisDriver
56
56
  reply = @reader.gets
57
57
  return reply if reply
58
58
 
59
- while (data = @connection.readpartial(8192))
59
+ @connection.read_loop do |data|
60
60
  @reader.feed(data)
61
61
  reply = @reader.gets
62
62
  return reply unless reply == false
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../polyphony'
4
+ require 'sequel'
5
+
6
+ module Polyphony
7
+ # Sequel ConnectionPool that delegates to Polyphony::ResourcePool.
8
+ class FiberConnectionPool < Sequel::ConnectionPool
9
+ def initialize(db, opts = OPTS)
10
+ super
11
+ max_size = Integer(opts[:max_connections] || 4)
12
+ @pool = Polyphony::ResourcePool.new(limit: max_size) { make_new(:default) }
13
+ end
14
+
15
+ def hold(_server = nil)
16
+ @pool.acquire do |conn|
17
+ yield conn
18
+ rescue Polyphony::BaseException
19
+ # The connection may be in an unrecoverable state if interrupted,
20
+ # discard the connection from the pool so it isn't reused.
21
+ @pool.discard!
22
+ raise
23
+ end
24
+ end
25
+
26
+ def size
27
+ @pool.size
28
+ end
29
+
30
+ def max_size
31
+ @pool.limit
32
+ end
33
+
34
+ def preconnect(_concurrent = false)
35
+ @pool.preheat!
36
+ end
37
+ end
38
+
39
+ # Override Sequel::Database to use FiberConnectionPool by default.
40
+ Sequel::Database.prepend(Module.new do
41
+ def connection_pool_default_options
42
+ { pool_class: FiberConnectionPool }
43
+ end
44
+ end)
45
+ end
@@ -33,4 +33,15 @@ module Polyphony
33
33
 
34
34
  # Restart is used to restart a fiber
35
35
  class Restart < BaseException; end
36
+
37
+ # Interjection is used to run arbitrary code on arbitrary fibers at any point
38
+ class Interjection < BaseException
39
+ def initialize(proc)
40
+ @proc = proc
41
+ end
42
+
43
+ def invoke
44
+ @proc.call
45
+ end
46
+ end
36
47
  end
@@ -19,13 +19,18 @@ module Polyphony
19
19
  fiber = ::Fiber.current
20
20
  canceller = spin do
21
21
  sleep interval
22
- exception = with_exception.is_a?(Class) ?
23
- with_exception.new : RuntimeError.new(with_exception)
22
+ exception = cancel_exception(with_exception)
24
23
  fiber.schedule exception
25
24
  end
26
25
  block ? cancel_after_wrap_block(canceller, &block) : canceller
27
26
  end
28
27
 
28
+ def cancel_exception(exception)
29
+ return exception.new if exception.is_a?(Class)
30
+
31
+ RuntimeError.new(exception)
32
+ end
33
+
29
34
  def cancel_after_wrap_block(canceller, &block)
30
35
  block.call
31
36
  ensure
@@ -50,7 +55,7 @@ module Polyphony
50
55
  next_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + interval
51
56
  loop do
52
57
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
53
- Thread.current.agent.sleep(next_time - now)
58
+ Thread.current.backend.sleep(next_time - now)
54
59
  yield
55
60
  loop do
56
61
  next_time += interval
@@ -87,8 +92,8 @@ module Polyphony
87
92
  Fiber.current.receive
88
93
  end
89
94
 
90
- def receive_pending
91
- Fiber.current.receive_pending
95
+ def receive_all_pending
96
+ Fiber.current.receive_all_pending
92
97
  end
93
98
 
94
99
  def supervise(*args, &block)
@@ -98,20 +103,20 @@ module Polyphony
98
103
  def sleep(duration = nil)
99
104
  return sleep_forever unless duration
100
105
 
101
- Thread.current.agent.sleep duration
106
+ Thread.current.backend.sleep duration
102
107
  end
103
108
 
104
109
  def sleep_forever
105
- Thread.current.agent.ref
110
+ Thread.current.backend.ref
106
111
  loop { sleep 60 }
107
112
  ensure
108
- Thread.current.agent.unref
113
+ Thread.current.backend.unref
109
114
  end
110
115
 
111
- def throttled_loop(rate, count: nil, &block)
112
- throttler = Polyphony::Throttler.new(rate)
113
- if count
114
- count.times { |_i| throttler.(&block) }
116
+ def throttled_loop(rate = nil, **opts, &block)
117
+ throttler = Polyphony::Throttler.new(rate || opts)
118
+ if opts[:count]
119
+ opts[:count].times { |_i| throttler.(&block) }
115
120
  else
116
121
  loop { throttler.(&block) }
117
122
  end
@@ -20,19 +20,25 @@ module Polyphony
20
20
  @stock.size
21
21
  end
22
22
 
23
- def acquire
23
+ def acquire(&block)
24
24
  fiber = Fiber.current
25
- return @acquired_resources[fiber] if @acquired_resources[fiber]
25
+ return yield @acquired_resources[fiber] if @acquired_resources[fiber]
26
26
 
27
- add_to_stock if @size < @limit && @stock.empty?
27
+ acquire_from_stock(fiber, &block)
28
+ end
29
+
30
+ def acquire_from_stock(fiber)
31
+ add_to_stock if (@stock.empty? || @stock.pending?) && @size < @limit
28
32
  resource = @stock.shift
29
33
  @acquired_resources[fiber] = resource
30
34
  yield resource
31
35
  ensure
32
- @acquired_resources.delete(fiber)
33
- @stock.push resource if resource
36
+ if resource && @acquired_resources[fiber] == resource
37
+ @acquired_resources.delete(fiber)
38
+ @stock.push resource
39
+ end
34
40
  end
35
-
41
+
36
42
  def method_missing(sym, *args, &block)
37
43
  acquire { |r| r.send(sym, *args, &block) }
38
44
  end
@@ -45,7 +51,14 @@ module Polyphony
45
51
  # @return [any] allocated resource
46
52
  def add_to_stock
47
53
  @size += 1
48
- @stock << @allocator.call
54
+ resource = @allocator.call
55
+ @stock << resource
56
+ end
57
+
58
+ # Discards the currently-acquired resource
59
+ # instead of returning it to the pool when done.
60
+ def discard!
61
+ @size -= 1 if @acquired_resources.delete(Fiber.current)
49
62
  end
50
63
 
51
64
  def preheat!
@@ -8,16 +8,54 @@ module Polyphony
8
8
  @store << :token
9
9
  end
10
10
 
11
- def synchronize
11
+ def synchronize(&block)
12
12
  return yield if @holding_fiber == Fiber.current
13
13
 
14
- begin
15
- token = @store.shift
16
- @holding_fiber = Fiber.current
17
- yield
18
- ensure
19
- @holding_fiber = nil
20
- @store << token if token
14
+ synchronize_not_holding(&block)
15
+ end
16
+
17
+ def synchronize_not_holding
18
+ @token = @store.shift
19
+ @holding_fiber = Fiber.current
20
+ yield
21
+ ensure
22
+ @holding_fiber = nil
23
+ @store << @token if @token
24
+ end
25
+
26
+ def conditional_release
27
+ @store << @token
28
+ @token = nil
29
+ @holding_fiber = nil
30
+ end
31
+
32
+ def conditional_reacquire
33
+ @token = @store.shift
34
+ @holding_fiber = Fiber.current
35
+ end
36
+ end
37
+
38
+ # Implements a fiber-aware ConditionVariable
39
+ class ConditionVariable
40
+ def initialize
41
+ @queue = Polyphony::Queue.new
42
+ end
43
+
44
+ def wait(mutex, _timeout = nil)
45
+ mutex.conditional_release
46
+ @queue << Fiber.current
47
+ Thread.current.backend.wait_event(true)
48
+ mutex.conditional_reacquire
49
+ end
50
+
51
+ def signal
52
+ fiber = @queue.shift
53
+ fiber.schedule
54
+ end
55
+
56
+ def broadcast
57
+ while (fiber = @queue.shift)
58
+ fiber.schedule
21
59
  end
22
60
  end
23
61
  end
@@ -12,7 +12,7 @@ module Polyphony
12
12
  def call
13
13
  now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
14
14
  delta = @next_time - now
15
- Thread.current.agent.sleep(delta) if delta > 0
15
+ Thread.current.backend.sleep(delta) if delta > 0
16
16
  yield self
17
17
 
18
18
  loop do
@@ -46,6 +46,10 @@ class ::Exception
46
46
 
47
47
  backtrace.reject { |l| l[POLYPHONY_DIR] }
48
48
  end
49
+
50
+ def invoke
51
+ Kernel.raise(self)
52
+ end
49
53
  end
50
54
 
51
55
  # Overrides for Process
@@ -53,7 +57,7 @@ module ::Process
53
57
  class << self
54
58
  alias_method :orig_detach, :detach
55
59
  def detach(pid)
56
- fiber = spin { Thread.current.agent.waitpid(pid) }
60
+ fiber = spin { Thread.current.backend.waitpid(pid) }
57
61
  fiber.define_singleton_method(:pid) { pid }
58
62
  fiber
59
63
  end
@@ -112,56 +116,65 @@ module ::Kernel
112
116
  strs = args.inject([]) do |m, a|
113
117
  m << a.inspect << "\n"
114
118
  end
115
- STDOUT.write *strs
119
+ STDOUT.write(*strs)
116
120
  args.size == 1 ? args.first : args
117
121
  end
118
122
 
119
123
  alias_method :orig_system, :system
120
124
  def system(*args)
121
- Open3.popen2(*args) do |i, o, _t|
122
- i.close
123
- pipe_to_eof(o, $stdout)
125
+ Kernel.system(*args)
126
+ end
127
+
128
+ class << self
129
+ alias_method :orig_system, :system
130
+ def system(*args)
131
+ waiter = nil
132
+ Open3.popen2(*args) do |i, o, t|
133
+ waiter = t
134
+ i.close
135
+ pipe_to_eof(o, $stdout)
136
+ end
137
+ waiter.await.last == 0
138
+ rescue SystemCallError
139
+ nil
124
140
  end
125
- true
126
- rescue SystemCallError
127
- nil
128
141
  end
129
142
 
130
143
  def pipe_to_eof(src, dest)
131
- loop do
132
- data = src.readpartial(8192)
133
- dest << data
134
- rescue EOFError
135
- break
136
- end
144
+ src.read_loop { |data| dest << data }
137
145
  end
138
146
 
139
147
  alias_method :orig_trap, :trap
140
148
  def trap(sig, command = nil, &block)
141
149
  return orig_trap(sig, command) if command.is_a? String
142
-
143
- block = command if command.respond_to?(:call) && !block
144
- exception = command.is_a?(Class) && command.new
150
+
151
+ block = command if !block && command.respond_to?(:call)
152
+ exception = signal_exception(block, command)
145
153
 
146
154
  # The signal trap can be invoked at any time, including while the system
147
- # agent is blocking while polling for events. In order to deal with this
155
+ # backend is blocking while polling for events. In order to deal with this
148
156
  # correctly, we spin a fiber that will run the signal handler code, then
149
157
  # call break_out_of_ev_loop, which will put the fiber at the front of the
150
- # run queue, then wake up the system agent.
158
+ # run queue, then wake up the backend.
151
159
  #
152
160
  # If the command argument is an exception class however, it will be raised
153
161
  # directly in the context of the main fiber.
154
162
  orig_trap(sig) do
155
- if exception
156
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
157
- else
158
- fiber = spin { snooze; block.call }
159
- Thread.current.break_out_of_ev_loop(fiber, nil)
160
- end
163
+ Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, exception)
161
164
  end
162
165
  end
163
166
  end
164
167
 
168
+ def signal_exception(block, command)
169
+ if block
170
+ Polyphony::Interjection.new(block)
171
+ elsif command.is_a?(Class)
172
+ command.new
173
+ else
174
+ raise ArgumentError, 'Must supply block or exception or callable object'
175
+ end
176
+ end
177
+
165
178
  # Override Timeout to use cancel scope
166
179
  module ::Timeout
167
180
  def self.timeout(sec, klass = nil, message = nil, &block)