clogger 0.4.0 → 0.5.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/.document +1 -0
- data/COPYING +497 -160
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +19 -5
- data/LICENSE +16 -0
- data/README +10 -22
- data/clogger.gemspec +2 -2
- data/ext/clogger_ext/clogger.c +71 -13
- data/ext/clogger_ext/ruby_1_9_compat.h +37 -0
- data/lib/clogger.rb +17 -3
- data/lib/clogger/pure.rb +25 -4
- data/test/test_clogger.rb +10 -4
- data/test/test_clogger_to_path.rb +140 -0
- metadata +6 -2
data/GIT-VERSION-GEN
CHANGED
data/GNUmakefile
CHANGED
@@ -22,11 +22,24 @@ clean:
|
|
22
22
|
-$(MAKE) -C ext/clogger_ext clean
|
23
23
|
$(RM) ext/clogger_ext/Makefile lib/clogger_ext.$(DLEXT)
|
24
24
|
|
25
|
-
test
|
26
|
-
|
25
|
+
test_unit := $(wildcard test/test_*.rb)
|
26
|
+
test-unit: $(test_unit)
|
27
|
+
|
28
|
+
ifeq ($(CLOGGER_PURE),)
|
29
|
+
$(test_unit): export RUBYLIB := ext/clogger_ext:lib
|
30
|
+
$(test_unit): ext/clogger_ext/clogger.$(DLEXT)
|
31
|
+
else
|
32
|
+
$(test_unit): export RUBYLIB := lib
|
33
|
+
endif
|
34
|
+
|
35
|
+
$(test_unit):
|
36
|
+
$(RUBY) $@
|
37
|
+
|
38
|
+
test-ext:
|
39
|
+
CLOGGER_PURE=0 $(MAKE) test-unit
|
27
40
|
|
28
41
|
test-pure:
|
29
|
-
CLOGGER_PURE=
|
42
|
+
CLOGGER_PURE=1 $(MAKE) test-unit
|
30
43
|
|
31
44
|
test: test-ext test-pure
|
32
45
|
|
@@ -46,7 +59,7 @@ NEWS: GIT-VERSION-FILE .manifest
|
|
46
59
|
$(RAKE) -s news_rdoc > $@+
|
47
60
|
mv $@+ $@
|
48
61
|
|
49
|
-
SINCE = 0.
|
62
|
+
SINCE = 0.4.0
|
50
63
|
ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..)
|
51
64
|
ChangeLog: GIT-VERSION-FILE
|
52
65
|
@echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+
|
@@ -60,7 +73,7 @@ atom = <link rel="alternate" title="Atom feed" href="$(1)" \
|
|
60
73
|
type="application/atom+xml"/>
|
61
74
|
|
62
75
|
doc: .document NEWS ChangeLog
|
63
|
-
rdoc -
|
76
|
+
rdoc -a -t "$(shell sed -ne '1s/^= //p' README)"
|
64
77
|
install -m644 COPYING doc/COPYING
|
65
78
|
install -m644 $(shell grep '^[A-Z]' .document) doc/
|
66
79
|
$(RUBY) -i -p -e \
|
@@ -145,3 +158,4 @@ gem install-gem: GIT-VERSION-FILE
|
|
145
158
|
endif
|
146
159
|
|
147
160
|
.PHONY: .FORCE-GIT-VERSION-FILE test doc manifest
|
161
|
+
.PHONY: test test-ext test-pure $(test_unit)
|
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
\Clogger is copyrighted Free Software by all contributors, see logs in
|
2
|
+
revision control for names and email addresses of all of them.
|
3
|
+
|
4
|
+
You can redistribute it and/or modify it under the terms of the GNU Lesser
|
5
|
+
General Public License as published by the Free Software Foundation,
|
6
|
+
version 2.1 or later {LGPLv2.1}[http://www.gnu.org/licenses/lgpl-2.1.txt]
|
7
|
+
(see link:COPYING).
|
8
|
+
|
9
|
+
\Clogger is distributed in the hope that it will be useful, but WITHOUT
|
10
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
11
|
+
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
12
|
+
License for more details.
|
13
|
+
|
14
|
+
You should have received a copy of the GNU Lesser General Public License
|
15
|
+
along with this library; if not, write to the Free Software
|
16
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
data/README
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
= Clogger - configurable request logging for Rack
|
1
|
+
= \Clogger - configurable request logging for Rack
|
2
2
|
|
3
3
|
== DESCRIPTION
|
4
4
|
|
5
|
-
Clogger is Rack middleware for logging HTTP requests. The log format
|
5
|
+
\Clogger is Rack middleware for logging HTTP requests. The log format
|
6
6
|
is customizable so you can specify exactly which fields to log.
|
7
7
|
|
8
8
|
== FEATURES
|
@@ -21,13 +21,18 @@ is customizable so you can specify exactly which fields to log.
|
|
21
21
|
all bytes in the range of \x00-\x1F
|
22
22
|
|
23
23
|
* multi-instance capable and (optionally) reentrant. You can use
|
24
|
-
Clogger in a multi-threaded server, and even multiple Cloggers logging
|
24
|
+
\Clogger in a multi-threaded server, and even multiple Cloggers logging
|
25
25
|
to different locations and different formats in the same process.
|
26
26
|
|
27
|
+
* Pure Ruby version for non-MRI versions of Ruby (or via CLOGGER_PURE=1
|
28
|
+
in the environment). The optional C extension is loaded by default
|
29
|
+
and supported under MRI 1.8.7, 1.9.1, and 1.9.2.
|
30
|
+
|
27
31
|
== SYNOPSIS
|
28
32
|
|
29
|
-
Clogger may be loaded as Rack middleware in your config.ru:
|
33
|
+
\Clogger may be loaded as Rack middleware in your config.ru:
|
30
34
|
|
35
|
+
# ENV['CLOGGER_PURE'] = '1' # uncomment to disable C extension
|
31
36
|
require "clogger"
|
32
37
|
use Clogger,
|
33
38
|
:format => Clogger::Format::Combined,
|
@@ -81,7 +86,7 @@ somewhere inside the "Rails::Initializer.run do |config|" block:
|
|
81
86
|
|
82
87
|
== REQUIREMENTS
|
83
88
|
|
84
|
-
* Ruby, Rack
|
89
|
+
* {Ruby}[http://ruby-lang.org/], {Rack}[http://rack.rubyforge.org/]
|
85
90
|
|
86
91
|
== DEVELOPMENT
|
87
92
|
|
@@ -126,20 +131,3 @@ There is an optional C extension that should be compatible with MRI
|
|
126
131
|
other Ruby implementations, but be sure to let us know if that's not the
|
127
132
|
case. No pre-built binaries are currently distributed, let us know if
|
128
133
|
you're interested in helping with the release/support effort.
|
129
|
-
|
130
|
-
== LICENSE
|
131
|
-
|
132
|
-
Copyright (C) 2009 Eric Wong and contributors.
|
133
|
-
|
134
|
-
Clogger is free software; you can redistribute it and/or modify it under
|
135
|
-
the terms of the GNU Lesser General Public License as published by the
|
136
|
-
Free Software Foundation, version 3.0.
|
137
|
-
|
138
|
-
Clogger is distributed in the hope that it will be useful, but WITHOUT ANY
|
139
|
-
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
140
|
-
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
141
|
-
License in the COPYING file for more details.
|
142
|
-
|
143
|
-
You should have received a copy of the GNU Lesser General Public License
|
144
|
-
along with Clogger; if not, write to the Free Software Foundation, Inc.,
|
145
|
-
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
data/clogger.gemspec
CHANGED
@@ -35,11 +35,11 @@ is customizable so you can specify exactly which fields to log.
|
|
35
35
|
s.require_paths = %w(lib ext)
|
36
36
|
s.rubyforge_project = %q{clogger}
|
37
37
|
s.summary = %q{configurable request logging for Rack}
|
38
|
-
s.test_files = %w(test/test_clogger.rb)
|
38
|
+
s.test_files = %w(test/test_clogger.rb test/test_clogger_to_path.rb)
|
39
39
|
|
40
40
|
# HeaderHash wasn't case-insensitive in old versions
|
41
41
|
s.add_dependency(%q<rack>, ["> 0.9"])
|
42
42
|
s.extensions = %w(ext/clogger_ext/extconf.rb)
|
43
43
|
|
44
|
-
# s.license = "
|
44
|
+
# s.license = "LGPLv2.1+" # disabled for compatibility with older RubyGems
|
45
45
|
end
|
data/ext/clogger_ext/clogger.c
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
#define _BSD_SOURCE
|
2
2
|
#include <ruby.h>
|
3
|
+
#ifdef HAVE_RUBY_IO_H
|
4
|
+
# include <ruby/io.h>
|
5
|
+
#else
|
6
|
+
# include <rubyio.h>
|
7
|
+
#endif
|
3
8
|
#include <assert.h>
|
4
9
|
#include <unistd.h>
|
5
10
|
#include <sys/types.h>
|
11
|
+
#include <sys/stat.h>
|
6
12
|
#include <sys/time.h>
|
7
13
|
#include <time.h>
|
8
14
|
#include <errno.h>
|
@@ -89,7 +95,10 @@ static ID to_s_id;
|
|
89
95
|
static ID size_id;
|
90
96
|
static ID sq_brace_id;
|
91
97
|
static ID new_id;
|
98
|
+
static ID to_path_id;
|
99
|
+
static ID to_io_id;
|
92
100
|
static VALUE cClogger;
|
101
|
+
static VALUE cToPath;
|
93
102
|
static VALUE mFormat;
|
94
103
|
static VALUE cHeaderHash;
|
95
104
|
|
@@ -237,21 +246,31 @@ static void write_full(int fd, const void *buf, size_t count)
|
|
237
246
|
* allow us to use write_full() iff we detect a blocking file
|
238
247
|
* descriptor that wouldn't play nicely with Ruby threading/fibers
|
239
248
|
*/
|
240
|
-
static int raw_fd(VALUE
|
249
|
+
static int raw_fd(VALUE my_fd)
|
241
250
|
{
|
242
251
|
#if defined(HAVE_FCNTL) && defined(F_GETFL) && defined(O_NONBLOCK)
|
243
252
|
int fd;
|
244
253
|
int flags;
|
245
254
|
|
246
|
-
if (NIL_P(
|
255
|
+
if (NIL_P(my_fd))
|
247
256
|
return -1;
|
248
|
-
fd = NUM2INT(
|
257
|
+
fd = NUM2INT(my_fd);
|
249
258
|
|
250
259
|
flags = fcntl(fd, F_GETFL);
|
251
260
|
if (flags < 0)
|
252
261
|
rb_sys_fail("fcntl");
|
253
262
|
|
254
|
-
|
263
|
+
if (flags & O_NONBLOCK) {
|
264
|
+
struct stat sb;
|
265
|
+
|
266
|
+
if (fstat(fd, &sb) < 0)
|
267
|
+
return -1;
|
268
|
+
|
269
|
+
/* O_NONBLOCK is no-op for regular files: */
|
270
|
+
if (! S_ISREG(sb.st_mode))
|
271
|
+
return -1;
|
272
|
+
}
|
273
|
+
return fd;
|
255
274
|
#else /* platforms w/o fcntl/F_GETFL/O_NONBLOCK */
|
256
275
|
return -1;
|
257
276
|
#endif /* platforms w/o fcntl/F_GETFL/O_NONBLOCK */
|
@@ -638,12 +657,11 @@ static VALUE body_iter_i(VALUE str, VALUE memop)
|
|
638
657
|
return rb_yield(str);
|
639
658
|
}
|
640
659
|
|
641
|
-
static VALUE
|
660
|
+
static VALUE body_close(struct clogger *c)
|
642
661
|
{
|
643
|
-
c->
|
644
|
-
|
645
|
-
|
646
|
-
return c->body;
|
662
|
+
if (rb_respond_to(c->body, close_id))
|
663
|
+
return rb_funcall(c->body, close_id, 0);
|
664
|
+
return Qnil;
|
647
665
|
}
|
648
666
|
|
649
667
|
/**
|
@@ -659,8 +677,10 @@ static VALUE clogger_each(VALUE self)
|
|
659
677
|
struct clogger *c = clogger_get(self);
|
660
678
|
|
661
679
|
rb_need_block();
|
680
|
+
c->body_bytes_sent = 0;
|
681
|
+
rb_iterate(rb_each, c->body, body_iter_i, (VALUE)&c->body_bytes_sent);
|
662
682
|
|
663
|
-
return
|
683
|
+
return self;
|
664
684
|
}
|
665
685
|
|
666
686
|
/**
|
@@ -675,9 +695,7 @@ static VALUE clogger_close(VALUE self)
|
|
675
695
|
{
|
676
696
|
struct clogger *c = clogger_get(self);
|
677
697
|
|
678
|
-
|
679
|
-
return rb_funcall(c->body, close_id, 0);
|
680
|
-
return Qnil;
|
698
|
+
return rb_ensure(body_close, (VALUE)c, cwrite, (VALUE)c);
|
681
699
|
}
|
682
700
|
|
683
701
|
/* :nodoc: */
|
@@ -749,6 +767,9 @@ static VALUE clogger_call(VALUE self, VALUE env)
|
|
749
767
|
|
750
768
|
rv = ccall(c, env);
|
751
769
|
assert(!OBJ_FROZEN(rv) && "frozen response array");
|
770
|
+
|
771
|
+
if (rb_respond_to(c->body, to_path_id))
|
772
|
+
self = rb_funcall(cToPath, new_id, 1, self);
|
752
773
|
rb_ary_store(rv, 2, self);
|
753
774
|
|
754
775
|
return rv;
|
@@ -779,6 +800,39 @@ static VALUE clogger_init_copy(VALUE clone, VALUE orig)
|
|
779
800
|
|
780
801
|
#define CONST_GLOBAL_STR(val) CONST_GLOBAL_STR2(val, #val)
|
781
802
|
|
803
|
+
static VALUE to_path(VALUE self)
|
804
|
+
{
|
805
|
+
struct clogger *c = clogger_get(RSTRUCT_PTR(self)[0]);
|
806
|
+
VALUE path = rb_funcall(c->body, to_path_id, 0);
|
807
|
+
struct stat sb;
|
808
|
+
int rv;
|
809
|
+
unsigned devfd;
|
810
|
+
const char *cpath;
|
811
|
+
|
812
|
+
Check_Type(path, T_STRING);
|
813
|
+
cpath = RSTRING_PTR(path);
|
814
|
+
|
815
|
+
/* try to avoid an extra path lookup */
|
816
|
+
if (rb_respond_to(c->body, to_io_id))
|
817
|
+
rv = fstat(my_fileno(c->body), &sb);
|
818
|
+
/*
|
819
|
+
* Rainbows! can use "/dev/fd/%u" in to_path output to avoid
|
820
|
+
* extra open() syscalls, too.
|
821
|
+
*/
|
822
|
+
else if (sscanf(cpath, "/dev/fd/%u", &devfd) == 1)
|
823
|
+
rv = fstat((int)devfd, &sb);
|
824
|
+
else
|
825
|
+
rv = stat(cpath, &sb);
|
826
|
+
|
827
|
+
/*
|
828
|
+
* calling this method implies the web server will bypass
|
829
|
+
* the each method where body_bytes_sent is calculated,
|
830
|
+
* so we stat and set that value here.
|
831
|
+
*/
|
832
|
+
c->body_bytes_sent = rv == 0 ? sb.st_size : 0;
|
833
|
+
return path;
|
834
|
+
}
|
835
|
+
|
782
836
|
void Init_clogger_ext(void)
|
783
837
|
{
|
784
838
|
VALUE tmp;
|
@@ -792,6 +846,8 @@ void Init_clogger_ext(void)
|
|
792
846
|
size_id = rb_intern("size");
|
793
847
|
sq_brace_id = rb_intern("[]");
|
794
848
|
new_id = rb_intern("new");
|
849
|
+
to_path_id = rb_intern("to_path");
|
850
|
+
to_io_id = rb_intern("to_io");
|
795
851
|
cClogger = rb_define_class("Clogger", rb_cObject);
|
796
852
|
mFormat = rb_define_module_under(cClogger, "Format");
|
797
853
|
rb_define_alloc_func(cClogger, clogger_alloc);
|
@@ -821,4 +877,6 @@ void Init_clogger_ext(void)
|
|
821
877
|
tmp = rb_const_get(rb_cObject, rb_intern("Rack"));
|
822
878
|
tmp = rb_const_get(tmp, rb_intern("Utils"));
|
823
879
|
cHeaderHash = rb_const_get(tmp, rb_intern("HeaderHash"));
|
880
|
+
cToPath = rb_const_get(cClogger, rb_intern("ToPath"));
|
881
|
+
rb_define_method(cToPath, "to_path", to_path, 0);
|
824
882
|
}
|
@@ -11,6 +11,12 @@
|
|
11
11
|
#ifndef RARRAY_LEN
|
12
12
|
# define RARRAY_LEN(s) (RARRAY(s)->len)
|
13
13
|
#endif
|
14
|
+
#ifndef RSTRUCT_PTR
|
15
|
+
# define RSTRUCT_PTR(s) (RSTRUCT(s)->ptr)
|
16
|
+
#endif
|
17
|
+
#ifndef RSTRUCT_LEN
|
18
|
+
# define RSTRUCT_LEN(s) (RSTRUCT(s)->len)
|
19
|
+
#endif
|
14
20
|
|
15
21
|
#ifndef HAVE_RB_STR_SET_LEN
|
16
22
|
/* this is taken from Ruby 1.8.7, 1.8.6 may not have it */
|
@@ -21,3 +27,34 @@ static void rb_18_str_set_len(VALUE str, long len)
|
|
21
27
|
}
|
22
28
|
#define rb_str_set_len(str,len) rb_18_str_set_len(str,len)
|
23
29
|
#endif
|
30
|
+
|
31
|
+
#if ! HAVE_RB_IO_T
|
32
|
+
# define rb_io_t OpenFile
|
33
|
+
#endif
|
34
|
+
|
35
|
+
#ifdef GetReadFile
|
36
|
+
# define FPTR_TO_FD(fptr) (fileno(GetReadFile(fptr)))
|
37
|
+
#else
|
38
|
+
# if !HAVE_RB_IO_T || (RUBY_VERSION_MAJOR == 1 && RUBY_VERSION_MINOR == 8)
|
39
|
+
# define FPTR_TO_FD(fptr) fileno(fptr->f)
|
40
|
+
# else
|
41
|
+
# define FPTR_TO_FD(fptr) fptr->fd
|
42
|
+
# endif
|
43
|
+
#endif
|
44
|
+
|
45
|
+
static int my_fileno(VALUE io)
|
46
|
+
{
|
47
|
+
rb_io_t *fptr;
|
48
|
+
|
49
|
+
for (;;) {
|
50
|
+
switch (TYPE(io)) {
|
51
|
+
case T_FILE: {
|
52
|
+
GetOpenFile(io, fptr);
|
53
|
+
return FPTR_TO_FD(fptr);
|
54
|
+
}
|
55
|
+
default:
|
56
|
+
io = rb_convert_type(io, T_FILE, "IO", "to_io");
|
57
|
+
/* retry */
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
data/lib/clogger.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
|
2
|
+
require 'rack'
|
3
3
|
|
4
|
+
# See the README for usage instructions
|
4
5
|
class Clogger
|
5
|
-
VERSION = '0.4.0'
|
6
6
|
|
7
|
+
# the version of Clogger, currently 0.5.0
|
8
|
+
VERSION = '0.5.0'
|
9
|
+
|
10
|
+
# :stopdoc:
|
7
11
|
OP_LITERAL = 0
|
8
12
|
OP_REQUEST = 1
|
9
13
|
OP_RESPONSE = 2
|
@@ -36,6 +40,15 @@ class Clogger
|
|
36
40
|
:request_uri => 7
|
37
41
|
}
|
38
42
|
|
43
|
+
# proxy class to avoid clobbering the +to_path+ optimization when
|
44
|
+
# using static files
|
45
|
+
class ToPath < Struct.new(:clogger)
|
46
|
+
def each(&block); clogger.each(&block); end
|
47
|
+
def close; clogger.close; end
|
48
|
+
|
49
|
+
# to_path is defined in Clogger::Pure or the C extension
|
50
|
+
end
|
51
|
+
|
39
52
|
private
|
40
53
|
|
41
54
|
CGI_ENV = Regexp.new('\A\$(' <<
|
@@ -132,12 +145,13 @@ private
|
|
132
145
|
end
|
133
146
|
end
|
134
147
|
|
148
|
+
# :startdoc:
|
135
149
|
end
|
136
150
|
|
137
151
|
require 'clogger/format'
|
138
152
|
|
139
153
|
begin
|
140
|
-
raise LoadError if ENV['CLOGGER_PURE']
|
154
|
+
raise LoadError if ENV['CLOGGER_PURE'].to_i != 0
|
141
155
|
require 'clogger_ext'
|
142
156
|
rescue LoadError
|
143
157
|
require 'clogger/pure'
|
data/lib/clogger/pure.rb
CHANGED
@@ -5,6 +5,9 @@
|
|
5
5
|
# the original C extension code so it's not very Ruby-ish...
|
6
6
|
class Clogger
|
7
7
|
|
8
|
+
attr_accessor :env, :status, :headers, :body
|
9
|
+
attr_writer :body_bytes_sent
|
10
|
+
|
8
11
|
def initialize(app, opts = {})
|
9
12
|
# trigger autoload to avoid thread-safety issues later on
|
10
13
|
Rack::Utils::HeaderHash.new({})
|
@@ -30,8 +33,13 @@ class Clogger
|
|
30
33
|
headers = Rack::Utils::HeaderHash.new(headers) if @need_resp
|
31
34
|
if @wrap_body
|
32
35
|
@reentrant = env['rack.multithread'] if @reentrant.nil?
|
33
|
-
|
34
|
-
|
36
|
+
wbody = @reentrant ? self.dup : self
|
37
|
+
wbody.env = env
|
38
|
+
wbody.status = status
|
39
|
+
wbody.headers = headers
|
40
|
+
wbody.body = body
|
41
|
+
wbody = Clogger::ToPath.new(wbody) if body.respond_to?(:to_path)
|
42
|
+
return [ status, headers, wbody ]
|
35
43
|
end
|
36
44
|
log(env, status, headers)
|
37
45
|
[ status, headers, body ]
|
@@ -43,12 +51,13 @@ class Clogger
|
|
43
51
|
@body_bytes_sent += Rack::Utils.bytesize(part)
|
44
52
|
yield part
|
45
53
|
end
|
46
|
-
|
47
|
-
log(@env, @status, @headers)
|
54
|
+
self
|
48
55
|
end
|
49
56
|
|
50
57
|
def close
|
51
58
|
@body.close if @body.respond_to?(:close)
|
59
|
+
ensure
|
60
|
+
log(@env, @status, @headers)
|
52
61
|
end
|
53
62
|
|
54
63
|
def reentrant?
|
@@ -138,4 +147,16 @@ private
|
|
138
147
|
}.join('')
|
139
148
|
end
|
140
149
|
|
150
|
+
class ToPath
|
151
|
+
def to_path
|
152
|
+
rv = (body = clogger.body).to_path
|
153
|
+
|
154
|
+
# try to avoid unnecessary path lookups with to_io.stat instead of
|
155
|
+
# File.stat
|
156
|
+
clogger.body_bytes_sent =
|
157
|
+
(body.respond_to?(:to_io) ? body.to_io.stat : File.stat(rv)).size
|
158
|
+
rv
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
141
162
|
end
|