rims 0.2.1 → 0.2.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.
- checksums.yaml +4 -4
- data/ChangeLog +35 -2
- data/LICENSE.txt +17 -18
- data/lib/rims.rb +1 -0
- data/lib/rims/channel.rb +157 -0
- data/lib/rims/cmd.rb +3 -3
- data/lib/rims/mail_store.rb +26 -90
- data/lib/rims/pool.rb +15 -18
- data/lib/rims/protocol/decoder.rb +4 -4
- data/lib/rims/protocol/parser.rb +2 -2
- data/lib/rims/test.rb +39 -7
- data/lib/rims/version.rb +1 -1
- data/test/test_channel.rb +131 -0
- data/test/test_cksum_kvs.rb +4 -2
- data/test/test_config.rb +1 -1
- data/test/test_db.rb +10 -5
- data/test/test_db_recovery.rb +6 -3
- data/test/test_lock.rb +4 -2
- data/test/test_mail_store.rb +22 -6
- data/test/test_protocol_decoder.rb +2 -1
- data/test/test_protocol_fetch.rb +7 -3
- data/test/test_protocol_search.rb +66 -53
- data/test/test_rfc822.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 299a57cf042b4a2854a5e040af070bbef170044d8cedbac0c4587559018deb45
|
4
|
+
data.tar.gz: f9f6f5a9654c9b84c7978c4d6aa15d6243150811ee7a70b6110ee1e40eb1753f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f7009d59c5a09874933e75a9cea109367fb8f1ef4f1a2718a19536df1055fe7f5e7a60392bc50cf14b964e103ee7c2aa43493b6bf6790c8cba8083605edf3f8
|
7
|
+
data.tar.gz: 78c978afee91b8f5a7ef769a55d4a62d5b2e138d7ef8c6d9ddf5942a997ea772dc1aa317915f72973c42e6b77b7a2cd7acce8a8bae6f9f402d19268524e3b8dc
|
data/ChangeLog
CHANGED
@@ -1,8 +1,41 @@
|
|
1
|
+
2019-03-06 TOKI Yoshinori <toki@freedom.ne.jp>
|
2
|
+
|
3
|
+
* RIMS version 0.2.2 is released.
|
4
|
+
|
5
|
+
2019-03-03 TOKI Yoshinori <toki@freedom.ne.jp>
|
6
|
+
|
7
|
+
* lib/rims/test.rb: key-value store. closed error at test utility
|
8
|
+
is able to changed.
|
9
|
+
|
10
|
+
* test/test_cksum_kvs.rb, test/test_db.rb,
|
11
|
+
test/test_db_recovery.rb, test/test_lock.rb,
|
12
|
+
test/test_mail_store.rb, test/test_protocol_decoder.rb,
|
13
|
+
test/test_protocol_fetch.rb: check error messages at test.
|
14
|
+
|
15
|
+
* lib/rims/channel.rb: refactor channel for untagged server
|
16
|
+
response.
|
17
|
+
|
18
|
+
2019-03-02 TOKI Yoshinori <toki@freedom.ne.jp>
|
19
|
+
|
20
|
+
* lib/rims/channel.rb, lib/rims/mail_store.rb: untagged servere
|
21
|
+
response mechanism is changed. server response queue is replaced
|
22
|
+
to server response channel.
|
23
|
+
|
24
|
+
2019-02-26 TOKI Yoshinori <toki@freedom.ne.jp>
|
25
|
+
|
26
|
+
* lib/rims/mail_store.rb, lib/rims/pool.rb: generic object
|
27
|
+
pool. object's Lock has to be owned to the object that needs to
|
28
|
+
lock.
|
29
|
+
|
30
|
+
object pool no longer has object lock.
|
31
|
+
mail store now has read-write lock.
|
32
|
+
response queue bundle now has mutex lock.
|
33
|
+
|
1
34
|
2019-02-18 TOKI Yoshinori <toki@freedom.ne.jp>
|
2
35
|
|
3
|
-
*
|
36
|
+
* RIMS version 0.2.1 is released.
|
4
37
|
|
5
|
-
*
|
38
|
+
* README.md, rims.gemspec: fixed for release to rubygems.
|
6
39
|
|
7
40
|
2018-12-02 TOKI Yoshinori <toki@freedom.ne.jp>
|
8
41
|
|
data/LICENSE.txt
CHANGED
@@ -1,22 +1,21 @@
|
|
1
|
-
|
1
|
+
The MIT License (MIT)
|
2
2
|
|
3
|
-
|
3
|
+
Copyright (c) 2013-2019 TOKI Yoshinori
|
4
4
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
the following conditions:
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
12
11
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
14
|
-
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
15
14
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
OF
|
22
|
-
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/lib/rims.rb
CHANGED
@@ -33,6 +33,7 @@ module RIMS
|
|
33
33
|
autoload :ReadLockTimeoutError, 'rims/lock'
|
34
34
|
autoload :ReadWriteLock, 'rims/lock'
|
35
35
|
autoload :Server, 'rims/server'
|
36
|
+
autoload :ServerResponseChannel, 'rims/channel'
|
36
37
|
autoload :SyntaxError, 'rims/protocol'
|
37
38
|
autoload :Test, 'rims/test'
|
38
39
|
autoload :WriteLockError, 'rims/lock'
|
data/lib/rims/channel.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module RIMS
|
4
|
+
class ServerResponseChannel
|
5
|
+
def initialize
|
6
|
+
@mutex = Thread::Mutex.new
|
7
|
+
@channel = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def make_pub_sub_pair(mbox_id)
|
11
|
+
pub = ServerResponsePublisher.new(self, mbox_id)
|
12
|
+
sub = ServerResponseSubscriber.new(self, mbox_id, pub.pub_sub_pair_key)
|
13
|
+
return pub, attach(sub)
|
14
|
+
end
|
15
|
+
|
16
|
+
def attach(sub)
|
17
|
+
@mutex.synchronize{
|
18
|
+
@channel[sub.mbox_id] ||= {}
|
19
|
+
(@channel[sub.mbox_id].key? sub.pub_sub_pair_key) and raise ArgumentError, 'conflicted subscriber.'
|
20
|
+
@channel[sub.mbox_id][sub.pub_sub_pair_key] = sub
|
21
|
+
}
|
22
|
+
|
23
|
+
sub
|
24
|
+
end
|
25
|
+
private :attach
|
26
|
+
|
27
|
+
# do not call this method directly, call the following method
|
28
|
+
# instead.
|
29
|
+
# - ServerResponsePublisher#detach
|
30
|
+
# - ServerResponseSubscriber#detach
|
31
|
+
def detach(sub)
|
32
|
+
@mutex.synchronize{
|
33
|
+
((@channel.key? sub.mbox_id) && (@channel[sub.mbox_id].key? sub.pub_sub_pair_key)) or raise ArgumentError, 'unregistered pub-sub pair.'
|
34
|
+
(@channel[sub.mbox_id][sub.pub_sub_pair_key] == sub) or raise 'internal error: mismatched subscriber.'
|
35
|
+
|
36
|
+
@channel[sub.mbox_id].delete(sub.pub_sub_pair_key)
|
37
|
+
if (@channel[sub.mbox_id].empty?) then
|
38
|
+
@channel.delete(sub.mbox_id)
|
39
|
+
end
|
40
|
+
}
|
41
|
+
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# do not call this method directly, call the following method
|
46
|
+
# instead.
|
47
|
+
# - ServerResponsePublisher#publish
|
48
|
+
def publish(mbox_id, pub_sub_pair_key, response_message)
|
49
|
+
@mutex.synchronize{
|
50
|
+
@channel[mbox_id].each_value do |sub|
|
51
|
+
if (sub.pub_sub_pair_key != pub_sub_pair_key) then
|
52
|
+
sub.publish(response_message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class ServerResponsePublisher
|
62
|
+
# do not call this method directly, call the following method
|
63
|
+
# instead.
|
64
|
+
# - ServerResponseChannel#make_pub_sub_pair
|
65
|
+
def initialize(channel, mbox_id)
|
66
|
+
@channel = channel
|
67
|
+
@mbox_id = mbox_id
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :mbox_id
|
71
|
+
|
72
|
+
def pub_sub_pair_key
|
73
|
+
object_id
|
74
|
+
end
|
75
|
+
|
76
|
+
def publish(response_message)
|
77
|
+
@channel or raise 'detached publisher.'
|
78
|
+
@channel.publish(@mbox_id, pub_sub_pair_key, response_message)
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def detach
|
83
|
+
@channel = nil
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class ServerResponseSubscriber
|
89
|
+
# do not call this method directly, call the following method
|
90
|
+
# instead.
|
91
|
+
# - ServerResponseChannel#make_pub_sub_pair
|
92
|
+
def initialize(channel, mbox_id, pub_sub_pair_key)
|
93
|
+
@channel = channel
|
94
|
+
@mbox_id = mbox_id
|
95
|
+
@pub_sub_pair_key = pub_sub_pair_key
|
96
|
+
@queue = Thread::Queue.new
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :mbox_id
|
100
|
+
attr_reader :pub_sub_pair_key
|
101
|
+
|
102
|
+
# do not call this method directly, call the following method
|
103
|
+
# instead.
|
104
|
+
# - ServerResponsePublisher#publish
|
105
|
+
def publish(response_message)
|
106
|
+
@queue.push(response_message)
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def detach
|
111
|
+
@channel.detach(self)
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def message?
|
116
|
+
! @queue.empty?
|
117
|
+
end
|
118
|
+
|
119
|
+
def fetch
|
120
|
+
while (message?)
|
121
|
+
response_message = @queue.pop(true)
|
122
|
+
yield(response_message)
|
123
|
+
end
|
124
|
+
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def idle_wait
|
129
|
+
catch(:idle_interrupt) {
|
130
|
+
while (response_message = @queue.pop(false))
|
131
|
+
message_list = [ response_message ]
|
132
|
+
fetch{|next_response_message|
|
133
|
+
if (next_response_message) then
|
134
|
+
message_list << next_response_message
|
135
|
+
else
|
136
|
+
yield(message_list)
|
137
|
+
throw(:idle_interrupt)
|
138
|
+
end
|
139
|
+
}
|
140
|
+
yield(message_list)
|
141
|
+
end
|
142
|
+
}
|
143
|
+
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
def idle_interrupt
|
148
|
+
@queue.push(nil)
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Local Variables:
|
155
|
+
# mode: Ruby
|
156
|
+
# indent-tabs-mode: nil
|
157
|
+
# End:
|
data/lib/rims/cmd.rb
CHANGED
@@ -706,11 +706,11 @@ module RIMS
|
|
706
706
|
conf.help_option(add_banner: <<-'EOF'.chomp)
|
707
707
|
passwd_plain.yml
|
708
708
|
Example
|
709
|
-
$ cat passwd_plain.yml
|
709
|
+
$ cat passwd_plain.yml
|
710
710
|
- { user: foo, pass: open_sesame }
|
711
711
|
- { user: "#postman", pass: "#postman" }
|
712
|
-
$ rims pass-hash passwd_plain.yml >passwd_hash.yml
|
713
|
-
$ cat passwd_hash.yml
|
712
|
+
$ rims pass-hash passwd_plain.yml >passwd_hash.yml
|
713
|
+
$ cat passwd_hash.yml
|
714
714
|
---
|
715
715
|
- user: foo
|
716
716
|
hash: SHA256:10000:YkslZucwN2QJ7LOft59Pgw==:d5dca9109cc787220eba65810e40165079ce3292407e74e8fbd5c6a8a9b12204
|
data/lib/rims/mail_store.rb
CHANGED
@@ -3,13 +3,16 @@
|
|
3
3
|
require 'forwardable'
|
4
4
|
require 'logger'
|
5
5
|
require 'set'
|
6
|
-
require 'thread'
|
7
6
|
|
8
7
|
module RIMS
|
9
8
|
class MailStore
|
9
|
+
extend Forwardable
|
10
|
+
|
10
11
|
MSG_FLAG_NAMES = %w[ answered flagged deleted seen draft recent ].each{|n| n.freeze }.freeze
|
11
12
|
|
12
13
|
def initialize(meta_db, msg_db, &mbox_db_factory) # :yields: mbox_id
|
14
|
+
@rw_lock = ReadWriteLock.new
|
15
|
+
|
13
16
|
@meta_db = meta_db
|
14
17
|
@msg_db = msg_db
|
15
18
|
@mbox_db_factory = mbox_db_factory
|
@@ -26,11 +29,11 @@ module RIMS
|
|
26
29
|
@meta_db.dirty = true
|
27
30
|
end
|
28
31
|
|
29
|
-
@
|
30
|
-
MailboxServerResponseQueueBundleHolder.new(object_pool, mbox_id, object_lock)
|
31
|
-
}
|
32
|
+
@channel = ServerResponseChannel.new
|
32
33
|
end
|
33
34
|
|
35
|
+
def_delegators :@rw_lock, :read_synchronize, :write_synchronize
|
36
|
+
|
34
37
|
def get_mbox_db(mbox_id)
|
35
38
|
if (@mbox_db.key? mbox_id) then
|
36
39
|
@mbox_db[mbox_id] ||= @mbox_db_factory.call(mbox_id)
|
@@ -347,29 +350,30 @@ module RIMS
|
|
347
350
|
|
348
351
|
def select_mbox(mbox_id)
|
349
352
|
@meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
|
350
|
-
MailFolder.new(mbox_id, self).
|
353
|
+
MailFolder.new(mbox_id, self).attach(@channel)
|
351
354
|
end
|
352
355
|
|
353
356
|
def examine_mbox(mbox_id)
|
354
357
|
@meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
|
355
|
-
MailFolder.new(mbox_id, self, read_only: true).
|
358
|
+
MailFolder.new(mbox_id, self, read_only: true).attach(@channel)
|
356
359
|
end
|
357
360
|
|
358
361
|
def self.build_pool(kvs_meta_open, kvs_text_open)
|
359
|
-
RIMS::ObjectPool.new{|object_pool, unique_user_id
|
360
|
-
RIMS::MailStoreHolder.build(object_pool, unique_user_id,
|
362
|
+
RIMS::ObjectPool.new{|object_pool, unique_user_id|
|
363
|
+
RIMS::MailStoreHolder.build(object_pool, unique_user_id, kvs_meta_open, kvs_text_open)
|
361
364
|
}
|
362
365
|
end
|
363
366
|
end
|
364
367
|
|
365
368
|
class MailFolder
|
369
|
+
extend Forwardable
|
370
|
+
|
366
371
|
MessageStruct = Struct.new(:uid, :num)
|
367
372
|
|
368
373
|
def initialize(mbox_id, mail_store, read_only: false)
|
369
374
|
@mbox_id = mbox_id
|
370
375
|
@mail_store = mail_store
|
371
376
|
@read_only = read_only
|
372
|
-
@mail_folder_key = object_id
|
373
377
|
|
374
378
|
# late loding
|
375
379
|
@cnum = nil
|
@@ -377,50 +381,16 @@ module RIMS
|
|
377
381
|
@uid_map = nil
|
378
382
|
end
|
379
383
|
|
380
|
-
def
|
381
|
-
@
|
382
|
-
@server_response_queue = @server_response_queue_bundle.attach_queue(@mail_folder_key)
|
383
|
-
self
|
384
|
-
end
|
385
|
-
|
386
|
-
def server_response_multicast_push(server_response_message)
|
387
|
-
@server_response_queue_bundle.multicast_push(server_response_message, @mail_folder_key)
|
388
|
-
self
|
389
|
-
end
|
390
|
-
|
391
|
-
def server_response?
|
392
|
-
! @server_response_queue.empty?
|
393
|
-
end
|
394
|
-
|
395
|
-
def server_response_fetch
|
396
|
-
while (server_response?)
|
397
|
-
server_response_message = @server_response_queue.pop(true)
|
398
|
-
yield(server_response_message)
|
399
|
-
end
|
400
|
-
self
|
401
|
-
end
|
402
|
-
|
403
|
-
def server_response_idle_wait
|
404
|
-
catch(:server_response_idle_wait_interrupt) {
|
405
|
-
while (server_response_message = @server_response_queue.pop(false))
|
406
|
-
server_response_list = [ server_response_message ]
|
407
|
-
server_response_fetch{|next_response_message|
|
408
|
-
if (next_response_message) then
|
409
|
-
server_response_list.push(next_response_message)
|
410
|
-
else
|
411
|
-
yield(server_response_list)
|
412
|
-
throw(:server_response_idle_wait_interrupt)
|
413
|
-
end
|
414
|
-
}
|
415
|
-
yield(server_response_list)
|
416
|
-
end
|
417
|
-
}
|
384
|
+
def attach(server_response_channel)
|
385
|
+
@pub, @sub = server_response_channel.make_pub_sub_pair(@mbox_id)
|
418
386
|
self
|
419
387
|
end
|
420
388
|
|
421
|
-
|
422
|
-
|
423
|
-
|
389
|
+
def_delegator :@pub, :publish, :server_response_multicast_push
|
390
|
+
def_delegator :@sub, :message?, :server_response?
|
391
|
+
def_delegator :@sub, :fetch, :server_response_fetch
|
392
|
+
def_delegator :@sub, :idle_wait, :server_response_idle_wait
|
393
|
+
def_delegator :@sub, :idle_interrupt, :server_response_idle_interrupt
|
424
394
|
|
425
395
|
def reload
|
426
396
|
@cnum = @mail_store.cnum
|
@@ -532,11 +502,8 @@ module RIMS
|
|
532
502
|
end
|
533
503
|
end
|
534
504
|
@mail_store = nil
|
535
|
-
|
536
|
-
@
|
537
|
-
@server_response_queue_bundle.return_pool
|
538
|
-
@server_response_queue_bundle = nil
|
539
|
-
|
505
|
+
@pub.detach
|
506
|
+
@sub.detach
|
540
507
|
self
|
541
508
|
end
|
542
509
|
|
@@ -596,7 +563,7 @@ module RIMS
|
|
596
563
|
class MailStoreHolder < ObjectPool::ObjectHolder
|
597
564
|
extend Forwardable
|
598
565
|
|
599
|
-
def self.build(object_pool, unique_user_id,
|
566
|
+
def self.build(object_pool, unique_user_id, kvs_meta_open, kvs_text_open)
|
600
567
|
kvs_build = proc{|kvs_open, db_name|
|
601
568
|
kvs_open.call(MAILBOX_DATA_STRUCTURE_VERSION, unique_user_id, db_name)
|
602
569
|
}
|
@@ -607,54 +574,23 @@ module RIMS
|
|
607
574
|
}
|
608
575
|
mail_store.add_mbox('INBOX') unless mail_store.mbox_id('INBOX')
|
609
576
|
|
610
|
-
new(object_pool, unique_user_id,
|
577
|
+
new(object_pool, unique_user_id, mail_store)
|
611
578
|
end
|
612
579
|
|
613
|
-
def initialize(object_pool, unique_user_id,
|
580
|
+
def initialize(object_pool, unique_user_id, mail_store)
|
614
581
|
super(object_pool, unique_user_id)
|
615
|
-
@object_lock = object_lock
|
616
582
|
@mail_store = mail_store
|
617
583
|
end
|
618
584
|
|
619
585
|
alias unique_user_id object_key
|
620
586
|
attr_reader :mail_store
|
621
587
|
|
622
|
-
|
623
|
-
def_delegator :@object_lock, :write_synchronize
|
588
|
+
def_delegators :@mail_store, :read_synchronize, :write_synchronize
|
624
589
|
|
625
590
|
def object_destroy
|
626
591
|
@mail_store.close
|
627
592
|
end
|
628
593
|
end
|
629
|
-
|
630
|
-
class MailboxServerResponseQueueBundleHolder < ObjectPool::ObjectHolder
|
631
|
-
def initialize(object_pool, mbox_id, object_lock)
|
632
|
-
super(object_pool, mbox_id)
|
633
|
-
@object_lock = object_lock
|
634
|
-
@queue_map = Hash.new{|h, k| h[k] = Thread::Queue.new }
|
635
|
-
end
|
636
|
-
|
637
|
-
alias mbox_id object_id
|
638
|
-
|
639
|
-
def attach_queue(mail_folder_key)
|
640
|
-
@object_lock.write_synchronize{ @queue_map[mail_folder_key] }
|
641
|
-
end
|
642
|
-
|
643
|
-
def detach_queue(mail_folder_key)
|
644
|
-
@object_lock.write_synchronize{ @queue_map.delete(mail_folder_key) } or raise "not found a queue at mail folder key: #{mail_folder_key}"
|
645
|
-
self
|
646
|
-
end
|
647
|
-
|
648
|
-
def multicast_push(server_response_message, this_mail_folder_key)
|
649
|
-
@object_lock.read_synchronize{
|
650
|
-
for mail_folder_key, queue in @queue_map
|
651
|
-
next if (mail_folder_key == this_mail_folder_key)
|
652
|
-
queue.push(server_response_message)
|
653
|
-
end
|
654
|
-
}
|
655
|
-
self
|
656
|
-
end
|
657
|
-
end
|
658
594
|
end
|
659
595
|
|
660
596
|
# Local Variables:
|