sleepy_penguin 3.2.0 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +0 -1
- data/Rakefile +0 -37
- data/ext/sleepy_penguin/epoll.c +3 -15
- data/ext/sleepy_penguin/extconf.rb +9 -2
- data/ext/sleepy_penguin/init.c +59 -1
- data/ext/sleepy_penguin/inotify.c +16 -36
- data/ext/sleepy_penguin/kqueue.c +3 -15
- data/ext/sleepy_penguin/missing_clock_gettime.h +36 -0
- data/ext/sleepy_penguin/sleepy_penguin.h +1 -0
- data/ext/sleepy_penguin/util.c +16 -8
- data/ext/sleepy_penguin/value2timespec.h +2 -2
- data/test/helper.rb +17 -0
- data/test/test_constants.rb +2 -2
- data/test/test_epoll.rb +10 -9
- data/test/test_epoll_gc.rb +2 -2
- data/test/test_epoll_io.rb +2 -2
- data/test/test_epoll_optimizations.rb +2 -2
- data/test/test_eventfd.rb +3 -7
- data/test/test_inotify.rb +3 -7
- data/test/test_kqueue.rb +2 -2
- data/test/test_kqueue_io.rb +2 -2
- data/test/test_timerfd.rb +3 -7
- metadata +65 -60
- data/ext/sleepy_penguin/signalfd.c +0 -342
- data/test/test_signalfd.rb +0 -94
- data/test/test_signalfd_siginfo.rb +0 -32
data/GIT-VERSION-GEN
CHANGED
data/GNUmakefile
CHANGED
data/Rakefile
CHANGED
@@ -31,40 +31,3 @@ task :publish_news do
|
|
31
31
|
rf.login
|
32
32
|
rf.post_news('rainbows', subject, body)
|
33
33
|
end
|
34
|
-
|
35
|
-
desc "post to RAA"
|
36
|
-
task :raa_update do
|
37
|
-
require 'net/http'
|
38
|
-
require 'net/netrc'
|
39
|
-
rc = Net::Netrc.locate('sleepy_penguin-raa') or abort "~/.netrc not found"
|
40
|
-
password = rc.password
|
41
|
-
|
42
|
-
s = Gem::Specification.load('sleepy_penguin.gemspec')
|
43
|
-
desc = [ s.description.strip ]
|
44
|
-
desc << ""
|
45
|
-
desc << "* #{s.email}"
|
46
|
-
desc << "* #{git_url}"
|
47
|
-
desc << "* #{cgit_url}"
|
48
|
-
desc = desc.join("\n")
|
49
|
-
uri = URI.parse('http://raa.ruby-lang.org/regist.rhtml')
|
50
|
-
form = {
|
51
|
-
:name => s.name,
|
52
|
-
:short_description => s.summary,
|
53
|
-
:version => s.version.to_s,
|
54
|
-
:status => 'experimental',
|
55
|
-
:owner => s.authors.first,
|
56
|
-
:email => s.email,
|
57
|
-
:category_major => 'Library',
|
58
|
-
:category_minor => 'System',
|
59
|
-
:url => s.homepage,
|
60
|
-
:download => 'http://rubyforge.org/frs/?group_id=8977',
|
61
|
-
:license => "LGPL",
|
62
|
-
:description_style => 'Plain',
|
63
|
-
:description => desc,
|
64
|
-
:pass => password,
|
65
|
-
:submit => 'Update',
|
66
|
-
}
|
67
|
-
res = Net::HTTP.post_form(uri, form)
|
68
|
-
p res
|
69
|
-
puts res.body
|
70
|
-
end
|
data/ext/sleepy_penguin/epoll.c
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#include "sleepy_penguin.h"
|
2
2
|
#ifdef HAVE_SYS_EPOLL_H
|
3
3
|
#include <sys/epoll.h>
|
4
|
-
#include <unistd.h>
|
5
4
|
#include <time.h>
|
5
|
+
#include "missing_clock_gettime.h"
|
6
6
|
#include "missing_epoll.h"
|
7
7
|
#include "missing_rb_thread_fd_close.h"
|
8
8
|
#include "missing_rb_update_max_fd.h"
|
@@ -51,10 +51,8 @@ static int ep_fd_check(struct ep_per_thread *ept)
|
|
51
51
|
|
52
52
|
static struct ep_per_thread *ept_get(VALUE self, int maxevents)
|
53
53
|
{
|
54
|
-
|
54
|
+
struct ep_per_thread *ept;
|
55
55
|
size_t size;
|
56
|
-
int err;
|
57
|
-
void *ptr;
|
58
56
|
|
59
57
|
/* error check here to prevent OOM from posix_memalign */
|
60
58
|
if (maxevents <= 0) {
|
@@ -62,21 +60,11 @@ static struct ep_per_thread *ept_get(VALUE self, int maxevents)
|
|
62
60
|
rb_sys_fail("epoll_wait maxevents <= 0");
|
63
61
|
}
|
64
62
|
|
65
|
-
if (ept && ept->capa >= maxevents)
|
66
|
-
goto out;
|
67
|
-
|
68
63
|
size = sizeof(struct ep_per_thread) +
|
69
64
|
sizeof(struct epoll_event) * maxevents;
|
70
65
|
|
71
|
-
|
72
|
-
err = posix_memalign(&ptr, rb_sp_l1_cache_line_size, size);
|
73
|
-
if (err) {
|
74
|
-
errno = err;
|
75
|
-
rb_memerror();
|
76
|
-
}
|
77
|
-
ept = ptr;
|
66
|
+
ept = rb_sp_gettlsbuf(&size);
|
78
67
|
ept->capa = maxevents;
|
79
|
-
out:
|
80
68
|
ept->maxevents = maxevents;
|
81
69
|
ept->io = self;
|
82
70
|
ept->fd = rb_sp_fileno(ept->io);
|
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'mkmf'
|
2
2
|
have_header('sys/epoll.h')
|
3
3
|
dir_config('kqueue')
|
4
|
-
|
5
|
-
|
4
|
+
if have_header('sys/event.h')
|
5
|
+
have_library('kqueue')
|
6
|
+
end
|
6
7
|
have_header('sys/mount.h')
|
7
8
|
have_header('sys/eventfd.h')
|
8
9
|
|
@@ -13,6 +14,11 @@ have_header('sys/eventfd.h')
|
|
13
14
|
have_header('sys/timerfd.h')
|
14
15
|
have_header('sys/inotify.h')
|
15
16
|
have_header('ruby/io.h') and have_struct_member('rb_io_t', 'fd', 'ruby/io.h')
|
17
|
+
unless have_macro('CLOCK_MONOTONIC', 'time.h')
|
18
|
+
have_func('CLOCK_MONOTONIC', 'time.h')
|
19
|
+
end
|
20
|
+
have_type('clockid_t', 'time.h')
|
21
|
+
have_func('clock_gettime', 'time.h')
|
16
22
|
have_func('epoll_create1', %w(sys/epoll.h))
|
17
23
|
have_func('rb_thread_call_without_gvl')
|
18
24
|
have_func('rb_thread_blocking_region')
|
@@ -20,4 +26,5 @@ have_func('rb_thread_io_blocking_region')
|
|
20
26
|
have_func('rb_thread_fd_close')
|
21
27
|
have_func('rb_update_max_fd')
|
22
28
|
have_func('rb_fd_fix_cloexec')
|
29
|
+
have_func('rb_io_get_io')
|
23
30
|
create_makefile('sleepy_penguin_ext')
|
data/ext/sleepy_penguin/init.c
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
-
#define _GNU_SOURCE
|
2
1
|
#include <ruby.h>
|
2
|
+
#ifndef _GNU_SOURCE
|
3
|
+
# define _GNU_SOURCE /* TODO: confirm this is needed */
|
4
|
+
#endif
|
5
|
+
|
3
6
|
#include <unistd.h>
|
7
|
+
#include <pthread.h>
|
4
8
|
#include <sys/types.h>
|
5
9
|
#include "git_version.h"
|
10
|
+
#include "sleepy_penguin.h"
|
6
11
|
#define L1_CACHE_LINE_MAX 128 /* largest I've seen (Pentium 4) */
|
7
12
|
size_t rb_sp_l1_cache_line_size;
|
13
|
+
static pthread_key_t rb_sp_key;
|
14
|
+
struct rb_sp_tlsbuf {
|
15
|
+
size_t capa;
|
16
|
+
unsigned char ptr[FLEX_ARRAY];
|
17
|
+
};
|
8
18
|
|
9
19
|
#ifdef HAVE_SYS_EVENT_H
|
10
20
|
void sleepy_penguin_init_kqueue(void);
|
@@ -53,9 +63,57 @@ static size_t l1_cache_line_size_detect(void)
|
|
53
63
|
return L1_CACHE_LINE_MAX;
|
54
64
|
}
|
55
65
|
|
66
|
+
static void sp_once(void)
|
67
|
+
{
|
68
|
+
int err = pthread_key_create(&rb_sp_key, free);
|
69
|
+
|
70
|
+
if (err) {
|
71
|
+
errno = err;
|
72
|
+
rb_sys_fail( "pthread_key_create");
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
void *rb_sp_gettlsbuf(size_t *size)
|
77
|
+
{
|
78
|
+
struct rb_sp_tlsbuf *buf = pthread_getspecific(rb_sp_key);
|
79
|
+
void *ptr;
|
80
|
+
int err;
|
81
|
+
size_t bytes;
|
82
|
+
|
83
|
+
if (buf && buf->capa >= *size) {
|
84
|
+
*size = buf->capa;
|
85
|
+
goto out;
|
86
|
+
}
|
87
|
+
|
88
|
+
free(buf);
|
89
|
+
bytes = *size + sizeof(struct rb_sp_tlsbuf);
|
90
|
+
err = posix_memalign(&ptr, rb_sp_l1_cache_line_size, bytes);
|
91
|
+
if (err) {
|
92
|
+
errno = err;
|
93
|
+
rb_memerror(); /* fatal */
|
94
|
+
}
|
95
|
+
|
96
|
+
buf = ptr;
|
97
|
+
buf->capa = *size;
|
98
|
+
err = pthread_setspecific(rb_sp_key, buf);
|
99
|
+
if (err != 0) {
|
100
|
+
errno = err;
|
101
|
+
rb_sys_fail("BUG: pthread_setspecific");
|
102
|
+
}
|
103
|
+
out:
|
104
|
+
return buf->ptr;
|
105
|
+
}
|
106
|
+
|
56
107
|
void Init_sleepy_penguin_ext(void)
|
57
108
|
{
|
58
109
|
VALUE mSleepyPenguin;
|
110
|
+
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
111
|
+
int err = pthread_once(&once, sp_once);
|
112
|
+
|
113
|
+
if (err) {
|
114
|
+
errno = err;
|
115
|
+
rb_sys_fail("pthread_once");
|
116
|
+
}
|
59
117
|
|
60
118
|
rb_sp_l1_cache_line_size = l1_cache_line_size_detect();
|
61
119
|
|
@@ -4,11 +4,6 @@
|
|
4
4
|
#include <sys/ioctl.h>
|
5
5
|
#include "missing_inotify.h"
|
6
6
|
|
7
|
-
struct inbuf {
|
8
|
-
size_t capa;
|
9
|
-
void *ptr;
|
10
|
-
};
|
11
|
-
|
12
7
|
static ID id_inotify_tmp, id_mask;
|
13
8
|
static VALUE cEvent, checks;
|
14
9
|
|
@@ -137,43 +132,31 @@ static VALUE event_new(struct inotify_event *e)
|
|
137
132
|
|
138
133
|
struct inread_args {
|
139
134
|
int fd;
|
140
|
-
|
135
|
+
size_t size;
|
136
|
+
void *buf;
|
141
137
|
};
|
142
138
|
|
143
139
|
static VALUE inread(void *ptr)
|
144
140
|
{
|
145
141
|
struct inread_args *args = ptr;
|
146
142
|
|
147
|
-
return (VALUE)read(args->fd, args->
|
148
|
-
}
|
149
|
-
|
150
|
-
static void inbuf_grow(struct inbuf *inbuf, size_t size)
|
151
|
-
{
|
152
|
-
int err;
|
153
|
-
|
154
|
-
if (inbuf->capa >= size)
|
155
|
-
return;
|
156
|
-
free(inbuf->ptr);
|
157
|
-
err = posix_memalign(&inbuf->ptr, rb_sp_l1_cache_line_size, size);
|
158
|
-
if (err) {
|
159
|
-
errno = err;
|
160
|
-
rb_memerror();
|
161
|
-
}
|
162
|
-
inbuf->capa = size;
|
143
|
+
return (VALUE)read(args->fd, args->buf, args->size);
|
163
144
|
}
|
164
145
|
|
165
146
|
static void resize_internal_buffer(struct inread_args *args)
|
166
147
|
{
|
167
148
|
int newlen;
|
168
149
|
|
169
|
-
if (args->
|
150
|
+
if (args->size > 0x10000)
|
170
151
|
rb_raise(rb_eRuntimeError, "path too long");
|
171
152
|
|
172
153
|
if (ioctl(args->fd, FIONREAD, &newlen) != 0)
|
173
154
|
rb_sys_fail("ioctl(inotify,FIONREAD)");
|
174
155
|
|
175
|
-
if (newlen > 0)
|
176
|
-
|
156
|
+
if (newlen > 0) {
|
157
|
+
args->size = (size_t)newlen;
|
158
|
+
args->buf = rb_sp_gettlsbuf(&args->size);
|
159
|
+
}
|
177
160
|
|
178
161
|
if (newlen == 0) /* race: some other thread grabbed the data */
|
179
162
|
return;
|
@@ -192,8 +175,6 @@ static void resize_internal_buffer(struct inread_args *args)
|
|
192
175
|
*/
|
193
176
|
static VALUE take(int argc, VALUE *argv, VALUE self)
|
194
177
|
{
|
195
|
-
static __thread struct inbuf inbuf;
|
196
|
-
|
197
178
|
struct inread_args args;
|
198
179
|
VALUE tmp = rb_ivar_get(self, id_inotify_tmp);
|
199
180
|
struct inotify_event *e, *end;
|
@@ -206,9 +187,9 @@ static VALUE take(int argc, VALUE *argv, VALUE self)
|
|
206
187
|
|
207
188
|
rb_scan_args(argc, argv, "01", &nonblock);
|
208
189
|
|
209
|
-
inbuf_grow(&inbuf, 128);
|
210
190
|
args.fd = rb_sp_fileno(self);
|
211
|
-
args.
|
191
|
+
args.size = 128;
|
192
|
+
args.buf = rb_sp_gettlsbuf(&args.size);
|
212
193
|
|
213
194
|
if (RTEST(nonblock))
|
214
195
|
rb_sp_set_nonblock(args.fd);
|
@@ -228,9 +209,8 @@ static VALUE take(int argc, VALUE *argv, VALUE self)
|
|
228
209
|
rb_sys_fail("read(inotify)");
|
229
210
|
} else {
|
230
211
|
/* buffer in userspace to minimize read() calls */
|
231
|
-
end = (struct inotify_event *)
|
232
|
-
|
233
|
-
for (e = args.inbuf->ptr; e < end; ) {
|
212
|
+
end = (struct inotify_event *)((char *)args.buf + r);
|
213
|
+
for (e = args.buf; e < end; ) {
|
234
214
|
VALUE event = event_new(e);
|
235
215
|
if (NIL_P(rv))
|
236
216
|
rv = event;
|
@@ -255,15 +235,15 @@ static VALUE take(int argc, VALUE *argv, VALUE self)
|
|
255
235
|
static VALUE events(VALUE self)
|
256
236
|
{
|
257
237
|
long len = RARRAY_LEN(checks);
|
258
|
-
|
238
|
+
long i;
|
259
239
|
VALUE sym;
|
260
240
|
VALUE rv = rb_ary_new();
|
261
241
|
uint32_t mask;
|
262
242
|
uint32_t event_mask = NUM2UINT(rb_funcall(self, id_mask, 0));
|
263
243
|
|
264
|
-
for (
|
265
|
-
sym =
|
266
|
-
mask = NUM2UINT(
|
244
|
+
for (i = 0; i < len; ) {
|
245
|
+
sym = rb_ary_entry(checks, i++);
|
246
|
+
mask = NUM2UINT(rb_ary_entry(checks, i++));
|
267
247
|
if ((event_mask & mask) == mask)
|
268
248
|
rb_ary_push(rv, sym);
|
269
249
|
}
|
data/ext/sleepy_penguin/kqueue.c
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
#include <sys/time.h>
|
6
6
|
#include <unistd.h>
|
7
7
|
#include <time.h>
|
8
|
+
#include "missing_clock_gettime.h"
|
8
9
|
#include "missing_rb_thread_fd_close.h"
|
9
10
|
#include "missing_rb_update_max_fd.h"
|
10
11
|
#include "value2timespec.h"
|
@@ -73,10 +74,8 @@ static int kq_fd_check(struct kq_per_thread *kpt)
|
|
73
74
|
|
74
75
|
static struct kq_per_thread *kpt_get(VALUE self, int nchanges, int nevents)
|
75
76
|
{
|
76
|
-
|
77
|
+
struct kq_per_thread *kpt;
|
77
78
|
size_t size;
|
78
|
-
void *ptr;
|
79
|
-
int err;
|
80
79
|
int max = nchanges > nevents ? nchanges : nevents;
|
81
80
|
|
82
81
|
/* error check here to prevent OOM from posix_memalign */
|
@@ -85,20 +84,9 @@ static struct kq_per_thread *kpt_get(VALUE self, int nchanges, int nevents)
|
|
85
84
|
rb_sys_fail("kevent got negative events < 0");
|
86
85
|
}
|
87
86
|
|
88
|
-
if (kpt && kpt->capa >= max)
|
89
|
-
goto out;
|
90
|
-
|
91
87
|
size = sizeof(struct kq_per_thread) + sizeof(struct kevent) * max;
|
92
|
-
|
93
|
-
free(kpt); /* free(NULL) is POSIX */
|
94
|
-
err = posix_memalign(&ptr, rb_sp_l1_cache_line_size, size);
|
95
|
-
if (err) {
|
96
|
-
errno = err;
|
97
|
-
rb_memerror();
|
98
|
-
}
|
99
|
-
kpt = ptr;
|
88
|
+
kpt = rb_sp_gettlsbuf(&size);
|
100
89
|
kpt->capa = max;
|
101
|
-
out:
|
102
90
|
kpt->nchanges = nchanges;
|
103
91
|
kpt->nevents = nevents;
|
104
92
|
kpt->io = self;
|
@@ -0,0 +1,36 @@
|
|
1
|
+
/*
|
2
|
+
* this header includes functions to support broken systems
|
3
|
+
* without clock_gettime() or CLOCK_MONOTONIC
|
4
|
+
*/
|
5
|
+
|
6
|
+
#ifndef HAVE_TYPE_CLOCKID_T
|
7
|
+
typedef int clockid_t;
|
8
|
+
#endif
|
9
|
+
|
10
|
+
#ifndef HAVE_CLOCK_GETTIME
|
11
|
+
# ifndef CLOCK_REALTIME
|
12
|
+
# define CLOCK_REALTIME 0 /* whatever */
|
13
|
+
# endif
|
14
|
+
static int fake_clock_gettime(clockid_t clk_id, struct timespec *res)
|
15
|
+
{
|
16
|
+
struct timeval tv;
|
17
|
+
int r = gettimeofday(&tv, NULL);
|
18
|
+
|
19
|
+
assert(0 == r && "gettimeofday() broke!?");
|
20
|
+
res->tv_sec = tv.tv_sec;
|
21
|
+
res->tv_nsec = tv.tv_usec * 1000;
|
22
|
+
|
23
|
+
return r;
|
24
|
+
}
|
25
|
+
# define clock_gettime fake_clock_gettime
|
26
|
+
#endif /* broken systems w/o clock_gettime() */
|
27
|
+
|
28
|
+
/*
|
29
|
+
* UGH
|
30
|
+
* CLOCK_MONOTONIC is not guaranteed to be a macro, either
|
31
|
+
*/
|
32
|
+
#ifndef CLOCK_MONOTONIC
|
33
|
+
# if (!defined(_POSIX_MONOTONIC_CLOCK) || !defined(HAVE_CLOCK_MONOTONIC))
|
34
|
+
# define CLOCK_MONOTONIC CLOCK_REALTIME
|
35
|
+
# endif
|
36
|
+
#endif
|
@@ -77,6 +77,7 @@ static inline VALUE fake_blocking_region(VALUE (*fn)(void *), void *data)
|
|
77
77
|
|
78
78
|
typedef int rb_sp_waitfn(int fd);
|
79
79
|
int rb_sp_wait(rb_sp_waitfn waiter, VALUE obj, int *fd);
|
80
|
+
void *rb_sp_gettlsbuf(size_t *size);
|
80
81
|
|
81
82
|
/* Flexible array elements are standard in C99 */
|
82
83
|
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
|
data/ext/sleepy_penguin/util.c
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
#include "sleepy_penguin.h"
|
2
2
|
|
3
|
+
#ifndef HAVE_RB_IO_GET_IO
|
4
|
+
static VALUE my_io_get_io(VALUE io)
|
5
|
+
{
|
6
|
+
return rb_convert_type(io, T_FILE, "IO", "to_io");
|
7
|
+
}
|
8
|
+
# define rb_io_get_io(io) my_io_get_io((io))
|
9
|
+
#endif /* HAVE_RB_IO_GET_IO */
|
10
|
+
|
3
11
|
static VALUE klass_for(VALUE klass)
|
4
12
|
{
|
5
13
|
return (TYPE(klass) == T_CLASS) ? klass : CLASS_OF(klass);
|
@@ -14,13 +22,13 @@ int rb_sp_get_flags(VALUE klass, VALUE flags, int default_flags)
|
|
14
22
|
case T_SYMBOL:
|
15
23
|
return NUM2INT(rb_const_get(klass_for(klass), SYM2ID(flags)));
|
16
24
|
case T_ARRAY: {
|
17
|
-
|
25
|
+
long i;
|
18
26
|
long len = RARRAY_LEN(flags);
|
19
27
|
int rv = 0;
|
20
28
|
|
21
29
|
klass = klass_for(klass);
|
22
|
-
|
23
|
-
VALUE tmp =
|
30
|
+
for (i = 0; i < len; i++) {
|
31
|
+
VALUE tmp = rb_ary_entry(flags, i);
|
24
32
|
|
25
33
|
Check_Type(tmp, T_SYMBOL);
|
26
34
|
tmp = rb_const_get(klass, SYM2ID(tmp));
|
@@ -42,13 +50,13 @@ unsigned rb_sp_get_uflags(VALUE klass, VALUE flags)
|
|
42
50
|
case T_SYMBOL:
|
43
51
|
return NUM2UINT(rb_const_get(klass_for(klass), SYM2ID(flags)));
|
44
52
|
case T_ARRAY: {
|
45
|
-
|
53
|
+
long i;
|
46
54
|
long len = RARRAY_LEN(flags);
|
47
55
|
unsigned rv = 0;
|
48
56
|
|
49
57
|
klass = klass_for(klass);
|
50
|
-
|
51
|
-
VALUE tmp =
|
58
|
+
for (i = 0; i < len; i++) {
|
59
|
+
VALUE tmp = rb_ary_entry(flags, i);
|
52
60
|
|
53
61
|
Check_Type(tmp, T_SYMBOL);
|
54
62
|
tmp = rb_const_get(klass, SYM2ID(tmp));
|
@@ -100,7 +108,7 @@ int rb_sp_io_closed(VALUE io)
|
|
100
108
|
case T_FILE:
|
101
109
|
break;
|
102
110
|
default:
|
103
|
-
io =
|
111
|
+
io = rb_io_get_io(io);
|
104
112
|
}
|
105
113
|
|
106
114
|
return my_rb_io_closed(io);
|
@@ -110,7 +118,7 @@ int rb_sp_fileno(VALUE io)
|
|
110
118
|
{
|
111
119
|
rb_io_t *fptr;
|
112
120
|
|
113
|
-
io =
|
121
|
+
io = rb_io_get_io(io);
|
114
122
|
GetOpenFile(io, fptr);
|
115
123
|
return FPTR_TO_FD(fptr);
|
116
124
|
}
|