uringmachine 0.29.2 → 0.30.0

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.
data/ext/um/um_ssl.c CHANGED
@@ -75,7 +75,7 @@ void um_ssl_set_bio(struct um *machine, VALUE ssl_obj)
75
75
  SSL_set0_wbio(ssl, bio);
76
76
  }
77
77
 
78
- int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr, int maxlen) {
78
+ int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr, size_t maxlen) {
79
79
  SSL *ssl = RTYPEDDATA_GET_DATA(ssl_obj);
80
80
 
81
81
  int ret = SSL_read(ssl, ptr, maxlen);
@@ -84,22 +84,23 @@ int um_ssl_read_raw(struct um *machine, VALUE ssl_obj, char *ptr, int maxlen) {
84
84
  return ret;
85
85
  }
86
86
 
87
- int um_ssl_read(struct um *machine, VALUE ssl_obj, VALUE buf, int maxlen) {
87
+ int um_ssl_read(struct um *machine, VALUE ssl_obj, VALUE buf, size_t maxlen) {
88
88
  void *ptr = um_prepare_read_buffer(buf, maxlen, 0);
89
89
  int ret = um_ssl_read_raw(machine, ssl_obj, ptr, maxlen);
90
90
  um_update_read_buffer(buf, 0, ret);
91
91
  return ret;
92
92
  }
93
93
 
94
- int um_ssl_write(struct um *machine, VALUE ssl_obj, VALUE buf, int len) {
94
+ int um_ssl_write(struct um *machine, VALUE ssl_obj, VALUE buf, size_t len) {
95
95
  SSL *ssl = RTYPEDDATA_GET_DATA(ssl_obj);
96
96
  const void *base;
97
97
  size_t size;
98
98
  um_get_buffer_bytes_for_writing(buf, &base, &size, true);
99
- if ((len == (int)-1) || (len > (int)size)) len = (int)size;
99
+ if (!len || (len > size)) len = size;
100
+ if (len > INT_MAX) len = INT_MAX;
100
101
  if (unlikely(!len)) return INT2NUM(0);
101
102
 
102
- int ret = SSL_write(ssl, base, len);
103
+ int ret = SSL_write(ssl, base, (int)len);
103
104
  if (ret <= 0) rb_raise(eUMError, "Failed to write");
104
105
 
105
106
  return ret;
data/ext/um/um_stream.c CHANGED
@@ -1,5 +1,6 @@
1
- #include "um.h"
2
1
  #include <stdlib.h>
2
+ #include <ruby/io/buffer.h>
3
+ #include "um.h"
3
4
 
4
5
  inline void stream_add_segment(struct um_stream *stream, struct um_segment *segment) {
5
6
  segment->next = NULL;
@@ -234,6 +235,13 @@ inline void stream_shift_head(struct um_stream *stream) {
234
235
  stream->pos = 0;
235
236
  }
236
237
 
238
+ inline VALUE make_segment_io_buffer(struct um_segment *segment, size_t pos) {
239
+ return rb_io_buffer_new(
240
+ segment->ptr + pos, segment->len - pos,
241
+ RB_IO_BUFFER_LOCKED|RB_IO_BUFFER_READONLY
242
+ );
243
+ }
244
+
237
245
  inline void stream_skip(struct um_stream *stream, size_t inc, int safe_inc) {
238
246
  if (unlikely(stream->eof && !stream->head)) return;
239
247
  if (safe_inc && !stream->tail && !stream_get_more_segments(stream)) return;
@@ -254,6 +262,30 @@ inline void stream_skip(struct um_stream *stream, size_t inc, int safe_inc) {
254
262
  }
255
263
  }
256
264
 
265
+ inline void stream_each(struct um_stream *stream) {
266
+ if (unlikely(stream->eof && !stream->head)) return;
267
+ if (!stream->tail && !stream_get_more_segments(stream)) return;
268
+
269
+ struct um_segment *current = stream->head;
270
+ size_t pos = stream->pos;
271
+
272
+ VALUE buffer = Qnil;
273
+ while (true) {
274
+ struct um_segment *next = current->next;
275
+ buffer = make_segment_io_buffer(current, pos);
276
+ rb_yield(buffer);
277
+ rb_io_buffer_free_locked(buffer);
278
+ stream_shift_head(stream);
279
+
280
+ if (!next) {
281
+ if (!stream_get_more_segments(stream)) return;
282
+ }
283
+ current = stream->head;
284
+ pos = 0;
285
+ }
286
+ RB_GC_GUARD(buffer);
287
+ }
288
+
257
289
  inline void stream_copy(struct um_stream *stream, char *dest, size_t len) {
258
290
  while (len) {
259
291
  char *segment_ptr = stream->head->ptr + stream->pos;
@@ -194,6 +194,19 @@ VALUE Stream_skip(VALUE self, VALUE len) {
194
194
  return len;
195
195
  }
196
196
 
197
+ /* call-seq:
198
+ * stream.each { |data| } -> stream
199
+ *
200
+ * Reads from the target, passing each chunk to the given block.
201
+ *
202
+ * @return [UringMachine::Stream] stream
203
+ */
204
+ VALUE Stream_each(VALUE self) {
205
+ struct um_stream *stream = um_get_stream(self);
206
+ stream_each(stream);
207
+ return self;
208
+ }
209
+
197
210
  /* call-seq:
198
211
  * stream.resp_decode -> obj
199
212
  *
@@ -286,6 +299,7 @@ void Init_Stream(void) {
286
299
  rb_define_method(cStream, "get_line", Stream_get_line, 1);
287
300
  rb_define_method(cStream, "get_string", Stream_get_string, 1);
288
301
  rb_define_method(cStream, "skip", Stream_skip, 1);
302
+ rb_define_method(cStream, "each", Stream_each, 0);
289
303
 
290
304
  rb_define_method(cStream, "resp_decode", Stream_resp_decode, 0);
291
305
  rb_define_singleton_method(cStream, "resp_encode", Stream_resp_encode, 2);
@@ -0,0 +1,267 @@
1
+ # Final Report for Ruby Association Grant Program 2025
2
+
3
+ ## Project Summary
4
+
5
+ Io_uring is a relatively new Linux API, permitting the asynchronous invocation
6
+ of Linux system calls. UringMachine is a gem that brings low-level access to the
7
+ io_uring interface to Ruby programs, and permits not only asynchronous I/O on
8
+ files and sockets, but also timeouts, futex wait/wake, statx and other system
9
+ calls, for use with fiber-based concurrency. This project aims to enhance
10
+ UringMachine to include a fiber scheduler implementation for usage with the
11
+ standard Ruby I/O classes, to have builtin support for SSL, to support more
12
+ io_uring ops such as writev, splice, fsync, mkdir, fadvise, etc.
13
+
14
+ I'd like to express my deep gratitude to Samuel Williams for his help and
15
+ guidance throughout this project.
16
+
17
+ ## About UringMachine
18
+
19
+ UringMachine provides an API that closely follows the shape of various Linux
20
+ system calls for I/O operations, such as `open`, `read`, `accept`, `setsockopt`
21
+ etc. Behind the scenes, UringMachine uses io_uring to submit I/O operations and
22
+ automatically switch between fibers when waiting for those operations to
23
+ complete. Here's a simple example of an echo server using UringMachine:
24
+
25
+ ```ruby
26
+ require 'uringmachine'
27
+
28
+ machine = UM.new
29
+ server_fd = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
30
+ machine.bind(server_fd, '0.0.0.0', 1234)
31
+ machine.listen(server_fd, UM::SOMAXCONN)
32
+
33
+ def handle_connection(machine, fd)
34
+ buf = +''
35
+ while true
36
+ res = machine.recv(fd, buf, 8192, 0)
37
+ break if res == 0
38
+
39
+ machine.send(fd, buf, res, 0)
40
+ end
41
+ ensure
42
+ machine.close(fd)
43
+ end
44
+
45
+ while true
46
+ fd = machine.accept(server_fd)
47
+ machine.spin(fd) {
48
+ handle_connection(machine, it)
49
+ }
50
+ end
51
+ ```
52
+
53
+ UringMachine also provides some higher-level methods and abstractions that make
54
+ use of more advanced io_uring features such as [multishot
55
+ operations](https://man.archlinux.org/man/io_uring_multishot.7.en) (e.g.
56
+ `UM#accept_each`) and [provided buffer
57
+ rings](https://man.archlinux.org/man/io_uring_provided_buffers.7.en) (the
58
+ `UM::Stream` class). This not only results in more concise code, but also has
59
+ [performance](#benchmarking) benefits. In addition, UringMachine provides some
60
+ convenience methods, such as `tcp_listen` which returns a TCP socket fd bound to
61
+ the given address and ready for accepting incoming connections.
62
+
63
+ Here's the same echo server program but using UringMachine's more advanced
64
+ features:
65
+
66
+ ```ruby
67
+ require 'uringmachine'
68
+
69
+ machine = UM.new
70
+ # Combined socket, bind, listen
71
+ server_fd = machine.tcp_listen('0.0.0.0', 1234)
72
+
73
+ def handle_connection(machine, fd)
74
+ stream = machine.stream(fd)
75
+ stream.each { |buf| machine.send(fd, buf, buf.size, 0) }
76
+ ensure
77
+ machine.close(fd)
78
+ end
79
+
80
+ machine.accept_each(server_fd) do |fd|
81
+ machine.spin(fd) {
82
+ handle_connection(machine, it)
83
+ }
84
+ end
85
+ ```
86
+
87
+ UringMachine also includes a fiber scheduler implementation that effectively
88
+ provides an alternative backend for performing I/O operations while still using
89
+ the same familiar API of `IO`, `Socket` and related standard classes:
90
+
91
+ ```ruby
92
+ machine = UM.new
93
+ scheduler = UM::FiberScheduler.new(machine)
94
+ Fiber.set_scheduler(scheduler)
95
+
96
+ Fiber.schedule {
97
+ puts('Hello from UringMachine!')
98
+ }
99
+
100
+ # await all pending fibers
101
+ scheduler.join
102
+ ```
103
+
104
+ ## The Work Done
105
+
106
+ ### Improvements to the Ruby `Fiber::Scheduler` interface
107
+
108
+ - [PR](https://github.com/ruby/ruby/pull/15213) to expose
109
+ `rb_process_status_new` internal Ruby C API
110
+ (https://bugs.ruby-lang.org/issues/21704). This is needed in order to allow
111
+ FiberScheduler implementations to instantiate `Process::Status` objects in the
112
+ `#process_wait` hook. This PR is still not merged.
113
+
114
+ - [PR](https://github.com/ruby/ruby/pull/15385) to cleanup FiberScheduler and
115
+ fiber state in a forked process (https://bugs.ruby-lang.org/issues/21717).
116
+ This was merged into Ruby 4.0.
117
+
118
+ - [PR](https://github.com/ruby/ruby/pull/15609) to invoke FiberScheduler
119
+ `io_write` hook on IO flush (https://bugs.ruby-lang.org/issues/21789). This
120
+ was merged into Ruby 4.0.
121
+
122
+ - Found an issue while implementing the `#io_pwrite` hook, which resulted in a
123
+ [PR](https://github.com/ruby/ruby/pull/15428) submitted by Samuel Williams,
124
+ and merged into Ruby 4.0.
125
+
126
+ - Worked with Samuel Williams on how to implement the `#io_close` hook, which
127
+ resulted in a [PR](https://github.com/ruby/ruby/pull/15434) submitted by
128
+ Samuel and merged into Ruby 4.0.
129
+
130
+ - [PR](https://github.com/ruby/ruby/pull/15865) to add socket I/O hooks to the
131
+ FiberScheduler interface (https://bugs.ruby-lang.org/issues/21837). This PR is
132
+ currently in draft phase, waiting input from Samuel Williams.
133
+
134
+ ### UringMachine `Fiber::Scheduler` Implementation
135
+
136
+ - I developed a [full
137
+ implementation](https://github.com/digital-fabric/uringmachine/blob/main/lib/uringmachine/fiber_scheduler.rb)
138
+ of the `Fiber::Scheduler` interface using UringMachine, with methods for all
139
+ hooks:
140
+
141
+ - `#scheduler_close`
142
+ - `#fiber`, `#yield`
143
+ - `#blocking_operation_wait`, `#block`, `#unblock`, `#fiber_interrupt`
144
+ - `#kernel_sleep`, `#timeout_after`
145
+ - `#io_read`, `#io_write`, `#io_pread`, `#io_pwrite`, `#io_close`
146
+ - `#io_wait`, `#io_select`
147
+ - `#process_wait` (relies on the `rb_process_status_new` PR)
148
+ - `#address_resolve`
149
+
150
+ - Wrote [extensive
151
+ tests](https://github.com/digital-fabric/uringmachine/blob/main/test/test_fiber_scheduler.rb)
152
+ for the UringMachine fiber scheduler.
153
+
154
+ ### SSL Integration
155
+
156
+ - I've added UringMachine method that installs a [custom OpenSSL
157
+ BIO](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_ssl.c)
158
+ on the given SSLSocket, as well as read/write methods that provide higher
159
+ throughput on SSL connections:
160
+
161
+ ```ruby
162
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, OpenSSL::SSL::SSLContext.new)
163
+ machine.ssl_set_bio(ssl)
164
+ msg = 'foo'
165
+ ssl.write(msg) # Performs IO using the UringMachine BIO
166
+
167
+ # Or: bypass the OpenSSL gem, directly invoking SSL_write and using the
168
+ # UringMachine BIO
169
+ machine.ssl_write(ssl, msg, msg.bytesize)
170
+ ```
171
+
172
+ - I've also submitted a [PR to the OpenSSL
173
+ gem](https://github.com/ruby/openssl/pull/1000) that adds support for using a
174
+ custom BIO method for reading and writing on SSL connections.
175
+
176
+ ### Buffered Reads Using io_uring Provided Buffers
177
+
178
+ - I developed the `Stream` API that uses io_uring [provided
179
+ buffers](https://man.archlinux.org/man/io_uring_provided_buffers.7.en) to
180
+ allow buffered reads (e.g. similar behavior to `IO#gets`, `IO#readpartial`)
181
+ while minimizing copying of data.
182
+ - The [buffer
183
+ pool](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_buffer_pool.c):
184
+ UringMachine allocates and provides buffers for kernel usage in multishot
185
+ read/recv operations. An adaptive algorithm ensures that the kernel always has
186
+ enough buffer space for reading data. When a buffer is no longer used by the
187
+ kernel or the stream, it is recycled and eventually provided back to the
188
+ kernel.
189
+ - Streams are
190
+ [implemented](https://github.com/digital-fabric/uringmachine/blob/main/ext/um/um_stream.c)
191
+ as a linked list of buffer segments, directly referencing the buffers shared
192
+ with the kernel and referenced in CQEs.
193
+ - Stream read methods suitable for both line-based protocols and binary
194
+ protocols.
195
+ - An `:ssl` stream mode allows reading from a `SSLSocket` instead of from an fd,
196
+ using the custom SSL BIO discussed above.
197
+
198
+ ### Other Improvements to UringMachine
199
+
200
+ - Improved various internal aspects of the C-extension: performance and
201
+ correctness of mutex and queue implementations.
202
+
203
+ - Added support for accepting instances of `IO::Buffer` as buffer for the
204
+ various I/O operations, in order to facilitate the `Fiber::Scheduler`
205
+ implementation.
206
+
207
+ - Added various methods for working with processes:
208
+
209
+ - `UringMachine#waitid`
210
+ - `UringMachine.pidfd_open`
211
+ - `UringMachine.pidfd_send_signal`
212
+
213
+ - Added detailed internal metrics.
214
+
215
+ - Added support for vectorized write/send using io_uring: `UringMachine#writev`
216
+ and `UringMachine#sendv`.
217
+
218
+ - Added support for `SQPOLL` mode - this io_uring mode lets us avoid entering
219
+ the kernel when submitting I/O operations as the kernel is busy polling the SQ
220
+ ring.
221
+
222
+ - Added support for sidecar mode: an auxiliary thread is used to enter the
223
+ kernel and wait for CQE's (I/O operation completion entries), letting the Ruby
224
+ thread avoid entering the kernel in order to wait for CQEs.
225
+
226
+ - Added support for watching file system events using the `inotify` interface.
227
+ - Methods for low-level work with `inotify`:
228
+
229
+ ```ruby
230
+ fd = UM.inotify_init
231
+ wd = UM.inotify_add_watch(fd, '/tmp', UM::IN_CLOSE_WRITE)
232
+ IO.write('/tmp/foo.txt', 'foofoo')
233
+ events = machine.inotify_get_events(fd)
234
+ #=> { wd: wd, mask: UM::IN_CLOSE_WRITE, name: 'foo.txt' }
235
+ ```
236
+
237
+ - A higher-level API for watching directories:
238
+
239
+ ```ruby
240
+ mask = UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE
241
+ machine.file_watch(FileUtils.pwd, mask) { handle_fs_event(it) }
242
+ ```
243
+
244
+ - Added more low-level methods for performing I/O operations supported by
245
+ io_uring: `splice`, `tee`, `fsync`.
246
+
247
+ ### Benchmarking
248
+
249
+ - I did extensive benchmarking comparing different solutions for performing
250
+ concurrent I/O in Ruby:
251
+
252
+ - Using normal Ruby threads
253
+ - Using Samuel's [Async](https://github.com/socketry/async/) gem which
254
+ implements a `Fiber::Scheduler`
255
+ - Using the UringMachine `Fiber::Scheduler`
256
+ - Using the UringMachine low-level API
257
+
258
+ - The benchmarks simulate different kinds of workloads:
259
+
260
+ - Writing and reading from pipes
261
+ - Writing and reading from sockets
262
+ - Doing CPU-bound work synchronized by mutex
263
+ - Doing I/O-bound work synchronized by mutex
264
+ - Pushing and pulling items from queues
265
+ - Running queries on a PostgreSQL database
266
+
267
+ - The results are here: https://github.com/digital-fabric/uringmachine/blob/main/benchmark/README.md
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '0.29.2'
4
+ VERSION = '0.30.0'
5
5
  end
data/lib/uringmachine.rb CHANGED
@@ -194,6 +194,23 @@ class UringMachine
194
194
  close_async(fd)
195
195
  end
196
196
 
197
+ # call-seq:
198
+ # machine.stream(fd, mode = nil) -> stream
199
+ # machine.stream(fd, mode = nil) { |stream| }
200
+ #
201
+ # Creates a stream for reading from the given target fd or other object. The
202
+ # mode indicates the type of target and how it is read from:
203
+ #
204
+ # - :bp_read - read from the given fd using the buffer pool (default mode)
205
+ # - :bp_recv - receive from the given socket fd using the buffer pool
206
+ # - :ssl - read from the given SSL connection
207
+ #
208
+ # If a block is given, the block will be called with the stream object and the
209
+ # method will return the block's return value.
210
+ #
211
+ # @param target [Integer, OpenSSL::SSL::SSLSocket] fd or ssl connection
212
+ # @param mode [Symbol, nil] stream mode
213
+ # @return [UringMachine::Stream] stream object
197
214
  def stream(target, mode = nil)
198
215
  stream = UM::Stream.new(self, target, mode)
199
216
  return stream if !block_given?
@@ -203,6 +220,30 @@ class UringMachine
203
220
  res
204
221
  end
205
222
 
223
+ # Creates, binds and sets up a TCP socket for listening on the given host and
224
+ # port.
225
+ #
226
+ # @param host [String] host IP address
227
+ # @param port [Integer] TCP port
228
+ # @return [Integer] socket fd
229
+ def tcp_listen(host, port)
230
+ fd = socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
231
+ bind(fd, host, port)
232
+ listen(fd, UM::SOMAXCONN)
233
+ fd
234
+ end
235
+
236
+ # Creates and connects a TCP socket to the given host and port.
237
+ #
238
+ # @param host [String] host IP address
239
+ # @param port [Integer] TCP port
240
+ # @return [Integer] socket fd
241
+ def tcp_connect(host, port)
242
+ fd = socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
243
+ connect(fd, host, port)
244
+ fd
245
+ end
246
+
206
247
  private
207
248
 
208
249
  # @param block [Proc]
data/test/test_ssl.rb CHANGED
@@ -82,4 +82,31 @@ class SSLTest < UMBaseTest
82
82
  sock1&.close rescue nil
83
83
  sock2&.close rescue nil
84
84
  end
85
+
86
+ def test_ssl_write_0
87
+ sock1, sock2 = UNIXSocket.pair
88
+
89
+ s1 = OpenSSL::SSL::SSLSocket.new(sock1, @server_ctx)
90
+ s1.sync_close = true
91
+ s2 = OpenSSL::SSL::SSLSocket.new(sock2, OpenSSL::SSL::SSLContext.new)
92
+ s2.sync_close = true
93
+
94
+ @machine.ssl_set_bio(s2)
95
+ assert_equal true, s2.instance_variable_get(:@__um_bio__)
96
+
97
+ t = Thread.new { s1.accept rescue nil }
98
+
99
+ assert_equal 0, @machine.metrics[:total_ops]
100
+ s2.connect
101
+ refute_equal 0, @machine.metrics[:total_ops]
102
+
103
+ assert_equal 6, @machine.ssl_write(s1, 'foobar', 0)
104
+ buf = +''
105
+ assert_equal 6, @machine.ssl_read(s2, buf, 6)
106
+ assert_equal 'foobar', buf
107
+ ensure
108
+ t&.join(0.1)
109
+ sock1&.close rescue nil
110
+ sock2&.close rescue nil
111
+ end
85
112
  end
data/test/test_stream.rb CHANGED
@@ -251,6 +251,39 @@ class StreamTest < StreamBaseTest
251
251
  assert_equal 9, received.size
252
252
  assert_equal data, received.join
253
253
  end
254
+
255
+ def test_stream_each
256
+ bufs = []
257
+ f = machine.spin do
258
+ bufs << :ready
259
+ stream.each {
260
+ assert_kind_of IO::Buffer, it
261
+ bufs << it.get_string
262
+ }
263
+ bufs << :done
264
+ rescue => e
265
+ p e
266
+ p e.backtrace
267
+ end
268
+
269
+ machine.snooze
270
+ assert_equal [:ready], bufs
271
+
272
+ machine.write(@wfd, 'foo')
273
+ machine.snooze
274
+ assert_equal [:ready, 'foo'], bufs
275
+
276
+ machine.write(@wfd, 'barb')
277
+ machine.snooze
278
+ assert_equal [:ready, 'foo', 'barb'], bufs
279
+
280
+ machine.close(@wfd)
281
+ machine.snooze
282
+ assert_equal [:ready, 'foo', 'barb', :done], bufs
283
+ ensure
284
+ machine.terminate(f)
285
+ machine.join(f)
286
+ end
254
287
  end
255
288
 
256
289
  class StreamRespTest < StreamBaseTest
data/test/test_um.rb CHANGED
@@ -3556,4 +3556,161 @@ class StreamMethodTest < UMBaseTest
3556
3556
  assert_equal ['foo', 'bar'], bufs
3557
3557
  assert_equal :foo, res
3558
3558
  end
3559
+ end
3560
+
3561
+ class TCPHelperMethodsTest < UMBaseTest
3562
+ def setup
3563
+ super
3564
+ @port = assign_port
3565
+ end
3566
+
3567
+ def test_tcp_listen
3568
+ server_fd = machine.tcp_listen('0.0.0.0', @port)
3569
+ assert_kind_of Integer, server_fd
3570
+
3571
+ fd_c = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
3572
+ machine.connect(fd_c, '127.0.0.1', @port)
3573
+
3574
+ fd_s = machine.accept(server_fd)
3575
+ assert_equal 3, machine.send(fd_s, 'foo', 3, 0)
3576
+
3577
+ buf = +''
3578
+ machine.recv(fd_c, buf, 8192, 0)
3579
+ assert_equal 'foo', buf
3580
+ ensure
3581
+ machine.close(fd_s) rescue nil
3582
+ machine.close(fd_c) rescue nil
3583
+ machine.close(server_fd) rescue nil
3584
+ end
3585
+
3586
+ def test_tcp_listen_invalid_args
3587
+ assert_raises(TypeError) { machine.tcp_listen('0.0.0.0', :foo) }
3588
+ assert_raises(TypeError) { machine.tcp_listen(1234, 5678) }
3589
+ end
3590
+
3591
+ def test_tcp_connect
3592
+ server_fd = machine.tcp_listen('0.0.0.0', @port)
3593
+ assert_kind_of Integer, server_fd
3594
+
3595
+ fd_c = machine.tcp_connect('127.0.0.1', @port)
3596
+ assert_kind_of Integer, fd_c
3597
+
3598
+ fd_s = machine.accept(server_fd)
3599
+ assert_equal 3, machine.send(fd_s, 'foo', 3, 0)
3600
+
3601
+ buf = +''
3602
+ machine.recv(fd_c, buf, 8192, 0)
3603
+ assert_equal 'foo', buf
3604
+ ensure
3605
+ machine.close(fd_s) rescue nil
3606
+ machine.close(fd_c) rescue nil
3607
+ machine.close(server_fd) rescue nil
3608
+ end
3609
+
3610
+ def test_tcp_connect_invalid_args
3611
+ server_fd = machine.tcp_listen('0.0.0.0', @port)
3612
+
3613
+ assert_raises(Errno::ECONNREFUSED) {
3614
+ machine.tcp_connect('127.0.0.1', @port + 1)
3615
+ }
3616
+ assert_raises(TypeError) { machine.tcp_connect('127.0.0.1', :foo) }
3617
+ assert_raises(TypeError) { machine.tcp_connect(1234, 5678) }
3618
+ ensure
3619
+ machine.close(server_fd)
3620
+ end
3621
+ end
3622
+
3623
+ class SpliceTest < UMBaseTest
3624
+ def test_splice
3625
+ i1, o1 = UM.pipe
3626
+ i2, o2 = UM.pipe
3627
+ len = nil
3628
+
3629
+ f = machine.spin do
3630
+ len = machine.splice(i1, o2, 1000)
3631
+ ensure
3632
+ machine.close(o2)
3633
+ end
3634
+
3635
+ machine.write(o1, 'foobar')
3636
+ buf = +''
3637
+ machine.read(i2, buf, 1000)
3638
+
3639
+ assert_equal 'foobar', buf
3640
+ assert_equal 6, len
3641
+ ensure
3642
+ machine.terminate(f)
3643
+ machine.join(f)
3644
+ [i1, o1, i2, o2].each { machine.close(it) rescue nil }
3645
+ end
3646
+
3647
+ def test_splice_bad_args
3648
+ fn = "/tmp/um_#{SecureRandom.hex}"
3649
+ IO.write(fn, 'foo')
3650
+ f1 = machine.open(fn, UM::O_RDWR)
3651
+ f2 = machine.open(fn, UM::O_RDWR)
3652
+ assert_raises(Errno::EINVAL) { machine.splice(f1, f2, 1000) }
3653
+
3654
+ i1, o1 = UM.pipe
3655
+ i2, o2 = UM.pipe
3656
+ machine.write(o1, 'foo')
3657
+
3658
+ assert_raises(Errno::EBADF) { machine.splice(i1, o2 + 1, 1000) }
3659
+ ensure
3660
+ [f1, f2, i1, o1, i2, o2].each { machine.close(it) rescue nil }
3661
+ end
3662
+ end
3663
+
3664
+ class TeeTest < UMBaseTest
3665
+ def test_tee
3666
+ i_src, o_src = UM.pipe
3667
+ i_dest1, o_dest1 = UM.pipe
3668
+ i_dest2, o_dest2 = UM.pipe
3669
+
3670
+ len1 = len2 = nil
3671
+
3672
+ f = machine.spin do
3673
+ len1 = machine.tee(i_src, o_dest1, 1000)
3674
+ len2 = machine.splice(i_src, o_dest2, 1000)
3675
+ ensure
3676
+ machine.close(o_dest1)
3677
+ machine.close(o_dest2)
3678
+ end
3679
+
3680
+ machine.write(o_src, 'foobar')
3681
+ machine.close(o_src)
3682
+ result1 = +''
3683
+ machine.read(i_dest1, result1, 1000)
3684
+ result2 = +''
3685
+ machine.read(i_dest2, result2, 1000)
3686
+
3687
+ assert_equal 'foobar', result1
3688
+ assert_equal 6, len1
3689
+
3690
+ assert_equal 'foobar', result2
3691
+ assert_equal 6, len2
3692
+ ensure
3693
+ machine.terminate(f)
3694
+ machine.join(f)
3695
+ [i_src, o_src, i_dest1, o_dest1, i_dest2, o_dest2].each {
3696
+ machine.close(it) rescue nil
3697
+ }
3698
+ end
3699
+ end
3700
+
3701
+ class FsyncTest < UMBaseTest
3702
+ def test_fsync
3703
+ fn = "/tmp/um_#{SecureRandom.hex}"
3704
+ fd = machine.open(fn, UM::O_CREAT | UM::O_WRONLY)
3705
+
3706
+ machine.write(fd, 'foo')
3707
+ assert_equal 0, machine.fsync(fd)
3708
+ ensure
3709
+ machine.close(fd)
3710
+ FileUtils.rm(fn) rescue nil
3711
+ end
3712
+
3713
+ def test_fsync_bad_args
3714
+ assert_raises(Errno::EINVAL) { machine.fsync(1) }
3715
+ end
3559
3716
  end