sinotify 0.0.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.
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Inode based directory notification for Linux
3
+ *
4
+ * Copyright (C) 2005 John McCutchan
5
+ */
6
+
7
+ #ifndef _LINUX_INOTIFY_H
8
+ #define _LINUX_INOTIFY_H
9
+
10
+ #include <linux/types.h>
11
+
12
+ /*
13
+ * struct inotify_event - structure read from the inotify device for each event
14
+ *
15
+ * When you are watching a directory, you will receive the filename for events
16
+ * such as IN_CREATE, IN_DELETE, IN_OPEN, IN_CLOSE, ..., relative to the wd.
17
+ */
18
+ struct inotify_event {
19
+ __s32 wd; /* watch descriptor */
20
+ __u32 mask; /* watch mask */
21
+ __u32 cookie; /* cookie to synchronize two events */
22
+ __u32 len; /* length (including nulls) of name */
23
+ char name[0]; /* stub for possible name */
24
+ };
25
+
26
+ /* the following are legal, implemented events that user-space can watch for */
27
+ #define IN_ACCESS 0x00000001 /* File was accessed */
28
+ #define IN_MODIFY 0x00000002 /* File was modified */
29
+ #define IN_ATTRIB 0x00000004 /* Metadata changed */
30
+ #define IN_CLOSE_WRITE 0x00000008 /* Writtable file was closed */
31
+ #define IN_CLOSE_NOWRITE 0x00000010 /* Unwrittable file closed */
32
+ #define IN_OPEN 0x00000020 /* File was opened */
33
+ #define IN_MOVED_FROM 0x00000040 /* File was moved from X */
34
+ #define IN_MOVED_TO 0x00000080 /* File was moved to Y */
35
+ #define IN_CREATE 0x00000100 /* Subfile was created */
36
+ #define IN_DELETE 0x00000200 /* Subfile was deleted */
37
+ #define IN_DELETE_SELF 0x00000400 /* Self was deleted */
38
+ #define IN_MOVE_SELF 0x00000800 /* Self was moved */
39
+
40
+ /* the following are legal events. they are sent as needed to any watch */
41
+ #define IN_UNMOUNT 0x00002000 /* Backing fs was unmounted */
42
+ #define IN_Q_OVERFLOW 0x00004000 /* Event queued overflowed */
43
+ #define IN_IGNORED 0x00008000 /* File was ignored */
44
+
45
+ /* helper events */
46
+ #define IN_CLOSE (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) /* close */
47
+ #define IN_MOVE (IN_MOVED_FROM | IN_MOVED_TO) /* moves */
48
+
49
+ /* special flags */
50
+ #define IN_ONLYDIR 0x01000000 /* only watch the path if it is a directory */
51
+ #define IN_DONT_FOLLOW 0x02000000 /* don't follow a sym link */
52
+ #define IN_MASK_ADD 0x20000000 /* add to the mask of an already existing watch */
53
+ #define IN_ISDIR 0x40000000 /* event occurred against dir */
54
+ #define IN_ONESHOT 0x80000000 /* only send event once */
55
+
56
+ /*
57
+ * All of the events - we build the list by hand so that we can add flags in
58
+ * the future and not break backward compatibility. Apps will get only the
59
+ * events that they originally wanted. Be sure to add new events here!
60
+ */
61
+ #define IN_ALL_EVENTS (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | \
62
+ IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | \
63
+ IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF | \
64
+ IN_MOVE_SELF)
65
+
66
+ #ifdef __KERNEL__
67
+
68
+ #include <linux/dcache.h>
69
+ #include <linux/fs.h>
70
+ #include <linux/config.h>
71
+
72
+ #ifdef CONFIG_INOTIFY
73
+
74
+ extern void inotify_inode_queue_event(struct inode *, __u32, __u32,
75
+ const char *);
76
+ extern void inotify_dentry_parent_queue_event(struct dentry *, __u32, __u32,
77
+ const char *);
78
+ extern void inotify_unmount_inodes(struct list_head *);
79
+ extern void inotify_inode_is_dead(struct inode *);
80
+ extern u32 inotify_get_cookie(void);
81
+
82
+ #else
83
+
84
+ static inline void inotify_inode_queue_event(struct inode *inode,
85
+ __u32 mask, __u32 cookie,
86
+ const char *filename)
87
+ {
88
+ }
89
+
90
+ static inline void inotify_dentry_parent_queue_event(struct dentry *dentry,
91
+ __u32 mask, __u32 cookie,
92
+ const char *filename)
93
+ {
94
+ }
95
+
96
+ static inline void inotify_unmount_inodes(struct list_head *list)
97
+ {
98
+ }
99
+
100
+ static inline void inotify_inode_is_dead(struct inode *inode)
101
+ {
102
+ }
103
+
104
+ static inline u32 inotify_get_cookie(void)
105
+ {
106
+ return 0;
107
+ }
108
+
109
+ #endif /* CONFIG_INOTIFY */
110
+
111
+ #endif /* __KERNEL __ */
112
+
113
+ #endif /* _LINUX_INOTIFY_H */
@@ -0,0 +1,205 @@
1
+ #include <ruby.h>
2
+
3
+ // includes that are different for ruby 1.8 vs. ruby 1.9
4
+ #ifdef RUBY_19
5
+ #warning "INCLUDING RUBY19 HEADERS (not really a warning, just for info)"
6
+ #include <ruby/io.h>
7
+ #else
8
+ #warning "INCLUDING RUBY18 HEADERS (not really a warning, just for info) "
9
+ #include <rubyio.h>
10
+
11
+ #endif
12
+
13
+ #ifdef HAVE_VERSION_H
14
+ #include <version.h>
15
+ #endif
16
+
17
+ #ifdef HAVE_LINUX_INOTIFY_H
18
+ #include <asm/unistd.h>
19
+ #include <linux/inotify.h>
20
+ #else
21
+ #include "inotify.h"
22
+ #include "inotify-syscalls.h"
23
+ #endif
24
+
25
+ #include <sys/syscall.h>
26
+ #include <unistd.h>
27
+
28
+ static inline int inotify_init (void)
29
+ {
30
+ return syscall (__NR_inotify_init);
31
+ }
32
+
33
+ static inline int inotify_add_watch (int fd, const char *name, __u32 mask)
34
+ {
35
+ return syscall (__NR_inotify_add_watch, fd, name, mask);
36
+ }
37
+
38
+ static inline int inotify_rm_watch (int fd, __u32 wd)
39
+ {
40
+ return syscall (__NR_inotify_rm_watch, fd, wd);
41
+ }
42
+
43
+ VALUE rb_cSinotify;
44
+ VALUE rb_cNotifier;
45
+ VALUE rb_cSinotifyEvent;
46
+
47
+ int event_check (int fd) {
48
+ struct timeval;
49
+ int r;
50
+
51
+ fd_set rfds;
52
+ FD_ZERO(&rfds);
53
+ FD_SET(fd, &rfds);
54
+
55
+ // Not using timout anymore
56
+ r = rb_thread_select (fd+1, &rfds, NULL, NULL, NULL);
57
+ return r;
58
+ }
59
+
60
+ static VALUE rb_inotify_event_new(struct inotify_event *event) {
61
+ VALUE retval;
62
+ retval = Data_Wrap_Struct(rb_cSinotifyEvent, NULL, free, event);
63
+ rb_obj_call_init(retval, 0, NULL);
64
+ return retval;
65
+ }
66
+
67
+ static VALUE rb_inotify_new(VALUE klass) {
68
+ int *fd;
69
+ VALUE retval;
70
+ fd = malloc(sizeof(int));
71
+ *fd = inotify_init();
72
+ if(*fd < 0) rb_sys_fail("inotify_init()");
73
+ retval = Data_Wrap_Struct(klass, NULL, free, fd);
74
+ rb_obj_call_init(retval, 0, NULL);
75
+ return retval;
76
+ }
77
+
78
+ static VALUE rb_inotify_add_watch(VALUE self, VALUE filename, VALUE mask) {
79
+ int *fd, wd;
80
+ Data_Get_Struct(self, int, fd);
81
+ wd = inotify_add_watch(*fd, RSTRING_PTR(filename), NUM2INT(mask));
82
+ if(wd < 0) {
83
+ rb_sys_fail(RSTRING_PTR(filename));
84
+ }
85
+ return INT2NUM(wd);
86
+ }
87
+
88
+ static VALUE rb_inotify_rm_watch(VALUE self, VALUE wdnum) {
89
+ int *fd;
90
+ Data_Get_Struct(self, int, fd);
91
+ if(inotify_rm_watch(*fd, NUM2INT(wdnum)) < 0) {
92
+ rb_sys_fail("removing watch");
93
+ }
94
+ return Qtrue;
95
+ }
96
+
97
+ static VALUE rb_inotify_each_event(VALUE self) {
98
+ int *fd, r;
99
+ struct inotify_event *event, *pevent;
100
+ char buffer[16384];
101
+ size_t buffer_n, event_size;
102
+
103
+ Data_Get_Struct(self, int, fd);
104
+ while(1) {
105
+ r = event_check(*fd);
106
+ if(r == 0) {
107
+ continue;
108
+ }
109
+ if((r = read(*fd, buffer, 16384)) < 0) {
110
+ rb_sys_fail("reading event");
111
+ }
112
+ buffer_n = 0;
113
+ while (buffer_n < r) {
114
+ pevent = (struct inotify_event *)&buffer[buffer_n];
115
+ event_size = sizeof(struct inotify_event) + pevent->len;
116
+ event = malloc(event_size);
117
+ memmove(event, pevent, event_size);
118
+ buffer_n += event_size;
119
+ rb_yield(rb_inotify_event_new(event));
120
+ }
121
+ }
122
+ return Qnil;
123
+ }
124
+
125
+ static VALUE rb_inotify_close(VALUE self) {
126
+ int *fd;
127
+ Data_Get_Struct(self, int, fd);
128
+ if(close(*fd) != 0) {
129
+ rb_sys_fail("closing inotify");
130
+ }
131
+ return Qnil;
132
+ }
133
+
134
+ static VALUE rb_inotify_event_name(VALUE self) {
135
+ struct inotify_event *event;
136
+ Data_Get_Struct(self, struct inotify_event, event);
137
+ if(event->len) {
138
+ return rb_str_new2(event->name);
139
+ } else {
140
+ return Qnil;
141
+ }
142
+ }
143
+
144
+ static VALUE rb_inotify_event_wd(VALUE self) {
145
+ struct inotify_event *event;
146
+ Data_Get_Struct(self, struct inotify_event, event);
147
+ return INT2NUM(event->wd);
148
+ }
149
+
150
+
151
+ static VALUE rb_inotify_event_mask(VALUE self) {
152
+ struct inotify_event *event;
153
+ Data_Get_Struct(self, struct inotify_event, event);
154
+ return LONG2NUM(event->mask);
155
+ }
156
+
157
+ void Init_sinotify () {
158
+ rb_cSinotify = rb_define_module("Sinotify");
159
+
160
+ //
161
+ // The Sinotify::PrimNotifier class
162
+ //
163
+ rb_cNotifier = rb_define_class_under(rb_cSinotify, "PrimNotifier", rb_cObject);
164
+
165
+ rb_define_singleton_method(rb_cNotifier, "new", rb_inotify_new, 0);
166
+ rb_define_method(rb_cNotifier, "add_watch", rb_inotify_add_watch, 2);
167
+ rb_define_method(rb_cNotifier, "rm_watch", rb_inotify_rm_watch, 1);
168
+ rb_define_method(rb_cNotifier, "each_event", rb_inotify_each_event, 0);
169
+ rb_define_method(rb_cNotifier, "close", rb_inotify_close, 0);
170
+
171
+ //
172
+ // The Sinotify::PrimEvent class
173
+ //
174
+ rb_cSinotifyEvent = rb_define_class_under(rb_cSinotify, "PrimEvent", rb_cObject);
175
+ rb_define_method(rb_cSinotifyEvent, "prim_name", rb_inotify_event_name, 0);
176
+ rb_define_method(rb_cSinotifyEvent, "prim_wd", rb_inotify_event_wd, 0);
177
+ rb_define_method(rb_cSinotifyEvent, "prim_mask", rb_inotify_event_mask, 0);
178
+
179
+ // following inotify masks taken from inotify lib, see 'man inotify', section
180
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ACCESS"), INT2NUM(IN_ACCESS));
181
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MODIFY"), INT2NUM(IN_MODIFY));
182
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ATTRIB"), INT2NUM(IN_ATTRIB));
183
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_WRITE"), INT2NUM(IN_CLOSE_WRITE));
184
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE_NOWRITE"), INT2NUM(IN_CLOSE_NOWRITE));
185
+ rb_const_set(rb_cSinotifyEvent, rb_intern("OPEN"), INT2NUM(IN_OPEN));
186
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_FROM"), INT2NUM(IN_MOVED_FROM));
187
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVED_TO"), INT2NUM(IN_MOVED_TO));
188
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CREATE"), INT2NUM(IN_CREATE));
189
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE"), INT2NUM(IN_DELETE));
190
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DELETE_SELF"), INT2NUM(IN_DELETE_SELF));
191
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE_SELF"), INT2NUM(IN_MOVE_SELF));
192
+ rb_const_set(rb_cSinotifyEvent, rb_intern("UNMOUNT"), INT2NUM(IN_UNMOUNT));
193
+ rb_const_set(rb_cSinotifyEvent, rb_intern("Q_OVERFLOW"), INT2NUM(IN_Q_OVERFLOW));
194
+ rb_const_set(rb_cSinotifyEvent, rb_intern("IGNORED"), INT2NUM(IN_IGNORED));
195
+ rb_const_set(rb_cSinotifyEvent, rb_intern("CLOSE"), INT2NUM(IN_CLOSE));
196
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MOVE"), INT2NUM(IN_MOVE));
197
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ONLYDIR"), INT2NUM(IN_ONLYDIR));
198
+ rb_const_set(rb_cSinotifyEvent, rb_intern("DONT_FOLLOW"), INT2NUM(IN_DONT_FOLLOW));
199
+ rb_const_set(rb_cSinotifyEvent, rb_intern("MASK_ADD"), INT2NUM(IN_MASK_ADD));
200
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ISDIR"), INT2NUM(IN_ISDIR));
201
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ONESHOT"), INT2NUM(IN_ONESHOT));
202
+ rb_const_set(rb_cSinotifyEvent, rb_intern("ALL_EVENTS"), INT2NUM(IN_ALL_EVENTS));
203
+
204
+
205
+ }
@@ -0,0 +1,18 @@
1
+ ext_lib = File.join(File.dirname(__FILE__), '../ext/sinotify.so')
2
+ unless File.exist?(ext_lib)
3
+ raise "Could not find ext/sinotify.so. \n" \
4
+ + "Please build the sinotify.so extention first (cd [sinotify gem]/ext && ruby extconf.rb && make)"
5
+ end
6
+
7
+ # load base info and c lib
8
+ require ext_lib
9
+ require File.join(File.dirname(__FILE__), 'sinotify_info')
10
+
11
+ # load external dependencies
12
+ require 'rubygems'
13
+ require 'cosell'
14
+ require 'logger'
15
+
16
+ # load application
17
+ Sinotify.require_all_libs_relative_to(__FILE__)
18
+
@@ -0,0 +1,185 @@
1
+ module Sinotify
2
+
3
+ #
4
+ # Sinotify events are triggered as they come in to the Notifier ('announced'
5
+ # in the parlance of the Cosell announcement framework that sinotify uses).
6
+ # Each event has the 'path' of the file or dir that was effected, the
7
+ # timestamp of the event (generated in ruby, not at the primitive level), and
8
+ # whether the event was on a file or a directory. Also available is the event
9
+ # type, called the 'etype,' which can be :modify, :create, :delete, etc. The
10
+ # list of event types is below.
11
+ #
12
+ # A Sinotify::Event does not perfectly model a linux inotify event. See
13
+ # Sinotify::PrimEvent for that.
14
+ #
15
+ # This event class deviates from Sinotify::PrimEvent in one significant regard. Sinotify does not
16
+ # pass events about children of a given directory, it only passes events about the directory
17
+ # (or file) itself. That is _not_ to say you can't setup a recursive watch in the Notifier class,
18
+ # just that _the event itself_ only pertains the the inode/file/directory being altered, not to
19
+ # its children.
20
+ #
21
+ # This is perhaps best illustrated by an example. Let's say you
22
+ #
23
+ # 1. Create a directory called '/tmp/test'
24
+ # 2. Create a file in '/tmp/test' called '/tmp/test/blah'
25
+ # 3. You put a watch on the directory '/tmp/test'
26
+ # 4. You then do a 'rm -rf /tmp/test'
27
+ # (thus deleting both the file /tmp/test/blah AND the directory /tmp/test)
28
+ #
29
+ # In linux inotify, you would get two events in this scenario, _both_ on the
30
+ # watch for the /tmp/test directory. One of the events would be a ':delete' event
31
+ # (that is, the mask of the event would be equal to
32
+ # Sinotify::PrimEvent::DELETE, or the 'etype' of the PrimEvent would equal
33
+ # ':delete'), and the 'name' slot in the event would be 'blah.' This is your
34
+ # cue that the event _really_ happened on a child of the thing being watched
35
+ # ('/tmp/test'), not to the directory itself. Since you deleted both the file
36
+ # and the directory with your 'rm -rf' command, another event would come in
37
+ # of the etype :delete_self for the directory, and 'is_dir' would be in the
38
+ # mask (ie. the mask would be Sinotify::PrimEvent::DELETE & Sinotify::PrimEvent::IS_DIR).
39
+ #
40
+ # Sinotify events would be a bit different in the example above.
41
+ # You would still get 2 events, but both would be :delete events,
42
+ # one where the 'path' is '/tmp/test', and the other where the 'path'
43
+ # is '/tmp/test/blah'. In the case of the event for '/tmp/test', the call
44
+ # to 'directory?' would return true.
45
+ #
46
+ # If you want to work with an event notifier that works more like the low level linux inotify
47
+ # (receiving both :delete with name slot filled in and another event w/ :delete_self),
48
+ # you will have to work directly with PrimNotifier and PrimEvent (along with their irritating
49
+ # linux inotify-style blocking synchronous event loop)
50
+ #
51
+ # Here is the list of possible events adapted from the definitions
52
+ # in [linux_src]/include/linux/inotify.h:
53
+ #
54
+ # File related:
55
+ # :access # File was accessed
56
+ # :modify # file modified
57
+ # :attrib # meta data changed
58
+ # :close_write # writable file was closed
59
+ # :close_nowrite # unwritable file was closed
60
+ # :open # file was opened
61
+ # :moved_from # file moved from X
62
+ # :moved_to # file moved to Y
63
+ # :create # file created
64
+ # :delete # file deleted
65
+ # :delete_self # self was deleted
66
+ # :move_self # self was moved
67
+ #
68
+ # File related helpers:
69
+ #
70
+ # :close # (close_write | close_nowrite)
71
+ # :move # (moved_from | moved_to)
72
+ #
73
+ # Misc events
74
+ #
75
+ # :unmount # backing fs was unmounted
76
+ # :q_overflow # event queue overflowed
77
+ # :ignored # file was ignored
78
+ # :mask_add # added to mask of already existing event
79
+ # :isdir # event occurred against dir
80
+ # :oneshot # only send event once
81
+ #
82
+ class Event
83
+
84
+ attr_accessor :prim_event, :path, :timestamp, :is_dir
85
+
86
+ # a few attr declarations just so they show up in rdoc
87
+
88
+ # Given a prim_event, and the Watch associated with the event's watch descriptor,
89
+ # return a Sinotify::Event.
90
+ def self.from_prim_event_and_watch(prim_event, watch)
91
+ path = watch.path # path for the watch associated w/ this even
92
+ is_dir = watch.directory? # original watch was on a dir or a file?
93
+
94
+ # This gets a little odd. The prim_event's 'name' field
95
+ # will be nil if the change was to a directory itself, or if
96
+ # the watch was on a file to begin with. However,
97
+ # when a watch is on a dir, but the event occurs on a file in that dir
98
+ # inotify sets the 'name' field to the file. :isdir will be in the etypes
99
+ # if that file happens to be a subdir. Sinotify events do not
100
+ # play this game, only sending events for the thing that was altered
101
+ # in the first place. So right here is where we deduce if the
102
+ # event was _really_ on a file or a dir.
103
+ unless prim_event.name.nil?
104
+ path = File.join(path, prim_event.name)
105
+ is_dir = prim_event.etypes.include?(:isdir)
106
+ end
107
+
108
+ # is_dir must be passed along, since it may no longer exist (and thus cant be deduced later)
109
+ # inotify prim_events to not retain enough information to make it possible to deduce the
110
+ # original fullpath and whether it was a file or directory, so this info must be passed around.
111
+ return Sinotify::Event.new(:prim_event => prim_event,
112
+ :path => path,
113
+ :timestamp => Time.now, # any way to get this from prim event?
114
+ :is_dir => is_dir)
115
+ end
116
+
117
+ def initialize(args={})
118
+ args.each{|k,v| self.send("#{k}=",v)}
119
+ @timestamp ||= Time.now
120
+
121
+ # initialize a few variables just to shut up the ruby warnings
122
+ @etypes = nil
123
+ end
124
+
125
+ # The Sinotify::PrimEvent associated with this event (a straight
126
+ # wrapper around the linux inotify event)
127
+ def prim_event; @prim_event; end
128
+
129
+ # The full path of the file or directory on which the event happened
130
+ def path; @path; end
131
+
132
+ # when the event happened
133
+ def timestamp; @timestamp; end
134
+
135
+
136
+ def to_s; self.inspect_or_to_s(false); end
137
+ def inspect; self.inspect_or_to_s(true); end
138
+
139
+ # The etypes associated with this event (eg. :create, :modify, :delete, etc)
140
+ def etypes
141
+ # The etype/mask functions delegated to prim_event, EXCEPT: when :delete_self is in
142
+ # the list, and path is a directory, change it to 'delete'. If you want
143
+ # the etypes in the original prim_event, ask for event.prim_event.etypes
144
+ if @etypes.nil?
145
+ @etypes = self.prim_event.etypes
146
+
147
+ # change :delete_self into :delete
148
+ if self.directory? and @etypes.include?(:delete_self)
149
+ @etypes.delete(:delete_self)
150
+ @etypes << :delete
151
+ end
152
+
153
+ # add :close if :close_write or :close_nowrite are there, but :close is not
154
+ if @etypes.include?(:close_write) || @etypes.include?(:close_nowrite)
155
+ (@etypes << :close) unless @etypes.include?(:close)
156
+ end
157
+
158
+ end
159
+ @etypes
160
+ end
161
+
162
+ def directory?
163
+ self.is_dir.eql?(true)
164
+ end
165
+
166
+ def has_etype? etype
167
+ self.etypes.include?(etype)
168
+ end
169
+
170
+ def watch_descriptor
171
+ self.prim_event.watch_descriptor
172
+ end
173
+
174
+ protected
175
+
176
+ # :stopdoc:
177
+ def inspect_or_to_s(show_prim_event = false)
178
+ prim_event = (show_prim_event)? ", :prim_event => #{self.prim_event.inspect}" : ''
179
+ "<#{self.class} :path => '#{self.path}', dir? => #{self.directory?}, :etypes => #{self.etypes.inspect rescue 'could not determine'}#{prim_event}>"
180
+ end
181
+
182
+ # :startdoc:
183
+ end
184
+
185
+ end