rims 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|